mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Implement additional message request improvements.
This commit is contained in:
@@ -28,7 +28,7 @@ import java.util.concurrent.TimeUnit;
|
||||
* are not yet ready to be activated.
|
||||
*
|
||||
* When creating a new flag:
|
||||
* - Create a new string constant using {@link #generateKey(String)})
|
||||
* - Create a new string constant. This should almost certainly be prefixed with "android."
|
||||
* - Add a method to retrieve the value using {@link #getValue(String, boolean)}. You can also add
|
||||
* other checks here, like requiring other flags.
|
||||
* - If you want to be able to change a flag remotely, place it in {@link #REMOTE_CAPABLE}.
|
||||
@@ -38,22 +38,22 @@ import java.util.concurrent.TimeUnit;
|
||||
* Other interesting things you can do:
|
||||
* - Make a flag {@link #HOT_SWAPPABLE}
|
||||
* - Make a flag {@link #STICKY}
|
||||
* - Register a listener for flag changes in {@link #FLAG_CHANGE_LISTENERS}
|
||||
*/
|
||||
public final class FeatureFlags {
|
||||
|
||||
private static final String TAG = Log.tag(FeatureFlags.class);
|
||||
|
||||
private static final String PREFIX = "android.";
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
||||
|
||||
private static final String UUIDS = generateKey("uuids");
|
||||
private static final String MESSAGE_REQUESTS = generateKey("messageRequests");
|
||||
private static final String USERNAMES = generateKey("usernames");
|
||||
private static final String STORAGE_SERVICE = generateKey("storageService");
|
||||
private static final String PINS_FOR_ALL = generateKey("pinsForAll");
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = generateKey("pinsMegaphoneKillSwitch");
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = generateKey("profileNamesMegaphone");
|
||||
private static final String VIDEO_TRIMMING = generateKey("videoTrimming");
|
||||
private static final String UUIDS = "android.uuids";
|
||||
private static final String MESSAGE_REQUESTS = "android.messageRequests";
|
||||
private static final String USERNAMES = "android.usernames";
|
||||
private static final String STORAGE_SERVICE = "android.storageService";
|
||||
private static final String PINS_FOR_ALL = "android.pinsForAll";
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
||||
private static final String VIDEO_TRIMMING = "android.videoTrimming";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -97,17 +97,39 @@ public final class FeatureFlags {
|
||||
PINS_FOR_ALL
|
||||
);
|
||||
|
||||
/**
|
||||
* Listeners that are called when the value in {@link #REMOTE_VALUES} changes. That means that
|
||||
* hot-swappable flags will have this invoked as soon as we know about that change, but otherwise
|
||||
* these will only run during initialization.
|
||||
*
|
||||
* These can be called on any thread, including the main thread, so be careful!
|
||||
*
|
||||
* Also note that this doesn't play well with {@link #FORCED_VALUES} -- changes there will not
|
||||
* trigger changes in this map, so you'll have to do some manually hacking to get yourself in the
|
||||
* desired test state.
|
||||
*/
|
||||
private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
|
||||
put(MESSAGE_REQUESTS, (change) -> SignalStore.setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0));
|
||||
}};
|
||||
|
||||
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>();
|
||||
|
||||
private FeatureFlags() {}
|
||||
|
||||
public static synchronized void init() {
|
||||
REMOTE_VALUES.putAll(parseStoredConfig());
|
||||
Map<String, Boolean> current = parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig());
|
||||
Map<String, Boolean> pending = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
|
||||
Map<String, Change> changes = computeChanges(current, pending);
|
||||
|
||||
SignalStore.remoteConfigValues().setCurrentConfig(mapToJson(pending));
|
||||
REMOTE_VALUES.putAll(pending);
|
||||
triggerFlagChangeListeners(changes);
|
||||
|
||||
Log.i(TAG, "init() " + REMOTE_VALUES.toString());
|
||||
}
|
||||
|
||||
public static synchronized void refresh() {
|
||||
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.getRemoteConfigLastFetchTime();
|
||||
public static synchronized void refreshIfNecessary() {
|
||||
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.remoteConfigValues().getLastFetchTime();
|
||||
|
||||
if (timeSinceLastFetch > FETCH_INTERVAL) {
|
||||
Log.i(TAG, "Scheduling remote config refresh.");
|
||||
@@ -118,13 +140,16 @@ public final class FeatureFlags {
|
||||
}
|
||||
|
||||
public static synchronized void update(@NonNull Map<String, Boolean> config) {
|
||||
Map<String, Boolean> memory = REMOTE_VALUES;
|
||||
Map<String, Boolean> disk = parseStoredConfig();
|
||||
UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY);
|
||||
Map<String, Boolean> memory = REMOTE_VALUES;
|
||||
Map<String, Boolean> disk = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
|
||||
UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY);
|
||||
|
||||
SignalStore.setRemoteConfig(mapToJson(result.getDisk()).toString());
|
||||
SignalStore.remoteConfigValues().setPendingConfig(mapToJson(result.getDisk()));
|
||||
REMOTE_VALUES.clear();
|
||||
REMOTE_VALUES.putAll(result.getMemory());
|
||||
triggerFlagChangeListeners(result.getChanges());
|
||||
|
||||
SignalStore.remoteConfigValues().setLastFetchTime(System.currentTimeMillis());
|
||||
|
||||
Log.i(TAG, "[Memory] Before: " + memory.toString());
|
||||
Log.i(TAG, "[Memory] After : " + result.getMemory().toString());
|
||||
@@ -189,7 +214,7 @@ public final class FeatureFlags {
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Boolean> getDiskValues() {
|
||||
return new TreeMap<>(parseStoredConfig());
|
||||
return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig()));
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
@@ -239,11 +264,31 @@ public final class FeatureFlags {
|
||||
}
|
||||
});
|
||||
|
||||
return new UpdateResult(newMemory, newDisk);
|
||||
return new UpdateResult(newMemory, newDisk, computeChanges(localMemory, newMemory));
|
||||
}
|
||||
|
||||
private static @NonNull String generateKey(@NonNull String key) {
|
||||
return PREFIX + key;
|
||||
@VisibleForTesting
|
||||
static @NonNull Map<String, Change> computeChanges(@NonNull Map<String, Boolean> oldMap, @NonNull Map<String, Boolean> newMap) {
|
||||
Map<String, Change> changes = new HashMap<>();
|
||||
Set<String> allKeys = new HashSet<>();
|
||||
|
||||
allKeys.addAll(oldMap.keySet());
|
||||
allKeys.addAll(newMap.keySet());
|
||||
|
||||
for (String key : allKeys) {
|
||||
Boolean oldValue = oldMap.get(key);
|
||||
Boolean newValue = newMap.get(key);
|
||||
|
||||
if (oldValue == null && newValue == null) {
|
||||
throw new AssertionError("Should not be possible.");
|
||||
} else if (oldValue != null && newValue == null) {
|
||||
changes.put(key, Change.REMOVED);
|
||||
} else if (newValue != oldValue) {
|
||||
changes.put(key, newValue ? Change.ENABLED : Change.DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static boolean getValue(@NonNull String key, boolean defaultValue) {
|
||||
@@ -260,9 +305,8 @@ public final class FeatureFlags {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static Map<String, Boolean> parseStoredConfig() {
|
||||
private static Map<String, Boolean> parseStoredConfig(String stored) {
|
||||
Map<String, Boolean> parsed = new HashMap<>();
|
||||
String stored = SignalStore.getRemoteConfig();
|
||||
|
||||
if (TextUtils.isEmpty(stored)) {
|
||||
Log.i(TAG, "No remote config stored. Skipping.");
|
||||
@@ -278,14 +322,13 @@ public final class FeatureFlags {
|
||||
parsed.put(key, root.getBoolean(key));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
SignalStore.setRemoteConfig(null);
|
||||
throw new AssertionError("Failed to parse! Cleared storage.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static JSONObject mapToJson(@NonNull Map<String, Boolean> map) {
|
||||
private static @NonNull String mapToJson(@NonNull Map<String, Boolean> map) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
|
||||
@@ -293,12 +336,23 @@ public final class FeatureFlags {
|
||||
json.put(entry.getKey(), (boolean) entry.getValue());
|
||||
}
|
||||
|
||||
return json;
|
||||
return json.toString();
|
||||
} catch (JSONException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void triggerFlagChangeListeners(Map<String, Change> changes) {
|
||||
for (Map.Entry<String, Change> change : changes.entrySet()) {
|
||||
OnFlagChange listener = FLAG_CHANGE_LISTENERS.get(change.getKey());
|
||||
|
||||
if (listener != null) {
|
||||
Log.i(TAG, "Triggering change listener for: " + change.getKey());
|
||||
listener.onFlagChange(change.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MissingFlagRequirementError extends Error {
|
||||
}
|
||||
|
||||
@@ -306,10 +360,12 @@ public final class FeatureFlags {
|
||||
static final class UpdateResult {
|
||||
private final Map<String, Boolean> memory;
|
||||
private final Map<String, Boolean> disk;
|
||||
private final Map<String, Change> changes;
|
||||
|
||||
UpdateResult(@NonNull Map<String, Boolean> memory, @NonNull Map<String, Boolean> disk) {
|
||||
this.memory = memory;
|
||||
this.disk = disk;
|
||||
UpdateResult(@NonNull Map<String, Boolean> memory, @NonNull Map<String, Boolean> disk, @NonNull Map<String, Change> changes) {
|
||||
this.memory = memory;
|
||||
this.disk = disk;
|
||||
this.changes = changes;
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Boolean> getMemory() {
|
||||
@@ -319,6 +375,19 @@ public final class FeatureFlags {
|
||||
public @NonNull Map<String, Boolean> getDisk() {
|
||||
return disk;
|
||||
}
|
||||
|
||||
public @NonNull Map<String, Change> getChanges() {
|
||||
return changes;
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
interface OnFlagChange {
|
||||
void onFlagChange(@NonNull Change change);
|
||||
}
|
||||
|
||||
enum Change {
|
||||
ENABLED, DISABLED, REMOVED
|
||||
}
|
||||
|
||||
/** Read and write versioned profile information. */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -45,6 +44,14 @@ public class GroupUtil {
|
||||
return Hex.fromStringCondensed(groupId.split("!", 2)[1]);
|
||||
}
|
||||
|
||||
public static byte[] getDecodedIdOrThrow(String groupId) {
|
||||
try {
|
||||
return getDecodedId(groupId);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isEncodedGroup(@NonNull String groupId) {
|
||||
return groupId.startsWith(ENCODED_SIGNAL_GROUP_PREFIX) || groupId.startsWith(ENCODED_MMS_GROUP_PREFIX);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user