2019-09-06 14:09:34 +00:00
|
|
|
package org.thoughtcrime.securesms.util;
|
|
|
|
|
2019-12-19 22:41:21 +00:00
|
|
|
import android.text.TextUtils;
|
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
2020-01-24 20:29:03 +00:00
|
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
|
|
|
|
import com.annimon.stream.Stream;
|
2019-12-19 22:41:21 +00:00
|
|
|
|
|
|
|
import org.json.JSONException;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
|
|
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
|
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
|
|
|
|
|
|
import java.util.HashMap;
|
2020-01-24 20:29:03 +00:00
|
|
|
import java.util.HashSet;
|
2019-12-19 22:41:21 +00:00
|
|
|
import java.util.Iterator;
|
|
|
|
import java.util.Map;
|
2020-01-24 20:29:03 +00:00
|
|
|
import java.util.Set;
|
2019-12-19 22:41:21 +00:00
|
|
|
import java.util.TreeMap;
|
2020-01-24 20:29:03 +00:00
|
|
|
import java.util.TreeSet;
|
2019-12-19 22:41:21 +00:00
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
2019-09-06 14:09:34 +00:00
|
|
|
/**
|
2019-12-19 22:41:21 +00:00
|
|
|
* A location for flags that can be set locally and remotely. These flags can guard features that
|
|
|
|
* are not yet ready to be activated.
|
|
|
|
*
|
|
|
|
* When creating a new flag:
|
|
|
|
* - Create a new string constant using {@link #generateKey(String)})
|
|
|
|
* - 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 would like to force a value for testing, place an entry in {@link #FORCED_VALUES}. When
|
|
|
|
* launching a feature that is planned to be updated via a remote config, do not forget to
|
|
|
|
* remove the entry!
|
2020-01-24 20:29:03 +00:00
|
|
|
*
|
|
|
|
* Other interesting things you can do:
|
|
|
|
* - Make a flag {@link #HOT_SWAPPABLE}
|
|
|
|
* - Make a flag {@link #STICKY}
|
2019-09-06 14:09:34 +00:00
|
|
|
*/
|
2019-12-19 22:41:21 +00:00
|
|
|
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 String UUIDS = generateKey("uuids");
|
|
|
|
private static final String PROFILE_DISPLAY = generateKey("profileDisplay");
|
|
|
|
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 REACTION_SENDING = generateKey("reactionSending");
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Values in this map will take precedence over any value. If you do not wish to have any sort of
|
|
|
|
* override, simply don't put a value in this map. You should never commit additions to this map
|
|
|
|
* for flags that you plan on updating remotely.
|
|
|
|
*/
|
|
|
|
private static final Map<String, Boolean> FORCED_VALUES = new HashMap<String, Boolean>() {{
|
|
|
|
put(UUIDS, false);
|
|
|
|
put(PROFILE_DISPLAY, false);
|
|
|
|
put(MESSAGE_REQUESTS, false);
|
|
|
|
put(USERNAMES, false);
|
|
|
|
put(STORAGE_SERVICE, false);
|
|
|
|
}};
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
/**
|
|
|
|
* By default, flags are only updated once at app start. This is to ensure that values don't
|
|
|
|
* change within an app session, simplifying logic. However, given that this can delay how often
|
|
|
|
* a flag is updated, you can put a flag in here to mark it as 'hot swappable'. Flags in this set
|
|
|
|
* will be updated arbitrarily at runtime. This will make values more responsive, but also places
|
|
|
|
* more burden on the reader to ensure that the app experience remains consistent.
|
|
|
|
*/
|
|
|
|
private static final Set<String> HOT_SWAPPABLE = new TreeSet<String>() {{
|
|
|
|
}};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flags in this set will stay true forever once they receive a true value from a remote config.
|
|
|
|
*/
|
|
|
|
private static final Set<String> STICKY = new HashSet<String>() {{
|
|
|
|
}};
|
|
|
|
|
|
|
|
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>();
|
2019-12-19 22:41:21 +00:00
|
|
|
|
|
|
|
private FeatureFlags() {}
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized void init() {
|
2019-12-19 22:41:21 +00:00
|
|
|
REMOTE_VALUES.putAll(parseStoredConfig());
|
2020-01-24 20:29:03 +00:00
|
|
|
Log.i(TAG, "init() " + REMOTE_VALUES.toString());
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized void refresh() {
|
|
|
|
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.getRemoteConfigLastFetchTime();
|
2019-12-19 22:41:21 +00:00
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
if (timeSinceLastFetch > FETCH_INTERVAL) {
|
|
|
|
Log.i(TAG, "Scheduling remote config refresh.");
|
|
|
|
ApplicationDependencies.getJobManager().add(new RemoteConfigRefreshJob());
|
|
|
|
} else {
|
|
|
|
Log.i(TAG, "Skipping remote config refresh. Refreshed " + timeSinceLastFetch + " ms ago.");
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
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, HOT_SWAPPABLE, STICKY);
|
|
|
|
|
|
|
|
SignalStore.setRemoteConfig(mapToJson(result.getDisk()).toString());
|
|
|
|
REMOTE_VALUES.clear();
|
|
|
|
REMOTE_VALUES.putAll(result.getMemory());
|
|
|
|
|
|
|
|
Log.i(TAG, "[Memory] Before: " + memory.toString());
|
|
|
|
Log.i(TAG, "[Memory] After : " + result.getMemory().toString());
|
|
|
|
Log.i(TAG, "[Disk] Before: " + disk.toString());
|
|
|
|
Log.i(TAG, "[Disk] After : " + result.getDisk().toString());
|
|
|
|
}
|
|
|
|
|
2019-09-07 03:40:06 +00:00
|
|
|
/** UUID-related stuff that shouldn't be activated until the user-facing launch. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean uuids() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return getValue(UUIDS, false);
|
|
|
|
}
|
2019-10-09 16:57:36 +00:00
|
|
|
|
2019-10-29 00:16:11 +00:00
|
|
|
/** Favoring profile names when displaying contacts. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean profileDisplay() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return getValue(PROFILE_DISPLAY, false);
|
|
|
|
}
|
2019-11-19 16:01:07 +00:00
|
|
|
|
|
|
|
/** MessageRequest stuff */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean messageRequests() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return getValue(MESSAGE_REQUESTS, false);
|
|
|
|
}
|
2019-10-29 00:16:11 +00:00
|
|
|
|
2019-12-19 22:41:21 +00:00
|
|
|
/** Creating usernames, sending messages by username. Requires {@link #uuids()}. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean usernames() {
|
2019-12-19 22:41:21 +00:00
|
|
|
boolean value = getValue(USERNAMES, false);
|
|
|
|
if (value && !uuids()) throw new MissingFlagRequirementError();
|
|
|
|
return value;
|
|
|
|
}
|
2019-12-03 17:31:23 +00:00
|
|
|
|
2020-01-23 21:49:19 +00:00
|
|
|
/** Storage service. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean storageService() {
|
2020-01-23 21:49:19 +00:00
|
|
|
return getValue(STORAGE_SERVICE, false);
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
2019-09-26 14:12:51 +00:00
|
|
|
|
2019-12-03 21:57:21 +00:00
|
|
|
/** Send support for reactions. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean reactionSending() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return getValue(REACTION_SENDING, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Only for rendering debug info. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized @NonNull Map<String, Boolean> getRemoteValues() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return new TreeMap<>(REMOTE_VALUES);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Only for rendering debug info. */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized @NonNull Map<String, Boolean> getForcedValues() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return new TreeMap<>(FORCED_VALUES);
|
|
|
|
}
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
@VisibleForTesting
|
|
|
|
static @NonNull UpdateResult updateInternal(@NonNull Map<String, Boolean> remote,
|
|
|
|
@NonNull Map<String, Boolean> localMemory,
|
|
|
|
@NonNull Map<String, Boolean> localDisk,
|
|
|
|
@NonNull Set<String> hotSwap,
|
|
|
|
@NonNull Set<String> sticky)
|
|
|
|
{
|
|
|
|
Map<String, Boolean> newMemory = new TreeMap<>(localMemory);
|
|
|
|
Map<String, Boolean> newDisk = new TreeMap<>(localDisk);
|
|
|
|
|
|
|
|
Set<String> allKeys = new HashSet<>();
|
|
|
|
allKeys.addAll(remote.keySet());
|
|
|
|
allKeys.addAll(localDisk.keySet());
|
|
|
|
allKeys.addAll(localMemory.keySet());
|
|
|
|
|
|
|
|
Stream.of(allKeys)
|
|
|
|
.filter(k -> k.startsWith(PREFIX))
|
|
|
|
.forEach(key -> {
|
|
|
|
Boolean remoteValue = remote.get(key);
|
|
|
|
Boolean diskValue = localDisk.get(key);
|
|
|
|
Boolean newValue = remoteValue;
|
|
|
|
|
|
|
|
if (sticky.contains(key)) {
|
|
|
|
newValue = diskValue == Boolean.TRUE ? Boolean.TRUE : newValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newValue != null) {
|
|
|
|
newDisk.put(key, newValue);
|
|
|
|
} else {
|
|
|
|
newDisk.remove(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hotSwap.contains(key)) {
|
|
|
|
if (newValue != null) {
|
|
|
|
newMemory.put(key, newValue);
|
|
|
|
} else {
|
|
|
|
newMemory.remove(key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return new UpdateResult(newMemory, newDisk);
|
|
|
|
}
|
|
|
|
|
2019-12-19 22:41:21 +00:00
|
|
|
private static @NonNull String generateKey(@NonNull String key) {
|
|
|
|
return PREFIX + key;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static boolean getValue(@NonNull String key, boolean defaultValue) {
|
|
|
|
Boolean forced = FORCED_VALUES.get(key);
|
|
|
|
if (forced != null) {
|
|
|
|
return forced;
|
|
|
|
}
|
|
|
|
|
|
|
|
Boolean remote = REMOTE_VALUES.get(key);
|
|
|
|
if (remote != null) {
|
|
|
|
return remote;
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Map<String, Boolean> parseStoredConfig() {
|
|
|
|
Map<String, Boolean> parsed = new HashMap<>();
|
|
|
|
String stored = SignalStore.getRemoteConfig();
|
|
|
|
|
|
|
|
if (TextUtils.isEmpty(stored)) {
|
|
|
|
Log.i(TAG, "No remote config stored. Skipping.");
|
|
|
|
return parsed;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
JSONObject root = new JSONObject(stored);
|
|
|
|
Iterator<String> iter = root.keys();
|
|
|
|
|
|
|
|
while (iter.hasNext()) {
|
|
|
|
String key = iter.next();
|
|
|
|
parsed.put(key, root.getBoolean(key));
|
|
|
|
}
|
|
|
|
} catch (JSONException e) {
|
|
|
|
SignalStore.setRemoteConfig(null);
|
|
|
|
throw new AssertionError("Failed to parse! Cleared storage.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
}
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
private static JSONObject mapToJson(@NonNull Map<String, Boolean> map) {
|
|
|
|
try {
|
|
|
|
JSONObject json = new JSONObject();
|
|
|
|
|
|
|
|
for (Map.Entry<String, Boolean> entry : map.entrySet()) {
|
|
|
|
json.put(entry.getKey(), (boolean) entry.getValue());
|
|
|
|
}
|
|
|
|
|
|
|
|
return json;
|
|
|
|
} catch (JSONException e) {
|
|
|
|
throw new AssertionError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 22:41:21 +00:00
|
|
|
private static final class MissingFlagRequirementError extends Error {
|
|
|
|
}
|
2020-01-24 20:29:03 +00:00
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
static final class UpdateResult {
|
|
|
|
private final Map<String, Boolean> memory;
|
|
|
|
private final Map<String, Boolean> disk;
|
|
|
|
|
|
|
|
UpdateResult(@NonNull Map<String, Boolean> memory, @NonNull Map<String, Boolean> disk) {
|
|
|
|
this.memory = memory;
|
|
|
|
this.disk = disk;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Map<String, Boolean> getMemory() {
|
|
|
|
return memory;
|
|
|
|
}
|
|
|
|
|
|
|
|
public @NonNull Map<String, Boolean> getDisk() {
|
|
|
|
return disk;
|
|
|
|
}
|
|
|
|
}
|
2019-09-26 14:12:51 +00:00
|
|
|
}
|