From 2784285d47ab2790c1998519f2f4f92d09bb9a94 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 8 Sep 2020 18:03:56 -0400 Subject: [PATCH 01/13] Add support for fetching remote deprecation. --- .../securesms/ApplicationContext.java | 10 +++ .../reminder/ExpiredBuildReminder.java | 4 +- .../reminder/OutdatedBuildReminder.java | 9 +- .../thoughtcrime/securesms/jobs/SendJob.java | 5 +- .../keyvalue/MiscellaneousValues.java | 9 ++ .../linkpreview/LinkPreviewUtil.java | 23 +---- ...DeprecatedClientPreventionInterceptor.java | 41 +++++++++ .../RemoteDeprecationDetectorInterceptor.java | 31 +++++++ .../net/StandardUserAgentInterceptor.java | 2 +- .../push/SignalServiceNetworkAccess.java | 5 +- .../securesms/util/DateUtils.java | 34 +++++++ .../securesms/util/FeatureFlags.java | 31 +++++-- .../securesms/util/RemoteDeprecation.java | 89 +++++++++++++++++++ .../securesms/util/SemanticVersion.java | 70 +++++++++++++++ .../org/thoughtcrime/securesms/util/Util.java | 25 +++++- .../securesms/testutil/EmptyLogger.java | 26 ++++++ ...xpirationTest_getTimeUntilDeprecation.java | 88 ++++++++++++++++++ .../util/SemanticVersionTest_compareTo.java | 46 ++++++++++ .../util/SemanticVersionTest_parse.java | 41 +++++++++ .../DeprecatedVersionException.java | 4 + .../internal/push/PushServiceSocket.java | 5 ++ 21 files changed, 559 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/RemoteExpirationTest_getTimeUntilDeprecation.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_compareTo.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_parse.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/DeprecatedVersionException.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index b6b7ffa05..fbb8a4e80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.Log; @@ -70,6 +71,7 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.webrtc.voiceengine.WebRtcAudioManager; @@ -157,6 +159,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi KeyCachingService.onAppForegrounded(this); ApplicationDependencies.getFrameRateTracker().begin(); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); + checkBuildExpiration(); } @Override @@ -192,6 +195,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi return persistentLogger; } + public void checkBuildExpiration() { + if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Build expired!"); + SignalStore.misc().markDeprecated(); + } + } + private void initializeSecurityProvider() { try { Class.forName("org.signal.aesgcmprovider.AesGcmCipher"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java index b418155ed..5a3b911d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java @@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.components.reminder; import android.content.Context; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.PlayStoreUtil; -import org.thoughtcrime.securesms.util.Util; public class ExpiredBuildReminder extends Reminder { @@ -20,7 +20,7 @@ public class ExpiredBuildReminder extends Reminder { } public static boolean isEligible() { - return Util.getDaysTillBuildExpiry() <= 0; + return SignalStore.misc().isClientDeprecated(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java index 28477d003..3bcbecb5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java @@ -6,6 +6,8 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.Util; +import java.util.concurrent.TimeUnit; + public class OutdatedBuildReminder extends Reminder { public OutdatedBuildReminder(final Context context) { @@ -15,7 +17,7 @@ public class OutdatedBuildReminder extends Reminder { } private static CharSequence getPluralsText(final Context context) { - int days = Util.getDaysTillBuildExpiry() - 1; + int days = getDaysUntilExpiry() - 1; if (days == 0) { return context.getString(R.string.reminder_header_outdated_build_details_today); } @@ -28,7 +30,10 @@ public class OutdatedBuildReminder extends Reminder { } public static boolean isEligible() { - return Util.getDaysTillBuildExpiry() <= 10; + return getDaysUntilExpiry() <= 10; } + private static int getDaysUntilExpiry() { + return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry()); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java index ae773ef7e..1ce3aee3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendJob.java @@ -8,9 +8,8 @@ import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.MediaConstraints; -import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.Util; import java.util.List; @@ -26,7 +25,7 @@ public abstract class SendJob extends BaseJob { @Override public final void onRun() throws Exception { - if (Util.getDaysTillBuildExpiry() <= 0) { + if (SignalStore.misc().isClientDeprecated()) { throw new TextSecureExpiredException(String.format("TextSecure expired (build %d, now %d)", BuildConfig.BUILD_TIMESTAMP, System.currentTimeMillis())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index 58df4c97d..975a114ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -8,6 +8,7 @@ public final class MiscellaneousValues extends SignalStoreValues { private static final String MESSAGE_REQUEST_ENABLE_TIME = "message_request_enable_time"; private static final String LAST_PROFILE_REFRESH_TIME = "misc.last_profile_refresh_time"; private static final String USERNAME_SHOW_REMINDER = "username.show.reminder"; + private static final String CLIENT_DEPRECATED = "misc.client_deprecated"; MiscellaneousValues(@NonNull KeyValueStore store) { super(store); @@ -45,4 +46,12 @@ public final class MiscellaneousValues extends SignalStoreValues { public boolean shouldShowUsernameReminder() { return getBoolean(USERNAME_SHOW_REMINDER, true); } + + public boolean isClientDeprecated() { + return getBoolean(CLIENT_DEPRECATED, false); + } + + public void markDeprecated() { + putBoolean(CLIENT_DEPRECATED, true); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java index 57274988b..4b47c4cf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/linkpreview/LinkPreviewUtil.java @@ -17,6 +17,7 @@ import com.google.android.collect.Sets; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.stickers.StickerUrl; +import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.util.OptionalUtil; @@ -203,18 +204,11 @@ public final class LinkPreviewUtil { @SuppressLint("ObsoleteSdkInt") public long getDate() { - SimpleDateFormat format; - if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); - } else { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); - } - return Stream.of(values.get(KEY_PUBLISHED_TIME_1), values.get(KEY_PUBLISHED_TIME_2), values.get(KEY_MODIFIED_TIME_1), values.get(KEY_MODIFIED_TIME_2)) - .map(dateString -> parseDate(format, dateString)) + .map(DateUtils::parseIso8601) .filter(time -> time > 0) .findFirst() .orElse(0L); @@ -223,19 +217,6 @@ public final class LinkPreviewUtil { public @NonNull Optional getDescription() { return OptionalUtil.absentIfEmpty(values.get(KEY_DESCRIPTION_URL)); } - - private static long parseDate(DateFormat dateFormat, String dateString) { - if (Util.isEmpty(dateString)) { - return 0; - } - - try { - return dateFormat.parse(dateString).getTime(); - } catch (ParseException e) { - Log.w(TAG, "Failed to parse date.", e); - return 0; - } - } } public interface HtmlDecoder { diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java new file mode 100644 index 000000000..cadf5427b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/net/DeprecatedClientPreventionInterceptor.java @@ -0,0 +1,41 @@ +package org.thoughtcrime.securesms.net; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; +import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * Disallows network requests when your client has been deprecated. When the client is deprecated, + * we simply fake a 499 response. + */ +public final class DeprecatedClientPreventionInterceptor implements Interceptor { + + private static final String TAG = Log.tag(DeprecatedClientPreventionInterceptor.class); + + @Override + public @NonNull Response intercept(@NonNull Chain chain) throws IOException { + if (SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Preventing request because client is deprecated."); + return new Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_1_1) + .receivedResponseAtMillis(System.currentTimeMillis()) + .message("") + .body(ResponseBody.create(null, "")) + .code(499) + .build(); + } else { + return chain.proceed(chain.request()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java new file mode 100644 index 000000000..80258eb81 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.net; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * Marks the client as remotely-deprecated when it receives a 499 response. + */ +public final class RemoteDeprecationDetectorInterceptor implements Interceptor { + + private static final String TAG = Log.tag(RemoteDeprecationDetectorInterceptor.class); + + @Override + public @NonNull Response intercept(@NonNull Chain chain) throws IOException { + Response response = chain.proceed(chain.request()); + + if (response.code() == 499 && !SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Received 499. Client version is deprecated."); + SignalStore.misc().markDeprecated(); + } + + return response; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java index 0150b46a9..7b17fa1a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/net/StandardUserAgentInterceptor.java @@ -10,6 +10,6 @@ import org.thoughtcrime.securesms.BuildConfig; public class StandardUserAgentInterceptor extends UserAgentInterceptor { public StandardUserAgentInterceptor() { - super("Signal-Android " + BuildConfig.VERSION_NAME + " (API " + Build.VERSION.SDK_INT + ")"); + super("Signal-Android/" + BuildConfig.VERSION_NAME + " Android/" + Build.VERSION.SDK_INT); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java index d9d4e2146..6942dce11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java @@ -7,6 +7,8 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.net.CustomDns; +import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor; +import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor; import org.thoughtcrime.securesms.net.SequentialDns; import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; import org.thoughtcrime.securesms.util.Base64; @@ -21,6 +23,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -161,7 +164,7 @@ public class SignalServiceNetworkAccess { final SignalStorageUrl omanGoogleStorage = new SignalStorageUrl("https://www.google.com.om/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final List interceptors = Collections.singletonList(new StandardUserAgentInterceptor()); + final List interceptors = Arrays.asList(new StandardUserAgentInterceptor(), new RemoteDeprecationDetectorInterceptor(), new DeprecatedClientPreventionInterceptor()); final Optional dns = Optional.of(DNS); final byte[] zkGroupServerPublicParams; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index 3e193f302..5e355866a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -16,13 +16,18 @@ */ package org.thoughtcrime.securesms.util; +import android.annotation.SuppressLint; import android.content.Context; +import android.os.Build; import android.text.format.DateFormat; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logging.Log; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -170,4 +175,33 @@ public class DateUtils extends android.text.format.DateUtils { private static String getLocalizedPattern(String template, Locale locale) { return DateFormat.getBestDateTimePattern(locale, template); } + + /** + * e.g. 2020-09-04T19:17:51Z + * https://www.iso.org/iso-8601-date-and-time-format.html + * + * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. + * + * @return The timestamp if able to be parsed, otherwise -1. + */ + @SuppressLint("ObsoleteSdkInt") + public static long parseIso8601(@Nullable String date) { + SimpleDateFormat format; + if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); + } else { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + } + + if (Util.isEmpty(date)) { + return -1; + } + + try { + return format.parse(date).getTime(); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse date.", e); + return -1; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index 792d47412..6f2dc9e70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -64,6 +64,7 @@ public final class FeatureFlags { private static final String MENTIONS = "android.mentions"; private static final String VERIFY_V2 = "android.verifyV2"; private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion"; + private static final String CLIENT_EXPIRATION = "android.clientExpiration"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -82,7 +83,8 @@ public final class FeatureFlags { INTERNAL_USER, USERNAMES, MENTIONS, - VERIFY_V2 + VERIFY_V2, + CLIENT_EXPIRATION ); /** @@ -107,7 +109,8 @@ public final class FeatureFlags { GROUPS_V2_CREATE_VERSION, GROUPS_V2_JOIN_VERSION, VERIFY_V2, - CDS_VERSION + CDS_VERSION, + CLIENT_EXPIRATION ); /** @@ -280,6 +283,11 @@ public final class FeatureFlags { return getBoolean(VERIFY_V2, false); } + /** The raw client expiration JSON string. */ + public static String clientExpiration() { + return getString(CLIENT_EXPIRATION, null); + } + /** * Whether the user can choose phone number privacy settings, and; * Whether to fetch and store the secondary certificate @@ -463,6 +471,20 @@ public final class FeatureFlags { return defaultValue; } + private static String getString(@NonNull String key, String defaultValue) { + String forced = (String) FORCED_VALUES.get(key); + if (forced != null) { + return forced; + } + + Object remote = REMOTE_VALUES.get(key); + if (remote instanceof String) { + return (String) remote; + } + + return defaultValue; + } + private static Map parseStoredConfig(String stored) { Map parsed = new HashMap<>(); @@ -511,14 +533,11 @@ public final class FeatureFlags { } } - private static final class MissingFlagRequirementError extends Error { - } - @VisibleForTesting static final class UpdateResult { private final Map memory; private final Map disk; - private final Map memoryChanges; + private final Map memoryChanges; UpdateResult(@NonNull Map memory, @NonNull Map disk, @NonNull Map memoryChanges) { this.memory = memory; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java new file mode 100644 index 000000000..864687674 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.annimon.stream.Stream; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.logging.Log; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public final class RemoteDeprecation { + + private static final String TAG = Log.tag(RemoteDeprecation.class); + + private RemoteDeprecation() { } + + /** + * @return The amount of time (in milliseconds) until this client version expires, or -1 if + * there's no pending expiration. + */ + public static long getTimeUntilDeprecation() { + return getTimeUntilDeprecation(FeatureFlags.clientExpiration(), System.currentTimeMillis(), BuildConfig.VERSION_NAME); + } + + /** + * @return The amount of time (in milliseconds) until this client version expires, or -1 if + * there's no pending expiration. + */ + @VisibleForTesting + static long getTimeUntilDeprecation(String json, long currentTime, @NonNull String currentVersion) { + if (Util.isEmpty(json)) { + return -1; + } + + try { + SemanticVersion ourVersion = Objects.requireNonNull(SemanticVersion.parse(currentVersion)); + ClientExpiration[] expirations = JsonUtils.fromJson(json, ClientExpiration[].class); + + ClientExpiration expiration = Stream.of(expirations) + .filter(c -> c.getVersion() != null && c.getExpiration() != -1) + .filter(c -> c.requireVersion().compareTo(ourVersion) > 0) + .sortBy(ClientExpiration::getExpiration) + .findFirst() + .orElse(null); + + if (expiration != null) { + return Math.max(expiration.getExpiration() - currentTime, 0); + } + } catch (IOException e) { + Log.w(TAG, e); + } + + return -1; + } + + private static final class ClientExpiration { + @JsonProperty + private final String minVersion; + + @JsonProperty + private final String iso8601; + + ClientExpiration(@Nullable @JsonProperty("minVersion") String minVersion, + @Nullable @JsonProperty("iso8601") String iso8601) + { + this.minVersion = minVersion; + this.iso8601 = iso8601; + } + + public @Nullable SemanticVersion getVersion() { + return SemanticVersion.parse(minVersion); + } + + public @NonNull SemanticVersion requireVersion() { + return Objects.requireNonNull(getVersion()); + } + + public long getExpiration() { + return DateUtils.parseIso8601(iso8601); + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java b/app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java new file mode 100644 index 000000000..a34db6b0c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SemanticVersion.java @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.ComparatorCompat; + +import java.util.Comparator; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class SemanticVersion implements Comparable { + + private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)$"); + + private static final Comparator MAJOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.major, s2.major); + private static final Comparator MINOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.minor, s2.minor); + private static final Comparator PATCH_COMPARATOR = (s1, s2) -> Integer.compare(s1.patch, s2.patch); + private static final Comparator COMPARATOR = ComparatorCompat.chain(MAJOR_COMPARATOR) + .thenComparing(MINOR_COMPARATOR) + .thenComparing(PATCH_COMPARATOR); + + private final int major; + private final int minor; + private final int patch; + + public SemanticVersion(int major, int minor, int patch) { + this.major = major; + this.minor = minor; + this.patch = patch; + } + + public static @Nullable SemanticVersion parse(@Nullable String value) { + if (value == null) { + return null; + } + + Matcher matcher = VERSION_PATTERN.matcher(value); + if (Util.isEmpty(value) || !matcher.matches()) { + return null; + } + + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + int patch = Integer.parseInt(matcher.group(3)); + + return new SemanticVersion(major, minor, patch); + } + + @Override + public int compareTo(SemanticVersion other) { + return COMPARATOR.compare(this, other); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SemanticVersion that = (SemanticVersion) o; + return major == that.major && + minor == that.minor && + patch == that.patch; + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 583e0c9d8..cec591636 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -49,6 +49,7 @@ import com.google.i18n.phonenumbers.Phonenumber; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.components.ComposeText; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection; import org.whispersystems.libsignal.util.guava.Optional; @@ -77,6 +78,8 @@ import java.util.concurrent.TimeUnit; public class Util { private static final String TAG = Util.class.getSimpleName(); + private static final long BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90); + private static volatile Handler handler; public static List asList(T... elements) { @@ -458,9 +461,25 @@ public class Util { return secret; } - public static int getDaysTillBuildExpiry() { - int age = (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP); - return 90 - age; + /** + * @return The amount of time (in ms) until this build of Signal will be considered 'expired'. + * Takes into account both the build age as well as any remote deprecation values. + */ + public static long getTimeUntilBuildExpiry() { + if (SignalStore.misc().isClientDeprecated()) { + return 0; + } + + long buildAge = System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP; + long timeUntilBuildDeprecation = BUILD_LIFESPAN - buildAge; + long timeUntilRemoteDeprecation = RemoteDeprecation.getTimeUntilDeprecation(); + + if (timeUntilRemoteDeprecation != -1) { + long timeUntilDeprecation = Math.min(timeUntilBuildDeprecation, timeUntilRemoteDeprecation); + return Math.max(timeUntilDeprecation, 0); + } else { + return Math.max(timeUntilBuildDeprecation, 0); + } } @TargetApi(VERSION_CODES.LOLLIPOP) diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java new file mode 100644 index 000000000..4475d1947 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.testutil; + +import org.thoughtcrime.securesms.logging.Log; + +public class EmptyLogger extends Log.Logger { + @Override + public void v(String tag, String message, Throwable t) { } + + @Override + public void d(String tag, String message, Throwable t) { } + + @Override + public void i(String tag, String message, Throwable t) { } + + @Override + public void w(String tag, String message, Throwable t) { } + + @Override + public void e(String tag, String message, Throwable t) { } + + @Override + public void wtf(String tag, String message, Throwable t) { } + + @Override + public void blockUntilAllWritesFinished() { } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/RemoteExpirationTest_getTimeUntilDeprecation.java b/app/src/test/java/org/thoughtcrime/securesms/util/RemoteExpirationTest_getTimeUntilDeprecation.java new file mode 100644 index 000000000..394d655ad --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/RemoteExpirationTest_getTimeUntilDeprecation.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.util; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.testutil.EmptyLogger; + +import java.util.Arrays; +import java.util.Collection; + +import static junit.framework.TestCase.assertEquals; + +@RunWith(Parameterized.class) +public class RemoteExpirationTest_getTimeUntilDeprecation { + + private final String json; + private final long currentDate; + private final String currentVersion; + private final long timeUntilExpiration; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + // Null json, invalid + { null, DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", -1 }, + + // Empty json, no expiration + { "[]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", -1 }, + + // Badly formatted minVersion, no expiration + { "[ {\"minVersion\": \"1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // Badly formatted date, no expiration + { "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"20-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // Missing minVersion, no expiration + { "[ {\"iso8601\": \"20-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // Missing date, no expiration + { "[ {\"minVersion\": \"1.1.1\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // Missing expiration and date, no expiration + { "[ {} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // Invalid inner object, no expiration + { "[ { \"a\": 1 } ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // Invalid json, no expiration + { "[ {", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // We meet the min version, no expiration + { "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.1", -1 }, + + // We exceed the min version, no expiration + { "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.2", -1 }, + + // We expire in 1 second + { "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", 1000 }, + + // We have already expired + { "[ {\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:02Z"), "1.1.0", 0 }, + + // Use the closest expiration when multiple ones are listed + { "[ {\"minVersion\": \"1.1.2\", \"iso8601\": \"2020-02-01T00:00:00Z\"}," + + "{\"minVersion\": \"1.1.3\", \"iso8601\": \"2020-03-01T00:00:00Z\"}," + + "{\"minVersion\": \"1.1.1\", \"iso8601\": \"2020-01-01T00:00:01Z\"} ]", DateUtils.parseIso8601("2020-01-01T00:00:00Z"), "1.1.0", 1000 }, + }); + } + + public RemoteExpirationTest_getTimeUntilDeprecation(String json, long currentDate, String currentVersion, long timeUntilExpiration) { + this.json = json; + this.currentDate = currentDate; + this.currentVersion = currentVersion; + this.timeUntilExpiration = timeUntilExpiration; + } + + @BeforeClass + public static void setup() { + Log.initialize(new EmptyLogger()); + } + + @Test + public void getTimeUntilExpiration() { + assertEquals(timeUntilExpiration, RemoteDeprecation.getTimeUntilDeprecation(json, currentDate, currentVersion)); + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_compareTo.java b/app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_compareTo.java new file mode 100644 index 000000000..27a58061e --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_compareTo.java @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +import static junit.framework.TestCase.assertEquals; + +@RunWith(Parameterized.class) +public class SemanticVersionTest_compareTo { + + private final SemanticVersion first; + private final SemanticVersion second; + private final int output; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + { new SemanticVersion(1, 0, 0), new SemanticVersion(0, 1, 0), 1 }, + { new SemanticVersion(1, 0, 0), new SemanticVersion(0, 0, 1), 1 }, + { new SemanticVersion(1, 0, 0), new SemanticVersion(0, 0, 0), 1 }, + { new SemanticVersion(0, 1, 0), new SemanticVersion(0, 0, 1), 1 }, + { new SemanticVersion(0, 1, 0), new SemanticVersion(0, 0, 0), 1 }, + { new SemanticVersion(0, 0, 1), new SemanticVersion(0, 0, 0), 1 }, + { new SemanticVersion(1, 1, 0), new SemanticVersion(1, 0, 0), 1 }, + { new SemanticVersion(1, 1, 1), new SemanticVersion(1, 1, 0), 1 }, + { new SemanticVersion(0, 0, 1), new SemanticVersion(1, 0, 0), -1 }, + { new SemanticVersion(1, 1, 1), new SemanticVersion(1, 1, 1), 0 }, + { new SemanticVersion(0, 0, 0), new SemanticVersion(0, 0, 0), 0 }, + }); + } + + public SemanticVersionTest_compareTo(SemanticVersion first, SemanticVersion second, int output) { + this.first = first; + this.second = second; + this.output = output; + } + + @Test + public void compareTo() { + assertEquals(output, first.compareTo(second)); + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_parse.java b/app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_parse.java new file mode 100644 index 000000000..203b61611 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/SemanticVersionTest_parse.java @@ -0,0 +1,41 @@ +package org.thoughtcrime.securesms.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; + +import java.util.Arrays; +import java.util.Collection; + +import static junit.framework.TestCase.assertEquals; + +@RunWith(Parameterized.class) +public class SemanticVersionTest_parse { + + private final String input; + private final SemanticVersion output; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + { "0.0.0", new SemanticVersion(0, 0, 0)}, + { "1.2.3", new SemanticVersion(1, 2, 3)}, + { "111.222.333", new SemanticVersion(111, 222, 333)}, + { "v1.2.3", null }, + { "1.2.3x", null }, + { "peter.ben.parker", null }, + { "", null} + }); + } + + public SemanticVersionTest_parse(String input, SemanticVersion output) { + this.input = input; + this.output = output; + } + + @Test + public void parse() { + assertEquals(output, SemanticVersion.parse(input)); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/DeprecatedVersionException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/DeprecatedVersionException.java new file mode 100644 index 000000000..1136044f2 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/DeprecatedVersionException.java @@ -0,0 +1,4 @@ +package org.whispersystems.signalservice.api.push.exceptions; + +public class DeprecatedVersionException extends NonSuccessfulResponseCodeException { +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 11439ad2f..11eade9a2 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -49,6 +49,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.ContactManifestMismatchException; +import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.push.exceptions.NoContentException; @@ -1459,6 +1460,8 @@ public class PushServiceSocket { throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining, basicStorageCredentials); + case 499: + throw new DeprecatedVersionException(); } if (responseCode != 200 && responseCode != 204) { @@ -1688,6 +1691,8 @@ public class PushServiceSocket { } case 429: throw new RateLimitException("Rate limit exceeded: " + response.code()); + case 499: + throw new DeprecatedVersionException(); } throw new NonSuccessfulResponseCodeException("Response: " + response); From 7c70ea4d3e1817c8b0120df94b2df4f62f26914d Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 8 Sep 2020 18:05:24 -0400 Subject: [PATCH 02/13] Change directory refresh interval to every 24 hours. --- .../securesms/service/DirectoryRefreshListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java b/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java index 7f8bf1ff6..adc0dd8eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/DirectoryRefreshListener.java @@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit; public class DirectoryRefreshListener extends PersistentAlarmManagerListener { - private static final long INTERVAL = TimeUnit.HOURS.toMillis(6); + private static final long INTERVAL = TimeUnit.HOURS.toMillis(24); @Override protected long getNextScheduledExecutionTime(Context context) { From 19ce5b5c76d43d82c33c5865a86ded817c000b67 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 8 Sep 2020 18:08:40 -0400 Subject: [PATCH 03/13] Reduce APNGParser logging. --- .../main/java/org/signal/glide/apng/decode/APNGParser.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java index 79dff419d..04d52757a 100644 --- a/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java +++ b/app/src/main/java/org/signal/glide/apng/decode/APNGParser.java @@ -96,9 +96,7 @@ public class APNGParser { } } } catch (IOException e) { - if (!(e instanceof FormatException)) { - e.printStackTrace(); - } + return false; } return false; } From d8a489971cd5d4694d3797e9ae351c0a4317bd5e Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 9 Sep 2020 10:42:14 -0300 Subject: [PATCH 04/13] Fix missing reply arrows. --- .../main/res/layout/conversation_item_received_multimedia.xml | 2 +- .../main/res/layout/conversation_item_received_text_only.xml | 2 +- app/src/main/res/layout/conversation_item_sent_multimedia.xml | 2 +- app/src/main/res/layout/conversation_item_sent_text_only.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/conversation_item_received_multimedia.xml b/app/src/main/res/layout/conversation_item_received_multimedia.xml index 61ed8c838..22ef259d5 100644 --- a/app/src/main/res/layout/conversation_item_received_multimedia.xml +++ b/app/src/main/res/layout/conversation_item_received_multimedia.xml @@ -22,7 +22,7 @@ android:clipToPadding="false" android:clipChildren="false"> - - - - Date: Wed, 9 Sep 2020 10:22:22 -0400 Subject: [PATCH 05/13] Implement new client deprecation UI. --- app/src/main/AndroidManifest.xml | 5 ++ .../securesms/ApplicationContext.java | 2 +- .../reminder/ExpiredBuildReminder.java | 24 ++++++- .../reminder/OutdatedBuildReminder.java | 14 +++-- .../components/reminder/Reminder.java | 4 +- .../components/reminder/ReminderView.java | 41 +++++++----- .../conversation/ConversationActivity.java | 5 ++ .../ConversationListFragment.java | 21 ++++++- .../keyvalue/MiscellaneousValues.java | 6 +- .../megaphone/ClientDeprecatedActivity.java | 62 +++++++++++++++++++ .../securesms/megaphone/Megaphone.java | 29 ++++++--- .../megaphone/MegaphoneRepository.java | 10 +++ .../securesms/megaphone/Megaphones.java | 38 +++++++----- .../migrations/ApplicationMigrations.java | 1 + .../RemoteDeprecationDetectorInterceptor.java | 2 +- .../securesms/util/VersionTracker.java | 13 +++- .../res/drawable/ic_signal_logo_large.xml | 9 +++ .../drawable/reminder_background_terminal.xml | 6 ++ .../res/layout/client_deprecated_activity.xml | 62 +++++++++++++++++++ .../res/layout/reminder_action_button.xml | 2 +- app/src/main/res/layout/reminder_header.xml | 15 ++--- app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/ids.xml | 1 + app/src/main/res/values/strings.xml | 28 ++++++--- app/src/main/res/values/styles.xml | 4 ++ app/src/main/res/values/themes.xml | 4 ++ 26 files changed, 341 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java create mode 100644 app/src/main/res/drawable/ic_signal_logo_large.xml create mode 100644 app/src/main/res/drawable/reminder_background_terminal.xml create mode 100644 app/src/main/res/layout/client_deprecated_activity.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a93978503..298775a11 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -523,6 +523,11 @@ + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index fbb8a4e80..b1a13a1eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -198,7 +198,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi public void checkBuildExpiration() { if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) { Log.w(TAG, "Build expired!"); - SignalStore.misc().markDeprecated(); + SignalStore.misc().markClientDeprecated(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java index 5a3b911d8..b5089cf4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java @@ -2,16 +2,25 @@ package org.thoughtcrime.securesms.components.reminder; import android.content.Context; +import androidx.annotation.NonNull; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.PlayStoreUtil; +import java.util.List; + +/** + * Showed when a build has fully expired (either via the compile-time constant, or remote + * deprecation). + */ public class ExpiredBuildReminder extends Reminder { public ExpiredBuildReminder(final Context context) { - super(context.getString(R.string.reminder_header_expired_build), - context.getString(R.string.reminder_header_expired_build_details)); + super(null, context.getString(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired)); + setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)); + addAction(new Action(context.getString(R.string.ExpiredBuildReminder_update_now), R.id.reminder_action_update_now)); } @Override @@ -19,8 +28,17 @@ public class ExpiredBuildReminder extends Reminder { return false; } + @Override + public List getActions() { + return super.getActions(); + } + + @Override + public @NonNull Importance getImportance() { + return Importance.TERMINAL; + } + public static boolean isEligible() { return SignalStore.misc().isClientDeprecated(); } - } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java index 3bcbecb5a..93d4853d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java @@ -8,20 +8,22 @@ import org.thoughtcrime.securesms.util.Util; import java.util.concurrent.TimeUnit; +/** + * Reminder that is shown when a build is getting close to expiry (either because of the + * compile-time constant, or remote deprecation). + */ public class OutdatedBuildReminder extends Reminder { public OutdatedBuildReminder(final Context context) { - super(context.getString(R.string.reminder_header_outdated_build), - getPluralsText(context)); + super(null, getPluralsText(context)); + setOkListener(v -> PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)); + addAction(new Action(context.getString(R.string.OutdatedBuildReminder_update_now), R.id.reminder_action_update_now)); } private static CharSequence getPluralsText(final Context context) { int days = getDaysUntilExpiry() - 1; - if (days == 0) { - return context.getString(R.string.reminder_header_outdated_build_details_today); - } - return context.getResources().getQuantityString(R.plurals.reminder_header_outdated_build_details, days, days); + return context.getResources().getQuantityString(R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, days, days); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java index 109d9f449..df3c946fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/Reminder.java @@ -58,7 +58,7 @@ public abstract class Reminder { return Importance.NORMAL; } - public void addAction(@NonNull Action action) { + protected void addAction(@NonNull Action action) { actions.add(action); } @@ -71,7 +71,7 @@ public abstract class Reminder { } public enum Importance { - NORMAL, ERROR + NORMAL, ERROR, TERMINAL } public final class Action { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java index 2ea65728d..a87d49985 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ReminderView.java @@ -1,8 +1,6 @@ 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; @@ -19,7 +17,6 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.ViewUtil; import java.util.List; @@ -48,7 +45,6 @@ public final class ReminderView extends FrameLayout { initialize(); } - @TargetApi(VERSION_CODES.HONEYCOMB) public ReminderView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(); @@ -56,14 +52,14 @@ public final class ReminderView extends FrameLayout { private void initialize() { LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true); - progressBar = ViewUtil.findById(this, R.id.reminder_progress); - progressText = ViewUtil.findById(this, R.id.reminder_progress_text); - container = ViewUtil.findById(this, R.id.container); - closeButton = ViewUtil.findById(this, R.id.cancel); - title = ViewUtil.findById(this, R.id.reminder_title); - text = ViewUtil.findById(this, R.id.reminder_text); - space = ViewUtil.findById(this, R.id.reminder_space); - actionsRecycler = ViewUtil.findById(this, R.id.reminder_actions); + progressBar = findViewById(R.id.reminder_progress); + progressText = findViewById(R.id.reminder_progress_text); + container = findViewById(R.id.container); + closeButton = findViewById(R.id.cancel); + title = findViewById(R.id.reminder_title); + text = findViewById(R.id.reminder_text); + space = findViewById(R.id.reminder_space); + actionsRecycler = findViewById(R.id.reminder_actions); } public void showReminder(final Reminder reminder) { @@ -76,9 +72,26 @@ public final class ReminderView extends FrameLayout { title.setVisibility(GONE); space.setVisibility(VISIBLE); } + + if (!reminder.isDismissable()) { + space.setVisibility(GONE); + } + text.setText(reminder.getText()); - container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error - : R.drawable.reminder_background_normal); + + switch (reminder.getImportance()) { + case NORMAL: + container.setBackgroundResource(R.drawable.reminder_background_normal); + break; + case ERROR: + container.setBackgroundResource(R.drawable.reminder_background_error); + break; + case TERMINAL: + container.setBackgroundResource(R.drawable.reminder_background_terminal); + break; + default: + throw new IllegalStateException(); + } setOnClickListener(reminder.getOkListener()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index b33b619ed..f59a7aa03 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -240,6 +240,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MessageUtil; +import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode; @@ -1685,6 +1686,7 @@ public class ConversationActivity extends PassphraseRequiredActivity reminderView.get().showReminder(new UnauthorizedReminder(this)); } else if (ExpiredBuildReminder.isEligible()) { reminderView.get().showReminder(new ExpiredBuildReminder(this)); + reminderView.get().setOnActionClickListener(this::handleReminderAction); } else if (ServiceOutageReminder.isEligible(this)) { ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob()); reminderView.get().showReminder(new ServiceOutageReminder(this)); @@ -1710,6 +1712,9 @@ public class ConversationActivity extends PassphraseRequiredActivity case R.id.reminder_action_view_insights: InsightsLauncher.showInsightsDashboard(getSupportFragmentManager()); break; + case R.id.reminder_action_update_now: + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this); + break; default: throw new IllegalArgumentException("Unknown ID: " + reminderActionId); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index e8021604f..b200b3055 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -56,6 +56,7 @@ import androidx.appcompat.widget.TooltipCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.view.ViewCompat; import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner; import androidx.lifecycle.ViewModelProviders; @@ -117,6 +118,7 @@ import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AvatarUtil; +import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.SnapToTopDataObserver; import org.thoughtcrime.securesms.util.StickyHeaderDecoration; @@ -176,6 +178,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private ViewGroup megaphoneContainer; private SnapToTopDataObserver snapToTopDataObserver; private Drawable archiveDrawable; + private LifecycleObserver visibilityLifecycleObserver; public static ConversationListFragment newInstance() { return new ConversationListFragment(); @@ -214,6 +217,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode cameraFab.show(); reminderView.setOnDismissListener(this::updateReminders); + reminderView.setOnActionClickListener(this::onReminderAction); list.setLayoutManager(new LinearLayoutManager(requireActivity())); list.setItemAnimator(new DeleteItemAnimator()); @@ -272,6 +276,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode public void onStart() { super.onStart(); ConversationFragment.prepare(requireContext()); + ProcessLifecycleOwner.get().getLifecycle().addObserver(visibilityLifecycleObserver); } @Override @@ -283,6 +288,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode EventBus.getDefault().unregister(this); } + @Override + public void onStop() { + super.onStop(); + ProcessLifecycleOwner.get().getLifecycle().removeObserver(visibilityLifecycleObserver); + } + @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { menu.clear(); @@ -412,6 +423,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.onMegaphoneCompleted(event); } + private void onReminderAction(@IdRes int reminderActionId) { + if (reminderActionId == R.id.reminder_action_update_now) { + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); + } + } + private void hideKeyboard() { InputMethodManager imm = ServiceUtil.getInputMethodManager(requireContext()); imm.hideSoftInputFromWindow(requireView().getWindowToken(), 0); @@ -508,12 +525,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode viewModel.getConversationList().observe(getViewLifecycleOwner(), this::onSubmitList); viewModel.hasNoConversations().observe(getViewLifecycleOwner(), this::updateEmptyState); - ProcessLifecycleOwner.get().getLifecycle().addObserver(new DefaultLifecycleObserver() { + visibilityLifecycleObserver = new DefaultLifecycleObserver() { @Override public void onStart(@NonNull LifecycleOwner owner) { viewModel.onVisible(); } - }); + }; } private void onSearchResultChanged(@Nullable SearchResult result) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java index 975a114ad..1a49d01f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.java @@ -51,7 +51,11 @@ public final class MiscellaneousValues extends SignalStoreValues { return getBoolean(CLIENT_DEPRECATED, false); } - public void markDeprecated() { + public void markClientDeprecated() { putBoolean(CLIENT_DEPRECATED, true); } + + public void clearClientDeprecated() { + putBoolean(CLIENT_DEPRECATED, false); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java new file mode 100644 index 000000000..7ab4bd66d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/ClientDeprecatedActivity.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.megaphone; + +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; + +import org.thoughtcrime.securesms.PassphraseRequiredActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.PlayStoreUtil; +import org.thoughtcrime.securesms.util.Util; + +/** + * Shown when a users build fully expires. Controlled by {@link Megaphones.Event#CLIENT_DEPRECATED}. + */ +public class ClientDeprecatedActivity extends PassphraseRequiredActivity { + + private final DynamicTheme theme = new DynamicNoActionBarTheme(); + + @Override + protected void onCreate(Bundle savedInstanceState, boolean ready) { + setContentView(R.layout.client_deprecated_activity); + + findViewById(R.id.client_deprecated_update_button).setOnClickListener(v -> onUpdateClicked()); + findViewById(R.id.client_deprecated_dont_update_button).setOnClickListener(v -> onDontUpdateClicked()); + } + + @Override + protected void onPreCreate() { + theme.onCreate(this); + } + + @Override + protected void onResume() { + super.onResume(); + theme.onResume(this); + } + + @Override + public void onBackPressed() { + // Disabled + } + + private void onUpdateClicked() { + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(this); + } + + private void onDontUpdateClicked() { + new AlertDialog.Builder(this) + .setTitle(R.string.ClientDeprecatedActivity_warning) + .setMessage(R.string.ClientDeprecatedActivity_your_version_of_signal_has_expired_you_can_view_your_message_history) + .setPositiveButton(R.string.ClientDeprecatedActivity_dont_update, (dialog, which) -> { + ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.CLIENT_DEPRECATED, () -> { + Util.runOnMain(this::finish); + }); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()) + .show(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java index 1b6094229..a001cb3e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java @@ -20,7 +20,7 @@ public class Megaphone { private final Event event; private final Style style; - private final boolean mandatory; + private final Priority priority; private final boolean canSnooze; private final int titleRes; private final int bodyRes; @@ -33,7 +33,7 @@ public class Megaphone { private Megaphone(@NonNull Builder builder) { this.event = builder.event; this.style = builder.style; - this.mandatory = builder.mandatory; + this.priority = builder.priority; this.canSnooze = builder.canSnooze; this.titleRes = builder.titleRes; this.bodyRes = builder.bodyRes; @@ -48,8 +48,8 @@ public class Megaphone { return event; } - public boolean isMandatory() { - return mandatory; + public @NonNull Priority getPriority() { + return priority; } public boolean canSnooze() { @@ -97,7 +97,7 @@ public class Megaphone { private final Event event; private final Style style; - private boolean mandatory; + private Priority priority; private boolean canSnooze; private int titleRes; private int bodyRes; @@ -111,13 +111,14 @@ public class Megaphone { public Builder(@NonNull Event event, @NonNull Style style) { this.event = event; this.style = style; + this.priority = Priority.DEFAULT; } /** * Prioritizes this megaphone over others that do not set this flag. */ - public @NonNull Builder setMandatory(boolean mandatory) { - this.mandatory = mandatory; + public @NonNull Builder setPriority(@NonNull Priority priority) { + this.priority = priority; return this; } @@ -192,6 +193,20 @@ public class Megaphone { POPUP } + enum Priority { + DEFAULT(0), HIGH(1), CLIENT_EXPIRATION(1000); + + int priorityValue; + + Priority(int priorityValue) { + this.priorityValue = priorityValue; + } + + public int getPriorityValue() { + return priorityValue; + } + } + public interface EventListener { void onEvent(@NonNull Megaphone megaphone, @NonNull MegaphoneActionController listener); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java index 95ff61eba..a0d4c73c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/MegaphoneRepository.java @@ -5,6 +5,7 @@ import android.content.Context; import androidx.annotation.AnyThread; import androidx.annotation.MainThread; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.annimon.stream.Collectors; @@ -100,6 +101,11 @@ public class MegaphoneRepository { @AnyThread public void markFinished(@NonNull Event event) { + markFinished(event, null); + } + + @AnyThread + public void markFinished(@NonNull Event event, @Nullable Runnable onComplete) { executor.execute(() -> { MegaphoneRecord record = databaseCache.get(event); if (record != null && record.isFinished()) { @@ -108,6 +114,10 @@ public class MegaphoneRepository { database.markFinished(event); resetDatabaseCache(); + + if (onComplete != null) { + onComplete.run(); + } }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 0721ae3b0..4f5766a62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -34,12 +34,12 @@ import java.util.Objects; * Creating a new megaphone: * - Add an enum to {@link Event} * - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)} - * - Include the event in {@link #buildDisplayOrder()} + * - Include the event in {@link #buildDisplayOrder(Context)} * * Common patterns: * - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}. * - For events guarded by feature flags, set a {@link ForeverSchedule} with false in - * {@link #buildDisplayOrder()}. + * {@link #buildDisplayOrder(Context)}. * - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)} * based on whatever properties you're interested in. */ @@ -65,15 +65,9 @@ public final class Megaphones { .map(Map.Entry::getKey) .map(records::get) .map(record -> Megaphones.forRecord(context, record)) + .sortBy(m -> -m.getPriority().getPriorityValue()) .toList(); - boolean hasOptional = Stream.of(megaphones).anyMatch(m -> !m.isMandatory()); - boolean hasMandatory = Stream.of(megaphones).anyMatch(Megaphone::isMandatory); - - if (hasOptional && hasMandatory) { - megaphones = Stream.of(megaphones).filter(Megaphone::isMandatory).toList(); - } - if (megaphones.size() > 0) { return megaphones.get(0); } else { @@ -93,6 +87,7 @@ public final class Megaphones { put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER); put(Event.MENTIONS, shouldShowMentionsMegaphone() ? ALWAYS : NEVER); put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER); + put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER); }}; } @@ -110,6 +105,8 @@ public final class Megaphones { return buildMentionsMegaphone(); case LINK_PREVIEWS: return buildLinkPreviewsMegaphone(); + case CLIENT_DEPRECATED: + return buildClientDeprecatedMegaphone(context); default: throw new IllegalArgumentException("Event not handled!"); } @@ -117,14 +114,14 @@ public final class Megaphones { private static @NonNull Megaphone buildReactionsMegaphone() { return new Megaphone.Builder(Event.REACTIONS, Megaphone.Style.REACTIONS) - .setMandatory(false) + .setPriority(Megaphone.Priority.DEFAULT) .build(); } private static @NonNull Megaphone buildPinsForAllMegaphone(@NonNull MegaphoneRecord record) { if (PinsForAllSchedule.shouldDisplayFullScreen(record.getFirstVisible(), System.currentTimeMillis())) { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.FULLSCREEN) - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) .enableSnooze(null) .setOnVisibleListener((megaphone, listener) -> { if (new NetworkConstraint.Factory(ApplicationDependencies.getApplication()).create().isMet()) { @@ -134,7 +131,7 @@ public final class Megaphones { .build(); } else { return new Megaphone.Builder(Event.PINS_FOR_ALL, Megaphone.Style.BASIC) - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) .setImage(R.drawable.kbs_pin_megaphone) .setTitle(R.string.KbsMegaphone__create_a_pin) .setBody(R.string.KbsMegaphone__pins_keep_information_thats_stored_with_signal_encrytped) @@ -184,7 +181,7 @@ public final class Megaphones { private static @NonNull Megaphone buildMessageRequestsMegaphone(@NonNull Context context) { return new Megaphone.Builder(Event.MESSAGE_REQUESTS, Megaphone.Style.FULLSCREEN) .disableSnooze() - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) .setOnVisibleListener(((megaphone, listener) -> { listener.onMegaphoneNavigationRequested(new Intent(context, MessageRequestMegaphoneActivity.class), ConversationListFragment.MESSAGE_REQUESTS_REQUEST_CODE_CREATE_NAME); @@ -202,7 +199,17 @@ public final class Megaphones { private static @NonNull Megaphone buildLinkPreviewsMegaphone() { return new Megaphone.Builder(Event.LINK_PREVIEWS, Megaphone.Style.LINK_PREVIEWS) - .setMandatory(true) + .setPriority(Megaphone.Priority.HIGH) + .build(); + } + + private static @NonNull Megaphone buildClientDeprecatedMegaphone(@NonNull Context context) { + return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN) + .disableSnooze() + .setPriority(Megaphone.Priority.HIGH) + .setOnVisibleListener((megaphone, listener) -> { + listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class)); + }) .build(); } @@ -224,7 +231,8 @@ public final class Megaphones { PIN_REMINDER("pin_reminder"), MESSAGE_REQUESTS("message_requests"), MENTIONS("mentions"), - LINK_PREVIEWS("link_previews"); + LINK_PREVIEWS("link_previews"), + CLIENT_DEPRECATED("client_deprecated"); private final String key; diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 47df39184..5f8ab8703 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -75,6 +75,7 @@ public class ApplicationMigrations { if (!isUpdate(context)) { Log.d(TAG, "Not an update. Skipping."); + VersionTracker.updateLastSeenVersion(context); return; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java index 80258eb81..ff5b542ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java @@ -23,7 +23,7 @@ public final class RemoteDeprecationDetectorInterceptor implements Interceptor { if (response.code() == 499 && !SignalStore.misc().isClientDeprecated()) { Log.w(TAG, "Received 499. Client version is deprecated."); - SignalStore.misc().markDeprecated(); + SignalStore.misc().markClientDeprecated(); } return response; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java b/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java index 4c9a6ba97..e94710c02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.java @@ -3,10 +3,15 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + import java.io.IOException; public class VersionTracker { + private static final String TAG = Log.tag(VersionTracker.class); + public static int getLastSeenVersion(@NonNull Context context) { return TextSecurePreferences.getLastVersionCode(context); } @@ -14,7 +19,13 @@ public class VersionTracker { public static void updateLastSeenVersion(@NonNull Context context) { try { int currentVersionCode = Util.getCanonicalVersionCode(); - TextSecurePreferences.setLastVersionCode(context, currentVersionCode); + int lastVersionCode = TextSecurePreferences.getLastVersionCode(context); + + if (currentVersionCode != lastVersionCode) { + Log.i(TAG, "Upgraded from " + lastVersionCode + " to " + currentVersionCode); + SignalStore.misc().clearClientDeprecated(); + TextSecurePreferences.setLastVersionCode(context, currentVersionCode); + } } catch (IOException ioe) { throw new AssertionError(ioe); } diff --git a/app/src/main/res/drawable/ic_signal_logo_large.xml b/app/src/main/res/drawable/ic_signal_logo_large.xml new file mode 100644 index 000000000..cab7595e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_signal_logo_large.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/reminder_background_terminal.xml b/app/src/main/res/drawable/reminder_background_terminal.xml new file mode 100644 index 000000000..795dc3c25 --- /dev/null +++ b/app/src/main/res/drawable/reminder_background_terminal.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/layout/client_deprecated_activity.xml b/app/src/main/res/layout/client_deprecated_activity.xml new file mode 100644 index 000000000..31dbbba4f --- /dev/null +++ b/app/src/main/res/layout/client_deprecated_activity.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + +