That fuck shit the fascists are using
1package org.tm.archive.util;
2
3import android.text.TextUtils;
4
5import androidx.annotation.NonNull;
6import androidx.annotation.VisibleForTesting;
7import androidx.annotation.WorkerThread;
8
9import com.annimon.stream.Stream;
10
11import org.json.JSONException;
12import org.json.JSONObject;
13import org.signal.core.util.SetUtil;
14import org.signal.core.util.logging.Log;
15import org.tm.archive.BuildConfig;
16import org.tm.archive.dependencies.ApplicationDependencies;
17import org.tm.archive.groups.SelectionLimits;
18import org.tm.archive.jobs.RemoteConfigRefreshJob;
19import org.tm.archive.keyvalue.SignalStore;
20import org.tm.archive.messageprocessingalarm.RoutineMessageFetchReceiver;
21import org.whispersystems.signalservice.api.RemoteConfigResult;
22
23import java.io.IOException;
24import java.util.HashMap;
25import java.util.HashSet;
26import java.util.Iterator;
27import java.util.Map;
28import java.util.Objects;
29import java.util.Set;
30import java.util.TreeMap;
31import java.util.concurrent.TimeUnit;
32
33/**
34 * A location for flags that can be set locally and remotely. These flags can guard features that
35 * are not yet ready to be activated.
36 *
37 * When creating a new flag:
38 * - Create a new string constant. This should almost certainly be prefixed with "android."
39 * - Add a method to retrieve the value using {@link #getBoolean(String, boolean)}. You can also add
40 * other checks here, like requiring other flags.
41 * - If you want to be able to change a flag remotely, place it in {@link #REMOTE_CAPABLE}.
42 * - If you would like to force a value for testing, place an entry in {@link #FORCED_VALUES}.
43 * Do not commit changes to this map!
44 *
45 * Other interesting things you can do:
46 * - Make a flag {@link #HOT_SWAPPABLE}
47 * - Make a flag {@link #STICKY} -- booleans only!
48 * - Register a listener for flag changes in {@link #FLAG_CHANGE_LISTENERS}
49 */
50public final class FeatureFlags {
51
52 private static final String TAG = Log.tag(FeatureFlags.class);
53
54 private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
55
56 private static final String PAYMENTS_KILL_SWITCH = "android.payments.kill";
57 private static final String GROUPS_V2_RECOMMENDED_LIMIT = "global.groupsv2.maxGroupSize";
58 private static final String GROUPS_V2_HARD_LIMIT = "global.groupsv2.groupSizeHardLimit";
59 private static final String GROUP_NAME_MAX_LENGTH = "global.groupsv2.maxNameLength";
60 private static final String INTERNAL_USER = "android.internalUser";
61 private static final String VERIFY_V2 = "android.verifyV2";
62 private static final String CLIENT_EXPIRATION = "android.clientExpiration";
63 private static final String CUSTOM_VIDEO_MUXER = "android.customVideoMuxer.1";
64 private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds";
65 private static final String CDS_FOREGROUND_SYNC_INTERVAL = "cds.foregroundSyncInterval.seconds";
66 private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2";
67 private static final String AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval";
68 private static final String DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff";
69 private static final String SERVER_ERROR_MAX_BACKOFF = "android.serverErrorMaxBackoff";
70 private static final String OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry";
71 private static final String SHARE_SELECTION_LIMIT = "android.share.limit";
72 private static final String ANIMATED_STICKER_MIN_MEMORY = "android.animatedStickerMinMemory";
73 private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory";
74 private static final String MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins";
75 private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs";
76 private static final String MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels";
77 private static final String RETRY_RECEIPT_LIFESPAN = "android.retryReceiptLifespan";
78 private static final String RETRY_RESPOND_MAX_AGE = "android.retryRespondMaxAge";
79 private static final String SENDER_KEY_MAX_AGE = "android.senderKeyMaxAge";
80 private static final String RETRY_RECEIPTS = "android.retryReceipts";
81 private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize";
82 private static final String STORIES_TEXT_FUNCTIONS = "android.stories.text.functions";
83 private static final String HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList";
84 private static final String SOFTWARE_AEC_BLOCKLIST_MODELS = "android.calling.softwareAecBlockList";
85 private static final String USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29";
86 private static final String PAYMENTS_COUNTRY_BLOCKLIST = "global.payments.disabledRegions";
87 private static final String STORIES_AUTO_DOWNLOAD_MAXIMUM = "android.stories.autoDownloadMaximum";
88 private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList";
89 private static final String TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList";
90 private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList";
91 private static final String CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList";
92 private static final String PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow";
93 public static final String GOOGLE_PAY_DISABLED_REGIONS = "global.donations.gpayDisabledRegions";
94 public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
95 public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
96 private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
97 private static final String PAYPAL_ONE_TIME_DONATIONS = "android.oneTimePayPalDonations.2";
98 private static final String PAYPAL_RECURRING_DONATIONS = "android.recurringPayPalDonations.3";
99 private static final String ANY_ADDRESS_PORTS_KILL_SWITCH = "android.calling.fieldTrial.anyAddressPortsKillSwitch";
100 private static final String AD_HOC_CALLING = "android.calling.ad.hoc.3";
101 private static final String MAX_ATTACHMENT_COUNT = "android.attachments.maxCount";
102 private static final String MAX_ATTACHMENT_RECEIVE_SIZE_BYTES = "global.attachments.maxReceiveBytes";
103 private static final String MAX_ATTACHMENT_SIZE_BYTES = "global.attachments.maxBytes";
104 private static final String SVR2_KILLSWITCH = "android.svr2.killSwitch";
105 private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode";
106 private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch";
107 public static final String PROMPT_FOR_NOTIFICATION_LOGS = "android.logs.promptNotifications";
108 private static final String PROMPT_FOR_NOTIFICATION_CONFIG = "android.logs.promptNotificationsConfig";
109 public static final String PROMPT_BATTERY_SAVER = "android.promptBatterySaver";
110 public static final String INSTANT_VIDEO_PLAYBACK = "android.instantVideoPlayback.1";
111 public static final String CRASH_PROMPT_CONFIG = "android.crashPromptConfig";
112 private static final String SEPA_DEBIT_DONATIONS = "android.sepa.debit.donations.5";
113 private static final String IDEAL_DONATIONS = "android.ideal.donations.5";
114 public static final String IDEAL_ENABLED_REGIONS = "global.donations.idealEnabledRegions";
115 public static final String SEPA_ENABLED_REGIONS = "global.donations.sepaEnabledRegions";
116 private static final String CALLING_REACTIONS = "android.calling.reactions";
117 private static final String NOTIFICATION_THUMBNAIL_BLOCKLIST = "android.notificationThumbnailProductBlocklist";
118 private static final String CALLING_RAISE_HAND = "android.calling.raiseHand";
119 private static final String USE_ACTIVE_CALL_MANAGER = "android.calling.useActiveCallManager.4";
120 private static final String GIF_SEARCH = "global.gifSearch";
121 private static final String AUDIO_REMUXING = "android.media.audioRemux.1";
122 private static final String VIDEO_RECORD_1X_ZOOM = "android.media.videoCaptureDefaultZoom";
123 private static final String RETRY_RECEIPT_MAX_COUNT = "android.retryReceipt.maxCount";
124 private static final String RETRY_RECEIPT_MAX_COUNT_RESET_AGE = "android.retryReceipt.maxCountResetAge";
125 private static final String PREKEY_FORCE_REFRESH_INTERVAL = "android.prekeyForceRefreshInterval";
126 private static final String CDSI_LIBSIGNAL_NET = "android.cds.libsignal.2";
127 private static final String RX_MESSAGE_SEND = "android.rxMessageSend";
128 private static final String LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds";
129 private static final String MESSAGE_BACKUPS = "android.messageBackups";
130 private static final String NICKNAMES = "android.nicknames";
131 private static final String CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController";
132
133 /**
134 * We will only store remote values for flags in this set. If you want a flag to be controllable
135 * remotely, place it in here.
136 */
137 @VisibleForTesting
138 static final Set<String> REMOTE_CAPABLE = SetUtil.newHashSet(
139 PAYMENTS_KILL_SWITCH,
140 GROUPS_V2_RECOMMENDED_LIMIT, GROUPS_V2_HARD_LIMIT,
141 INTERNAL_USER,
142 VERIFY_V2,
143 CLIENT_EXPIRATION,
144 CUSTOM_VIDEO_MUXER,
145 CDS_REFRESH_INTERVAL,
146 CDS_FOREGROUND_SYNC_INTERVAL,
147 GROUP_NAME_MAX_LENGTH,
148 AUTOMATIC_SESSION_RESET,
149 AUTOMATIC_SESSION_INTERVAL,
150 DEFAULT_MAX_BACKOFF,
151 SERVER_ERROR_MAX_BACKOFF,
152 OKHTTP_AUTOMATIC_RETRY,
153 SHARE_SELECTION_LIMIT,
154 ANIMATED_STICKER_MIN_MEMORY,
155 ANIMATED_STICKER_MIN_TOTAL_MEMORY,
156 MESSAGE_PROCESSOR_ALARM_INTERVAL,
157 MESSAGE_PROCESSOR_DELAY,
158 MEDIA_QUALITY_LEVELS,
159 RETRY_RECEIPT_LIFESPAN,
160 RETRY_RESPOND_MAX_AGE,
161 RETRY_RECEIPTS,
162 MAX_GROUP_CALL_RING_SIZE,
163 SENDER_KEY_MAX_AGE,
164 STORIES_TEXT_FUNCTIONS,
165 HARDWARE_AEC_BLOCKLIST_MODELS,
166 SOFTWARE_AEC_BLOCKLIST_MODELS,
167 USE_HARDWARE_AEC_IF_OLD,
168 PAYMENTS_COUNTRY_BLOCKLIST,
169 STORIES_AUTO_DOWNLOAD_MAXIMUM,
170 TELECOM_MANUFACTURER_ALLOWLIST,
171 TELECOM_MODEL_BLOCKLIST,
172 CAMERAX_MODEL_BLOCKLIST,
173 CAMERAX_MIXED_MODEL_BLOCKLIST,
174 PAYMENTS_REQUEST_ACTIVATE_FLOW,
175 GOOGLE_PAY_DISABLED_REGIONS,
176 CREDIT_CARD_DISABLED_REGIONS,
177 PAYPAL_DISABLED_REGIONS,
178 CDS_HARD_LIMIT,
179 PAYPAL_ONE_TIME_DONATIONS,
180 PAYPAL_RECURRING_DONATIONS,
181 ANY_ADDRESS_PORTS_KILL_SWITCH,
182 MAX_ATTACHMENT_COUNT,
183 MAX_ATTACHMENT_RECEIVE_SIZE_BYTES,
184 MAX_ATTACHMENT_SIZE_BYTES,
185 AD_HOC_CALLING,
186 SVR2_KILLSWITCH,
187 CDS_DISABLE_COMPAT_MODE,
188 FCM_MAY_HAVE_MESSAGES_KILL_SWITCH,
189 PROMPT_FOR_NOTIFICATION_LOGS,
190 PROMPT_FOR_NOTIFICATION_CONFIG,
191 PROMPT_BATTERY_SAVER,
192 INSTANT_VIDEO_PLAYBACK,
193 CRASH_PROMPT_CONFIG,
194 SEPA_DEBIT_DONATIONS,
195 IDEAL_DONATIONS,
196 IDEAL_ENABLED_REGIONS,
197 SEPA_ENABLED_REGIONS,
198 CALLING_REACTIONS,
199 NOTIFICATION_THUMBNAIL_BLOCKLIST,
200 CALLING_RAISE_HAND,
201 USE_ACTIVE_CALL_MANAGER,
202 GIF_SEARCH,
203 AUDIO_REMUXING,
204 VIDEO_RECORD_1X_ZOOM,
205 RETRY_RECEIPT_MAX_COUNT,
206 RETRY_RECEIPT_MAX_COUNT_RESET_AGE,
207 PREKEY_FORCE_REFRESH_INTERVAL,
208 CDSI_LIBSIGNAL_NET,
209 RX_MESSAGE_SEND,
210 LINKED_DEVICE_LIFESPAN_SECONDS,
211 NICKNAMES,
212 CAMERAX_CUSTOM_CONTROLLER
213 );
214
215 @VisibleForTesting
216 static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS);
217
218 /**
219 * Values in this map will take precedence over any value. This should only be used for local
220 * development. Given that you specify a default when retrieving a value, and that we only store
221 * remote values for things in {@link #REMOTE_CAPABLE}, there should be no need to ever *commit*
222 * an addition to this map.
223 */
224 @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
225 @VisibleForTesting
226 static final Map<String, Object> FORCED_VALUES = new HashMap<String, Object>() {{
227 }};
228
229 /**
230 * By default, flags are only updated once at app start. This is to ensure that values don't
231 * change within an app session, simplifying logic. However, given that this can delay how often
232 * a flag is updated, you can put a flag in here to mark it as 'hot swappable'. Flags in this set
233 * will be updated arbitrarily at runtime. This will make values more responsive, but also places
234 * more burden on the reader to ensure that the app experience remains consistent.
235 */
236 @VisibleForTesting
237 static final Set<String> HOT_SWAPPABLE = SetUtil.newHashSet(
238 VERIFY_V2,
239 CLIENT_EXPIRATION,
240 CUSTOM_VIDEO_MUXER,
241 CDS_REFRESH_INTERVAL,
242 CDS_FOREGROUND_SYNC_INTERVAL,
243 GROUP_NAME_MAX_LENGTH,
244 AUTOMATIC_SESSION_RESET,
245 AUTOMATIC_SESSION_INTERVAL,
246 DEFAULT_MAX_BACKOFF,
247 SERVER_ERROR_MAX_BACKOFF,
248 OKHTTP_AUTOMATIC_RETRY,
249 SHARE_SELECTION_LIMIT,
250 ANIMATED_STICKER_MIN_MEMORY,
251 ANIMATED_STICKER_MIN_TOTAL_MEMORY,
252 MESSAGE_PROCESSOR_ALARM_INTERVAL,
253 MESSAGE_PROCESSOR_DELAY,
254 MEDIA_QUALITY_LEVELS,
255 RETRY_RECEIPT_LIFESPAN,
256 RETRY_RESPOND_MAX_AGE,
257 RETRY_RECEIPTS,
258 MAX_GROUP_CALL_RING_SIZE,
259 SENDER_KEY_MAX_AGE,
260 HARDWARE_AEC_BLOCKLIST_MODELS,
261 SOFTWARE_AEC_BLOCKLIST_MODELS,
262 USE_HARDWARE_AEC_IF_OLD,
263 PAYMENTS_COUNTRY_BLOCKLIST,
264 TELECOM_MANUFACTURER_ALLOWLIST,
265 TELECOM_MODEL_BLOCKLIST,
266 CAMERAX_MODEL_BLOCKLIST,
267 PAYMENTS_REQUEST_ACTIVATE_FLOW,
268 CDS_HARD_LIMIT,
269 MAX_ATTACHMENT_COUNT,
270 MAX_ATTACHMENT_RECEIVE_SIZE_BYTES,
271 MAX_ATTACHMENT_SIZE_BYTES,
272 SVR2_KILLSWITCH,
273 CDS_DISABLE_COMPAT_MODE,
274 FCM_MAY_HAVE_MESSAGES_KILL_SWITCH,
275 PROMPT_FOR_NOTIFICATION_LOGS,
276 PROMPT_FOR_NOTIFICATION_CONFIG,
277 PROMPT_BATTERY_SAVER,
278 CRASH_PROMPT_CONFIG,
279 CALLING_REACTIONS,
280 NOTIFICATION_THUMBNAIL_BLOCKLIST,
281 CALLING_RAISE_HAND,
282 VIDEO_RECORD_1X_ZOOM,
283 RETRY_RECEIPT_MAX_COUNT,
284 RETRY_RECEIPT_MAX_COUNT_RESET_AGE,
285 PREKEY_FORCE_REFRESH_INTERVAL,
286 CDSI_LIBSIGNAL_NET,
287 RX_MESSAGE_SEND,
288 LINKED_DEVICE_LIFESPAN_SECONDS,
289 CAMERAX_CUSTOM_CONTROLLER,
290 NICKNAMES
291 );
292
293 /**
294 * Flags in this set will stay true forever once they receive a true value from a remote config.
295 */
296 @VisibleForTesting
297 static final Set<String> STICKY = SetUtil.newHashSet(
298 VERIFY_V2,
299 SVR2_KILLSWITCH,
300 FCM_MAY_HAVE_MESSAGES_KILL_SWITCH
301 );
302
303 /**
304 * Listeners that are called when the value in {@link #REMOTE_VALUES} changes. That means that
305 * hot-swappable flags will have this invoked as soon as we know about that change, but otherwise
306 * these will only run during initialization.
307 *
308 * These can be called on any thread, including the main thread, so be careful!
309 *
310 * Also note that this doesn't play well with {@link #FORCED_VALUES} -- changes there will not
311 * trigger changes in this map, so you'll have to do some manual hacking to get yourself in the
312 * desired test state.
313 */
314 private static final Map<String, OnFlagChange> FLAG_CHANGE_LISTENERS = new HashMap<String, OnFlagChange>() {{
315 put(MESSAGE_PROCESSOR_ALARM_INTERVAL, change -> RoutineMessageFetchReceiver.startOrUpdateAlarm(ApplicationDependencies.getApplication()));
316 }};
317
318 private static final Map<String, Object> REMOTE_VALUES = new TreeMap<>();
319
320 private FeatureFlags() {}
321
322 public static synchronized void init() {
323 Map<String, Object> current = parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig());
324 Map<String, Object> pending = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
325 Map<String, Change> changes = computeChanges(current, pending);
326
327 SignalStore.remoteConfigValues().setCurrentConfig(mapToJson(pending));
328 REMOTE_VALUES.putAll(pending);
329 triggerFlagChangeListeners(changes);
330
331 Log.i(TAG, "init() " + REMOTE_VALUES.toString());
332 }
333
334 public static void refreshIfNecessary() {
335 long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.remoteConfigValues().getLastFetchTime();
336
337 if (timeSinceLastFetch < 0 || timeSinceLastFetch > FETCH_INTERVAL) {
338 Log.i(TAG, "Scheduling remote config refresh.");
339 ApplicationDependencies.getJobManager().add(new RemoteConfigRefreshJob());
340 } else {
341 Log.i(TAG, "Skipping remote config refresh. Refreshed " + timeSinceLastFetch + " ms ago.");
342 }
343 }
344
345 @WorkerThread
346 public static void refreshSync() throws IOException {
347 RemoteConfigResult result = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
348 FeatureFlags.update(result.getConfig());
349 }
350
351 public static synchronized void update(@NonNull Map<String, Object> config) {
352 Map<String, Object> memory = REMOTE_VALUES;
353 Map<String, Object> disk = parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig());
354 UpdateResult result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY);
355
356 SignalStore.remoteConfigValues().setPendingConfig(mapToJson(result.getDisk()));
357 REMOTE_VALUES.clear();
358 REMOTE_VALUES.putAll(result.getMemory());
359 triggerFlagChangeListeners(result.getMemoryChanges());
360
361 SignalStore.remoteConfigValues().setLastFetchTime(System.currentTimeMillis());
362
363 Log.i(TAG, "[Memory] Before: " + memory.toString());
364 Log.i(TAG, "[Memory] After : " + result.getMemory().toString());
365 Log.i(TAG, "[Disk] Before: " + disk.toString());
366 Log.i(TAG, "[Disk] After : " + result.getDisk().toString());
367 }
368
369 /**
370 * Maximum number of members allowed in a group.
371 */
372 public static SelectionLimits groupLimits() {
373 return new SelectionLimits(getInteger(GROUPS_V2_RECOMMENDED_LIMIT, 151),
374 getInteger(GROUPS_V2_HARD_LIMIT, 1001));
375 }
376
377 /** Payments Support */
378 public static boolean payments() {
379 return !getBoolean(PAYMENTS_KILL_SWITCH, false);
380 }
381
382 /** Internal testing extensions. */
383 public static boolean internalUser() {
384 return getBoolean(INTERNAL_USER, false) || Environment.IS_PNP || Environment.IS_STAGING;
385 }
386
387 /** Whether or not to use the UUID in verification codes. */
388 public static boolean verifyV2() {
389 return getBoolean(VERIFY_V2, false);
390 }
391
392 /** The raw client expiration JSON string. */
393 public static String clientExpiration() {
394 return getString(CLIENT_EXPIRATION, null);
395 }
396
397 /** Whether to use the custom streaming muxer or built in android muxer. */
398 public static boolean useStreamingVideoMuxer() {
399 return getBoolean(CUSTOM_VIDEO_MUXER, false);
400 }
401
402 /** The time in between routine CDS refreshes, in seconds. */
403 public static int cdsRefreshIntervalSeconds() {
404 return getInteger(CDS_REFRESH_INTERVAL, (int) TimeUnit.HOURS.toSeconds(48));
405 }
406
407 /** The minimum time in between foreground CDS refreshes initiated via message requests, in milliseconds. */
408 public static Long cdsForegroundSyncInterval() {
409 return TimeUnit.SECONDS.toMillis(getInteger(CDS_FOREGROUND_SYNC_INTERVAL, (int) TimeUnit.HOURS.toSeconds(4)));
410 }
411
412 public static @NonNull SelectionLimits shareSelectionLimit() {
413 int limit = getInteger(SHARE_SELECTION_LIMIT, 5);
414 return new SelectionLimits(limit, limit);
415 }
416
417 /** The maximum number of grapheme */
418 public static int getMaxGroupNameGraphemeLength() {
419 return Math.max(32, getInteger(GROUP_NAME_MAX_LENGTH, -1));
420 }
421
422 /** Whether or not to allow automatic session resets. */
423 public static boolean automaticSessionReset() {
424 return getBoolean(AUTOMATIC_SESSION_RESET, true);
425 }
426
427 /** How often we allow an automatic session reset. */
428 public static int automaticSessionResetIntervalSeconds() {
429 return getInteger(AUTOMATIC_SESSION_RESET, (int) TimeUnit.HOURS.toSeconds(1));
430 }
431
432 /** The default maximum backoff for jobs. */
433 public static long getDefaultMaxBackoff() {
434 return TimeUnit.SECONDS.toMillis(getInteger(DEFAULT_MAX_BACKOFF, 60));
435 }
436
437 /** The maximum backoff for network jobs that hit a 5xx error. */
438 public static long getServerErrorMaxBackoff() {
439 return TimeUnit.SECONDS.toMillis(getInteger(SERVER_ERROR_MAX_BACKOFF, (int) TimeUnit.HOURS.toSeconds(6)));
440 }
441
442 /** Whether or not to allow automatic retries from OkHttp */
443 public static boolean okHttpAutomaticRetry() {
444 return getBoolean(OKHTTP_AUTOMATIC_RETRY, true);
445 }
446
447 /** The minimum memory class required for rendering animated stickers in the keyboard and such */
448 public static int animatedStickerMinimumMemoryClass() {
449 return getInteger(ANIMATED_STICKER_MIN_MEMORY, 193);
450 }
451
452 /** The minimum total memory for rendering animated stickers in the keyboard and such */
453 public static int animatedStickerMinimumTotalMemoryMb() {
454 return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3));
455 }
456
457 public static @NonNull String getMediaQualityLevels() {
458 return getString(MEDIA_QUALITY_LEVELS, "");
459 }
460
461 /** Whether or not sending or responding to retry receipts is enabled. */
462 public static boolean retryReceipts() {
463 return getBoolean(RETRY_RECEIPTS, true);
464 }
465
466 /** How old a message is allowed to be while still resending in response to a retry receipt . */
467 public static long retryRespondMaxAge() {
468 return getLong(RETRY_RESPOND_MAX_AGE, TimeUnit.DAYS.toMillis(14));
469 }
470
471 /**
472 * The max number of retry receipts sends we allow (within @link{#retryReceiptMaxCountResetAge()}) before we consider the volume too large and stop responding.
473 */
474 public static long retryReceiptMaxCount() {
475 return getLong(RETRY_RECEIPT_MAX_COUNT, 10);
476 }
477
478 /**
479 * If the last retry receipt send was older than this, then we reset the retry receipt sent count. (For use with @link{#retryReceiptMaxCount()})
480 */
481 public static long retryReceiptMaxCountResetAge() {
482 return getLong(RETRY_RECEIPT_MAX_COUNT_RESET_AGE, TimeUnit.HOURS.toMillis(3));
483 }
484
485 /** How long a sender key can live before it needs to be rotated. */
486 public static long senderKeyMaxAge() {
487 return Math.min(getLong(SENDER_KEY_MAX_AGE, TimeUnit.DAYS.toMillis(14)), TimeUnit.DAYS.toMillis(90));
488 }
489
490 /** Max group size that can be use group call ringing. */
491 public static long maxGroupCallRingSize() {
492 return getLong(MAX_GROUP_CALL_RING_SIZE, 16);
493 }
494
495 /** A comma-separated list of country codes where payments should be disabled. */
496 public static String paymentsCountryBlocklist() {
497 return getString(PAYMENTS_COUNTRY_BLOCKLIST, "98,963,53,850,7");
498 }
499
500 /**
501 * Whether users can apply alignment and scale to text posts
502 *
503 * NOTE: This feature is still under ongoing development, do not enable.
504 */
505 public static boolean storiesTextFunctions() {
506 return getBoolean(STORIES_TEXT_FUNCTIONS, false);
507 }
508
509 /** A comma-separated list of models that should *not* use hardware AEC for calling. */
510 public static @NonNull String hardwareAecBlocklistModels() {
511 return getString(HARDWARE_AEC_BLOCKLIST_MODELS, "");
512 }
513
514 /** A comma-separated list of models that should *not* use software AEC for calling. */
515 public static @NonNull String softwareAecBlocklistModels() {
516 return getString(SOFTWARE_AEC_BLOCKLIST_MODELS, "");
517 }
518
519 /** A comma-separated list of manufacturers that *should* use Telecom for calling. */
520 public static @NonNull String telecomManufacturerAllowList() {
521 return getString(TELECOM_MANUFACTURER_ALLOWLIST, "");
522 }
523
524 /** A comma-separated list of manufacturers that *should* use Telecom for calling. */
525 public static @NonNull String telecomModelBlockList() {
526 return getString(TELECOM_MODEL_BLOCKLIST, "");
527 }
528
529 /** A comma-separated list of manufacturers that should *not* use CameraX. */
530 public static @NonNull String cameraXModelBlocklist() {
531 return getString(CAMERAX_MODEL_BLOCKLIST, "");
532 }
533
534 /** A comma-separated list of manufacturers that should *not* use CameraX mixed mode. */
535 public static @NonNull String cameraXMixedModelBlocklist() {
536 return getString(CAMERAX_MIXED_MODEL_BLOCKLIST, "");
537 }
538
539 /** Whether or not hardware AEC should be used for calling on devices older than API 29. */
540 public static boolean useHardwareAecIfOlderThanApi29() {
541 return getBoolean(USE_HARDWARE_AEC_IF_OLD, false);
542 }
543
544 /**
545 * Prefetch count for stories from a given user.
546 */
547 public static int storiesAutoDownloadMaximum() {
548 return getInteger(STORIES_AUTO_DOWNLOAD_MAXIMUM, 2);
549 }
550
551 /** Whether client supports sending a request to another to activate payments */
552 public static boolean paymentsRequestActivateFlow() {
553 return getBoolean(PAYMENTS_REQUEST_ACTIVATE_FLOW, false);
554 }
555
556 /**
557 * @return Serialized list of regions in which Google Pay is disabled for donations
558 */
559 public static @NonNull String googlePayDisabledRegions() {
560 return getString(GOOGLE_PAY_DISABLED_REGIONS, "*");
561 }
562
563 /**
564 * @return Serialized list of regions in which credit cards are disabled for donations
565 */
566 public static @NonNull String creditCardDisabledRegions() {
567 return getString(CREDIT_CARD_DISABLED_REGIONS, "*");
568 }
569
570 /**
571 * @return Serialized list of regions in which PayPal is disabled for donations
572 */
573 public static @NonNull String paypalDisabledRegions() {
574 return getString(PAYPAL_DISABLED_REGIONS, "*");
575 }
576
577 /**
578 * If the user has more than this number of contacts, the CDS request will certainly be rejected, so we must fail.
579 */
580 public static int cdsHardLimit() {
581 return getInteger(CDS_HARD_LIMIT, 50_000);
582 }
583
584 /**
585 * Whether or not we should allow PayPal payments for one-time donations
586 */
587 public static boolean paypalOneTimeDonations() {
588 return getBoolean(PAYPAL_ONE_TIME_DONATIONS, Environment.IS_STAGING);
589 }
590
591 /**
592 * Whether or not we should allow PayPal payments for recurring donations
593 */
594 public static boolean paypalRecurringDonations() {
595 return getBoolean(PAYPAL_RECURRING_DONATIONS, Environment.IS_STAGING);
596 }
597
598 /**
599 * Enable/disable RingRTC field trial for "AnyAddressPortsKillSwitch"
600 */
601 public static boolean callingFieldTrialAnyAddressPortsKillSwitch() {
602 return getBoolean(ANY_ADDRESS_PORTS_KILL_SWITCH, false);
603 }
604
605 /**
606 * Enable/disable for notification when we cannot fetch messages despite receiving an urgent push.
607 */
608 public static boolean fcmMayHaveMessagesNotificationKillSwitch() {
609 return getBoolean(FCM_MAY_HAVE_MESSAGES_KILL_SWITCH, false);
610 }
611
612 /**
613 * Whether or not ad-hoc calling is enabled
614 */
615 public static boolean adHocCalling() {
616 return getBoolean(AD_HOC_CALLING, false);
617 }
618
619 /** Maximum number of attachments allowed to be sent/received. */
620 public static int maxAttachmentCount() {
621 return getInteger(MAX_ATTACHMENT_COUNT, 32);
622 }
623
624 /** Maximum attachment size for ciphertext in bytes. */
625 public static long maxAttachmentReceiveSizeBytes() {
626 long maxAttachmentSize = maxAttachmentSizeBytes();
627 long maxReceiveSize = getLong(MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, (int) (maxAttachmentSize * 1.25));
628 return Math.max(maxAttachmentSize, maxReceiveSize);
629 }
630
631 /** Maximum attachment ciphertext size when sending in bytes */
632 public static long maxAttachmentSizeBytes() {
633 return getLong(MAX_ATTACHMENT_SIZE_BYTES, ByteUnit.MEGABYTES.toBytes(100));
634 }
635
636 /**
637 * Allow the video players to read from the temporary download files for attachments.
638 * @return whether this functionality is enabled.
639 */
640 public static boolean instantVideoPlayback() {
641 return getBoolean(INSTANT_VIDEO_PLAYBACK, false);
642 }
643
644 public static String promptForDelayedNotificationLogs() {
645 return getString(PROMPT_FOR_NOTIFICATION_LOGS, "*");
646 }
647
648 public static String delayedNotificationsPromptConfig() {
649 return getString(PROMPT_FOR_NOTIFICATION_CONFIG, "");
650 }
651
652 public static String promptBatterySaver() {
653 return getString(PROMPT_BATTERY_SAVER, "*");
654 }
655
656 /** Config object for what crashes to prompt about. */
657 public static String crashPromptConfig() {
658 return getString(CRASH_PROMPT_CONFIG, "");
659 }
660
661 /**
662 * Whether or not SEPA debit payments for donations are enabled.
663 * WARNING: This feature is under heavy development and is *not* ready for wider use.
664 */
665 public static boolean sepaDebitDonations() {
666 return getBoolean(SEPA_DEBIT_DONATIONS, false);
667 }
668
669 public static boolean idealDonations() {
670 return getBoolean(IDEAL_DONATIONS, false);
671 }
672
673 public static String idealEnabledRegions() {
674 return getString(IDEAL_ENABLED_REGIONS, "");
675 }
676
677 public static String sepaEnabledRegions() {
678 return getString(SEPA_ENABLED_REGIONS, "");
679 }
680
681 /**
682 * Whether or not group call reactions are enabled.
683 */
684 public static boolean groupCallReactions() {
685 return getBoolean(CALLING_REACTIONS, false);
686 }
687
688 /**
689 * Whether or not group call raise hand is enabled.
690 */
691 public static boolean groupCallRaiseHand() {
692 return getBoolean(CALLING_RAISE_HAND, false);
693 }
694
695 /** List of device products that are blocked from showing notification thumbnails. */
696 public static String notificationThumbnailProductBlocklist() {
697 return getString(NOTIFICATION_THUMBNAIL_BLOCKLIST, "");
698 }
699
700 /** Whether or not to use active call manager instead of WebRtcCallService. */
701 public static boolean useActiveCallManager() {
702 return getBoolean(USE_ACTIVE_CALL_MANAGER, false);
703 }
704
705 /** Whether the in-app GIF search is available for use. */
706 public static boolean gifSearchAvailable() {
707 return getBoolean(GIF_SEARCH, true);
708 }
709
710 /** Allow media converters to remux audio instead of transcoding it. */
711 public static boolean allowAudioRemuxing() {
712 return getBoolean(AUDIO_REMUXING, false);
713 }
714
715 /** Get the default video zoom, expressed as 10x the actual Float value due to the service limiting us to whole numbers. */
716 public static boolean startVideoRecordAt1x() {
717 return getBoolean(VIDEO_RECORD_1X_ZOOM, false);
718 }
719
720 /** How often we allow a forced prekey refresh. */
721 public static long preKeyForceRefreshInterval() {
722 return getLong(PREKEY_FORCE_REFRESH_INTERVAL, TimeUnit.HOURS.toMillis(1));
723 }
724
725 /** Make CDSI lookups via libsignal-net instead of native websocket. */
726 public static boolean useLibsignalNetForCdsiLookup() {
727 return getBoolean(CDSI_LIBSIGNAL_NET, false);
728 }
729
730 /** Use Rx threading model to do sends. */
731 public static boolean useRxMessageSending() {
732 return getBoolean(RX_MESSAGE_SEND, false);
733 }
734
735 /** The lifespan of a linked device (i.e. the time it can be inactive for before it expires), in milliseconds. */
736 public static long linkedDeviceLifespan() {
737 long seconds = getLong(LINKED_DEVICE_LIFESPAN_SECONDS, TimeUnit.DAYS.toSeconds(30));
738 return TimeUnit.SECONDS.toMillis(seconds);
739 }
740
741 /**
742 * Enable Message Backups UI
743 * Note: This feature is in active development and is not intended to currently function.
744 */
745 public static boolean messageBackups() {
746 return getBoolean(MESSAGE_BACKUPS, false);
747 }
748
749 /** Whether or not the nicknames feature is available */
750 public static boolean nicknames() {
751 return getBoolean(NICKNAMES, true);
752 }
753
754 /** Whether or not to use the custom CameraX controller class */
755 public static boolean customCameraXController() {
756 return getBoolean(CAMERAX_CUSTOM_CONTROLLER, false);
757 }
758
759 /** Only for rendering debug info. */
760 public static synchronized @NonNull Map<String, Object> getMemoryValues() {
761 return new TreeMap<>(REMOTE_VALUES);
762 }
763
764 /** Only for rendering debug info. */
765 public static synchronized @NonNull Map<String, Object> getDiskValues() {
766 return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getCurrentConfig()));
767 }
768
769 /** Only for rendering debug info. */
770 public static synchronized @NonNull Map<String, Object> getPendingDiskValues() {
771 return new TreeMap<>(parseStoredConfig(SignalStore.remoteConfigValues().getPendingConfig()));
772 }
773
774 /** Only for rendering debug info. */
775 public static synchronized @NonNull Map<String, Object> getForcedValues() {
776 return new TreeMap<>(FORCED_VALUES);
777 }
778
779 @VisibleForTesting
780 static @NonNull UpdateResult updateInternal(@NonNull Map<String, Object> remote,
781 @NonNull Map<String, Object> localMemory,
782 @NonNull Map<String, Object> localDisk,
783 @NonNull Set<String> remoteCapable,
784 @NonNull Set<String> hotSwap,
785 @NonNull Set<String> sticky)
786 {
787 Map<String, Object> newMemory = new TreeMap<>(localMemory);
788 Map<String, Object> newDisk = new TreeMap<>(localDisk);
789
790 Set<String> allKeys = new HashSet<>();
791 allKeys.addAll(remote.keySet());
792 allKeys.addAll(localDisk.keySet());
793 allKeys.addAll(localMemory.keySet());
794
795 Stream.of(allKeys)
796 .filter(remoteCapable::contains)
797 .forEach(key -> {
798 Object remoteValue = remote.get(key);
799 Object diskValue = localDisk.get(key);
800 Object newValue = remoteValue;
801
802 if (newValue != null && diskValue != null && newValue.getClass() != diskValue.getClass()) {
803 Log.w(TAG, "Type mismatch! key: " + key);
804
805 newDisk.remove(key);
806
807 if (hotSwap.contains(key)) {
808 newMemory.remove(key);
809 }
810
811 return;
812 }
813
814 if (sticky.contains(key) && (newValue instanceof Boolean || diskValue instanceof Boolean)) {
815 newValue = diskValue == Boolean.TRUE ? Boolean.TRUE : newValue;
816 } else if (sticky.contains(key)) {
817 Log.w(TAG, "Tried to make a non-boolean sticky! Ignoring. (key: " + key + ")");
818 }
819
820 if (newValue != null) {
821 newDisk.put(key, newValue);
822 } else {
823 newDisk.remove(key);
824 }
825
826 if (hotSwap.contains(key)) {
827 if (newValue != null) {
828 newMemory.put(key, newValue);
829 } else {
830 newMemory.remove(key);
831 }
832 }
833 });
834
835 Stream.of(allKeys)
836 .filterNot(remoteCapable::contains)
837 .filterNot(key -> sticky.contains(key) && localDisk.get(key) == Boolean.TRUE)
838 .forEach(key -> {
839 newDisk.remove(key);
840
841 if (hotSwap.contains(key)) {
842 newMemory.remove(key);
843 }
844 });
845
846 return new UpdateResult(newMemory, newDisk, computeChanges(localMemory, newMemory));
847 }
848
849 @VisibleForTesting
850 static @NonNull Map<String, Change> computeChanges(@NonNull Map<String, Object> oldMap, @NonNull Map<String, Object> newMap) {
851 Map<String, Change> changes = new HashMap<>();
852 Set<String> allKeys = new HashSet<>();
853
854 allKeys.addAll(oldMap.keySet());
855 allKeys.addAll(newMap.keySet());
856
857 for (String key : allKeys) {
858 Object oldValue = oldMap.get(key);
859 Object newValue = newMap.get(key);
860
861 if (oldValue == null && newValue == null) {
862 throw new AssertionError("Should not be possible.");
863 } else if (oldValue != null && newValue == null) {
864 changes.put(key, Change.REMOVED);
865 } else if (newValue != oldValue && newValue instanceof Boolean) {
866 changes.put(key, (boolean) newValue ? Change.ENABLED : Change.DISABLED);
867 } else if (!Objects.equals(oldValue, newValue)) {
868 changes.put(key, Change.CHANGED);
869 }
870 }
871
872 return changes;
873 }
874
875 private static @NonNull VersionFlag getVersionFlag(@NonNull String key) {
876 int versionFromKey = getInteger(key, 0);
877
878 if (versionFromKey == 0) {
879 return VersionFlag.OFF;
880 }
881
882 if (BuildConfig.CANONICAL_VERSION_CODE >= versionFromKey) {
883 return VersionFlag.ON;
884 } else {
885 return VersionFlag.ON_IN_FUTURE_VERSION;
886 }
887 }
888
889 public static long getBackgroundMessageProcessInterval() {
890 int delayMinutes = getInteger(MESSAGE_PROCESSOR_ALARM_INTERVAL, (int) TimeUnit.HOURS.toMinutes(6));
891 return TimeUnit.MINUTES.toMillis(delayMinutes);
892 }
893
894 /**
895 * How long before a "Checking messages" foreground notification is shown to the user.
896 */
897 public static long getBackgroundMessageProcessForegroundDelay() {
898 return getInteger(MESSAGE_PROCESSOR_DELAY, 300);
899 }
900
901 /**
902 * Whether or not SVR2 should be used at all. Defaults to true. In practice this is reserved as a killswitch.
903 */
904 public static boolean svr2() {
905 // Despite us always inverting the value, it's important that this defaults to false so that the STICKY property works as intended
906 return !getBoolean(SVR2_KILLSWITCH, false);
907 }
908
909 private enum VersionFlag {
910 /** The flag is no set */
911 OFF,
912
913 /** The flag is set on for a version higher than the current client version */
914 ON_IN_FUTURE_VERSION,
915
916 /** The flag is set on for this version or earlier */
917 ON
918 }
919
920 private static boolean getBoolean(@NonNull String key, boolean defaultValue) {
921 Boolean forced = (Boolean) FORCED_VALUES.get(key);
922 if (forced != null) {
923 return forced;
924 }
925
926 Object remote = REMOTE_VALUES.get(key);
927 if (remote instanceof Boolean) {
928 return (boolean) remote;
929 } else if (remote instanceof String) {
930 String stringValue = ((String) remote).toLowerCase();
931 if (stringValue.equals("true")) {
932 return true;
933 } else if (stringValue.equals("false")) {
934 return false;
935 } else {
936 Log.w(TAG, "Expected a boolean for key '" + key + "', but got something else (" + stringValue + ")! Falling back to the default.");
937 }
938 } else if (remote != null) {
939 Log.w(TAG, "Expected a boolean for key '" + key + "', but got something else! Falling back to the default.");
940 }
941
942 return defaultValue;
943 }
944
945 private static int getInteger(@NonNull String key, int defaultValue) {
946 Integer forced = (Integer) FORCED_VALUES.get(key);
947 if (forced != null) {
948 return forced;
949 }
950
951 Object remote = REMOTE_VALUES.get(key);
952 if (remote instanceof String) {
953 try {
954 return Integer.parseInt((String) remote);
955 } catch (NumberFormatException e) {
956 Log.w(TAG, "Expected an int for key '" + key + "', but got something else! Falling back to the default.");
957 }
958 }
959
960 return defaultValue;
961 }
962
963 private static long getLong(@NonNull String key, long defaultValue) {
964 Long forced = (Long) FORCED_VALUES.get(key);
965 if (forced != null) {
966 return forced;
967 }
968
969 Object remote = REMOTE_VALUES.get(key);
970 if (remote instanceof String) {
971 try {
972 return Long.parseLong((String) remote);
973 } catch (NumberFormatException e) {
974 Log.w(TAG, "Expected a long for key '" + key + "', but got something else! Falling back to the default.");
975 }
976 }
977
978 return defaultValue;
979 }
980
981 private static String getString(@NonNull String key, String defaultValue) {
982 String forced = (String) FORCED_VALUES.get(key);
983 if (forced != null) {
984 return forced;
985 }
986
987 Object remote = REMOTE_VALUES.get(key);
988 if (remote instanceof String) {
989 return (String) remote;
990 }
991
992 return defaultValue;
993 }
994
995 private static Map<String, Object> parseStoredConfig(String stored) {
996 Map<String, Object> parsed = new HashMap<>();
997
998 if (TextUtils.isEmpty(stored)) {
999 Log.i(TAG, "No remote config stored. Skipping.");
1000 return parsed;
1001 }
1002
1003 try {
1004 JSONObject root = new JSONObject(stored);
1005 Iterator<String> iter = root.keys();
1006
1007 while (iter.hasNext()) {
1008 String key = iter.next();
1009 parsed.put(key, root.get(key));
1010 }
1011 } catch (JSONException e) {
1012 throw new AssertionError("Failed to parse! Cleared storage.");
1013 }
1014
1015 return parsed;
1016 }
1017
1018 private static @NonNull String mapToJson(@NonNull Map<String, Object> map) {
1019 try {
1020 JSONObject json = new JSONObject();
1021
1022 for (Map.Entry<String, Object> entry : map.entrySet()) {
1023 json.put(entry.getKey(), entry.getValue());
1024 }
1025
1026 return json.toString();
1027 } catch (JSONException e) {
1028 throw new AssertionError(e);
1029 }
1030 }
1031
1032 private static void triggerFlagChangeListeners(Map<String, Change> changes) {
1033 for (Map.Entry<String, Change> change : changes.entrySet()) {
1034 OnFlagChange listener = FLAG_CHANGE_LISTENERS.get(change.getKey());
1035
1036 if (listener != null) {
1037 Log.i(TAG, "Triggering change listener for: " + change.getKey());
1038 listener.onFlagChange(change.getValue());
1039 }
1040 }
1041 }
1042
1043 @VisibleForTesting
1044 static final class UpdateResult {
1045 private final Map<String, Object> memory;
1046 private final Map<String, Object> disk;
1047 private final Map<String, Change> memoryChanges;
1048
1049 UpdateResult(@NonNull Map<String, Object> memory, @NonNull Map<String, Object> disk, @NonNull Map<String, Change> memoryChanges) {
1050 this.memory = memory;
1051 this.disk = disk;
1052 this.memoryChanges = memoryChanges;
1053 }
1054
1055 public @NonNull Map<String, Object> getMemory() {
1056 return memory;
1057 }
1058
1059 public @NonNull Map<String, Object> getDisk() {
1060 return disk;
1061 }
1062
1063 public @NonNull Map<String, Change> getMemoryChanges() {
1064 return memoryChanges;
1065 }
1066 }
1067
1068 @VisibleForTesting
1069 interface OnFlagChange {
1070 void onFlagChange(@NonNull Change change);
1071 }
1072
1073 enum Change {
1074 ENABLED, DISABLED, CHANGED, REMOVED
1075 }
1076}