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;
|
2020-01-24 23:38:48 +00:00
|
|
|
import com.google.android.collect.Sets;
|
2019-12-19 22:41:21 +00:00
|
|
|
|
|
|
|
import org.json.JSONException;
|
|
|
|
import org.json.JSONObject;
|
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
2020-05-26 19:01:01 +00:00
|
|
|
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
2020-05-11 14:22:32 +00:00
|
|
|
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
2019-12-19 22:41:21 +00:00
|
|
|
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;
|
|
|
|
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:
|
2020-02-21 18:52:27 +00:00
|
|
|
* - Create a new string constant. This should almost certainly be prefixed with "android."
|
2020-04-27 21:17:34 +00:00
|
|
|
* - Add a method to retrieve the value using {@link #getBoolean(String, boolean)}. You can also add
|
2019-12-19 22:41:21 +00:00
|
|
|
* other checks here, like requiring other flags.
|
2020-02-06 02:53:04 +00:00
|
|
|
* - If you want to be able to change a flag remotely, place it in {@link #REMOTE_CAPABLE}.
|
|
|
|
* - If you would like to force a value for testing, place an entry in {@link #FORCED_VALUES}.
|
|
|
|
* Do not commit changes to this map!
|
2020-01-24 20:29:03 +00:00
|
|
|
*
|
|
|
|
* Other interesting things you can do:
|
|
|
|
* - Make a flag {@link #HOT_SWAPPABLE}
|
2020-04-27 21:17:34 +00:00
|
|
|
* - Make a flag {@link #STICKY} -- booleans only!
|
2020-02-21 18:52:27 +00:00
|
|
|
* - Register a listener for flag changes in {@link #FLAG_CHANGE_LISTENERS}
|
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);
|
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
|
2019-12-19 22:41:21 +00:00
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
private static final String UUIDS = "android.uuids";
|
|
|
|
private static final String MESSAGE_REQUESTS = "android.messageRequests";
|
|
|
|
private static final String USERNAMES = "android.usernames";
|
2020-04-13 14:19:46 +00:00
|
|
|
private static final String PINS_FOR_ALL_LEGACY = "android.pinsForAll";
|
|
|
|
private static final String PINS_FOR_ALL = "android.pinsForAll.2";
|
2020-04-01 23:55:18 +00:00
|
|
|
private static final String PINS_FOR_ALL_MANDATORY = "android.pinsForAllMandatory";
|
2020-02-21 18:52:27 +00:00
|
|
|
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
|
|
|
|
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
2020-04-06 00:32:06 +00:00
|
|
|
private static final String ATTACHMENTS_V3 = "android.attachmentsV3";
|
2020-04-15 18:56:58 +00:00
|
|
|
private static final String REMOTE_DELETE = "android.remoteDelete";
|
2020-04-23 19:20:59 +00:00
|
|
|
private static final String PROFILE_FOR_CALLING = "android.profileForCalling";
|
|
|
|
private static final String CALLING_PIP = "android.callingPip";
|
2020-05-05 17:53:57 +00:00
|
|
|
private static final String REACT_WITH_ANY_EMOJI = "android.reactWithAnyEmoji";
|
2020-05-26 19:01:01 +00:00
|
|
|
private static final String NEW_GROUP_UI = "android.newGroupUI";
|
|
|
|
private static final String VERSIONED_PROFILES = "android.versionedProfiles";
|
2020-05-08 13:46:16 +00:00
|
|
|
private static final String GROUPS_V2 = "android.groupsv2";
|
2020-05-08 16:04:04 +00:00
|
|
|
private static final String GROUPS_V2_CREATE = "android.groupsv2.create";
|
2020-05-29 15:20:07 +00:00
|
|
|
private static final String GROUPS_V2_CAPACITY = "android.groupsv2.capacity";
|
2019-12-19 22:41:21 +00:00
|
|
|
|
|
|
|
/**
|
2020-02-06 02:53:04 +00:00
|
|
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
|
|
|
* remotely, place it in here.
|
2019-12-19 22:41:21 +00:00
|
|
|
*/
|
2020-02-06 02:53:04 +00:00
|
|
|
|
2020-02-06 02:53:04 +00:00
|
|
|
private static final Set<String> REMOTE_CAPABLE = Sets.newHashSet(
|
2020-04-13 14:19:46 +00:00
|
|
|
PINS_FOR_ALL_LEGACY,
|
2020-02-06 02:02:18 +00:00
|
|
|
PINS_FOR_ALL,
|
2020-04-01 23:55:18 +00:00
|
|
|
PINS_FOR_ALL_MANDATORY,
|
2020-02-06 21:01:26 +00:00
|
|
|
PINS_MEGAPHONE_KILL_SWITCH,
|
2020-02-19 22:08:34 +00:00
|
|
|
PROFILE_NAMES_MEGAPHONE,
|
2020-02-10 18:42:43 +00:00
|
|
|
MESSAGE_REQUESTS,
|
2020-04-15 18:56:58 +00:00
|
|
|
ATTACHMENTS_V3,
|
2020-04-23 19:20:59 +00:00
|
|
|
REMOTE_DELETE,
|
|
|
|
PROFILE_FOR_CALLING,
|
|
|
|
CALLING_PIP,
|
2020-05-05 17:53:57 +00:00
|
|
|
NEW_GROUP_UI,
|
2020-05-26 19:01:01 +00:00
|
|
|
REACT_WITH_ANY_EMOJI,
|
2020-05-26 19:17:41 +00:00
|
|
|
VERSIONED_PROFILES,
|
|
|
|
GROUPS_V2,
|
|
|
|
GROUPS_V2_CREATE,
|
2020-05-29 15:20:07 +00:00
|
|
|
GROUPS_V2_CAPACITY,
|
2020-05-26 19:17:41 +00:00
|
|
|
NEW_GROUP_UI
|
2020-02-06 02:53:04 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Values in this map will take precedence over any value. This should only be used for local
|
|
|
|
* development. Given that you specify a default when retrieving a value, and that we only store
|
|
|
|
* remote values for things in {@link #REMOTE_CAPABLE}, there should be no need to ever *commit*
|
|
|
|
* an addition to this map.
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
2020-04-27 21:17:34 +00:00
|
|
|
private static final Map<String, Object> FORCED_VALUES = new HashMap<String, Object>() {{
|
2019-12-19 22:41:21 +00:00
|
|
|
}};
|
|
|
|
|
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.
|
|
|
|
*/
|
2020-01-24 23:38:48 +00:00
|
|
|
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
|
2020-02-10 18:42:43 +00:00
|
|
|
PINS_MEGAPHONE_KILL_SWITCH,
|
2020-05-05 17:53:57 +00:00
|
|
|
ATTACHMENTS_V3,
|
|
|
|
REACT_WITH_ANY_EMOJI
|
2020-01-24 23:38:48 +00:00
|
|
|
);
|
2020-01-24 20:29:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Flags in this set will stay true forever once they receive a true value from a remote config.
|
|
|
|
*/
|
2020-01-24 23:38:48 +00:00
|
|
|
private static final Set<String> STICKY = Sets.newHashSet(
|
2020-04-13 14:19:46 +00:00
|
|
|
PINS_FOR_ALL_LEGACY,
|
2020-05-08 13:46:16 +00:00
|
|
|
PINS_FOR_ALL,
|
2020-05-26 19:01:01 +00:00
|
|
|
VERSIONED_PROFILES,
|
2020-05-08 13:46:16 +00:00
|
|
|
GROUPS_V2
|
2020-01-24 23:38:48 +00:00
|
|
|
);
|
2020-01-24 20:29:03 +00:00
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
/**
|
|
|
|
* 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>() {{
|
2020-05-26 19:01:01 +00:00
|
|
|
put(MESSAGE_REQUESTS, (change) -> SignalStore.setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0));
|
|
|
|
put(VERSIONED_PROFILES, (change) -> ApplicationDependencies.getJobManager().add(new ProfileUploadJob()));
|
|
|
|
put(GROUPS_V2, (change) -> ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()));
|
2020-02-21 18:52:27 +00:00
|
|
|
}};
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
private static final Map<String, Object> 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() {
|
2020-04-27 21:17:34 +00:00
|
|
|
Map<String, Object> current = parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig());
|
|
|
|
Map<String, Object> pending = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
|
|
|
|
Map<String, Change> changes = computeChanges(current, pending);
|
2020-02-21 18:52:27 +00:00
|
|
|
|
|
|
|
SignalStore.remoteConfigValues().setCurrentConfig(mapToJson(pending));
|
|
|
|
REMOTE_VALUES.putAll(pending);
|
|
|
|
triggerFlagChangeListeners(changes);
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
Log.i(TAG, "init() " + REMOTE_VALUES.toString());
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
public static synchronized void refreshIfNecessary() {
|
|
|
|
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.remoteConfigValues().getLastFetchTime();
|
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-04-27 21:17:34 +00:00
|
|
|
public static synchronized void update(@NonNull Map<String, Object> config) {
|
|
|
|
Map<String, Object> memory = REMOTE_VALUES;
|
|
|
|
Map<String, Object> disk = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
|
|
|
|
UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY);
|
2020-01-24 20:29:03 +00:00
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
SignalStore.remoteConfigValues().setPendingConfig(mapToJson(result.getDisk()));
|
2020-01-24 20:29:03 +00:00
|
|
|
REMOTE_VALUES.clear();
|
|
|
|
REMOTE_VALUES.putAll(result.getMemory());
|
2020-02-28 22:33:19 +00:00
|
|
|
triggerFlagChangeListeners(result.getMemoryChanges());
|
2020-02-21 18:52:27 +00:00
|
|
|
|
|
|
|
SignalStore.remoteConfigValues().setLastFetchTime(System.currentTimeMillis());
|
2020-01-24 20:29:03 +00:00
|
|
|
|
|
|
|
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() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(UUIDS, false);
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
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() {
|
2020-02-19 22:08:34 +00:00
|
|
|
return messageRequests();
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
2019-11-19 16:01:07 +00:00
|
|
|
|
|
|
|
/** MessageRequest stuff */
|
2020-01-24 20:29:03 +00:00
|
|
|
public static synchronized boolean messageRequests() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(MESSAGE_REQUESTS, false);
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
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() {
|
2020-04-27 21:17:34 +00:00
|
|
|
boolean value = getBoolean(USERNAMES, false);
|
2019-12-19 22:41:21 +00:00
|
|
|
if (value && !uuids()) throw new MissingFlagRequirementError();
|
|
|
|
return value;
|
|
|
|
}
|
2019-12-03 17:31:23 +00:00
|
|
|
|
2020-04-02 21:09:25 +00:00
|
|
|
/**
|
|
|
|
* - Starts showing prompts for users to create PINs.
|
|
|
|
* - Shows new reminder UI.
|
|
|
|
* - Shows new settings UI.
|
|
|
|
* - Syncs to storage service.
|
|
|
|
*/
|
2020-01-30 20:23:29 +00:00
|
|
|
public static boolean pinsForAll() {
|
2020-02-06 02:02:18 +00:00
|
|
|
return SignalStore.registrationValues().pinWasRequiredAtRegistration() ||
|
2020-04-02 21:09:25 +00:00
|
|
|
SignalStore.kbsValues().isV2RegistrationLockEnabled() ||
|
2020-04-24 23:40:50 +00:00
|
|
|
SignalStore.kbsValues().hasPin() ||
|
2020-04-01 23:55:18 +00:00
|
|
|
pinsForAllMandatory() ||
|
2020-04-27 21:17:34 +00:00
|
|
|
getBoolean(PINS_FOR_ALL_LEGACY, false) ||
|
|
|
|
getBoolean(PINS_FOR_ALL, false);
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
|
2020-04-01 23:55:18 +00:00
|
|
|
/** Makes it so the user will eventually see a fullscreen splash requiring them to create a PIN. */
|
|
|
|
public static boolean pinsForAllMandatory() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(PINS_FOR_ALL_MANDATORY, false);
|
2020-04-01 23:55:18 +00:00
|
|
|
}
|
|
|
|
|
2020-01-31 14:50:43 +00:00
|
|
|
/** Safety flag to disable Pins for All Megaphone */
|
|
|
|
public static boolean pinsForAllMegaphoneKillSwitch() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(PINS_MEGAPHONE_KILL_SWITCH, false);
|
2020-01-31 14:50:43 +00:00
|
|
|
}
|
|
|
|
|
2020-02-06 21:01:26 +00:00
|
|
|
/** Safety switch for disabling profile names megaphone */
|
2020-02-19 22:08:34 +00:00
|
|
|
public static boolean profileNamesMegaphone() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(PROFILE_NAMES_MEGAPHONE, false) &&
|
2020-02-06 21:01:26 +00:00
|
|
|
TextSecurePreferences.getFirstInstallVersion(ApplicationDependencies.getApplication()) < 600;
|
|
|
|
}
|
|
|
|
|
2020-04-06 00:32:06 +00:00
|
|
|
/** Whether or not we use the attachments v3 form. */
|
|
|
|
public static boolean attachmentsV3() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(ATTACHMENTS_V3, false);
|
2020-04-06 00:32:06 +00:00
|
|
|
}
|
|
|
|
|
2020-04-15 18:56:58 +00:00
|
|
|
/** Send support for remotely deleting a message. */
|
|
|
|
public static boolean remoteDelete() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(REMOTE_DELETE, false);
|
2020-04-15 18:56:58 +00:00
|
|
|
}
|
|
|
|
|
2020-04-23 19:20:59 +00:00
|
|
|
/** Whether or not profile sharing is required for calling */
|
|
|
|
public static boolean profileForCalling() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return messageRequests() && getBoolean(PROFILE_FOR_CALLING, false);
|
2020-04-23 19:20:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Whether or not to display Calling PIP */
|
|
|
|
public static boolean callingPip() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(CALLING_PIP, false);
|
2020-04-23 19:20:59 +00:00
|
|
|
}
|
|
|
|
|
2020-04-24 13:13:07 +00:00
|
|
|
/** New group UI elements. */
|
|
|
|
public static boolean newGroupUI() {
|
2020-04-27 21:17:34 +00:00
|
|
|
return getBoolean(NEW_GROUP_UI, false);
|
2020-04-24 13:13:07 +00:00
|
|
|
}
|
|
|
|
|
2020-05-05 17:53:57 +00:00
|
|
|
/** React with Any Emoji */
|
|
|
|
public static boolean reactWithAnyEmoji() {
|
|
|
|
return getBoolean(REACT_WITH_ANY_EMOJI, false);
|
|
|
|
}
|
|
|
|
|
2020-05-26 19:01:01 +00:00
|
|
|
/** Read and write versioned profile information. */
|
|
|
|
public static boolean versionedProfiles() {
|
|
|
|
return getBoolean(VERSIONED_PROFILES, false);
|
|
|
|
}
|
|
|
|
|
2020-05-08 13:46:16 +00:00
|
|
|
/** Groups v2 send and receive. */
|
|
|
|
public static boolean groupsV2() {
|
2020-05-26 19:01:01 +00:00
|
|
|
return versionedProfiles() && getBoolean(GROUPS_V2, false);
|
2020-05-08 13:46:16 +00:00
|
|
|
}
|
|
|
|
|
2020-05-08 16:04:04 +00:00
|
|
|
/** Groups v2 send and receive. */
|
|
|
|
public static boolean groupsV2create() {
|
|
|
|
return groupsV2() && getBoolean(GROUPS_V2_CREATE, false);
|
|
|
|
}
|
|
|
|
|
2020-05-29 15:20:07 +00:00
|
|
|
/**
|
|
|
|
* Maximum number of members allowed in a group.
|
|
|
|
*/
|
|
|
|
public static int gv2GroupCapacity() {
|
|
|
|
return getInteger(GROUPS_V2_CAPACITY, 100);
|
|
|
|
}
|
|
|
|
|
2019-12-19 22:41:21 +00:00
|
|
|
/** Only for rendering debug info. */
|
2020-04-27 21:17:34 +00:00
|
|
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return new TreeMap<>(REMOTE_VALUES);
|
|
|
|
}
|
|
|
|
|
2020-02-06 02:53:04 +00:00
|
|
|
/** Only for rendering debug info. */
|
2020-04-27 21:17:34 +00:00
|
|
|
public static synchronized @NonNull Map<String, Object> getDiskValues() {
|
2020-02-21 18:52:27 +00:00
|
|
|
return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig()));
|
2020-02-06 02:53:04 +00:00
|
|
|
}
|
|
|
|
|
2019-12-19 22:41:21 +00:00
|
|
|
/** Only for rendering debug info. */
|
2020-04-27 21:17:34 +00:00
|
|
|
public static synchronized @NonNull Map<String, Object> getPendingDiskValues() {
|
|
|
|
return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig()));
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Only for rendering debug info. */
|
|
|
|
public static synchronized @NonNull Map<String, Object> getForcedValues() {
|
2019-12-19 22:41:21 +00:00
|
|
|
return new TreeMap<>(FORCED_VALUES);
|
|
|
|
}
|
|
|
|
|
2020-01-24 20:29:03 +00:00
|
|
|
@VisibleForTesting
|
2020-04-27 21:17:34 +00:00
|
|
|
static @NonNull UpdateResult updateInternal(@NonNull Map<String, Object> remote,
|
|
|
|
@NonNull Map<String, Object> localMemory,
|
|
|
|
@NonNull Map<String, Object> localDisk,
|
|
|
|
@NonNull Set<String> remoteCapable,
|
|
|
|
@NonNull Set<String> hotSwap,
|
|
|
|
@NonNull Set<String> sticky)
|
2020-01-24 20:29:03 +00:00
|
|
|
{
|
2020-04-27 21:17:34 +00:00
|
|
|
Map<String, Object> newMemory = new TreeMap<>(localMemory);
|
|
|
|
Map<String, Object> newDisk = new TreeMap<>(localDisk);
|
2020-01-24 20:29:03 +00:00
|
|
|
|
|
|
|
Set<String> allKeys = new HashSet<>();
|
|
|
|
allKeys.addAll(remote.keySet());
|
|
|
|
allKeys.addAll(localDisk.keySet());
|
|
|
|
allKeys.addAll(localMemory.keySet());
|
|
|
|
|
|
|
|
Stream.of(allKeys)
|
2020-02-06 02:53:04 +00:00
|
|
|
.filter(remoteCapable::contains)
|
2020-01-24 20:29:03 +00:00
|
|
|
.forEach(key -> {
|
2020-04-27 21:17:34 +00:00
|
|
|
Object remoteValue = remote.get(key);
|
|
|
|
Object diskValue = localDisk.get(key);
|
|
|
|
Object newValue = remoteValue;
|
|
|
|
|
|
|
|
if (newValue != null && diskValue != null && newValue.getClass() != diskValue.getClass()) {
|
|
|
|
Log.w(TAG, "Type mismatch! key: " + key);
|
|
|
|
|
|
|
|
newDisk.remove(key);
|
|
|
|
|
|
|
|
if (hotSwap.contains(key)) {
|
|
|
|
newMemory.remove(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
2020-01-24 20:29:03 +00:00
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
if (sticky.contains(key) && (newValue instanceof Boolean || diskValue instanceof Boolean)) {
|
2020-01-24 20:29:03 +00:00
|
|
|
newValue = diskValue == Boolean.TRUE ? Boolean.TRUE : newValue;
|
2020-04-27 21:17:34 +00:00
|
|
|
} else if (sticky.contains(key)) {
|
|
|
|
Log.w(TAG, "Tried to make a non-boolean sticky! Ignoring. (key: " + key + ")");
|
2020-01-24 20:29:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-02-28 22:33:19 +00:00
|
|
|
Stream.of(allKeys)
|
|
|
|
.filterNot(remoteCapable::contains)
|
|
|
|
.filterNot(key -> sticky.contains(key) && localDisk.get(key) == Boolean.TRUE)
|
|
|
|
.forEach(key -> {
|
|
|
|
newDisk.remove(key);
|
|
|
|
|
|
|
|
if (hotSwap.contains(key)) {
|
|
|
|
newMemory.remove(key);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
return new UpdateResult(newMemory, newDisk, computeChanges(localMemory, newMemory));
|
2020-01-24 20:29:03 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
@VisibleForTesting
|
2020-04-27 21:17:34 +00:00
|
|
|
static @NonNull Map<String, Change> computeChanges(@NonNull Map<String, Object> oldMap, @NonNull Map<String, Object> newMap) {
|
2020-02-21 18:52:27 +00:00
|
|
|
Map<String, Change> changes = new HashMap<>();
|
|
|
|
Set<String> allKeys = new HashSet<>();
|
|
|
|
|
|
|
|
allKeys.addAll(oldMap.keySet());
|
|
|
|
allKeys.addAll(newMap.keySet());
|
|
|
|
|
|
|
|
for (String key : allKeys) {
|
2020-04-27 21:17:34 +00:00
|
|
|
Object oldValue = oldMap.get(key);
|
|
|
|
Object newValue = newMap.get(key);
|
2020-02-21 18:52:27 +00:00
|
|
|
|
|
|
|
if (oldValue == null && newValue == null) {
|
|
|
|
throw new AssertionError("Should not be possible.");
|
|
|
|
} else if (oldValue != null && newValue == null) {
|
|
|
|
changes.put(key, Change.REMOVED);
|
2020-04-27 21:17:34 +00:00
|
|
|
} else if (newValue != oldValue && newValue instanceof Boolean) {
|
|
|
|
changes.put(key, (boolean) newValue ? Change.ENABLED : Change.DISABLED);
|
2020-02-21 18:52:27 +00:00
|
|
|
} else if (newValue != oldValue) {
|
2020-04-27 21:17:34 +00:00
|
|
|
changes.put(key, Change.CHANGED);
|
2020-02-21 18:52:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return changes;
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
private static boolean getBoolean(@NonNull String key, boolean defaultValue) {
|
|
|
|
Boolean forced = (Boolean) FORCED_VALUES.get(key);
|
2019-12-19 22:41:21 +00:00
|
|
|
if (forced != null) {
|
|
|
|
return forced;
|
|
|
|
}
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
Object remote = REMOTE_VALUES.get(key);
|
|
|
|
if (remote instanceof Boolean) {
|
|
|
|
return (boolean) remote;
|
|
|
|
} else if (remote != null) {
|
|
|
|
Log.w(TAG, "Expected a boolean for key '" + key + "', but got something else! Falling back to the default.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int getInteger(@NonNull String key, int defaultValue) {
|
|
|
|
Integer forced = (Integer) FORCED_VALUES.get(key);
|
|
|
|
if (forced != null) {
|
|
|
|
return forced;
|
|
|
|
}
|
|
|
|
|
|
|
|
String remote = (String) REMOTE_VALUES.get(key);
|
2019-12-19 22:41:21 +00:00
|
|
|
if (remote != null) {
|
2020-04-27 21:17:34 +00:00
|
|
|
try {
|
|
|
|
return Integer.parseInt(remote);
|
|
|
|
} catch (NumberFormatException e) {
|
|
|
|
Log.w(TAG, "Expected an int for key '" + key + "', but got something else! Falling back to the default.");
|
|
|
|
}
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
private static Map<String, Object> parseStoredConfig(String stored) {
|
|
|
|
Map<String, Object> parsed = new HashMap<>();
|
2019-12-19 22:41:21 +00:00
|
|
|
|
|
|
|
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();
|
2020-04-27 21:17:34 +00:00
|
|
|
parsed.put(key, root.get(key));
|
2019-12-19 22:41:21 +00:00
|
|
|
}
|
|
|
|
} catch (JSONException e) {
|
|
|
|
throw new AssertionError("Failed to parse! Cleared storage.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsed;
|
|
|
|
}
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
private static @NonNull String mapToJson(@NonNull Map<String, Object> map) {
|
2020-01-24 20:29:03 +00:00
|
|
|
try {
|
|
|
|
JSONObject json = new JSONObject();
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
|
|
|
json.put(entry.getKey(), entry.getValue());
|
2020-01-24 20:29:03 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
return json.toString();
|
2020-01-24 20:29:03 +00:00
|
|
|
} catch (JSONException e) {
|
|
|
|
throw new AssertionError(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-21 18:52:27 +00:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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 {
|
2020-04-27 21:17:34 +00:00
|
|
|
private final Map<String, Object> memory;
|
|
|
|
private final Map<String, Object> disk;
|
|
|
|
private final Map<String, Change> memoryChanges;
|
2020-01-24 20:29:03 +00:00
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
UpdateResult(@NonNull Map<String, Object> memory, @NonNull Map<String, Object> disk, @NonNull Map<String, Change> memoryChanges) {
|
2020-02-28 22:33:19 +00:00
|
|
|
this.memory = memory;
|
|
|
|
this.disk = disk;
|
|
|
|
this.memoryChanges = memoryChanges;
|
2020-01-24 20:29:03 +00:00
|
|
|
}
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
public @NonNull Map<String, Object> getMemory() {
|
2020-01-24 20:29:03 +00:00
|
|
|
return memory;
|
|
|
|
}
|
|
|
|
|
2020-04-27 21:17:34 +00:00
|
|
|
public @NonNull Map<String, Object> getDisk() {
|
2020-01-24 20:29:03 +00:00
|
|
|
return disk;
|
|
|
|
}
|
2020-02-21 18:52:27 +00:00
|
|
|
|
2020-02-28 22:33:19 +00:00
|
|
|
public @NonNull Map<String, Change> getMemoryChanges() {
|
|
|
|
return memoryChanges;
|
2020-02-21 18:52:27 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
interface OnFlagChange {
|
|
|
|
void onFlagChange(@NonNull Change change);
|
|
|
|
}
|
|
|
|
|
|
|
|
enum Change {
|
2020-04-27 21:17:34 +00:00
|
|
|
ENABLED, DISABLED, CHANGED, REMOVED
|
2020-01-24 20:29:03 +00:00
|
|
|
}
|
2019-09-26 14:12:51 +00:00
|
|
|
}
|