From 2c17b54ef999ec75d9fb099f649dc65ae8cf1fed Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 11 Jun 2018 09:37:01 -0700 Subject: [PATCH] Show a banner in the event of a service outage. We will now determine if there has been a service outage and render a banner at the top of the conversation list if we detect that there has been one. --- build.gradle | 1 + res/drawable/reminder_background_error.xml | 6 ++ ...und.xml => reminder_background_normal.xml} | 0 res/layout/profile_group_share_view.xml | 2 +- res/layout/reminder_header.xml | 2 +- res/layout/unverified_banner_view.xml | 2 +- res/values/colors.xml | 3 + res/values/strings.xml | 1 + .../securesms/ConversationActivity.java | 7 +- .../securesms/ConversationListFragment.java | 7 +- .../components/reminder/Reminder.java | 20 +++-- .../components/reminder/ReminderView.java | 11 ++- .../reminder/ServiceOutageReminder.java | 30 +++++++ .../securesms/jobs/PushGroupSendJob.java | 2 + .../securesms/jobs/PushMediaSendJob.java | 2 + .../securesms/jobs/PushSendJob.java | 5 ++ .../securesms/jobs/PushTextSendJob.java | 2 + .../jobs/ServiceOutageDetectionJob.java | 78 +++++++++++++++++++ .../securesms/util/TextSecurePreferences.java | 20 +++++ 19 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 res/drawable/reminder_background_error.xml rename res/drawable/{reminder_background.xml => reminder_background_normal.xml} (100%) create mode 100644 src/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java create mode 100644 src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java diff --git a/build.gradle b/build.gradle index f0ec8c4e37..18f96273c4 100644 --- a/build.gradle +++ b/build.gradle @@ -270,6 +270,7 @@ android { buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" + buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "GIPHY_PROXY_HOST", "\"giphy-proxy-production.whispersystems.org\"" buildConfigField "int", "GIPHY_PROXY_PORT", "80" buildConfigField "String", "USER_AGENT", "\"OWA\"" diff --git a/res/drawable/reminder_background_error.xml b/res/drawable/reminder_background_error.xml new file mode 100644 index 0000000000..c5e5cd817f --- /dev/null +++ b/res/drawable/reminder_background_error.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/drawable/reminder_background.xml b/res/drawable/reminder_background_normal.xml similarity index 100% rename from res/drawable/reminder_background.xml rename to res/drawable/reminder_background_normal.xml diff --git a/res/layout/profile_group_share_view.xml b/res/layout/profile_group_share_view.xml index 863e7cc2a1..67560cd7fc 100644 --- a/res/layout/profile_group_share_view.xml +++ b/res/layout/profile_group_share_view.xml @@ -5,7 +5,7 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/reminder_background" + android:background="@drawable/reminder_background_normal" android:focusable="true" android:orientation="horizontal" tools:visibility="visible"> diff --git a/res/layout/reminder_header.xml b/res/layout/reminder_header.xml index a56e8837bd..8ec5032e50 100644 --- a/res/layout/reminder_header.xml +++ b/res/layout/reminder_header.xml @@ -5,7 +5,7 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/reminder_background" + android:background="@drawable/reminder_background_normal" android:focusable="true" android:nextFocusRight="@+id/cancel" android:orientation="horizontal" diff --git a/res/layout/unverified_banner_view.xml b/res/layout/unverified_banner_view.xml index d6a6ab318d..846f821c6a 100644 --- a/res/layout/unverified_banner_view.xml +++ b/res/layout/unverified_banner_view.xml @@ -5,7 +5,7 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/reminder_background" + android:background="@drawable/reminder_background_normal" android:focusable="true" android:nextFocusRight="@+id/cancel" android:orientation="horizontal" diff --git a/res/values/colors.xml b/res/values/colors.xml index 5cd5e555d7..2430179867 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -58,4 +58,7 @@ #00FFFFFF #88000000 + + #f44336 + #ef5350 diff --git a/res/values/strings.xml b/res/values/strings.xml index 763c38a404..a6f2536b3a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1361,6 +1361,7 @@ Take your conversation with %1$s to the next level. Invite your friends! The more friends use Signal, the better it gets. + Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible. Save diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index e91c03fbc3..bc132643e9 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder; import org.thoughtcrime.securesms.components.reminder.InviteReminder; import org.thoughtcrime.securesms.components.reminder.ReminderView; +import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; @@ -121,6 +122,7 @@ import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; +import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AudioSlide; @@ -1115,12 +1117,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } protected void updateReminders(boolean seenInvite) { - Log.w(TAG, "updateReminders(" + seenInvite+")"); + Log.w(TAG, "updateReminders(" + seenInvite + ")"); if (UnauthorizedReminder.isEligible(this)) { reminderView.get().showReminder(new UnauthorizedReminder(this)); } else if (ExpiredBuildReminder.isEligible()) { reminderView.get().showReminder(new ExpiredBuildReminder(this)); + } else if (ServiceOutageReminder.isEligible(this)) { + ApplicationContext.getInstance(this).getJobManager().add(new ServiceOutageDetectionJob(this)); + reminderView.get().showReminder(new ServiceOutageReminder(this)); } else if (TextSecurePreferences.isPushRegistered(this) && TextSecurePreferences.isShowInviteReminders(this) && !isSecureText && diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java index 531db2f936..4e3c1b5d1f 100644 --- a/src/org/thoughtcrime/securesms/ConversationListFragment.java +++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java @@ -66,14 +66,15 @@ import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder; 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.ServiceOutageReminder; import org.thoughtcrime.securesms.components.reminder.ShareReminder; import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder; import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.loaders.ConversationListLoader; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; +import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; @@ -81,6 +82,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask; +import org.whispersystems.jobqueue.JobManager; import org.whispersystems.libsignal.util.guava.Optional; import java.util.HashSet; @@ -190,6 +192,9 @@ public class ConversationListFragment extends Fragment return Optional.of(new UnauthorizedReminder(context)); } else if (ExpiredBuildReminder.isEligible()) { return Optional.of(new ExpiredBuildReminder(context)); + } else if (ServiceOutageReminder.isEligible(context)) { + ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob(context)); + return Optional.of(new ServiceOutageReminder(context)); } else if (OutdatedBuildReminder.isEligible()) { return Optional.of(new OutdatedBuildReminder(context)); } else if (DefaultSmsReminder.isEligible(context)) { diff --git a/src/org/thoughtcrime/securesms/components/reminder/Reminder.java b/src/org/thoughtcrime/securesms/components/reminder/Reminder.java index 4812007d51..9674197f9e 100644 --- a/src/org/thoughtcrime/securesms/components/reminder/Reminder.java +++ b/src/org/thoughtcrime/securesms/components/reminder/Reminder.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.components.reminder; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.view.View.OnClickListener; public abstract class Reminder { @@ -10,14 +11,14 @@ public abstract class Reminder { private OnClickListener okListener; private OnClickListener dismissListener; - public Reminder(@NonNull CharSequence title, - @NonNull CharSequence text) + public Reminder(@Nullable CharSequence title, + @NonNull CharSequence text) { - this.title = title; - this.text = text; + this.title = title; + this.text = text; } - public CharSequence getTitle() { + public @Nullable CharSequence getTitle() { return title; } @@ -44,4 +45,13 @@ public abstract class Reminder { public boolean isDismissable() { return true; } + + public @NonNull Importance getImportance() { + return Importance.NORMAL; + } + + + public enum Importance { + NORMAL, ERROR + } } diff --git a/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java b/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java index 2faadb44fb..18e29b3745 100644 --- a/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java +++ b/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.reminder; import android.annotation.TargetApi; import android.content.Context; import android.os.Build.VERSION_CODES; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -49,8 +50,16 @@ public class ReminderView extends LinearLayout { } public void showReminder(final Reminder reminder) { - title.setText(reminder.getTitle()); + if (!TextUtils.isEmpty(reminder.getTitle())) { + title.setText(reminder.getTitle()); + title.setVisibility(VISIBLE); + } else { + title.setText(""); + title.setVisibility(GONE); + } text.setText(reminder.getText()); + container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error + : R.drawable.reminder_background_normal); setOnClickListener(reminder.getOkListener()); diff --git a/src/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java b/src/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java new file mode 100644 index 0000000000..7ca091d97a --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/reminder/ServiceOutageReminder.java @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.components.reminder; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +public class ServiceOutageReminder extends Reminder { + + public ServiceOutageReminder(@NonNull Context context) { + super(null, + context.getString(R.string.reminder_header_service_outage_text)); + } + + public static boolean isEligible(@NonNull Context context) { + return TextSecurePreferences.getServiceOutage(context); + } + + @Override + public boolean isDismissable() { + return false; + } + + @NonNull + @Override + public Importance getImportance() { + return Importance.ERROR; + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 0b02c20f78..56f2ea0760 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -139,6 +139,8 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { @Override public void onCanceled() { + super.onCanceled(); + DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java index 51ed31232f..eb1fae63cb 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushMediaSendJob.java @@ -94,6 +94,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType { @Override public void onCanceled() { + super.onCanceled(); + DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId); notifyMediaMessageDeliveryFailed(context, messageId); } diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index 959ca038b4..4642768819 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -71,6 +71,11 @@ public abstract class PushSendJob extends SendJob { onPushSend(); } + @Override + public void onCanceled() { + ApplicationContext.getInstance(context).getJobManager().add(new ServiceOutageDetectionJob(context)); + } + protected Optional getProfileKey(@NonNull Recipient recipient) { if (!recipient.resolve().isSystemContact() && !recipient.resolve().isProfileSharing()) { return Optional.absent(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 58b3967c43..9efc8895f8 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -83,6 +83,8 @@ public class PushTextSendJob extends PushSendJob implements InjectableType { @Override public void onCanceled() { + super.onCanceled(); + DatabaseFactory.getSmsDatabase(context).markAsSentFailed(messageId); long threadId = DatabaseFactory.getSmsDatabase(context).getThreadIdForMessage(messageId); diff --git a/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java b/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java new file mode 100644 index 0000000000..a818f65d79 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/ServiceOutageDetectionJob.java @@ -0,0 +1,78 @@ +package org.thoughtcrime.securesms.jobs; + +import android.content.Context; +import android.util.Log; + +import org.greenrobot.eventbus.EventBus; +import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.events.ReminderUpdateEvent; +import org.thoughtcrime.securesms.transport.RetryLaterException; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class ServiceOutageDetectionJob extends ContextJob { + + private static final String TAG = ServiceOutageDetectionJob.class.getSimpleName(); + + private static final String IP_SUCCESS = "127.0.0.1"; + private static final String IP_FAILURE = "127.0.0.2"; + private static final long CHECK_TIME = 1000 * 60; + + public ServiceOutageDetectionJob(Context context) { + super(context, new JobParameters.Builder() + .withGroupId(ServiceOutageDetectionJob.class.getSimpleName()) + .withRequirement(new NetworkRequirement(context)) + .withRetryCount(5) + .create()); + } + + @Override + public void onAdded() { + } + + @Override + public void onRun() throws RetryLaterException { + long timeSinceLastCheck = System.currentTimeMillis() - TextSecurePreferences.getLastOutageCheckTime(context); + if (timeSinceLastCheck < CHECK_TIME) { + Log.w(TAG, "Skipping service outage check. Too soon."); + return; + } + + try { + InetAddress address = InetAddress.getByName(BuildConfig.SIGNAL_SERVICE_STATUS_URL); + + if (IP_SUCCESS.equals(address.getHostAddress())) { + Log.w(TAG, "Service is available."); + TextSecurePreferences.setServiceOutage(context, false); + } else if (IP_FAILURE.equals(address.getHostAddress())) { + Log.w(TAG, "Service is down."); + TextSecurePreferences.setServiceOutage(context, true); + } else { + Log.w(TAG, "Service status check returned an unrecognized IP address. Assuming outage."); + TextSecurePreferences.setServiceOutage(context, true); + } + + TextSecurePreferences.setLastOutageCheckTime(context, System.currentTimeMillis()); + EventBus.getDefault().post(new ReminderUpdateEvent()); + } catch (UnknownHostException e) { + throw new RetryLaterException(e); + } + } + + @Override + public boolean onShouldRetry(Exception e) { + return e instanceof RetryLaterException; + } + + @Override + public void onCanceled() { + Log.w(TAG, "Service status check could not complete. Assuming success to avoid false positives due to bad network."); + TextSecurePreferences.setServiceOutage(context, false); + TextSecurePreferences.setLastOutageCheckTime(context, System.currentTimeMillis()); + EventBus.getDefault().post(new ReminderUpdateEvent()); + } +} diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index a0068abb01..511f1e8836 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -15,6 +15,7 @@ import android.util.Log; import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirementProvider; import org.thoughtcrime.securesms.lock.RegistrationLockReminders; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; @@ -150,6 +151,9 @@ public class TextSecurePreferences { private static final String REGISTRATION_LOCK_LAST_REMINDER_TIME = "pref_registration_lock_last_reminder_time"; private static final String REGISTRATION_LOCK_NEXT_REMINDER_INTERVAL = "pref_registration_lock_next_reminder_interval"; + private static final String SERVICE_OUTAGE = "pref_service_outage"; + private static final String LAST_OUTAGE_CHECK_TIME = "pref_last_outage_check_time"; + public static boolean isScreenLockEnabled(@NonNull Context context) { return getBooleanPreference(context, SCREEN_LOCK, false); } @@ -903,6 +907,22 @@ public class TextSecurePreferences { new HashSet<>(Arrays.asList(context.getResources().getStringArray(defaultValuesRes)))); } + public static void setLastOutageCheckTime(Context context, long timestamp) { + setLongPreference(context, LAST_OUTAGE_CHECK_TIME, timestamp); + } + + public static long getLastOutageCheckTime(Context context) { + return getLongPreference(context, LAST_OUTAGE_CHECK_TIME, 0); + } + + public static void setServiceOutage(Context context, boolean isOutage) { + setBooleanPreference(context, SERVICE_OUTAGE, isOutage); + } + + public static boolean getServiceOutage(Context context) { + return getBooleanPreference(context, SERVICE_OUTAGE, false); + } + public static void setBooleanPreference(Context context, String key, boolean value) { PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply(); }