Add support for fetching remote deprecation.

This commit is contained in:
Greyson Parrelli
2020-09-08 18:03:56 -04:00
committed by GitHub
parent c946a7a1d5
commit 2784285d47
21 changed files with 559 additions and 39 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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)