From 4ffb1ea95eddf2c6e6ad1f570cd7c0dd28569cd9 Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Tue, 13 Oct 2015 21:44:01 -0700 Subject: [PATCH] in-conversation invite reminder Closes #4250 // FREEBIE --- res/layout/conversation_activity.xml | 5 + res/layout/conversation_list_fragment.xml | 2 +- res/layout/reminder_header.xml | 109 +++++++++--------- res/values/strings.xml | 12 +- .../securesms/ConversationActivity.java | 92 ++++++++++----- .../securesms/ConversationListFragment.java | 16 ++- .../securesms/components/ComposeText.java | 1 + .../components/ExpiredBuildReminder.java | 35 ------ .../securesms/components/Reminder.java | 53 --------- .../{ => reminder}/DefaultSmsReminder.java | 13 +-- .../reminder/ExpiredBuildReminder.java | 39 +++++++ .../components/reminder/InviteReminder.java | 34 ++++++ .../PushRegistrationReminder.java | 8 +- .../components/reminder/Reminder.java | 56 +++++++++ .../{ => reminder}/ReminderView.java | 38 +++--- .../SystemSmsImportReminder.java | 10 +- .../securesms/database/DatabaseFactory.java | 7 +- .../database/RecipientPreferenceDatabase.java | 60 ++++++---- .../concurrent/AssertedSuccessListener.java | 12 ++ .../ConversationListActivityTest.java | 8 +- 20 files changed, 368 insertions(+), 242 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/components/ExpiredBuildReminder.java delete mode 100644 src/org/thoughtcrime/securesms/components/Reminder.java rename src/org/thoughtcrime/securesms/components/{ => reminder}/DefaultSmsReminder.java (78%) create mode 100644 src/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java create mode 100644 src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java rename src/org/thoughtcrime/securesms/components/{ => reminder}/PushRegistrationReminder.java (80%) create mode 100644 src/org/thoughtcrime/securesms/components/reminder/Reminder.java rename src/org/thoughtcrime/securesms/components/{ => reminder}/ReminderView.java (59%) rename src/org/thoughtcrime/securesms/components/{ => reminder}/SystemSmsImportReminder.java (84%) create mode 100644 src/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index fd6e1786b3..48a4716e46 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -20,6 +20,11 @@ android:paddingTop="?attr/actionBarSize" android:gravity="bottom"> + + - diff --git a/res/layout/reminder_header.xml b/res/layout/reminder_header.xml index 2d737437e3..1af3d4ed06 100644 --- a/res/layout/reminder_header.xml +++ b/res/layout/reminder_header.xml @@ -1,59 +1,62 @@ - - + + - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:layout_margin="10dp" + android:orientation="vertical"> + + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 8a4be620d8..ba9d1c988f 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1037,12 +1037,20 @@ Your build of Signal has expired! Messages will no longer send successfully, please update to the most recent version. + UPGRADE Use as default SMS app? Tap to make Signal your default SMS app. + SET Import system SMS? Tap to copy your phone\'s SMS messages into its encrypted database. - Enable Signal messages? - Tap for instant delivery, stronger privacy, and no SMS fees. + IMPORT + Enable Signal? + Upgrade your messaging experience. + ENABLE + Invite to Signal? + Take your conversation with %1$s to the next level. + INVITE + CLOSE You diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 4427988c67..45496a27d8 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -66,6 +66,8 @@ import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; +import org.thoughtcrime.securesms.components.reminder.InviteReminder; +import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.SendButton; import org.thoughtcrime.securesms.components.camera.HidingImageButton; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer; @@ -85,6 +87,7 @@ import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; +import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; @@ -106,6 +109,7 @@ import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState; import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DirectoryHelper; @@ -117,16 +121,17 @@ import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.util.guava.Optional; import org.whispersystems.textsecure.api.util.InvalidNumberException; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.List; -import java.util.concurrent.ExecutionException; import static org.thoughtcrime.securesms.TransportOption.Type; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; @@ -175,6 +180,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private InputAwareLayout container; private View composePanel; private View composeBubble; + private ReminderView reminderView; private AttachmentTypeSelectorAdapter attachmentAdapter; private AttachmentManager attachmentManager; @@ -216,16 +222,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity initializeActionBar(); initializeViews(); initializeResources(); - initializeSecurity(false, false).addListener(new ListenableFuture.Listener() { + initializeSecurity(false, false).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { initializeDraft(); } - - @Override - public void onFailure(ExecutionException e) { - throw new AssertionError(e); - } }); } @@ -241,15 +242,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity setIntent(intent); initializeResources(); - initializeSecurity(false, false).addListener(new ListenableFuture.Listener() { + initializeSecurity(false, false).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { initializeDraft(); } - @Override - public void onFailure(ExecutionException e) { - throw new AssertionError(e); - } }); if (fragment != null) { @@ -804,14 +801,25 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity if (result.first != currentSecureText || result.second != currentSecureVoice) { handleSecurityChange(result.first, result.second); } - future.set(true); + onSecurityUpdated(); } }.execute(recipients); return future; } + private void onSecurityUpdated() { + updateInviteReminder(); + } + + private void updateInviteReminder() { + if (TextSecurePreferences.isPushRegistered(this) && !isSecureText && recipients.isSingleRecipient()) { + new ShowInviteReminderTask().execute(recipients); + } else { + reminderView.hide(); + } + } private void initializeMmsEnabledCheck() { new AsyncTask() { @@ -828,21 +836,21 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void initializeViews() { - titleView = (ConversationTitleView) getSupportActionBar().getCustomView(); - buttonToggle = (AnimatingToggle) findViewById(R.id.button_toggle); - sendButton = (SendButton) findViewById(R.id.send_button); - attachButton = (ImageButton) findViewById(R.id.attach_button); - composeText = (ComposeText) findViewById(R.id.embedded_text_editor); - charactersLeft = (TextView) findViewById(R.id.space_left); - emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle); - emojiDrawer = (EmojiDrawer) findViewById(R.id.emoji_drawer); - unblockButton = (Button) findViewById(R.id.unblock_button); - composePanel = findViewById(R.id.bottom_panel); - composeBubble = findViewById(R.id.compose_bubble); - container = (InputAwareLayout) findViewById(R.id.layout_container); - - quickAttachmentDrawer = (QuickAttachmentDrawer) findViewById(R.id.quick_attachment_drawer); - quickAttachmentToggle = (HidingImageButton) findViewById(R.id.quick_attachment_toggle); + titleView = (ConversationTitleView) getSupportActionBar().getCustomView(); + buttonToggle = ViewUtil.findById(this, R.id.button_toggle); + sendButton = ViewUtil.findById(this, R.id.send_button); + attachButton = ViewUtil.findById(this, R.id.attach_button); + composeText = ViewUtil.findById(this, R.id.embedded_text_editor); + charactersLeft = ViewUtil.findById(this, R.id.space_left); + emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle); + emojiDrawer = ViewUtil.findById(this, R.id.emoji_drawer); + unblockButton = ViewUtil.findById(this, R.id.unblock_button); + composePanel = ViewUtil.findById(this, R.id.bottom_panel); + composeBubble = ViewUtil.findById(this, R.id.compose_bubble); + container = ViewUtil.findById(this, R.id.layout_container); + reminderView = ViewUtil.findById(this, R.id.reminder); + quickAttachmentDrawer = ViewUtil.findById(this, R.id.quick_attachment_drawer); + quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle); container.addOnKeyboardShownListener(this); @@ -944,6 +952,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity titleView.setTitle(recipients); setBlockedUserState(recipients); setActionBarColor(recipients.getColor()); + updateInviteReminder(); } }); } @@ -1441,4 +1450,31 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity updateToggleButtonState(); } + private class ShowInviteReminderTask extends AsyncTask> { + @Override + protected Pair doInBackground(Recipients... recipients) { + if (recipients.length != 1 || recipients[0] == null) throw new AssertionError("task needs exactly one Recipients object"); + + Optional prefs = DatabaseFactory.getRecipientPreferenceDatabase(ConversationActivity.this) + .getRecipientsPreferences(recipients[0].getIds()); + return new Pair<>(recipients[0], prefs.isPresent() && prefs.get().hasSeenInviteReminder()); + } + + @Override + protected void onPostExecute(Pair result) { + if (!result.second && result.first == recipients) { + InviteReminder reminder = new InviteReminder(ConversationActivity.this, result.first); + reminder.setOkListener(new OnClickListener() { + @Override + public void onClick(View v) { + handleInviteLink(); + reminderView.requestDismiss(); + } + }); + reminderView.showReminder(reminder); + } else { + reminderView.hide(); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 007b940c75..65e29ae8ef 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -33,8 +33,6 @@ import android.support.v7.app.AppCompatActivity; import android.support.v7.view.ActionMode; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.RecyclerListener; -import android.support.v7.widget.RecyclerView.ViewHolder; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; @@ -48,12 +46,12 @@ import com.afollestad.materialdialogs.AlertDialogWrapper; import com.melnykov.fab.FloatingActionButton; import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener; -import org.thoughtcrime.securesms.components.DefaultSmsReminder; -import org.thoughtcrime.securesms.components.ExpiredBuildReminder; -import org.thoughtcrime.securesms.components.PushRegistrationReminder; -import org.thoughtcrime.securesms.components.Reminder; -import org.thoughtcrime.securesms.components.ReminderView; -import org.thoughtcrime.securesms.components.SystemSmsImportReminder; +import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder; +import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; +import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder; +import org.thoughtcrime.securesms.components.reminder.Reminder; +import org.thoughtcrime.securesms.components.reminder.ReminderView; +import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.loaders.ConversationListLoader; @@ -137,7 +135,7 @@ public class ConversationListFragment extends Fragment @Override protected Optional doInBackground(Context... params) { final Context context = params[0]; if (ExpiredBuildReminder.isEligible(context)) { - return Optional.of(new ExpiredBuildReminder()); + return Optional.of(new ExpiredBuildReminder(context)); } else if (DefaultSmsReminder.isEligible(context)) { return Optional.of(new DefaultSmsReminder(context)); } else if (SystemSmsImportReminder.isEligible(context)) { diff --git a/src/org/thoughtcrime/securesms/components/ComposeText.java b/src/org/thoughtcrime/securesms/components/ComposeText.java index 08e4624692..ae9b85d175 100644 --- a/src/org/thoughtcrime/securesms/components/ComposeText.java +++ b/src/org/thoughtcrime/securesms/components/ComposeText.java @@ -57,6 +57,7 @@ public class ComposeText extends EmojiEditText { } append(invite); + setSelection(getText().length()); } private boolean isLandscape() { diff --git a/src/org/thoughtcrime/securesms/components/ExpiredBuildReminder.java b/src/org/thoughtcrime/securesms/components/ExpiredBuildReminder.java deleted file mode 100644 index 55ded55da2..0000000000 --- a/src/org/thoughtcrime/securesms/components/ExpiredBuildReminder.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.provider.Telephony; -import android.util.Log; -import android.view.View; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.TextSecureExpiredException; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; - -public class ExpiredBuildReminder extends Reminder { - - private static final String TAG = ExpiredBuildReminder.class.getSimpleName(); - - public ExpiredBuildReminder() { - super(R.drawable.ic_warning_dark, - R.string.reminder_header_expired_build, - R.string.reminder_header_expired_build_details); - } - - @Override - public boolean isDismissable() { - return false; - } - - public static boolean isEligible(Context context) { - return !Util.isBuildFresh(); - } - -} diff --git a/src/org/thoughtcrime/securesms/components/Reminder.java b/src/org/thoughtcrime/securesms/components/Reminder.java deleted file mode 100644 index 090f35d3eb..0000000000 --- a/src/org/thoughtcrime/securesms/components/Reminder.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.content.Context; -import android.view.View.OnClickListener; -import android.view.ViewGroup; - -import org.thoughtcrime.securesms.R; - -public abstract class Reminder { - private int iconResId; - private int titleResId; - private int textResId; - private OnClickListener okListener; - private OnClickListener cancelListener; - - public Reminder(int iconResId, int titleResId, int textResId) { - this.iconResId = iconResId; - this.titleResId = titleResId; - this.textResId = textResId; - } - - public int getIconResId() { - return iconResId; - } - - public int getTitleResId() { - return titleResId; - } - - public int getTextResId() { - return textResId; - } - - public OnClickListener getOkListener() { - return okListener; - } - - public OnClickListener getCancelListener() { - return cancelListener; - } - - public void setOkListener(OnClickListener okListener) { - this.okListener = okListener; - } - - public void setCancelListener(OnClickListener cancelListener) { - this.cancelListener = cancelListener; - } - - public boolean isDismissable() { - return true; - } -} diff --git a/src/org/thoughtcrime/securesms/components/DefaultSmsReminder.java b/src/org/thoughtcrime/securesms/components/reminder/DefaultSmsReminder.java similarity index 78% rename from src/org/thoughtcrime/securesms/components/DefaultSmsReminder.java rename to src/org/thoughtcrime/securesms/components/reminder/DefaultSmsReminder.java index 6a2a0ea8ba..0035a42190 100644 --- a/src/org/thoughtcrime/securesms/components/DefaultSmsReminder.java +++ b/src/org/thoughtcrime/securesms/components/reminder/DefaultSmsReminder.java @@ -1,9 +1,8 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.components.reminder; import android.annotation.TargetApi; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Build.VERSION_CODES; import android.provider.Telephony; import android.view.View; @@ -17,9 +16,9 @@ public class DefaultSmsReminder extends Reminder { @TargetApi(VERSION_CODES.KITKAT) public DefaultSmsReminder(final Context context) { - super(R.drawable.sms_selection_icon, - R.string.reminder_header_sms_default_title, - R.string.reminder_header_sms_default_text); + super(context.getString(R.string.reminder_header_sms_default_title), + context.getString(R.string.reminder_header_sms_default_text), + context.getString(R.string.reminder_header_sms_default_button)); final OnClickListener okListener = new OnClickListener() { @Override @@ -30,14 +29,14 @@ public class DefaultSmsReminder extends Reminder { context.startActivity(intent); } }; - final OnClickListener cancelListener = new OnClickListener() { + final OnClickListener dismissListener = new OnClickListener() { @Override public void onClick(View v) { TextSecurePreferences.setPromptedDefaultSmsProvider(context, true); } }; setOkListener(okListener); - setCancelListener(cancelListener); + setDismissListener(dismissListener); } public static boolean isEligible(Context context) { diff --git a/src/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java b/src/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java new file mode 100644 index 0000000000..f5a5b0fc9c --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.components.reminder; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import android.view.View.OnClickListener; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.Util; + +public class ExpiredBuildReminder extends Reminder { + private static final String TAG = ExpiredBuildReminder.class.getSimpleName(); + + public ExpiredBuildReminder(final Context context) { + super(context.getString(R.string.reminder_header_expired_build), + context.getString(R.string.reminder_header_expired_build_details), + context.getString(R.string.reminder_header_expired_build_button)); + setOkListener(new OnClickListener() { + @Override public void onClick(View v) { + try { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName()))); + } catch (android.content.ActivityNotFoundException anfe) { + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName()))); + } + } + }); + } + + @Override + public boolean isDismissable() { + return false; + } + + public static boolean isEligible(Context context) { + return !Util.isBuildFresh(); + } + +} diff --git a/src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java b/src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java new file mode 100644 index 0000000000..255d944771 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.components.reminder; + +import android.content.Context; +import android.os.AsyncTask; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.View.OnClickListener; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.recipients.Recipients; + +public class InviteReminder extends Reminder { + + public InviteReminder(final @NonNull Context context, + final @NonNull Recipients recipients) + { + super(context.getString(R.string.reminder_header_invite_title), + context.getString(R.string.reminder_header_invite_text, recipients.toShortString()), + context.getString(R.string.reminder_header_invite_button)); + + setDismissListener(new OnClickListener() { + @Override public void onClick(View v) { + new AsyncTask() { + + @Override protected Void doInBackground(Void... params) { + DatabaseFactory.getRecipientPreferenceDatabase(context).setSeenInviteReminder(recipients, true); + return null; + } + }.execute(); + } + }); + } +} diff --git a/src/org/thoughtcrime/securesms/components/PushRegistrationReminder.java b/src/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java similarity index 80% rename from src/org/thoughtcrime/securesms/components/PushRegistrationReminder.java rename to src/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java index 6fbb3ef359..12843f4b1f 100644 --- a/src/org/thoughtcrime/securesms/components/PushRegistrationReminder.java +++ b/src/org/thoughtcrime/securesms/components/reminder/PushRegistrationReminder.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.components.reminder; import android.content.Context; import android.content.Intent; @@ -13,9 +13,9 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; public class PushRegistrationReminder extends Reminder { public PushRegistrationReminder(final Context context, final MasterSecret masterSecret) { - super(R.drawable.ic_push_registration_reminder, - R.string.reminder_header_push_title, - R.string.reminder_header_push_text); + super(context.getString(R.string.reminder_header_push_title), + context.getString(R.string.reminder_header_push_text), + context.getString(R.string.reminder_header_push_button)); final OnClickListener okListener = new OnClickListener() { @Override diff --git a/src/org/thoughtcrime/securesms/components/reminder/Reminder.java b/src/org/thoughtcrime/securesms/components/reminder/Reminder.java new file mode 100644 index 0000000000..652a59cecf --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/reminder/Reminder.java @@ -0,0 +1,56 @@ +package org.thoughtcrime.securesms.components.reminder; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; +import android.view.View.OnClickListener; + +public abstract class Reminder { + private CharSequence buttonText; + private CharSequence title; + private CharSequence text; + + private OnClickListener okListener; + private OnClickListener dismissListener; + + public Reminder(@NonNull CharSequence title, + @NonNull CharSequence text, + @NonNull CharSequence buttonText) + { + this.title = title; + this.text = text; + this.buttonText = buttonText; + } + + public CharSequence getTitle() { + return title; + } + + public CharSequence getText() { + return text; + } + + public CharSequence getButtonText() { + return buttonText; + } + + public OnClickListener getOkListener() { + return okListener; + } + + public OnClickListener getDismissListener() { + return dismissListener; + } + + public void setOkListener(OnClickListener okListener) { + this.okListener = okListener; + } + + public void setDismissListener(OnClickListener dismissListener) { + this.dismissListener = dismissListener; + } + + public boolean isDismissable() { + return true; + } +} diff --git a/src/org/thoughtcrime/securesms/components/ReminderView.java b/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java similarity index 59% rename from src/org/thoughtcrime/securesms/components/ReminderView.java rename to src/org/thoughtcrime/securesms/components/reminder/ReminderView.java index d543d55694..3657f2d0ed 100644 --- a/src/org/thoughtcrime/securesms/components/ReminderView.java +++ b/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.components.reminder; import android.annotation.TargetApi; import android.content.Context; @@ -7,23 +7,21 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.ViewUtil; /** * View to display actionable reminders to the user */ public class ReminderView extends LinearLayout { private ViewGroup container; - private ImageButton cancel; + private TextView acceptButton; + private TextView closeButton; private TextView title; private TextView text; - private ImageView icon; public ReminderView(Context context) { super(context); @@ -43,35 +41,39 @@ public class ReminderView extends LinearLayout { private void initialize() { LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true); - container = (ViewGroup ) findViewById(R.id.container); - cancel = (ImageButton) findViewById(R.id.cancel); - title = (TextView ) findViewById(R.id.reminder_title); - text = (TextView ) findViewById(R.id.reminder_text); - icon = (ImageView ) findViewById(R.id.icon); + container = ViewUtil.findById(this, R.id.container); + acceptButton = ViewUtil.findById(this, R.id.accept); + closeButton = ViewUtil.findById(this, R.id.cancel); + title = ViewUtil.findById(this, R.id.reminder_title); + text = ViewUtil.findById(this, R.id.reminder_text); } public void showReminder(final Reminder reminder) { - icon.setImageResource(reminder.getIconResId()); - title.setText(reminder.getTitleResId()); - text.setText(reminder.getTextResId()); + title.setText(reminder.getTitle()); + text.setText(reminder.getText()); + acceptButton.setText(reminder.getButtonText()); - this.setOnClickListener(reminder.getOkListener()); + acceptButton.setOnClickListener(reminder.getOkListener()); if (reminder.isDismissable()) { - cancel.setOnClickListener(new OnClickListener() { + closeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { hide(); - if (reminder.getCancelListener() != null) reminder.getCancelListener().onClick(v); + if (reminder.getDismissListener() != null) reminder.getDismissListener().onClick(v); } }); } else { - cancel.setVisibility(View.GONE); + closeButton.setVisibility(View.GONE); } container.setVisibility(View.VISIBLE); } + public void requestDismiss() { + closeButton.performClick(); + } + public void hide() { container.setVisibility(View.GONE); } diff --git a/src/org/thoughtcrime/securesms/components/SystemSmsImportReminder.java b/src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java similarity index 84% rename from src/org/thoughtcrime/securesms/components/SystemSmsImportReminder.java rename to src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java index 6377162141..56e5d40d49 100644 --- a/src/org/thoughtcrime/securesms/components/SystemSmsImportReminder.java +++ b/src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.components; +package org.thoughtcrime.securesms.components.reminder; import android.content.Context; import android.content.Intent; @@ -14,9 +14,9 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; public class SystemSmsImportReminder extends Reminder { public SystemSmsImportReminder(final Context context, final MasterSecret masterSecret) { - super(R.drawable.sms_system_import_icon, - R.string.reminder_header_sms_import_title, - R.string.reminder_header_sms_import_text); + super(context.getString(R.string.reminder_header_sms_import_title), + context.getString(R.string.reminder_header_sms_import_text), + context.getString(R.string.reminder_header_sms_import_button)); final OnClickListener okListener = new OnClickListener() { @Override @@ -42,7 +42,7 @@ public class SystemSmsImportReminder extends Reminder { } }; setOkListener(okListener); - setCancelListener(cancelListener); + setDismissListener(cancelListener); } public static boolean isEligible(Context context) { diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index d160c7243b..66a46ef716 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -66,7 +66,8 @@ public class DatabaseFactory { private static final int INTRODUCED_ENVELOPE_CONTENT_VERSION = 19; private static final int INTRODUCED_COLOR_PREFERENCE_VERSION = 20; private static final int INTRODUCED_DB_OPTIMIZATIONS_VERSION = 21; - private static final int DATABASE_VERSION = 21; + private static final int INTRODUCED_INVITE_REMINDERS_VERSION = 22; + private static final int DATABASE_VERSION = 22; private static final String DATABASE_NAME = "messages.db"; private static final Object lock = new Object(); @@ -768,6 +769,10 @@ public class DatabaseFactory { db.execSQL("CREATE INDEX IF NOT EXISTS mms_thread_date_index ON mms (thread_id, date_received);"); } + if (oldVersion < INTRODUCED_INVITE_REMINDERS_VERSION) { + db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN seen_invite_reminder INTEGER DEFAULT 0"); + } + db.setTransactionSuccessful(); db.endTransaction(); } diff --git a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java index 19f2630131..02f21f0f37 100644 --- a/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java +++ b/src/org/thoughtcrime/securesms/database/RecipientPreferenceDatabase.java @@ -23,14 +23,15 @@ public class RecipientPreferenceDatabase extends Database { private static final String TAG = RecipientPreferenceDatabase.class.getSimpleName(); private static final String RECIPIENT_PREFERENCES_URI = "content://textsecure/recipients/"; - private static final String TABLE_NAME = "recipient_preferences"; - private static final String ID = "_id"; - private static final String RECIPIENT_IDS = "recipient_ids"; - private static final String BLOCK = "block"; - private static final String NOTIFICATION = "notification"; - private static final String VIBRATE = "vibrate"; - private static final String MUTE_UNTIL = "mute_until"; - private static final String COLOR = "color"; + private static final String TABLE_NAME = "recipient_preferences"; + private static final String ID = "_id"; + private static final String RECIPIENT_IDS = "recipient_ids"; + private static final String BLOCK = "block"; + private static final String NOTIFICATION = "notification"; + private static final String VIBRATE = "vibrate"; + private static final String MUTE_UNTIL = "mute_until"; + private static final String COLOR = "color"; + private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder"; public enum VibrateState { DEFAULT(0), ENABLED(1), DISABLED(2); @@ -58,7 +59,8 @@ public class RecipientPreferenceDatabase extends Database { NOTIFICATION + " TEXT DEFAULT NULL, " + VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " + MUTE_UNTIL + " INTEGER DEFAULT 0, " + - COLOR + " TEXT DEFAULT NULL);"; + COLOR + " TEXT DEFAULT NULL, " + + SEEN_INVITE_REMINDER + " INTEGER DEFAULT 0);"; public RecipientPreferenceDatabase(Context context, SQLiteOpenHelper databaseHelper) { super(context, databaseHelper); @@ -86,12 +88,13 @@ public class RecipientPreferenceDatabase extends Database { null, null, null); if (cursor != null && cursor.moveToNext()) { - boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; - String notification = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION)); - int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); - long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); - String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); - Uri notificationUri = notification == null ? null : Uri.parse(notification); + boolean blocked = cursor.getInt(cursor.getColumnIndexOrThrow(BLOCK)) == 1; + String notification = cursor.getString(cursor.getColumnIndexOrThrow(NOTIFICATION)); + int vibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE)); + long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL)); + String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR)); + Uri notificationUri = notification == null ? null : Uri.parse(notification); + boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1; MaterialColor color; @@ -106,7 +109,7 @@ public class RecipientPreferenceDatabase extends Database { return Optional.of(new RecipientsPreferences(blocked, muteUntil, VibrateState.fromId(vibrateState), - notificationUri, color)); + notificationUri, color, seenInviteReminder)); } return Optional.absent(); @@ -146,6 +149,12 @@ public class RecipientPreferenceDatabase extends Database { updateOrInsert(recipients, values); } + public void setSeenInviteReminder(Recipients recipients, boolean seen) { + ContentValues values = new ContentValues(1); + values.put(SEEN_INVITE_REMINDER, seen ? 1 : 0); + updateOrInsert(recipients, values); + } + private void updateOrInsert(Recipients recipients, ContentValues contentValues) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); @@ -171,17 +180,20 @@ public class RecipientPreferenceDatabase extends Database { private final VibrateState vibrateState; private final Uri notification; private final MaterialColor color; + private final boolean seenInviteReminder; public RecipientsPreferences(boolean blocked, long muteUntil, @NonNull VibrateState vibrateState, @Nullable Uri notification, - @Nullable MaterialColor color) + @Nullable MaterialColor color, + boolean seenInviteReminder) { - this.blocked = blocked; - this.muteUntil = muteUntil; - this.vibrateState = vibrateState; - this.notification = notification; - this.color = color; + this.blocked = blocked; + this.muteUntil = muteUntil; + this.vibrateState = vibrateState; + this.notification = notification; + this.color = color; + this.seenInviteReminder = seenInviteReminder; } public @Nullable MaterialColor getColor() { @@ -203,5 +215,9 @@ public class RecipientPreferenceDatabase extends Database { public @Nullable Uri getRingtone() { return notification; } + + public boolean hasSeenInviteReminder() { + return seenInviteReminder; + } } } diff --git a/src/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java b/src/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java new file mode 100644 index 0000000000..1bd4e81248 --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/concurrent/AssertedSuccessListener.java @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.util.concurrent; + +import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; + +import java.util.concurrent.ExecutionException; + +public abstract class AssertedSuccessListener implements Listener { + @Override + public void onFailure(ExecutionException e) { + throw new AssertionError(e); + } +} diff --git a/test/androidTestEspresso/java/org/thoughtcrime/securesms/ConversationListActivityTest.java b/test/androidTestEspresso/java/org/thoughtcrime/securesms/ConversationListActivityTest.java index 292f3e2051..65d94888eb 100644 --- a/test/androidTestEspresso/java/org/thoughtcrime/securesms/ConversationListActivityTest.java +++ b/test/androidTestEspresso/java/org/thoughtcrime/securesms/ConversationListActivityTest.java @@ -23,10 +23,10 @@ import android.widget.TextView; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; -import org.thoughtcrime.securesms.components.DefaultSmsReminder; -import org.thoughtcrime.securesms.components.ExpiredBuildReminder; -import org.thoughtcrime.securesms.components.PushRegistrationReminder; -import org.thoughtcrime.securesms.components.SystemSmsImportReminder; +import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder; +import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; +import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder; +import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.util.TextSecurePreferences; import static android.support.test.espresso.Espresso.onView;