Add support for remote config v1.1

This commit is contained in:
Greyson Parrelli 2020-04-27 17:17:34 -04:00
parent 5eb663aa1b
commit f149005026
6 changed files with 160 additions and 85 deletions

View file

@ -50,7 +50,7 @@ public class RemoteConfigRefreshJob extends BaseJob {
return; return;
} }
Map<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig(); Map<String, Object> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
FeatureFlags.update(config); FeatureFlags.update(config);
} }

View file

@ -20,31 +20,39 @@ public class LogSectionFeatureFlags implements LogSection {
@Override @Override
public @NonNull CharSequence getContent(@NonNull Context context) { public @NonNull CharSequence getContent(@NonNull Context context) {
StringBuilder out = new StringBuilder(); StringBuilder out = new StringBuilder();
Map<String, Boolean> memory = FeatureFlags.getMemoryValues(); Map<String, Object> memory = FeatureFlags.getMemoryValues();
Map<String, Boolean> disk = FeatureFlags.getDiskValues(); Map<String, Object> disk = FeatureFlags.getDiskValues();
Map<String, Boolean> forced = FeatureFlags.getForcedValues(); Map<String, Object> pending = FeatureFlags.getPendingDiskValues();
int remoteLength = Stream.of(memory.keySet()).map(String::length).max(Integer::compareTo).orElse(0); Map<String, Object> forced = FeatureFlags.getForcedValues();
int diskLength = Stream.of(disk.keySet()).map(String::length).max(Integer::compareTo).orElse(0); int remoteLength = Stream.of(memory.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int forcedLength = Stream.of(forced.keySet()).map(String::length).max(Integer::compareTo).orElse(0); int diskLength = Stream.of(disk.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int pendingLength = Stream.of(pending.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int forcedLength = Stream.of(forced.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
out.append("-- Memory\n"); out.append("-- Memory\n");
for (Map.Entry<String, Boolean> entry : memory.entrySet()) { for (Map.Entry<String, Object> entry : memory.entrySet()) {
out.append(Util.rightPad(entry.getKey(), remoteLength)).append(": ").append(entry.getValue()).append("\n"); out.append(Util.rightPad(entry.getKey(), remoteLength)).append(": ").append(entry.getValue()).append("\n");
} }
out.append("\n"); out.append("\n");
out.append("-- Disk\n"); out.append("-- Current Disk\n");
for (Map.Entry<String, Boolean> entry : disk.entrySet()) { for (Map.Entry<String, Object> entry : disk.entrySet()) {
out.append(Util.rightPad(entry.getKey(), diskLength)).append(": ").append(entry.getValue()).append("\n"); out.append(Util.rightPad(entry.getKey(), diskLength)).append(": ").append(entry.getValue()).append("\n");
} }
out.append("\n"); out.append("\n");
out.append("-- Pending Disk\n");
for (Map.Entry<String, Object> entry : pending.entrySet()) {
out.append(Util.rightPad(entry.getKey(), pendingLength)).append(": ").append(entry.getValue()).append("\n");
}
out.append("\n");
out.append("-- Forced\n"); out.append("-- Forced\n");
if (forced.isEmpty()) { if (forced.isEmpty()) {
out.append("None\n"); out.append("None\n");
} else { } else {
for (Map.Entry<String, Boolean> entry : forced.entrySet()) { for (Map.Entry<String, Object> entry : forced.entrySet()) {
out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n"); out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n");
} }
} }

View file

@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit;
* *
* When creating a new flag: * When creating a new flag:
* - Create a new string constant. This should almost certainly be prefixed with "android." * - 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 * - Add a method to retrieve the value using {@link #getBoolean(String, boolean)}. You can also add
* other checks here, like requiring other flags. * other checks here, like requiring other flags.
* - If you want to be able to change a flag remotely, place it in {@link #REMOTE_CAPABLE}. * - 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}. * - If you would like to force a value for testing, place an entry in {@link #FORCED_VALUES}.
@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit;
* *
* Other interesting things you can do: * Other interesting things you can do:
* - Make a flag {@link #HOT_SWAPPABLE} * - Make a flag {@link #HOT_SWAPPABLE}
* - Make a flag {@link #STICKY} * - Make a flag {@link #STICKY} -- booleans only!
* - Register a listener for flag changes in {@link #FLAG_CHANGE_LISTENERS} * - Register a listener for flag changes in {@link #FLAG_CHANGE_LISTENERS}
*/ */
public final class FeatureFlags { public final class FeatureFlags {
@ -86,7 +86,7 @@ public final class FeatureFlags {
* an addition to this map. * an addition to this map.
*/ */
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private static final Map<String, Boolean> FORCED_VALUES = new HashMap<String, Boolean>() {{ private static final Map<String, Object> FORCED_VALUES = new HashMap<String, Object>() {{
}}; }};
/** /**
@ -124,14 +124,14 @@ public final class FeatureFlags {
put(MESSAGE_REQUESTS, (change) -> SignalStore.setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0)); put(MESSAGE_REQUESTS, (change) -> SignalStore.setMessageRequestEnableTime(change == Change.ENABLED ? System.currentTimeMillis() : 0));
}}; }};
private static final Map<String, Boolean> REMOTE_VALUES = new TreeMap<>(); private static final Map<String, Object> REMOTE_VALUES = new TreeMap<>();
private FeatureFlags() {} private FeatureFlags() {}
public static synchronized void init() { public static synchronized void init() {
Map<String, Boolean> current = parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig()); Map<String, Object> current = parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig());
Map<String, Boolean> pending = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig()); Map<String, Object> pending = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
Map<String, Change> changes = computeChanges(current, pending); Map<String, Change> changes = computeChanges(current, pending);
SignalStore.remoteConfigValues().setCurrentConfig(mapToJson(pending)); SignalStore.remoteConfigValues().setCurrentConfig(mapToJson(pending));
REMOTE_VALUES.putAll(pending); REMOTE_VALUES.putAll(pending);
@ -151,10 +151,10 @@ public final class FeatureFlags {
} }
} }
public static synchronized void update(@NonNull Map<String, Boolean> config) { public static synchronized void update(@NonNull Map<String, Object> config) {
Map<String, Boolean> memory = REMOTE_VALUES; Map<String, Object> memory = REMOTE_VALUES;
Map<String, Boolean> disk = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig()); Map<String, Object> disk = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY); UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY);
SignalStore.remoteConfigValues().setPendingConfig(mapToJson(result.getDisk())); SignalStore.remoteConfigValues().setPendingConfig(mapToJson(result.getDisk()));
REMOTE_VALUES.clear(); REMOTE_VALUES.clear();
@ -171,7 +171,7 @@ public final class FeatureFlags {
/** UUID-related stuff that shouldn't be activated until the user-facing launch. */ /** UUID-related stuff that shouldn't be activated until the user-facing launch. */
public static synchronized boolean uuids() { public static synchronized boolean uuids() {
return getValue(UUIDS, false); return getBoolean(UUIDS, false);
} }
/** Favoring profile names when displaying contacts. */ /** Favoring profile names when displaying contacts. */
@ -181,12 +181,12 @@ public final class FeatureFlags {
/** MessageRequest stuff */ /** MessageRequest stuff */
public static synchronized boolean messageRequests() { public static synchronized boolean messageRequests() {
return getValue(MESSAGE_REQUESTS, false); return getBoolean(MESSAGE_REQUESTS, false);
} }
/** Creating usernames, sending messages by username. Requires {@link #uuids()}. */ /** Creating usernames, sending messages by username. Requires {@link #uuids()}. */
public static synchronized boolean usernames() { public static synchronized boolean usernames() {
boolean value = getValue(USERNAMES, false); boolean value = getBoolean(USERNAMES, false);
if (value && !uuids()) throw new MissingFlagRequirementError(); if (value && !uuids()) throw new MissingFlagRequirementError();
return value; return value;
} }
@ -202,76 +202,81 @@ public final class FeatureFlags {
SignalStore.kbsValues().isV2RegistrationLockEnabled() || SignalStore.kbsValues().isV2RegistrationLockEnabled() ||
SignalStore.kbsValues().hasPin() || SignalStore.kbsValues().hasPin() ||
pinsForAllMandatory() || pinsForAllMandatory() ||
getValue(PINS_FOR_ALL_LEGACY, false) || getBoolean(PINS_FOR_ALL_LEGACY, false) ||
getValue(PINS_FOR_ALL, false); getBoolean(PINS_FOR_ALL, false);
} }
/** Makes it so the user will eventually see a fullscreen splash requiring them to create a PIN. */ /** Makes it so the user will eventually see a fullscreen splash requiring them to create a PIN. */
public static boolean pinsForAllMandatory() { public static boolean pinsForAllMandatory() {
return getValue(PINS_FOR_ALL_MANDATORY, false); return getBoolean(PINS_FOR_ALL_MANDATORY, false);
} }
/** Safety flag to disable Pins for All Megaphone */ /** Safety flag to disable Pins for All Megaphone */
public static boolean pinsForAllMegaphoneKillSwitch() { public static boolean pinsForAllMegaphoneKillSwitch() {
return getValue(PINS_MEGAPHONE_KILL_SWITCH, false); return getBoolean(PINS_MEGAPHONE_KILL_SWITCH, false);
} }
/** Safety switch for disabling profile names megaphone */ /** Safety switch for disabling profile names megaphone */
public static boolean profileNamesMegaphone() { public static boolean profileNamesMegaphone() {
return getValue(PROFILE_NAMES_MEGAPHONE, false) && return getBoolean(PROFILE_NAMES_MEGAPHONE, false) &&
TextSecurePreferences.getFirstInstallVersion(ApplicationDependencies.getApplication()) < 600; TextSecurePreferences.getFirstInstallVersion(ApplicationDependencies.getApplication()) < 600;
} }
/** Whether or not we use the attachments v3 form. */ /** Whether or not we use the attachments v3 form. */
public static boolean attachmentsV3() { public static boolean attachmentsV3() {
return getValue(ATTACHMENTS_V3, false); return getBoolean(ATTACHMENTS_V3, false);
} }
/** Send support for remotely deleting a message. */ /** Send support for remotely deleting a message. */
public static boolean remoteDelete() { public static boolean remoteDelete() {
return getValue(REMOTE_DELETE, false); return getBoolean(REMOTE_DELETE, false);
} }
/** Whether or not profile sharing is required for calling */ /** Whether or not profile sharing is required for calling */
public static boolean profileForCalling() { public static boolean profileForCalling() {
return messageRequests() && getValue(PROFILE_FOR_CALLING, false); return messageRequests() && getBoolean(PROFILE_FOR_CALLING, false);
} }
/** Whether or not to display Calling PIP */ /** Whether or not to display Calling PIP */
public static boolean callingPip() { public static boolean callingPip() {
return getValue(CALLING_PIP, false); return getBoolean(CALLING_PIP, false);
} }
/** New group UI elements. */ /** New group UI elements. */
public static boolean newGroupUI() { public static boolean newGroupUI() {
return getValue(NEW_GROUP_UI, false); return getBoolean(NEW_GROUP_UI, false);
} }
/** Only for rendering debug info. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() { public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES); return new TreeMap<>(REMOTE_VALUES);
} }
/** Only for rendering debug info. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getDiskValues() { public static synchronized @NonNull Map<String, Object> getDiskValues() {
return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig())); return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig()));
} }
/** Only for rendering debug info. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Boolean> getForcedValues() { 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() {
return new TreeMap<>(FORCED_VALUES); return new TreeMap<>(FORCED_VALUES);
} }
@VisibleForTesting @VisibleForTesting
static @NonNull UpdateResult updateInternal(@NonNull Map<String, Boolean> remote, static @NonNull UpdateResult updateInternal(@NonNull Map<String, Object> remote,
@NonNull Map<String, Boolean> localMemory, @NonNull Map<String, Object> localMemory,
@NonNull Map<String, Boolean> localDisk, @NonNull Map<String, Object> localDisk,
@NonNull Set<String> remoteCapable, @NonNull Set<String> remoteCapable,
@NonNull Set<String> hotSwap, @NonNull Set<String> hotSwap,
@NonNull Set<String> sticky) @NonNull Set<String> sticky)
{ {
Map<String, Boolean> newMemory = new TreeMap<>(localMemory); Map<String, Object> newMemory = new TreeMap<>(localMemory);
Map<String, Boolean> newDisk = new TreeMap<>(localDisk); Map<String, Object> newDisk = new TreeMap<>(localDisk);
Set<String> allKeys = new HashSet<>(); Set<String> allKeys = new HashSet<>();
allKeys.addAll(remote.keySet()); allKeys.addAll(remote.keySet());
@ -281,12 +286,26 @@ public final class FeatureFlags {
Stream.of(allKeys) Stream.of(allKeys)
.filter(remoteCapable::contains) .filter(remoteCapable::contains)
.forEach(key -> { .forEach(key -> {
Boolean remoteValue = remote.get(key); Object remoteValue = remote.get(key);
Boolean diskValue = localDisk.get(key); Object diskValue = localDisk.get(key);
Boolean newValue = remoteValue; Object newValue = remoteValue;
if (sticky.contains(key)) { 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;
}
if (sticky.contains(key) && (newValue instanceof Boolean || diskValue instanceof Boolean)) {
newValue = diskValue == Boolean.TRUE ? Boolean.TRUE : newValue; newValue = diskValue == Boolean.TRUE ? Boolean.TRUE : newValue;
} else if (sticky.contains(key)) {
Log.w(TAG, "Tried to make a non-boolean sticky! Ignoring. (key: " + key + ")");
} }
if (newValue != null) { if (newValue != null) {
@ -319,7 +338,7 @@ public final class FeatureFlags {
} }
@VisibleForTesting @VisibleForTesting
static @NonNull Map<String, Change> computeChanges(@NonNull Map<String, Boolean> oldMap, @NonNull Map<String, Boolean> newMap) { static @NonNull Map<String, Change> computeChanges(@NonNull Map<String, Object> oldMap, @NonNull Map<String, Object> newMap) {
Map<String, Change> changes = new HashMap<>(); Map<String, Change> changes = new HashMap<>();
Set<String> allKeys = new HashSet<>(); Set<String> allKeys = new HashSet<>();
@ -327,37 +346,59 @@ public final class FeatureFlags {
allKeys.addAll(newMap.keySet()); allKeys.addAll(newMap.keySet());
for (String key : allKeys) { for (String key : allKeys) {
Boolean oldValue = oldMap.get(key); Object oldValue = oldMap.get(key);
Boolean newValue = newMap.get(key); Object newValue = newMap.get(key);
if (oldValue == null && newValue == null) { if (oldValue == null && newValue == null) {
throw new AssertionError("Should not be possible."); throw new AssertionError("Should not be possible.");
} else if (oldValue != null && newValue == null) { } else if (oldValue != null && newValue == null) {
changes.put(key, Change.REMOVED); changes.put(key, Change.REMOVED);
} else if (newValue != oldValue && newValue instanceof Boolean) {
changes.put(key, (boolean) newValue ? Change.ENABLED : Change.DISABLED);
} else if (newValue != oldValue) { } else if (newValue != oldValue) {
changes.put(key, newValue ? Change.ENABLED : Change.DISABLED); changes.put(key, Change.CHANGED);
} }
} }
return changes; return changes;
} }
private static boolean getValue(@NonNull String key, boolean defaultValue) { private static boolean getBoolean(@NonNull String key, boolean defaultValue) {
Boolean forced = FORCED_VALUES.get(key); Boolean forced = (Boolean) FORCED_VALUES.get(key);
if (forced != null) { if (forced != null) {
return forced; return forced;
} }
Boolean remote = REMOTE_VALUES.get(key); Object remote = REMOTE_VALUES.get(key);
if (remote != null) { if (remote instanceof Boolean) {
return remote; 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; return defaultValue;
} }
private static Map<String, Boolean> parseStoredConfig(String stored) { private static int getInteger(@NonNull String key, int defaultValue) {
Map<String, Boolean> parsed = new HashMap<>(); Integer forced = (Integer) FORCED_VALUES.get(key);
if (forced != null) {
return forced;
}
String remote = (String) REMOTE_VALUES.get(key);
if (remote != null) {
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.");
}
}
return defaultValue;
}
private static Map<String, Object> parseStoredConfig(String stored) {
Map<String, Object> parsed = new HashMap<>();
if (TextUtils.isEmpty(stored)) { if (TextUtils.isEmpty(stored)) {
Log.i(TAG, "No remote config stored. Skipping."); Log.i(TAG, "No remote config stored. Skipping.");
@ -370,7 +411,7 @@ public final class FeatureFlags {
while (iter.hasNext()) { while (iter.hasNext()) {
String key = iter.next(); String key = iter.next();
parsed.put(key, root.getBoolean(key)); parsed.put(key, root.get(key));
} }
} catch (JSONException e) { } catch (JSONException e) {
throw new AssertionError("Failed to parse! Cleared storage."); throw new AssertionError("Failed to parse! Cleared storage.");
@ -379,12 +420,12 @@ public final class FeatureFlags {
return parsed; return parsed;
} }
private static @NonNull String mapToJson(@NonNull Map<String, Boolean> map) { private static @NonNull String mapToJson(@NonNull Map<String, Object> map) {
try { try {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
for (Map.Entry<String, Boolean> entry : map.entrySet()) { for (Map.Entry<String, Object> entry : map.entrySet()) {
json.put(entry.getKey(), (boolean) entry.getValue()); json.put(entry.getKey(), entry.getValue());
} }
return json.toString(); return json.toString();
@ -409,21 +450,21 @@ public final class FeatureFlags {
@VisibleForTesting @VisibleForTesting
static final class UpdateResult { static final class UpdateResult {
private final Map<String, Boolean> memory; private final Map<String, Object> memory;
private final Map<String, Boolean> disk; private final Map<String, Object> disk;
private final Map<String, Change> memoryChanges; private final Map<String, Change> memoryChanges;
UpdateResult(@NonNull Map<String, Boolean> memory, @NonNull Map<String, Boolean> disk, @NonNull Map<String, Change> memoryChanges) { UpdateResult(@NonNull Map<String, Object> memory, @NonNull Map<String, Object> disk, @NonNull Map<String, Change> memoryChanges) {
this.memory = memory; this.memory = memory;
this.disk = disk; this.disk = disk;
this.memoryChanges = memoryChanges; this.memoryChanges = memoryChanges;
} }
public @NonNull Map<String, Boolean> getMemory() { public @NonNull Map<String, Object> getMemory() {
return memory; return memory;
} }
public @NonNull Map<String, Boolean> getDisk() { public @NonNull Map<String, Object> getDisk() {
return disk; return disk;
} }
@ -438,7 +479,7 @@ public final class FeatureFlags {
} }
enum Change { enum Change {
ENABLED, DISABLED, REMOVED ENABLED, DISABLED, CHANGED, REMOVED
} }
/** Read and write versioned profile information. */ /** Read and write versioned profile information. */

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.util; package org.thoughtcrime.securesms.util;
import org.junit.Test; import org.junit.Test;
import org.thoughtcrime.securesms.BaseUnitTest;
import org.thoughtcrime.securesms.util.FeatureFlags.Change; import org.thoughtcrime.securesms.util.FeatureFlags.Change;
import org.thoughtcrime.securesms.util.FeatureFlags.UpdateResult; import org.thoughtcrime.securesms.util.FeatureFlags.UpdateResult;
@ -14,23 +15,23 @@ import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
public class FeatureFlagsTest { public class FeatureFlagsTest extends BaseUnitTest {
private static final String A = "A"; private static final String A = "A";
private static final String B = "B"; private static final String B = "B";
@Test @Test
public void updateInternal_newValue_ignoreNotInRemoteCapable() { public void updateInternal_newValue_ignoreNotInRemoteCapable() {
UpdateResult result = FeatureFlags.updateInternal(mapOf("A", true, UpdateResult result = FeatureFlags.updateInternal(mapOf(A, true,
"B", true), B, true),
mapOf(), mapOf(),
mapOf(), mapOf(),
setOf("A"), setOf(A),
setOf(), setOf(),
setOf()); setOf());
assertEquals(mapOf(), result.getMemory()); assertEquals(mapOf(), result.getMemory());
assertEquals(mapOf("A", true), result.getDisk()); assertEquals(mapOf(A, true), result.getDisk());
assertTrue(result.getMemoryChanges().isEmpty()); assertTrue(result.getMemoryChanges().isEmpty());
} }
@ -286,11 +287,25 @@ public class FeatureFlagsTest {
assertEquals(Change.REMOVED, result.getMemoryChanges().get(A)); assertEquals(Change.REMOVED, result.getMemoryChanges().get(A));
} }
@Test
public void updateInternal_removeValue_typeMismatch_hotSwap() {
UpdateResult result = FeatureFlags.updateInternal(mapOf(A, "5"),
mapOf(A, true),
mapOf(A, true),
setOf(A),
setOf(A),
setOf());
assertEquals(mapOf(), result.getMemory());
assertEquals(mapOf(), result.getDisk());
assertEquals(Change.REMOVED, result.getMemoryChanges().get(A));
}
@Test @Test
public void updateInternal_twoNewValues() { public void updateInternal_twoNewValues() {
UpdateResult result = FeatureFlags.updateInternal(mapOf(A, true, UpdateResult result = FeatureFlags.updateInternal(mapOf(A, true,
B, false), B, false),
mapOf(), mapOf(),
mapOf(), mapOf(),
setOf(A, B), setOf(A, B),
setOf(), setOf(),
@ -320,19 +335,22 @@ public class FeatureFlagsTest {
@Test @Test
public void computeChanges_generic() { public void computeChanges_generic() {
Map<String, Boolean> oldMap = new HashMap<String, Boolean>() {{ Map<String, Object> oldMap = new HashMap<String, Object>() {{
put("a", true); put("a", true);
put("b", false); put("b", false);
put("c", true); put("c", true);
put("d", false); put("d", false);
put("g", 5);
put("h", 5);
}}; }};
Map<String, Boolean> newMap = new HashMap<String, Boolean>() {{ Map<String, Object> newMap = new HashMap<String, Object>() {{
put("a", true); put("a", true);
put("b", true); put("b", true);
put("c", false); put("c", false);
put("e", true); put("e", true);
put("f", false); put("f", false);
put("h", 7);
}}; }};
Map<String, Change> changes = FeatureFlags.computeChanges(oldMap, newMap); Map<String, Change> changes = FeatureFlags.computeChanges(oldMap, newMap);
@ -343,6 +361,7 @@ public class FeatureFlagsTest {
assertEquals(Change.REMOVED, changes.get("d")); assertEquals(Change.REMOVED, changes.get("d"));
assertEquals(Change.ENABLED, changes.get("e")); assertEquals(Change.ENABLED, changes.get("e"));
assertEquals(Change.DISABLED, changes.get("f")); assertEquals(Change.DISABLED, changes.get("f"));
assertEquals(Change.CHANGED, changes.get("h"));
} }
private static <V> Set<V> setOf(V... values) { private static <V> Set<V> setOf(V... values) {

View file

@ -582,12 +582,12 @@ public class SignalServiceAccountManager {
} }
} }
public Map<String, Boolean> getRemoteConfig() throws IOException { public Map<String, Object> getRemoteConfig() throws IOException {
RemoteConfigResponse response = this.pushServiceSocket.getRemoteConfig(); RemoteConfigResponse response = this.pushServiceSocket.getRemoteConfig();
Map<String, Boolean> out = new HashMap<>(); Map<String, Object> out = new HashMap<>();
for (RemoteConfigResponse.Config config : response.getConfig()) { for (RemoteConfigResponse.Config config : response.getConfig()) {
out.put(config.getName(), config.isEnabled()); out.put(config.getName(), config.getValue() != null ? config.getValue() : config.isEnabled());
} }
return out; return out;

View file

@ -19,6 +19,9 @@ public class RemoteConfigResponse {
@JsonProperty @JsonProperty
private boolean enabled; private boolean enabled;
@JsonProperty
private String value;
public String getName() { public String getName() {
return name; return name;
} }
@ -26,5 +29,9 @@ public class RemoteConfigResponse {
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
public String getValue() {
return value;
}
} }
} }