Add support for fetching remote deprecation.
This commit is contained in:
parent
c946a7a1d5
commit
2784285d47
21 changed files with 559 additions and 39 deletions
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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 {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Interceptor> interceptors = Collections.singletonList(new StandardUserAgentInterceptor());
|
||||
final List<Interceptor> interceptors = Arrays.asList(new StandardUserAgentInterceptor(), new RemoteDeprecationDetectorInterceptor(), new DeprecatedClientPreventionInterceptor());
|
||||
final Optional<Dns> dns = Optional.of(DNS);
|
||||
|
||||
final byte[] zkGroupServerPublicParams;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, Object> parseStoredConfig(String stored) {
|
||||
Map<String, Object> 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<String, Object> memory;
|
||||
private final Map<String, Object> disk;
|
||||
private final Map<String, Change> memoryChanges;
|
||||
private final Map<String, Change> memoryChanges;
|
||||
|
||||
UpdateResult(@NonNull Map<String, Object> memory, @NonNull Map<String, Object> disk, @NonNull Map<String, Change> memoryChanges) {
|
||||
this.memory = memory;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<SemanticVersion> {
|
||||
|
||||
private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)$");
|
||||
|
||||
private static final Comparator<SemanticVersion> MAJOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.major, s2.major);
|
||||
private static final Comparator<SemanticVersion> MINOR_COMPARATOR = (s1, s2) -> Integer.compare(s1.minor, s2.minor);
|
||||
private static final Comparator<SemanticVersion> PATCH_COMPARATOR = (s1, s2) -> Integer.compare(s1.patch, s2.patch);
|
||||
private static final Comparator<SemanticVersion> 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);
|
||||
}
|
||||
}
|
|
@ -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 <T> List<T> 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)
|
||||
|
|
|
@ -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() { }
|
||||
}
|
|
@ -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<Object[]> 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));
|
||||
}
|
||||
}
|
|
@ -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<Object[]> 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));
|
||||
}
|
||||
}
|
|
@ -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<Object[]> 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));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package org.whispersystems.signalservice.api.push.exceptions;
|
||||
|
||||
public class DeprecatedVersionException extends NonSuccessfulResponseCodeException {
|
||||
}
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue