That fuck shit the fascists are using
at master 1076 lines 42 kB view raw
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}