mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Add support for fetching remote deprecation.
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user