diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt index d557880744..bb24b8e934 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/messages/SyncMessageProcessorTest_synchronizeDeleteForMe.kt @@ -44,7 +44,7 @@ class SyncMessageProcessorTest_synchronizeDeleteForMe { messageHelper = MessageHelper(harness) mockkStatic(FeatureFlags::class) - every { FeatureFlags.deleteSyncEnabled() } returns true + every { FeatureFlags.deleteSyncEnabled } returns true } @After diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/util/FeatureFlagsAccessor.java b/app/src/androidTest/java/org/thoughtcrime/securesms/util/FeatureFlagsAccessor.java deleted file mode 100644 index 437b1f6e77..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/util/FeatureFlagsAccessor.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.thoughtcrime.securesms.util; - -/** - * A class that allows us to inject feature flags during tests. - */ -public final class FeatureFlagsAccessor { - - public static void forceValue(String key, Object value) { - FeatureFlags.FORCED_VALUES.put(key, value); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 9d5d15768a..fcd737e071 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -20,7 +20,7 @@ import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash class MessageBackupsFlowViewModel : ViewModel() { private val internalState = mutableStateOf( MessageBackupsFlowState( - availableBackupTiers = if (!FeatureFlags.messageBackups()) { + availableBackupTiers = if (!FeatureFlags.messageBackups) { emptyList() } else { listOf(MessageBackupTier.FREE, MessageBackupTier.PAID) diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt index d0d43f97c0..7478ab8853 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/links/CallLinks.kt @@ -54,7 +54,7 @@ object CallLinks { @JvmStatic fun isCallLink(url: String): Boolean { - if (!FeatureFlags.adHocCalling()) { + if (!FeatureFlags.adHocCalling) { return false } @@ -67,7 +67,7 @@ object CallLinks { @JvmStatic fun parseUrl(url: String): CallLinkRootKey? { - if (!FeatureFlags.adHocCalling()) { + if (!FeatureFlags.adHocCalling) { return null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt index 5b583d1292..ed1fae9565 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogFragment.kt @@ -253,7 +253,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal MaterialAlertDialogBuilder(requireContext()) .setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count)) .setMessage( - if (FeatureFlags.adHocCalling()) { + if (FeatureFlags.adHocCalling) { getString(R.string.CallLogFragment__call_links_youve_created) } else { null @@ -403,7 +403,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal MaterialAlertDialogBuilder(requireContext()) .setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1)) .setMessage( - if (FeatureFlags.adHocCalling()) { + if (FeatureFlags.adHocCalling) { getString(R.string.CallLogFragment__call_links_youve_created) } else { null diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt index bb1830cb2d..45073e9112 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogPagedDataSource.kt @@ -10,7 +10,7 @@ class CallLogPagedDataSource( ) : PagedDataSource { private val hasFilter = filter == CallLogFilter.MISSED - private val hasCallLinkRow = FeatureFlags.adHocCalling() && filter == CallLogFilter.ALL && query.isNullOrEmpty() + private val hasCallLinkRow = FeatureFlags.adHocCalling && filter == CallLogFilter.ALL && query.isNullOrEmpty() private var callEventsCount = 0 private var callLinksCount = 0 diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogViewModel.kt index e30c7de46e..94c7fc656e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogViewModel.kt @@ -82,7 +82,7 @@ class CallLogViewModel( controller.onDataInvalidated() } - if (FeatureFlags.adHocCalling()) { + if (FeatureFlags.adHocCalling) { disposables += Observable .interval(30, TimeUnit.SECONDS, Schedulers.computation()) .flatMapCompletable { callLogRepository.peekCallLinks() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt index 17db1c3be0..26fb9a519c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt @@ -48,7 +48,7 @@ class DeleteSyncEducationDialog : ComposeBottomSheetDialogFragment() { fun shouldShow(): Boolean { return TextSecurePreferences.isMultiDevice(AppDependencies.application) && !SignalStore.uiHints().hasSeenDeleteSyncEducationSheet && - FeatureFlags.deleteSyncEnabled() + FeatureFlags.deleteSyncEnabled } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index f75273dbfd..70adca9ba8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -40,7 +40,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent { override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { if (intent?.hasExtra(ARG_NAV_GRAPH) != true) { - val navGraphResId = if (FeatureFlags.registrationV2()) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings + val navGraphResId = if (FeatureFlags.registrationV2) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings intent?.putExtra(ARG_NAV_GRAPH, navGraphResId) } @@ -197,7 +197,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent { fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, StartLocation.RECOVER_USERNAME) private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent { - val navGraphResId = if (FeatureFlags.registrationV2()) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings + val navGraphResId = if (FeatureFlags.registrationV2) R.navigation.app_settings_with_change_number_v2 else R.navigation.app_settings return Intent(context, AppSettingsActivity::class.java) .putExtra(ARG_NAV_GRAPH, navGraphResId) .putExtra(START_LOCATION, startLocation.code) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 8b3549f296..4e0ad44415 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -160,7 +160,7 @@ class AppSettingsFragment : DSLSettingsFragment( title = DSLSettingsText.from(R.string.preferences__linked_devices), icon = DSLSettingsIcon.from(R.drawable.symbol_devices_24), onClick = { - if (FeatureFlags.internalUser()) { + if (FeatureFlags.internalUser) { findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkDeviceFragment) } else { findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity) @@ -281,7 +281,7 @@ class AppSettingsFragment : DSLSettingsFragment( } ) - if (FeatureFlags.internalUser()) { + if (FeatureFlags.internalUser) { dividerPref() clickPref( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt index 56bfba179e..81a7f5130f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/ChatsSettingsFragment.kt @@ -84,7 +84,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch sectionHeaderPref(R.string.preferences_chats__backups) - if (FeatureFlags.messageBackups() || state.remoteBackupsEnabled) { + if (FeatureFlags.messageBackups || state.remoteBackupsEnabled) { clickPref( title = DSLSettingsText.from("Signal Backups"), // TODO [message-backups] -- Finalized copy summary = DSLSettingsText.from(if (state.remoteBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt index daff34acd9..bc0a1d71f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsFragment.kt @@ -138,7 +138,7 @@ class ManageStorageSettingsFragment : ComposeFragment() { dialog("confirm-delete-chat-history") { Dialogs.SimpleAlertDialog( title = stringResource(id = R.string.preferences_storage__delete_message_history), - body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && FeatureFlags.deleteSyncEnabled()) { + body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && FeatureFlags.deleteSyncEnabled) { stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device_linked_device) } else { stringResource(id = R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device) @@ -154,7 +154,7 @@ class ManageStorageSettingsFragment : ComposeFragment() { dialog("double-confirm-delete-chat-history", dialogProperties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) { Dialogs.SimpleAlertDialog( title = stringResource(id = R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history), - body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && FeatureFlags.deleteSyncEnabled()) { + body = if (TextSecurePreferences.isMultiDevice(LocalContext.current) && FeatureFlags.deleteSyncEnabled) { stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone_linked_device) } else { stringResource(id = R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt index c775b29ff6..160886d6e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppDonations.kt @@ -38,9 +38,9 @@ object InAppDonations { private fun isPayPalAvailableForDonateToSignalType(inAppPaymentType: InAppPaymentTable.Type): Boolean { return when (inAppPaymentType) { InAppPaymentTable.Type.UNKNOWN -> error("Unsupported type UNKNOWN") - InAppPaymentTable.Type.ONE_TIME_DONATION, InAppPaymentTable.Type.ONE_TIME_GIFT -> FeatureFlags.paypalOneTimeDonations() - InAppPaymentTable.Type.RECURRING_DONATION -> FeatureFlags.paypalRecurringDonations() - InAppPaymentTable.Type.RECURRING_BACKUP -> FeatureFlags.messageBackups() && FeatureFlags.paypalRecurringDonations() + InAppPaymentTable.Type.ONE_TIME_DONATION, InAppPaymentTable.Type.ONE_TIME_GIFT -> FeatureFlags.paypalOneTimeDonations + InAppPaymentTable.Type.RECURRING_DONATION -> FeatureFlags.paypalRecurringDonations + InAppPaymentTable.Type.RECURRING_BACKUP -> FeatureFlags.messageBackups && FeatureFlags.paypalRecurringDonations } && !LocaleFeatureFlags.isPayPalDisabled() } @@ -55,7 +55,7 @@ object InAppDonations { * Whether the user is in a region that supports PayPal, based off local phone number. */ fun isPayPalAvailable(): Boolean { - return (FeatureFlags.paypalOneTimeDonations() || FeatureFlags.paypalRecurringDonations()) && !LocaleFeatureFlags.isPayPalDisabled() + return (FeatureFlags.paypalOneTimeDonations || FeatureFlags.paypalRecurringDonations) && !LocaleFeatureFlags.isPayPalDisabled() } /** @@ -69,14 +69,14 @@ object InAppDonations { * Whether the user is in a region which supports SEPA Debit transfers, based off local phone number. */ fun isSEPADebitAvailable(): Boolean { - return Environment.IS_STAGING || (FeatureFlags.sepaDebitDonations() && LocaleFeatureFlags.isSepaEnabled()) + return Environment.IS_STAGING || (FeatureFlags.sepaDebitDonations && LocaleFeatureFlags.isSepaEnabled()) } /** * Whether the user is in a region which supports IDEAL transfers, based off local phone number. */ fun isIDEALAvailable(): Boolean { - return Environment.IS_STAGING || (FeatureFlags.idealDonations() && LocaleFeatureFlags.isIdealEnabled()) + return Environment.IS_STAGING || (FeatureFlags.idealDonations && LocaleFeatureFlags.isIdealEnabled()) } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt index daa7ab8f98..26bdb59894 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/stripe/Stripe3DSDialogFragment.kt @@ -84,7 +84,7 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen ) ) - if (FeatureFlags.internalUser() && args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) { + if (FeatureFlags.internalUser && args.inAppPayment.data.paymentMethodType == InAppPaymentData.PaymentMethodType.IDEAL) { val openApp = MaterialButton(requireContext()).apply { text = "Open App" layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt index 9d432c6dd0..8007c5f76a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt @@ -159,9 +159,9 @@ class ConversationSettingsRepository( members.addAll(groupRecord.members) members.addAll(pendingMembers) - GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits(), groupRecord.isAnnouncementGroup) + GroupCapacityResult(Recipient.self().id, members, FeatureFlags.groupLimits, groupRecord.isAnnouncementGroup) } else { - GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits(), false) + GroupCapacityResult(Recipient.self().id, groupRecord.members, FeatureFlags.groupLimits, false) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt index a96b2a86b7..651b23c096 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/ContactDiscoveryRefreshV2.kt @@ -98,7 +98,7 @@ object ContactDiscoveryRefreshV2 { Optional.empty(), BuildConfig.CDSI_MRENCLAVE, 10_000, - if (FeatureFlags.useLibsignalNetForCdsiLookup()) AppDependencies.libsignalNetwork else null + if (FeatureFlags.useLibsignalNetForCdsiLookup) AppDependencies.libsignalNetwork else null ) { Log.i(TAG, "Ignoring token for one-off lookup.") } @@ -145,8 +145,8 @@ object ContactDiscoveryRefreshV2 { return ContactDiscovery.RefreshResult(emptySet(), emptyMap()) } - if (newE164s.size > FeatureFlags.cdsHardLimit()) { - Log.w(TAG, "[$tag] Number of new contacts (${newE164s.size.roundedString()} > hard limit (${FeatureFlags.cdsHardLimit()}! Failing and marking ourselves as permanently blocked.") + if (newE164s.size > FeatureFlags.cdsHardLimit) { + Log.w(TAG, "[$tag] Number of new contacts (${newE164s.size.roundedString()} > hard limit (${FeatureFlags.cdsHardLimit}! Failing and marking ourselves as permanently blocked.") SignalStore.misc().markCdsPermanentlyBlocked() throw IOException("New contacts over the CDS hard limit!") } @@ -163,7 +163,7 @@ object ContactDiscoveryRefreshV2 { Optional.ofNullable(token), BuildConfig.CDSI_MRENCLAVE, timeoutMs, - if (FeatureFlags.useLibsignalNetForCdsiLookup()) AppDependencies.libsignalNetwork else null + if (FeatureFlags.useLibsignalNetForCdsiLookup) AppDependencies.libsignalNetwork else null ) { tokenToSave -> stopwatch.split("network-pre-token") if (!isPartialRefresh) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index 63c5df637e..6ec57082c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -124,7 +124,7 @@ class MultiselectForwardFragment : contactSearchMediator = ContactSearchMediator( this, emptySet(), - FeatureFlags.shareSelectionLimit(), + FeatureFlags.shareSelectionLimit, ContactSearchAdapter.DisplayOptions( displayCheckBox = !args.selectSingleRecipient, displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 168ca20b4a..b4640a73ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -1687,7 +1687,7 @@ class ConversationFragment : val keyboardPage = when (keyboardMode) { TextSecurePreferences.MediaKeyboardMode.EMOJI -> if (isSystemEmojiPreferred) KeyboardPage.STICKER else KeyboardPage.EMOJI TextSecurePreferences.MediaKeyboardMode.STICKER -> KeyboardPage.STICKER - TextSecurePreferences.MediaKeyboardMode.GIF -> if (FeatureFlags.gifSearchAvailable()) KeyboardPage.GIF else KeyboardPage.STICKER + TextSecurePreferences.MediaKeyboardMode.GIF -> if (FeatureFlags.gifSearchAvailable) KeyboardPage.GIF else KeyboardPage.STICKER } inputPanel.setMediaKeyboardToggleMode(keyboardPage) @@ -2408,7 +2408,7 @@ class ConversationFragment : disposables += DeleteDialog.show( context = requireContext(), messageRecords = records, - message = if (TextSecurePreferences.isMultiDevice(requireContext()) && FeatureFlags.deleteSyncEnabled()) { + message = if (TextSecurePreferences.isMultiDevice(requireContext()) && FeatureFlags.deleteSyncEnabled) { resources.getQuantityString(R.plurals.ConversationFragment_delete_on_linked_warning, records.size) } else { null diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt index e37f517cb3..087c345274 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt @@ -125,7 +125,7 @@ class ConversationDataSource( records = MessageDataFetcher.updateModelsWithData(records, extraData).toMutableList() stopwatch.split("models") - if (FeatureFlags.messageBackups() && SignalStore.backup().restoreState.inProgress) { + if (FeatureFlags.messageBackups && SignalStore.backup().restoreState.inProgress) { BackupRestoreManager.prioritizeAttachmentsIfNeeded(records) stopwatch.split("restore") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt index 0f687d8a82..fdb3a6d21c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/crash/CrashConfig.kt @@ -28,7 +28,7 @@ object CrashConfig { fun computePatterns(): List { val aci: ServiceId.ACI = SignalStore.account().aci ?: return emptyList() - val serialized = FeatureFlags.crashPromptConfig() + val serialized = FeatureFlags.crashPromptConfig if (serialized.isNullOrBlank()) { return emptyList() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index b412b91fd5..d3ed3d148e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -164,7 +164,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD @JvmOverloads fun getGalleryMediaForThread(threadId: Long, sorting: Sorting, limit: Int = 0): Cursor { - var query = if (FeatureFlags.messageBackups()) { + var query = if (FeatureFlags.messageBackups) { sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS_AND_THUMBNAILS)) } else { sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY_INCLUDING_TEMP_VIDEOS)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt index 338f8d47a0..474c24c621 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogTables.kt @@ -153,7 +153,7 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */ fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageId: MessageId, urgent: Boolean): Long { - if (!FeatureFlags.retryReceipts()) return -1 + if (!FeatureFlags.retryReceipts) return -1 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices)) @@ -165,7 +165,7 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */ fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageIds: List, urgent: Boolean): Long { - if (!FeatureFlags.retryReceipts()) return -1 + if (!FeatureFlags.retryReceipts) return -1 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices)) @@ -177,7 +177,7 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */ fun insertIfPossible(sentTimestamp: Long, possibleRecipients: List, results: List, contentHint: ContentHint, messageId: MessageId, urgent: Boolean): Long { - if (!FeatureFlags.retryReceipts()) return -1 + if (!FeatureFlags.retryReceipts) return -1 val accessList = RecipientAccessList(possibleRecipients) @@ -198,7 +198,7 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal } fun addRecipientToExistingEntryIfPossible(payloadId: Long, recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageId: MessageId, urgent: Boolean): Long { - if (!FeatureFlags.retryReceipts()) return payloadId + if (!FeatureFlags.retryReceipts) return payloadId if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { val db = databaseHelper.signalWritableDatabase @@ -274,9 +274,9 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal } fun getLogEntry(recipientId: RecipientId, device: Int, dateSent: Long): MessageLogEntry? { - if (!FeatureFlags.retryReceipts()) return null + if (!FeatureFlags.retryReceipts) return null - trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()) + trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge) val db = databaseHelper.signalReadableDatabase val table = "${MslPayloadTable.TABLE_NAME} LEFT JOIN ${MslRecipientTable.TABLE_NAME} ON ${MslPayloadTable.TABLE_NAME}.${MslPayloadTable.ID} = ${MslRecipientTable.TABLE_NAME}.${MslRecipientTable.PAYLOAD_ID}" @@ -356,7 +356,7 @@ class MessageSendLogTables constructor(context: Context?, databaseHelper: Signal } fun deleteAllForRecipient(recipientId: RecipientId) { - if (!FeatureFlags.retryReceipts()) return + if (!FeatureFlags.retryReceipts) return writableDatabase .delete(MslRecipientTable.TABLE_NAME) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptCache.kt b/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptCache.kt index 65b6f7dc57..a1ca0bfd8d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptCache.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PendingRetryReceiptCache.kt @@ -21,7 +21,7 @@ class PendingRetryReceiptCache @VisibleForTesting constructor( private var populated: Boolean = false fun insert(author: RecipientId, authorDevice: Int, sentTimestamp: Long, receivedTimestamp: Long, threadId: Long) { - if (!FeatureFlags.retryReceipts()) return + if (!FeatureFlags.retryReceipts) return ensurePopulated() val model: PendingRetryReceiptModel = database.insert(author, authorDevice, sentTimestamp, receivedTimestamp, threadId) synchronized(pendingRetries) { @@ -36,7 +36,7 @@ class PendingRetryReceiptCache @VisibleForTesting constructor( } fun get(author: RecipientId, sentTimestamp: Long): PendingRetryReceiptModel? { - if (!FeatureFlags.retryReceipts()) return null + if (!FeatureFlags.retryReceipts) return null ensurePopulated() synchronized(pendingRetries) { @@ -45,7 +45,7 @@ class PendingRetryReceiptCache @VisibleForTesting constructor( } fun getOldest(): PendingRetryReceiptModel? { - if (!FeatureFlags.retryReceipts()) return null + if (!FeatureFlags.retryReceipts) return null ensurePopulated() synchronized(pendingRetries) { @@ -54,7 +54,7 @@ class PendingRetryReceiptCache @VisibleForTesting constructor( } fun delete(model: PendingRetryReceiptModel) { - if (!FeatureFlags.retryReceipts()) return + if (!FeatureFlags.retryReceipts) return ensurePopulated() synchronized(pendingRetries) { @@ -64,7 +64,7 @@ class PendingRetryReceiptCache @VisibleForTesting constructor( } fun clear() { - if (!FeatureFlags.retryReceipts()) return + if (!FeatureFlags.retryReceipts) return synchronized(pendingRetries) { pendingRetries.clear() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index c2925ba52c..f0c82d1c3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -4152,7 +4152,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da * get them back through CDS). */ fun debugClearServiceIds(recipientId: RecipientId? = null) { - check(FeatureFlags.internalUser()) + check(FeatureFlags.internalUser) writableDatabase .update(TABLE_NAME) @@ -4177,7 +4177,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da * Should only be used for debugging! A very destructive action that clears all known profile keys and credentials. */ fun debugClearProfileData(recipientId: RecipientId? = null) { - check(FeatureFlags.internalUser()) + check(FeatureFlags.internalUser) writableDatabase .update(TABLE_NAME) @@ -4208,7 +4208,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da * Should only be used for debugging! Clears the E164 and PNI from a recipient. */ fun debugClearE164AndPni(recipientId: RecipientId) { - check(FeatureFlags.internalUser()) + check(FeatureFlags.internalUser) writableDatabase .update(TABLE_NAME) @@ -4228,7 +4228,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da * Only works if the recipient has a PNI. */ fun debugRemoveAci(recipientId: RecipientId) { - check(FeatureFlags.internalUser()) + check(FeatureFlags.internalUser) writableDatabase.execSQL( """ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 72ecd004fe..25cf4bbeae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -326,7 +326,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa return } - val syncThreadTrimDeletes = SignalStore.settings().shouldSyncThreadTrimDeletes() && FeatureFlags.deleteSyncEnabled() + val syncThreadTrimDeletes = SignalStore.settings().shouldSyncThreadTrimDeletes() && FeatureFlags.deleteSyncEnabled val threadTrimsToSync = mutableListOf>>() readableDatabase @@ -1131,7 +1131,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa var addressableMessages: Set = emptySet() writableDatabase.withinTransaction { db -> - if (syncThreadDeletes && FeatureFlags.deleteSyncEnabled()) { + if (syncThreadDeletes && FeatureFlags.deleteSyncEnabled) { addressableMessages = messages.getMostRecentAddressableMessages(threadId) } @@ -1164,7 +1164,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa db.deactivateThread(query) } - if (FeatureFlags.deleteSyncEnabled()) { + if (FeatureFlags.deleteSyncEnabled) { for (threadId in selectedConversations) { addressableMessages += threadId to messages.getMostRecentAddressableMessages(threadId) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt index 4804cdfe7e..4ffc3c64b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/gcm/FcmFetchManager.kt @@ -71,7 +71,7 @@ object FcmFetchManager { } private fun postMayHaveMessagesNotification(context: Context) { - if (FeatureFlags.fcmMayHaveMessagesNotificationKillSwitch()) { + if (FeatureFlags.fcmMayHaveMessagesNotificationKillSwitch) { Log.w(TAG, "May have messages notification kill switch") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt index 1235c5f297..2bc5ab9451 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.kt @@ -222,7 +222,7 @@ class AttachmentDownloadJob private constructor( attachmentId: AttachmentId, attachment: DatabaseAttachment ) { - val maxReceiveSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes() + val maxReceiveSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes val attachmentFile: File = SignalDatabase.attachments.getOrCreateTransferFile(attachmentId) var archiveFile: File? = null var useArchiveCdn = false @@ -401,7 +401,7 @@ class AttachmentDownloadJob private constructor( S3.getObject(attachment.fileName!!).use { response -> val body = response.body() if (body != null) { - if (body.contentLength() > FeatureFlags.maxAttachmentReceiveSizeBytes()) { + if (body.contentLength() > FeatureFlags.maxAttachmentReceiveSizeBytes) { throw MmsException("Attachment too large, failing download") } SignalDatabase.attachments.finalizeAttachmentAfterDownload(messageId, attachmentId, (body.source() as Source).buffer().inputStream()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt index 3ed44f1dcc..5d5c92be99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AttachmentUploadJob.kt @@ -66,7 +66,7 @@ class AttachmentUploadJob private constructor( @JvmStatic val maxPlaintextSize: Long get() { - val maxCipherTextSize = FeatureFlags.maxAttachmentSizeBytes() + val maxCipherTextSize = FeatureFlags.maxAttachmentSizeBytes val maxPaddedSize = AttachmentCipherStreamUtil.getPlaintextLength(maxCipherTextSize) return PaddingInputStream.getMaxUnpaddedSize(maxPaddedSize) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt index 0c480d5e49..20a5efa4a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkPeekJob.kt @@ -46,7 +46,7 @@ internal class CallLinkPeekJob private constructor( ) override fun onRun() { - if (!FeatureFlags.adHocCalling()) { + if (!FeatureFlags.adHocCalling) { Log.i(TAG, "Ad hoc calling is disabled. Dropping peek for call link.") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt index 9c5e270b26..003f3d343f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CallLinkUpdateSendJob.kt @@ -65,7 +65,7 @@ class CallLinkUpdateSendJob private constructor( override fun onFailure() = Unit override fun onRun() { - if (!FeatureFlags.adHocCalling()) { + if (!FeatureFlags.adHocCalling) { Log.i(TAG, "Call links are not enabled. Exiting.") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt index a13ca3d767..975ccd9e64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceDeleteSendSyncJob.kt @@ -58,7 +58,7 @@ class MultiDeviceDeleteSendSyncJob private constructor( return } - if (!FeatureFlags.deleteSyncEnabled()) { + if (!FeatureFlags.deleteSyncEnabled) { Log.i(TAG, "Delete sync support not enabled.") return } @@ -74,7 +74,7 @@ class MultiDeviceDeleteSendSyncJob private constructor( return } - if (!FeatureFlags.deleteSyncEnabled()) { + if (!FeatureFlags.deleteSyncEnabled) { Log.i(TAG, "Delete sync support not enabled.") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt index c3fc83ee60..1f5cd269b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt @@ -138,7 +138,7 @@ class PreKeysSyncJob private constructor( warn(TAG, "Forced rotation was requested, but the consistency checks passed!") val timeSinceLastForcedRotation = System.currentTimeMillis() - SignalStore.misc().lastForcedPreKeyRefresh // We check < 0 in case someone changed their clock and had a bad value set - timeSinceLastForcedRotation > FeatureFlags.preKeyForceRefreshInterval() || timeSinceLastForcedRotation < 0 + timeSinceLastForcedRotation > FeatureFlags.preKeyForceRefreshInterval || timeSinceLastForcedRotation < 0 } } else { false diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReclaimUsernameAndLinkJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReclaimUsernameAndLinkJob.kt index b9d02a3f2a..06bd13a120 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReclaimUsernameAndLinkJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReclaimUsernameAndLinkJob.kt @@ -48,7 +48,7 @@ class ReclaimUsernameAndLinkJob private constructor(parameters: Parameters) : Jo return when (UsernameRepository.reclaimUsernameIfNecessary()) { UsernameRepository.UsernameReclaimResult.SUCCESS -> Result.success() UsernameRepository.UsernameReclaimResult.PERMANENT_ERROR -> Result.success() - UsernameRepository.UsernameReclaimResult.NETWORK_ERROR -> Result.retry(BackoffUtil.exponentialBackoff(runAttempt + 1, FeatureFlags.getDefaultMaxBackoff())) + UsernameRepository.UsernameReclaimResult.NETWORK_ERROR -> Result.retry(BackoffUtil.exponentialBackoff(runAttempt + 1, FeatureFlags.defaultMaxBackoff)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt index f482a90cb6..31d3b8cd4e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentJob.kt @@ -208,7 +208,7 @@ class RestoreAttachmentJob private constructor( attachmentId: AttachmentId, attachment: DatabaseAttachment ) { - val maxReceiveSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes() + val maxReceiveSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes val attachmentFile: File = SignalDatabase.attachments.getOrCreateTransferFile(attachmentId) var archiveFile: File? = null var useArchiveCdn = false @@ -439,7 +439,7 @@ class RestoreAttachmentJob private constructor( return } - val maxThumbnailSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes() + val maxThumbnailSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes val thumbnailTransferFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile() val thumbnailFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile() diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt index 318c6a3209..b8d28c2fb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RestoreAttachmentThumbnailJob.kt @@ -197,7 +197,7 @@ class RestoreAttachmentThumbnailJob private constructor( return } - val maxThumbnailSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes() + val maxThumbnailSize: Long = FeatureFlags.maxAttachmentReceiveSizeBytes val thumbnailTransferFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile() val thumbnailFile: File = SignalDatabase.attachments.createArchiveThumbnailTransferFile() diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt index 6acba01de6..4752dee389 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt @@ -20,7 +20,7 @@ class KeyboardPagerViewModel : ViewModel() { startingPages.remove(KeyboardPage.EMOJI) } - if (!FeatureFlags.gifSearchAvailable()) { + if (!FeatureFlags.gifSearchAvailable) { startingPages.remove(KeyboardPage.GIF) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt index b9b42c183c..147dbe9287 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/PaymentsValues.kt @@ -118,7 +118,7 @@ internal class PaymentsValues internal constructor(store: KeyValueStore) : Signa if (!SignalStore.account().isRegistered) { return PaymentsAvailability.NOT_IN_REGION } - return if (FeatureFlags.payments()) { + return if (FeatureFlags.payments) { if (mobileCoinPaymentsEnabled()) { if (GeographicalRestrictions.e164Allowed(SignalStore.account().e164)) { PaymentsAvailability.WITHDRAW_AND_SEND diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java index 56df874a24..6d4bc34c10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionFeatureFlags.java @@ -24,11 +24,9 @@ public class LogSectionFeatureFlags implements LogSection { Map memory = FeatureFlags.getDebugMemoryValues(); Map disk = FeatureFlags.getDebugDiskValues(); Map pending = FeatureFlags.getDebugPendingDiskValues(); - Map forced = FeatureFlags.getDebugForcedValues(); int remoteLength = Stream.of(memory.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"); for (Map.Entry entry : memory.entrySet()) { @@ -48,15 +46,6 @@ public class LogSectionFeatureFlags implements LogSection { } out.append("\n"); - out.append("-- Forced\n"); - if (forced.isEmpty()) { - out.append("None\n"); - } else { - for (Map.Entry entry : forced.entrySet()) { - out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n"); - } - } - return out; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt index bba91f1e58..b6659334d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewRepository.kt @@ -85,7 +85,7 @@ class MediaPreviewRepository { fun localDelete(attachment: DatabaseAttachment): Completable { return Completable.fromRunnable { val deletedMessageRecord = AttachmentUtil.deleteAttachment(attachment) - if (deletedMessageRecord != null && FeatureFlags.deleteSyncEnabled()) { + if (deletedMessageRecord != null && FeatureFlags.deleteSyncEnabled) { MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(setOf(deletedMessageRecord)) } }.subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt index fd5e7c0103..22f9b10872 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -601,7 +601,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v MaterialAlertDialogBuilder(requireContext()).apply { setIcon(R.drawable.symbol_error_triangle_fill_24) setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title) - setMessage(if (TextSecurePreferences.isMultiDevice(requireContext()) && FeatureFlags.deleteSyncEnabled()) R.string.MediaPreviewActivity_media_delete_confirmation_message_linked_device else R.string.MediaPreviewActivity_media_delete_confirmation_message) + setMessage(if (TextSecurePreferences.isMultiDevice(requireContext()) && FeatureFlags.deleteSyncEnabled) R.string.MediaPreviewActivity_media_delete_confirmation_message_linked_device else R.string.MediaPreviewActivity_media_delete_confirmation_message) setCancelable(true) setNegativeButton(android.R.string.cancel, null) setPositiveButton(R.string.ConversationFragment_delete_for_me) { _, _ -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt index 42c1e1ebdb..8f13c156c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModePolicy.kt @@ -86,7 +86,7 @@ sealed class CameraXModePolicy { val isMixedModeSupported = isVideoSupported && Build.VERSION.SDK_INT >= 26 && CameraXUtil.isMixedModeSupported(context) && - !FeatureFlags.cameraXMixedModelBlocklist().asListContains(Build.MODEL) + !FeatureFlags.cameraXMixedModelBlocklist.asListContains(Build.MODEL) return when { isMixedModeSupported -> Mixed(isQrScanEnabled) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlocklist.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlocklist.kt index cfea065b89..eb1f399f6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlocklist.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXModelBlocklist.kt @@ -12,6 +12,6 @@ object CameraXModelBlocklist { @JvmStatic fun isBlocklisted(): Boolean { - return FeatureFlags.cameraXModelBlocklist().asListContains(Build.MODEL) + return FeatureFlags.cameraXModelBlocklist.asListContains(Build.MODEL) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index 84529d23e9..cc49f217fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -92,7 +92,7 @@ class MediaSelectionActivity : override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { setContentView(R.layout.media_selection_activity) - if (FeatureFlags.customCameraXController()) { + if (FeatureFlags.customCameraXController) { requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt index 1a9551bae8..b14f4df041 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionState.kt @@ -36,7 +36,7 @@ data class MediaSelectionState( val transcodingPreset: TranscodingPreset = MediaConstraints.getPushMediaConstraints(SentMediaQuality.fromCode(quality.code)).videoTranscodingSettings - val maxSelection = FeatureFlags.maxAttachmentCount() + val maxSelection = FeatureFlags.maxAttachmentCount val canSend = !isSent && selectedMedia.isNotEmpty() diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt index 0b5dade597..38e1546fe2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/capture/MediaCaptureFragment.kt @@ -94,7 +94,7 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme .setTitle(R.string.MediaCaptureFragment_device_link_dialog_title) .setMessage(R.string.MediaCaptureFragment_device_link_dialog_body) .setPositiveButton(R.string.MediaCaptureFragment_device_link_dialog_continue) { d, _ -> - if (FeatureFlags.internalUser()) { + if (FeatureFlags.internalUser) { startActivity(AppSettingsActivity.linkedDevices(requireContext())) } else { startActivity(DeviceActivity.getIntentForScanner(requireContext())) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt index c9f1be061f..d8b402d505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/ChooseGroupStoryBottomSheet.kt @@ -65,7 +65,7 @@ class ChooseGroupStoryBottomSheet : FixedRoundedCornerBottomSheetDialogFragment( val contactRecycler: RecyclerView = view.findViewById(R.id.contact_recycler) mediator = ContactSearchMediator( fragment = this, - selectionLimits = FeatureFlags.shareSelectionLimit(), + selectionLimits = FeatureFlags.shareSelectionLimit, displayOptions = ContactSearchAdapter.DisplayOptions( displayCheckBox = true, displaySecondaryInformation = ContactSearchAdapter.DisplaySecondaryInformation.NEVER diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationState.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationState.kt index 13ea17f1be..f48ac7e913 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationState.kt @@ -17,7 +17,7 @@ data class TextStoryPostCreationState( val body: CharSequence = "", val textColor: Int = HSVColorSlider.getLastColor(), val textColorStyle: TextColorStyle = TextColorStyle.NO_BACKGROUND, - val textAlignment: TextAlignment = if (FeatureFlags.storiesTextFunctions()) TextAlignment.START else TextAlignment.CENTER, + val textAlignment: TextAlignment = if (FeatureFlags.storiesTextFunctions) TextAlignment.START else TextAlignment.CENTER, val textFont: TextFont = TextFont.REGULAR, @IntRange(from = 0, to = 100) val textScale: Int = 50, val backgroundColor: ChatColors = TextStoryBackgroundColors.getInitialBackgroundColor(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostTextEntryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostTextEntryFragment.kt index 9500ed027b..85327fd7a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostTextEntryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostTextEntryFragment.kt @@ -105,7 +105,7 @@ class TextStoryPostTextEntryFragment : KeyboardEntryDialogFragment( backgroundButton ) - if (FeatureFlags.storiesTextFunctions()) { + if (FeatureFlags.storiesTextFunctions) { fadeableViews = fadeableViews + alignmentButton alignmentButton.visibility = View.VISIBLE scaleBar.visibility = View.VISIBLE diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index c8a135c5da..be434b1927 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -132,7 +132,7 @@ public final class Megaphones { return false; } - long expiringAt = device.lastActiveTimestamp + FeatureFlags.linkedDeviceLifespan(); + long expiringAt = device.lastActiveTimestamp + FeatureFlags.getLinkedDeviceLifespan(); long expiringIn = Math.max(expiringAt - System.currentTimeMillis(), 0); return expiringIn < TimeUnit.DAYS.toMillis(7) && expiringIn > 0; @@ -177,7 +177,7 @@ public final class Megaphones { throw new IllegalStateException("No linked device to show"); } - long expiringAt = device.lastActiveTimestamp + FeatureFlags.linkedDeviceLifespan(); + long expiringAt = device.lastActiveTimestamp + FeatureFlags.getLinkedDeviceLifespan(); long expiringIn = Math.max(expiringAt - System.currentTimeMillis(), 0); int expiringDays = (int) TimeUnit.MILLISECONDS.toDays(expiringIn); diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt index 358a6e3f1b..186e9bbc48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt @@ -112,7 +112,7 @@ object RemoteMegaphoneRepository { private fun checkCondition(conditionalId: String): Boolean { return when (conditionalId) { "standard_donate" -> shouldShowDonateMegaphone() - "internal_user" -> FeatureFlags.internalUser() + "internal_user" -> FeatureFlags.internalUser else -> false } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt index 3cc0f38ce4..b8034088d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt @@ -220,7 +220,7 @@ object DataMessageProcessor { if (insertResult != null && insertResult.threadWasNewlyCreated && !threadRecipient.isGroup && !threadRecipient.isSelf && !senderRecipient.isSystemContact) { val timeSinceLastSync = System.currentTimeMillis() - SignalStore.misc().lastCdsForegroundSyncTime - if (timeSinceLastSync > FeatureFlags.cdsForegroundSyncInterval() || timeSinceLastSync < 0) { + if (timeSinceLastSync > FeatureFlags.cdsForegroundSyncInterval || timeSinceLastSync < 0) { log(envelope.timestamp!!, "New 1:1 chat. Scheduling a CDS sync to see if they match someone in our contacts.") AppDependencies.jobManager.add(DirectoryRefreshJob(false)) SignalStore.misc().lastCdsForegroundSyncTime = System.currentTimeMillis() diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt index 41dd13111e..a5e7a63e43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.kt @@ -561,7 +561,7 @@ open class MessageContentProcessor(private val context: Context) { } private fun handleRetryReceipt(envelope: Envelope, metadata: EnvelopeMetadata, decryptionErrorMessage: DecryptionErrorMessage, senderRecipient: Recipient) { - if (!FeatureFlags.retryReceipts()) { + if (!FeatureFlags.retryReceipts) { warn(envelope.timestamp!!, "[RetryReceipt] Feature flag disabled, skipping retry receipt.") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt index 9f55e2c30a..a2d2c529c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageDecryptor.kt @@ -156,7 +156,7 @@ object MessageDecryptor { if (validationResult is EnvelopeContentValidator.Result.Invalid) { Log.w(TAG, "${logPrefix(envelope, cipherResult)} Invalid content! ${validationResult.reason}", validationResult.throwable) - if (FeatureFlags.internalUser()) { + if (FeatureFlags.internalUser) { postInvalidMessageNotification(context, validationResult.reason) } @@ -213,11 +213,11 @@ object MessageDecryptor { check(e is ProtocolException) Log.w(TAG, "${logPrefix(envelope, e)} Decryption error!", e, true) - if (FeatureFlags.internalUser()) { + if (FeatureFlags.internalUser) { postDecryptionErrorNotification(context) } - if (FeatureFlags.retryReceipts()) { + if (FeatureFlags.retryReceipts) { buildResultForDecryptionError(context, envelope, serverDeliveredTimestamp, followUpOperations, e) } else { Log.w(TAG, "${logPrefix(envelope, e)} Retry receipts disabled! Enqueuing a session reset job, which will also insert an error message.", e, true) @@ -296,7 +296,7 @@ object MessageDecryptor { val errorCount: DecryptionErrorCount = decryptionErrorCounts.getOrPut(sender.id) { DecryptionErrorCount(count = 0, lastReceivedTime = 0) } val timeSinceLastError = receivedTimestamp - errorCount.lastReceivedTime - if (timeSinceLastError > FeatureFlags.retryReceiptMaxCountResetAge() && errorCount.count > 0) { + if (timeSinceLastError > FeatureFlags.retryReceiptMaxCountResetAge && errorCount.count > 0) { Log.i(TAG, "${logPrefix(envelope, senderServiceId)} Resetting decryption error count for ${sender.id} because it has been $timeSinceLastError ms since the last error.", true) errorCount.count = 0 } @@ -304,8 +304,8 @@ object MessageDecryptor { errorCount.count++ errorCount.lastReceivedTime = receivedTimestamp - if (errorCount.count > FeatureFlags.retryReceiptMaxCount()) { - Log.w(TAG, "${logPrefix(envelope, senderServiceId)} This is error number ${errorCount.count} from ${sender.id}, which is greater than the maximum of ${FeatureFlags.retryReceiptMaxCount()}. Ignoring.", true) + if (errorCount.count > FeatureFlags.retryReceiptMaxCount) { + Log.w(TAG, "${logPrefix(envelope, senderServiceId)} This is error number ${errorCount.count} from ${sender.id}, which is greater than the maximum of ${FeatureFlags.retryReceiptMaxCount}. Ignoring.", true) if (contentHint == ContentHint.IMPLICIT) { Log.w(TAG, "${logPrefix(envelope, senderServiceId)} The content hint is $contentHint, so no error message is needed.", true) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt index 2a6ae1aca5..3ba63add30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SignalServiceProtoUtil.kt @@ -171,7 +171,7 @@ object SignalServiceProtoUtil { } fun List.toPointersWithinLimit(): List { - return mapNotNull { it.toPointer() }.take(FeatureFlags.maxAttachmentCount()) + return mapNotNull { it.toPointer() }.take(FeatureFlags.maxAttachmentCount) } fun AttachmentPointer.toPointer(stickerLocator: StickerLocator? = null): Attachment? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt index 397bd9bcc1..6f15372228 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/StoryMessageProcessor.kt @@ -90,7 +90,7 @@ object StoryMessageProcessor { } if (insertResult != null) { - Stories.enqueueNextStoriesForDownload(threadRecipient.id, false, FeatureFlags.storiesAutoDownloadMaximum()) + Stories.enqueueNextStoriesForDownload(threadRecipient.id, false, FeatureFlags.storiesAutoDownloadMaximum) AppDependencies.expireStoriesManager.scheduleIfNecessary() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt index fd08ee2718..8766e01ea2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/SyncMessageProcessor.kt @@ -1368,7 +1368,7 @@ object SyncMessageProcessor { @Throws(BadGroupIdException::class) private fun handleSynchronizeGroupOrAdHocCallEvent(callEvent: SyncMessage.CallEvent, envelopeTimestamp: Long) { - if (!FeatureFlags.adHocCalling() && callEvent.type == SyncMessage.CallEvent.Type.AD_HOC_CALL) { + if (!FeatureFlags.adHocCalling && callEvent.type == SyncMessage.CallEvent.Type.AD_HOC_CALL) { log(envelopeTimestamp, "Ad-Hoc calling is not currently supported by this client, ignoring.") return } @@ -1475,7 +1475,7 @@ object SyncMessageProcessor { } private fun handleSynchronizeDeleteForMe(context: Context, deleteForMe: SyncMessage.DeleteForMe, envelopeTimestamp: Long, earlyMessageCacheEntry: EarlyMessageCacheEntry?) { - if (!FeatureFlags.deleteSyncEnabled()) { + if (!FeatureFlags.deleteSyncEnabled) { warn(envelopeTimestamp, "Delete for me sync message dropped as support not enabled") return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/DefaultWebSocketShadowingBridge.kt b/app/src/main/java/org/thoughtcrime/securesms/net/DefaultWebSocketShadowingBridge.kt index 9f48d70481..e4e7f6c264 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/DefaultWebSocketShadowingBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/net/DefaultWebSocketShadowingBridge.kt @@ -37,7 +37,7 @@ class DefaultWebSocketShadowingBridge(private val context: Application) : WebSoc } override fun triggerFailureNotification(message: String) { - if (!FeatureFlags.internalUser()) { + if (!FeatureFlags.internalUser) { return } val notification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt index c99adb908c..b7e416a313 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SlowNotificationHeuristics.kt @@ -36,7 +36,7 @@ object SlowNotificationHeuristics { private val TAG = Log.tag(SlowNotificationHeuristics::class.java) fun getConfiguration(): Configuration { - val json = FeatureFlags.delayedNotificationsPromptConfig() + val json = FeatureFlags.delayedNotificationsPromptConfig return if (TextUtils.isEmpty(json)) { getDefaultConfiguration() } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt index f9e259d1b7..eedf43faec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationThumbnails.kt @@ -41,7 +41,7 @@ object NotificationThumbnails { * specifics here, we'll just disable notification thumbnails for them. */ private val isBlocklisted by lazy { - FeatureFlags.notificationThumbnailProductBlocklist().asListContains(Build.PRODUCT) + FeatureFlags.notificationThumbnailProductBlocklist.asListContains(Build.PRODUCT) } fun getWithoutModifying(notificationItem: NotificationItem): NotificationItem.ThumbnailInfo { diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt index 31ad5141c6..2c6850daed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/Svr3Migration.kt @@ -19,11 +19,11 @@ object Svr3Migration { * Whether or not you should write to SVR3. If [shouldWriteToSvr2] is also enabled, you should write to SVR3 first. */ val shouldWriteToSvr3: Boolean - get() = shouldReadFromSvr3 && FeatureFlags.svr3MigrationPhase().let { it == 1 || it == 2 } + get() = shouldReadFromSvr3 && FeatureFlags.svr3MigrationPhase.let { it == 1 || it == 2 } /** * Whether or not you should write to SVR2. If [shouldWriteToSvr3] is also enabled, you should write to SVR3 first. */ val shouldWriteToSvr2: Boolean - get() = !shouldReadFromSvr3 || FeatureFlags.svr3MigrationPhase() != 2 + get() = !shouldReadFromSvr3 || FeatureFlags.svr3MigrationPhase != 2 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt index 9a24a41420..6e56f79c06 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/RegistrationV2Activity.kt @@ -86,7 +86,7 @@ class RegistrationV2Activity : BaseActivity() { val startIntent = MainActivity.clearTop(this).apply { if (needsPin) { putExtra("next_intent", CreateSvrPinActivity.getIntentForPinCreate(this@RegistrationV2Activity)) - } else if (!SignalStore.registrationValues().hasSkippedTransferOrRestore() && FeatureFlags.messageBackups()) { + } else if (!SignalStore.registrationValues().hasSkippedTransferOrRestore() && FeatureFlags.messageBackups) { putExtra("next_intent", RemoteRestoreActivity.getIntent(this@RegistrationV2Activity)) } else if (needsProfile) { putExtra("next_intent", CreateProfileActivity.getIntentForUserProfile(this@RegistrationV2Activity)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt index 56abd4e0b3..5d0ac88b7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/v2/ui/welcome/WelcomeV2Fragment.kt @@ -61,7 +61,7 @@ class WelcomeV2Fragment : LoggingFragment(R.layout.fragment_registration_welcome binding.welcomeContinueButton.setOnClickListener { onContinueClicked() } binding.welcomeTermsButton.setOnClickListener { onTermsClicked() } binding.welcomeTransferOrRestore.setOnClickListener { onTransferOrRestoreClicked() } - binding.welcomeTransferOrRestore.visible = !FeatureFlags.restoreAfterRegistration() + binding.welcomeTransferOrRestore.visible = !FeatureFlags.restoreAfterRegistration } private fun onContinueClicked() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt index 81aaf595fa..3c8199e6f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/transferorrestore/TransferOrRestoreV2Fragment.kt @@ -47,8 +47,8 @@ class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_r binding.transferOrRestoreFragmentRestoreRemoteCard.visible = false } - binding.transferOrRestoreFragmentRestoreRemoteCard.visible = FeatureFlags.messageBackups() - binding.transferOrRestoreFragmentMoreOptions.visible = FeatureFlags.messageBackups() + binding.transferOrRestoreFragmentRestoreRemoteCard.visible = FeatureFlags.messageBackups + binding.transferOrRestoreFragmentMoreOptions.visible = FeatureFlags.messageBackups val description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device) val toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device) diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/MessageBackupListener.kt b/app/src/main/java/org/thoughtcrime/securesms/service/MessageBackupListener.kt index 53cddea22a..53537093a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/MessageBackupListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/MessageBackupListener.kt @@ -36,7 +36,7 @@ class MessageBackupListener : PersistentAlarmManagerListener() { @JvmStatic fun schedule(context: Context?) { - if (FeatureFlags.messageBackups() && SignalStore.backup().areBackupsEnabled) { + if (FeatureFlags.messageBackups && SignalStore.backup().areBackupsEnabled) { MessageBackupListener().onReceive(context, getScheduleIntent()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt index f5e9433338..cef14c371a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/AndroidTelecomUtil.kt @@ -192,7 +192,7 @@ object AndroidTelecomUtil { } private fun isTelecomAllowedForDevice(): Boolean { - if (FeatureFlags.internalUser()) { + if (FeatureFlags.internalUser) { return !SignalStore.internalValues().callingDisableTelecom() } return RingRtcDynamicConfiguration.isTelecomAllowedForDevice() diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt index d4011a23e1..4e33fc4a43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt @@ -22,19 +22,19 @@ object RingRtcDynamicConfiguration { return when { isHardwareBlocklisted() || isKnownFaultyHardwareImplementation() -> AudioProcessingMethod.ForceSoftwareAec3 isSoftwareBlocklisted() -> AudioProcessingMethod.ForceHardware - Build.VERSION.SDK_INT < 29 && FeatureFlags.useHardwareAecIfOlderThanApi29() -> AudioProcessingMethod.ForceHardware + Build.VERSION.SDK_INT < 29 && FeatureFlags.useHardwareAecIfOlderThanApi29 -> AudioProcessingMethod.ForceHardware Build.VERSION.SDK_INT < 29 -> AudioProcessingMethod.ForceSoftwareAec3 else -> AudioProcessingMethod.ForceHardware } } fun isTelecomAllowedForDevice(): Boolean { - return FeatureFlags.telecomManufacturerAllowList().lowercase().asListContains(Build.MANUFACTURER.lowercase()) && - !FeatureFlags.telecomModelBlockList().lowercase().asListContains(Build.MODEL.lowercase()) + return FeatureFlags.telecomManufacturerAllowList.lowercase().asListContains(Build.MANUFACTURER.lowercase()) && + !FeatureFlags.telecomModelBlocklist.lowercase().asListContains(Build.MODEL.lowercase()) } private fun isHardwareBlocklisted(): Boolean { - return FeatureFlags.hardwareAecBlocklistModels().asListContains(Build.MODEL) + return FeatureFlags.hardwareAecBlocklistModels.asListContains(Build.MODEL) } fun isKnownFaultyHardwareImplementation(): Boolean { @@ -44,6 +44,6 @@ object RingRtcDynamicConfiguration { } private fun isSoftwareBlocklisted(): Boolean { - return FeatureFlags.softwareAecBlocklistModels().asListContains(Build.MODEL) + return FeatureFlags.softwareAecBlocklistModels.asListContains(Build.MODEL) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareRepository.kt index 3eeedc3743..49e9c100f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/v2/ShareRepository.kt @@ -82,7 +82,7 @@ class ShareRepository(context: Context) { } val media: List = mimeTypes.toList() - .take(FeatureFlags.maxAttachmentCount()) + .take(FeatureFlags.maxAttachmentCount) .map { (uri, mimeType) -> val stream: InputStream = try { appContext.contentResolver.openInputStream(uri) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt index ea1400889b..e784e559fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerViewModel.kt @@ -172,7 +172,7 @@ class StoryViewerViewModel( .filter { it != RecipientId.UNKNOWN } .distinctUntilChanged() .subscribe { - Stories.enqueueNextStoriesForDownload(it, true, FeatureFlags.storiesAutoDownloadMaximum()) + Stories.enqueueNextStoriesForDownload(it, true, FeatureFlags.storiesAutoDownloadMaximum) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt index 21e713ab56..5b39c62392 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DeleteDialog.kt @@ -109,7 +109,7 @@ object DeleteDialog { } } - if (FeatureFlags.deleteSyncEnabled()) { + if (FeatureFlags.deleteSyncEnabled) { MultiDeviceDeleteSendSyncJob.enqueueMessageDeletes(messageRecords) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.kt b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.kt index 04ac274c3b..125870d21c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.kt @@ -4,24 +4,22 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import org.json.JSONException import org.json.JSONObject -import org.signal.core.util.SetUtil import org.signal.core.util.gibiBytes import org.signal.core.util.logging.Log import org.signal.core.util.mebiBytes import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies.application import org.thoughtcrime.securesms.dependencies.AppDependencies.jobManager -import org.thoughtcrime.securesms.dependencies.AppDependencies.signalServiceAccountManager import org.thoughtcrime.securesms.groups.SelectionLimits import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver import java.io.IOException -import java.util.Locale import java.util.TreeMap -import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.min +import kotlin.reflect.KProperty import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -32,288 +30,22 @@ import kotlin.time.Duration.Companion.seconds * are not yet ready to be activated. * * When creating a new flag: - * - Create a new string constant. This should almost certainly be prefixed with "android." - * - Add a method to retrieve the value using [.getBoolean]. You can also add - * other checks here, like requiring other flags. - * - If you want to be able to change a flag remotely, place it in [.REMOTE_CAPABLE]. - * - If you would like to force a value for testing, place an entry in [.FORCED_VALUES]. - * Do not commit changes to this map! - * - * Other interesting things you can do: - * - Make a flag [.HOT_SWAPPABLE] - * - Make a flag [.STICKY] -- booleans only! - * - Register a listener for flag changes in [.FLAG_CHANGE_LISTENERS] + * - At the bottom of the file, create a new `val` with the name you'd like. + * - Use one of the helper delegates, like [remoteBoolean] or [remoteValue], to define your `val`. + * - See the documentation for [Config] understand all of the fields. */ object FeatureFlags { private val TAG = Log.tag(FeatureFlags::class.java) - private val FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2) + // region Core behavior - private const val PAYMENTS_KILL_SWITCH = "android.payments.kill" - private const val GROUPS_V2_RECOMMENDED_LIMIT = "global.groupsv2.maxGroupSize" - private const val GROUPS_V2_HARD_LIMIT = "global.groupsv2.groupSizeHardLimit" - private const val GROUP_NAME_MAX_LENGTH = "global.groupsv2.maxNameLength" - private const val INTERNAL_USER = "android.internalUser" - private const val VERIFY_V2 = "android.verifyV2" - private const val CLIENT_EXPIRATION = "android.clientExpiration" - private const val CUSTOM_VIDEO_MUXER = "android.customVideoMuxer.1" - private const val CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds" - private const val CDS_FOREGROUND_SYNC_INTERVAL = "cds.foregroundSyncInterval.seconds" - private const val AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2" - private const val AUTOMATIC_SESSION_INTERVAL = "android.automaticSessionResetInterval" - private const val DEFAULT_MAX_BACKOFF = "android.defaultMaxBackoff" - private const val SERVER_ERROR_MAX_BACKOFF = "android.serverErrorMaxBackoff" - private const val OKHTTP_AUTOMATIC_RETRY = "android.okhttpAutomaticRetry" - private const val SHARE_SELECTION_LIMIT = "android.share.limit" - private const val ANIMATED_STICKER_MIN_MEMORY = "android.animatedStickerMinMemory" - private const val ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory" - private const val MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins" - private const val MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs" - private const val MEDIA_QUALITY_LEVELS = "android.mediaQuality.levels" - private const val RETRY_RECEIPT_LIFESPAN = "android.retryReceiptLifespan" - private const val RETRY_RESPOND_MAX_AGE = "android.retryRespondMaxAge" - private const val SENDER_KEY_MAX_AGE = "android.senderKeyMaxAge" - private const val RETRY_RECEIPTS = "android.retryReceipts" - private const val MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize" - private const val STORIES_TEXT_FUNCTIONS = "android.stories.text.functions" - private const val HARDWARE_AEC_BLOCKLIST_MODELS = "android.calling.hardwareAecBlockList" - private const val SOFTWARE_AEC_BLOCKLIST_MODELS = "android.calling.softwareAecBlockList" - private const val USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29" - private const val PAYMENTS_COUNTRY_BLOCKLIST = "global.payments.disabledRegions" - private const val STORIES_AUTO_DOWNLOAD_MAXIMUM = "android.stories.autoDownloadMaximum" - private const val TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList" - private const val TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList" - private const val CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList" - private const val CAMERAX_MIXED_MODEL_BLOCKLIST = "android.cameraXMixedModelBlockList" - private const val PAYMENTS_REQUEST_ACTIVATE_FLOW = "android.payments.requestActivateFlow" - const val GOOGLE_PAY_DISABLED_REGIONS: String = "global.donations.gpayDisabledRegions" - const val CREDIT_CARD_DISABLED_REGIONS: String = "global.donations.ccDisabledRegions" - const val PAYPAL_DISABLED_REGIONS: String = "global.donations.paypalDisabledRegions" - private const val CDS_HARD_LIMIT = "android.cds.hardLimit" - private const val PAYPAL_ONE_TIME_DONATIONS = "android.oneTimePayPalDonations.2" - private const val PAYPAL_RECURRING_DONATIONS = "android.recurringPayPalDonations.3" - private const val ANY_ADDRESS_PORTS_KILL_SWITCH = "android.calling.fieldTrial.anyAddressPortsKillSwitch" - private const val AD_HOC_CALLING = "android.calling.ad.hoc.3" - private const val MAX_ATTACHMENT_COUNT = "android.attachments.maxCount" - private const val MAX_ATTACHMENT_RECEIVE_SIZE_BYTES = "global.attachments.maxReceiveBytes" - private const val MAX_ATTACHMENT_SIZE_BYTES = "global.attachments.maxBytes" - private const val CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode" - private const val FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch" - const val PROMPT_FOR_NOTIFICATION_LOGS: String = "android.logs.promptNotifications" - private const val PROMPT_FOR_NOTIFICATION_CONFIG = "android.logs.promptNotificationsConfig" - const val PROMPT_BATTERY_SAVER: String = "android.promptBatterySaver" - const val CRASH_PROMPT_CONFIG: String = "android.crashPromptConfig" - private const val SEPA_DEBIT_DONATIONS = "android.sepa.debit.donations.5" - private const val IDEAL_DONATIONS = "android.ideal.donations.5" - const val IDEAL_ENABLED_REGIONS: String = "global.donations.idealEnabledRegions" - const val SEPA_ENABLED_REGIONS: String = "global.donations.sepaEnabledRegions" - private const val NOTIFICATION_THUMBNAIL_BLOCKLIST = "android.notificationThumbnailProductBlocklist" - private const val USE_ACTIVE_CALL_MANAGER = "android.calling.useActiveCallManager.5" - private const val GIF_SEARCH = "global.gifSearch" - private const val AUDIO_REMUXING = "android.media.audioRemux.1" - private const val VIDEO_RECORD_1X_ZOOM = "android.media.videoCaptureDefaultZoom" - private const val RETRY_RECEIPT_MAX_COUNT = "android.retryReceipt.maxCount" - private const val RETRY_RECEIPT_MAX_COUNT_RESET_AGE = "android.retryReceipt.maxCountResetAge" - private const val PREKEY_FORCE_REFRESH_INTERVAL = "android.prekeyForceRefreshInterval" - private const val CDSI_LIBSIGNAL_NET = "android.cds.libsignal.4" - private const val RX_MESSAGE_SEND = "android.rxMessageSend.2" - private const val LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds" - private const val MESSAGE_BACKUPS = "android.messageBackups" - private const val CAMERAX_CUSTOM_CONTROLLER = "android.cameraXCustomController" - private const val REGISTRATION_V2 = "android.registration.v2" - private const val LIBSIGNAL_WEB_SOCKET_ENABLED = "android.libsignalWebSocketEnabled" - private const val RESTORE_POST_REGISTRATION = "android.registration.restorePostRegistration" - private const val LIBSIGNAL_WEB_SOCKET_SHADOW_PCT = "android.libsignalWebSocketShadowingPercentage" - private const val DELETE_SYNC_SEND_RECEIVE = "android.deleteSyncSendReceive" - private const val SVR3_MIGRATION_PHASE = "global.svr3.phase" + private val FETCH_INTERVAL = 2.hours - /** - * We will only store remote values for flags in this set. If you want a flag to be controllable - * remotely, place it in here. - */ - @JvmField @VisibleForTesting - val REMOTE_CAPABLE: Set = SetUtil.newHashSet( - PAYMENTS_KILL_SWITCH, - GROUPS_V2_RECOMMENDED_LIMIT, GROUPS_V2_HARD_LIMIT, - INTERNAL_USER, - VERIFY_V2, - CLIENT_EXPIRATION, - CUSTOM_VIDEO_MUXER, - CDS_REFRESH_INTERVAL, - CDS_FOREGROUND_SYNC_INTERVAL, - GROUP_NAME_MAX_LENGTH, - AUTOMATIC_SESSION_RESET, - AUTOMATIC_SESSION_INTERVAL, - DEFAULT_MAX_BACKOFF, - SERVER_ERROR_MAX_BACKOFF, - OKHTTP_AUTOMATIC_RETRY, - SHARE_SELECTION_LIMIT, - ANIMATED_STICKER_MIN_MEMORY, - ANIMATED_STICKER_MIN_TOTAL_MEMORY, - MESSAGE_PROCESSOR_ALARM_INTERVAL, - MESSAGE_PROCESSOR_DELAY, - MEDIA_QUALITY_LEVELS, - RETRY_RECEIPT_LIFESPAN, - RETRY_RESPOND_MAX_AGE, - RETRY_RECEIPTS, - MAX_GROUP_CALL_RING_SIZE, - SENDER_KEY_MAX_AGE, - STORIES_TEXT_FUNCTIONS, - HARDWARE_AEC_BLOCKLIST_MODELS, - SOFTWARE_AEC_BLOCKLIST_MODELS, - USE_HARDWARE_AEC_IF_OLD, - PAYMENTS_COUNTRY_BLOCKLIST, - STORIES_AUTO_DOWNLOAD_MAXIMUM, - TELECOM_MANUFACTURER_ALLOWLIST, - TELECOM_MODEL_BLOCKLIST, - CAMERAX_MODEL_BLOCKLIST, - CAMERAX_MIXED_MODEL_BLOCKLIST, - PAYMENTS_REQUEST_ACTIVATE_FLOW, - GOOGLE_PAY_DISABLED_REGIONS, - CREDIT_CARD_DISABLED_REGIONS, - PAYPAL_DISABLED_REGIONS, - CDS_HARD_LIMIT, - PAYPAL_ONE_TIME_DONATIONS, - PAYPAL_RECURRING_DONATIONS, - ANY_ADDRESS_PORTS_KILL_SWITCH, - MAX_ATTACHMENT_COUNT, - MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, - MAX_ATTACHMENT_SIZE_BYTES, - AD_HOC_CALLING, - CDS_DISABLE_COMPAT_MODE, - FCM_MAY_HAVE_MESSAGES_KILL_SWITCH, - PROMPT_FOR_NOTIFICATION_LOGS, - PROMPT_FOR_NOTIFICATION_CONFIG, - PROMPT_BATTERY_SAVER, - CRASH_PROMPT_CONFIG, - SEPA_DEBIT_DONATIONS, - IDEAL_DONATIONS, - IDEAL_ENABLED_REGIONS, - SEPA_ENABLED_REGIONS, - NOTIFICATION_THUMBNAIL_BLOCKLIST, - USE_ACTIVE_CALL_MANAGER, - GIF_SEARCH, - AUDIO_REMUXING, - VIDEO_RECORD_1X_ZOOM, - RETRY_RECEIPT_MAX_COUNT, - RETRY_RECEIPT_MAX_COUNT_RESET_AGE, - PREKEY_FORCE_REFRESH_INTERVAL, - CDSI_LIBSIGNAL_NET, - RX_MESSAGE_SEND, - LINKED_DEVICE_LIFESPAN_SECONDS, - CAMERAX_CUSTOM_CONTROLLER, - LIBSIGNAL_WEB_SOCKET_ENABLED, - LIBSIGNAL_WEB_SOCKET_SHADOW_PCT, - DELETE_SYNC_SEND_RECEIVE, - SVR3_MIGRATION_PHASE - ) + val REMOTE_VALUES: MutableMap = TreeMap() - @JvmField @VisibleForTesting - val NOT_REMOTE_CAPABLE: Set = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION) - - /** - * Values in this map will take precedence over any value. This should only be used for local - * development. Given that you specify a default when retrieving a value, and that we only store - * remote values for things in [.REMOTE_CAPABLE], there should be no need to ever *commit* - * an addition to this map. - */ - @JvmField - @VisibleForTesting - val FORCED_VALUES: Map = mapOf() - - /** - * By default, flags are only updated once at app start. This is to ensure that values don't - * change within an app session, simplifying logic. However, given that this can delay how often - * a flag is updated, you can put a flag in here to mark it as 'hot swappable'. Flags in this set - * will be updated arbitrarily at runtime. This will make values more responsive, but also places - * more burden on the reader to ensure that the app experience remains consistent. - */ - @JvmField - @VisibleForTesting - val HOT_SWAPPABLE: Set = setOf( - VERIFY_V2, - CLIENT_EXPIRATION, - CUSTOM_VIDEO_MUXER, - CDS_REFRESH_INTERVAL, - CDS_FOREGROUND_SYNC_INTERVAL, - GROUP_NAME_MAX_LENGTH, - AUTOMATIC_SESSION_RESET, - AUTOMATIC_SESSION_INTERVAL, - DEFAULT_MAX_BACKOFF, - SERVER_ERROR_MAX_BACKOFF, - OKHTTP_AUTOMATIC_RETRY, - SHARE_SELECTION_LIMIT, - ANIMATED_STICKER_MIN_MEMORY, - ANIMATED_STICKER_MIN_TOTAL_MEMORY, - MESSAGE_PROCESSOR_ALARM_INTERVAL, - MESSAGE_PROCESSOR_DELAY, - MEDIA_QUALITY_LEVELS, - RETRY_RECEIPT_LIFESPAN, - RETRY_RESPOND_MAX_AGE, - RETRY_RECEIPTS, - MAX_GROUP_CALL_RING_SIZE, - SENDER_KEY_MAX_AGE, - HARDWARE_AEC_BLOCKLIST_MODELS, - SOFTWARE_AEC_BLOCKLIST_MODELS, - USE_HARDWARE_AEC_IF_OLD, - PAYMENTS_COUNTRY_BLOCKLIST, - TELECOM_MANUFACTURER_ALLOWLIST, - TELECOM_MODEL_BLOCKLIST, - CAMERAX_MODEL_BLOCKLIST, - PAYMENTS_REQUEST_ACTIVATE_FLOW, - CDS_HARD_LIMIT, - MAX_ATTACHMENT_COUNT, - MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, - MAX_ATTACHMENT_SIZE_BYTES, - CDS_DISABLE_COMPAT_MODE, - FCM_MAY_HAVE_MESSAGES_KILL_SWITCH, - PROMPT_FOR_NOTIFICATION_LOGS, - PROMPT_FOR_NOTIFICATION_CONFIG, - PROMPT_BATTERY_SAVER, - CRASH_PROMPT_CONFIG, - NOTIFICATION_THUMBNAIL_BLOCKLIST, - VIDEO_RECORD_1X_ZOOM, - RETRY_RECEIPT_MAX_COUNT, - RETRY_RECEIPT_MAX_COUNT_RESET_AGE, - PREKEY_FORCE_REFRESH_INTERVAL, - CDSI_LIBSIGNAL_NET, - RX_MESSAGE_SEND, - LINKED_DEVICE_LIFESPAN_SECONDS, - CAMERAX_CUSTOM_CONTROLLER, - DELETE_SYNC_SEND_RECEIVE, - SVR3_MIGRATION_PHASE - ) - - /** - * Flags in this set will stay true forever once they receive a true value from a remote config. - */ - @JvmField - @VisibleForTesting - val STICKY: Set = setOf( - VERIFY_V2, - FCM_MAY_HAVE_MESSAGES_KILL_SWITCH - ) - - /** - * Listeners that are called when the value in [.REMOTE_VALUES] changes. That means that - * hot-swappable flags will have this invoked as soon as we know about that change, but otherwise - * these will only run during initialization. - * - * These can be called on any thread, including the main thread, so be careful! - * - * Also note that this doesn't play well with [.FORCED_VALUES] -- changes there will not - * trigger changes in this map, so you'll have to do some manual hacking to get yourself in the - * desired test state. - */ - private val FLAG_CHANGE_LISTENERS: Map = mapOf( - MESSAGE_PROCESSOR_ALARM_INTERVAL to OnFlagChange { RoutineMessageFetchReceiver.startOrUpdateAlarm(application) } - // TODO [svr3] we need to know what it changed from and to so we can enqueue for 0 -> 1 -// put(SVR3_MIGRATION_PHASE, change -> if (change)); - ) - - private val REMOTE_VALUES: MutableMap = TreeMap() + val configsByKey: MutableMap> = mutableMapOf() @JvmStatic @Synchronized @@ -333,7 +65,7 @@ object FeatureFlags { fun refreshIfNecessary() { val timeSinceLastFetch = System.currentTimeMillis() - SignalStore.remoteConfigValues().lastFetchTime - if (timeSinceLastFetch < 0 || timeSinceLastFetch > FETCH_INTERVAL) { + if (timeSinceLastFetch < 0 || timeSinceLastFetch > FETCH_INTERVAL.inWholeMilliseconds) { Log.i(TAG, "Scheduling remote config refresh.") jobManager.add(RemoteConfigRefreshJob()) } else { @@ -345,7 +77,7 @@ object FeatureFlags { @WorkerThread @Throws(IOException::class) fun refreshSync() { - val result = signalServiceAccountManager.remoteConfig + val result = AppDependencies.signalServiceAccountManager.getRemoteConfig() update(result.config) } @@ -354,7 +86,12 @@ object FeatureFlags { fun update(config: Map) { val memory: Map = REMOTE_VALUES val disk = parseStoredConfig(SignalStore.remoteConfigValues().pendingConfig) - val result = updateInternal(config, memory, disk, REMOTE_CAPABLE, HOT_SWAPPABLE, STICKY) + + val remoteCapable: Set = configsByKey.filterValues { it.active }.keys + val hotSwap: Set = configsByKey.filterValues { it.hotSwappable }.keys + val sticky: Set = configsByKey.filterValues { it.sticky }.keys + + val result = updateInternal(config, memory, disk, remoteCapable, hotSwap, sticky) SignalStore.remoteConfigValues().pendingConfig = mapToJson(result.disk) REMOTE_VALUES.clear() @@ -369,451 +106,6 @@ object FeatureFlags { Log.i(TAG, "[Disk] After : ${result.disk}") } - /** - * Maximum number of members allowed in a group. - */ - @JvmStatic - fun groupLimits(): SelectionLimits { - return SelectionLimits(getInteger(GROUPS_V2_RECOMMENDED_LIMIT, 151), getInteger(GROUPS_V2_HARD_LIMIT, 1001)) - } - - /** Payments Support */ - fun payments(): Boolean { - return !getBoolean(PAYMENTS_KILL_SWITCH, false) - } - - /** Internal testing extensions. */ - @JvmStatic - fun internalUser(): Boolean { - return getBoolean(INTERNAL_USER, false) || Environment.IS_NIGHTLY || Environment.IS_STAGING - } - - /** Whether or not to use the UUID in verification codes. */ - fun verifyV2(): Boolean { - return getBoolean(VERIFY_V2, false) - } - - /** The raw client expiration JSON string. */ - @JvmStatic - fun clientExpiration(): String? { - return getNullableString(CLIENT_EXPIRATION, null) - } - - /** Whether to use the custom streaming muxer or built in android muxer. */ - @JvmStatic - fun useStreamingVideoMuxer(): Boolean { - return getBoolean(CUSTOM_VIDEO_MUXER, false) - } - - /** The time in between routine CDS refreshes, in seconds. */ - @JvmStatic - fun cdsRefreshIntervalSeconds(): Int { - return getInteger(CDS_REFRESH_INTERVAL, 48.hours.inWholeSeconds.toInt()) - } - - /** The minimum time in between foreground CDS refreshes initiated via message requests, in milliseconds. */ - fun cdsForegroundSyncInterval(): Long { - return getLong(CDS_FOREGROUND_SYNC_INTERVAL, 4.hours.inWholeSeconds).seconds.inWholeMilliseconds - } - - fun shareSelectionLimit(): SelectionLimits { - val limit = getInteger(SHARE_SELECTION_LIMIT, 5) - return SelectionLimits(limit, limit) - } - - @JvmStatic - val maxGroupNameGraphemeLength: Int - /** The maximum number of grapheme */ - get() = max(32.0, getInteger(GROUP_NAME_MAX_LENGTH, -1).toDouble()).toInt() - - /** Whether or not to allow automatic session resets. */ - @JvmStatic - fun automaticSessionReset(): Boolean { - return getBoolean(AUTOMATIC_SESSION_RESET, true) - } - - /** How often we allow an automatic session reset. */ - @JvmStatic - fun automaticSessionResetIntervalSeconds(): Int { - return getInteger(AUTOMATIC_SESSION_RESET, 1.hours.inWholeSeconds.toInt()) - } - - @JvmStatic - fun getDefaultMaxBackoff(): Long { - return getInteger(DEFAULT_MAX_BACKOFF, 60).seconds.inWholeMilliseconds - } - - @JvmStatic - fun getServerErrorMaxBackoff(): Long { - return getLong(SERVER_ERROR_MAX_BACKOFF, 6.hours.inWholeSeconds).seconds.inWholeMilliseconds - } - - /** Whether or not to allow automatic retries from OkHttp */ - @JvmStatic - fun okHttpAutomaticRetry(): Boolean { - return getBoolean(OKHTTP_AUTOMATIC_RETRY, true) - } - - /** The minimum memory class required for rendering animated stickers in the keyboard and such */ - @JvmStatic - fun animatedStickerMinimumMemoryClass(): Int { - return getInteger(ANIMATED_STICKER_MIN_MEMORY, 193) - } - - /** The minimum total memory for rendering animated stickers in the keyboard and such */ - @JvmStatic - fun animatedStickerMinimumTotalMemoryMb(): Int { - return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, 3.gibiBytes.inWholeMebiBytes.toInt()) - } - - @JvmStatic - fun getMediaQualityLevels(): String { - return getString(MEDIA_QUALITY_LEVELS, "") - } - - /** Whether or not sending or responding to retry receipts is enabled. */ - fun retryReceipts(): Boolean { - return getBoolean(RETRY_RECEIPTS, true) - } - - /** How old a message is allowed to be while still resending in response to a retry receipt . */ - @JvmStatic - fun retryRespondMaxAge(): Long { - return getLong(RETRY_RESPOND_MAX_AGE, 14.days.inWholeMilliseconds) - } - - /** - * The max number of retry receipts sends we allow (within @link{#retryReceiptMaxCountResetAge()}) before we consider the volume too large and stop responding. - */ - fun retryReceiptMaxCount(): Long { - return getLong(RETRY_RECEIPT_MAX_COUNT, 10) - } - - /** - * If the last retry receipt send was older than this, then we reset the retry receipt sent count. (For use with @link{#retryReceiptMaxCount()}) - */ - fun retryReceiptMaxCountResetAge(): Long { - return getLong(RETRY_RECEIPT_MAX_COUNT_RESET_AGE, 3.hours.inWholeMilliseconds) - } - - /** How long a sender key can live before it needs to be rotated. */ - @JvmStatic - fun senderKeyMaxAge(): Long { - val remoteValue = getLong(SENDER_KEY_MAX_AGE, 14.days.inWholeMilliseconds) - return min(remoteValue, 90.days.inWholeMilliseconds) - } - - /** Max group size that can be use group call ringing. */ - @JvmStatic - fun maxGroupCallRingSize(): Long { - return getLong(MAX_GROUP_CALL_RING_SIZE, 16) - } - - /** A comma-separated list of country codes where payments should be disabled. */ - @JvmStatic - fun paymentsCountryBlocklist(): String { - return getString(PAYMENTS_COUNTRY_BLOCKLIST, "98,963,53,850,7") - } - - /** - * Whether users can apply alignment and scale to text posts - * - * NOTE: This feature is still under ongoing development, do not enable. - */ - fun storiesTextFunctions(): Boolean { - return getBoolean(STORIES_TEXT_FUNCTIONS, false) - } - - /** A comma-separated list of models that should *not* use hardware AEC for calling. */ - fun hardwareAecBlocklistModels(): String { - return getString(HARDWARE_AEC_BLOCKLIST_MODELS, "") - } - - /** A comma-separated list of models that should *not* use software AEC for calling. */ - fun softwareAecBlocklistModels(): String { - return getString(SOFTWARE_AEC_BLOCKLIST_MODELS, "") - } - - /** A comma-separated list of manufacturers that *should* use Telecom for calling. */ - fun telecomManufacturerAllowList(): String { - return getString(TELECOM_MANUFACTURER_ALLOWLIST, "") - } - - /** A comma-separated list of manufacturers that *should* use Telecom for calling. */ - fun telecomModelBlockList(): String { - return getString(TELECOM_MODEL_BLOCKLIST, "") - } - - /** A comma-separated list of manufacturers that should *not* use CameraX. */ - fun cameraXModelBlocklist(): String { - return getString(CAMERAX_MODEL_BLOCKLIST, "") - } - - /** A comma-separated list of manufacturers that should *not* use CameraX mixed mode. */ - fun cameraXMixedModelBlocklist(): String { - return getString(CAMERAX_MIXED_MODEL_BLOCKLIST, "") - } - - /** Whether or not hardware AEC should be used for calling on devices older than API 29. */ - fun useHardwareAecIfOlderThanApi29(): Boolean { - return getBoolean(USE_HARDWARE_AEC_IF_OLD, false) - } - - /** - * Prefetch count for stories from a given user. - */ - fun storiesAutoDownloadMaximum(): Int { - return getInteger(STORIES_AUTO_DOWNLOAD_MAXIMUM, 2) - } - - /** Whether client supports sending a request to another to activate payments */ - @JvmStatic - fun paymentsRequestActivateFlow(): Boolean { - return getBoolean(PAYMENTS_REQUEST_ACTIVATE_FLOW, false) - } - - /** - * @return Serialized list of regions in which Google Pay is disabled for donations - */ - @JvmStatic - fun googlePayDisabledRegions(): String { - return getString(GOOGLE_PAY_DISABLED_REGIONS, "*") - } - - /** - * @return Serialized list of regions in which credit cards are disabled for donations - */ - @JvmStatic - fun creditCardDisabledRegions(): String { - return getString(CREDIT_CARD_DISABLED_REGIONS, "*") - } - - /** - * @return Serialized list of regions in which PayPal is disabled for donations - */ - @JvmStatic - fun paypalDisabledRegions(): String { - return getString(PAYPAL_DISABLED_REGIONS, "*") - } - - /** - * If the user has more than this number of contacts, the CDS request will certainly be rejected, so we must fail. - */ - fun cdsHardLimit(): Int { - return getInteger(CDS_HARD_LIMIT, 50000) - } - - /** - * Whether or not we should allow PayPal payments for one-time donations - */ - fun paypalOneTimeDonations(): Boolean { - return getBoolean(PAYPAL_ONE_TIME_DONATIONS, Environment.IS_STAGING) - } - - /** - * Whether or not we should allow PayPal payments for recurring donations - */ - fun paypalRecurringDonations(): Boolean { - return getBoolean(PAYPAL_RECURRING_DONATIONS, Environment.IS_STAGING) - } - - /** - * Enable/disable RingRTC field trial for "AnyAddressPortsKillSwitch" - */ - @JvmStatic - fun callingFieldTrialAnyAddressPortsKillSwitch(): Boolean { - return getBoolean(ANY_ADDRESS_PORTS_KILL_SWITCH, false) - } - - /** - * Enable/disable for notification when we cannot fetch messages despite receiving an urgent push. - */ - fun fcmMayHaveMessagesNotificationKillSwitch(): Boolean { - return getBoolean(FCM_MAY_HAVE_MESSAGES_KILL_SWITCH, false) - } - - /** - * Whether or not ad-hoc calling is enabled - */ - @JvmStatic - fun adHocCalling(): Boolean { - return getBoolean(AD_HOC_CALLING, false) - } - - /** Maximum number of attachments allowed to be sent/received. */ - fun maxAttachmentCount(): Int { - return getInteger(MAX_ATTACHMENT_COUNT, 32) - } - - /** Maximum attachment size for ciphertext in bytes. */ - fun maxAttachmentReceiveSizeBytes(): Long { - val maxAttachmentSize = maxAttachmentSizeBytes() - val maxReceiveSize = getLong(MAX_ATTACHMENT_RECEIVE_SIZE_BYTES, (maxAttachmentSize * 1.25).toInt().toLong()) - return max(maxAttachmentSize.toDouble(), maxReceiveSize.toDouble()).toLong() - } - - /** Maximum attachment ciphertext size when sending in bytes */ - fun maxAttachmentSizeBytes(): Long { - return getLong(MAX_ATTACHMENT_SIZE_BYTES, 100.mebiBytes.inWholeBytes) - } - - @JvmStatic - fun promptForDelayedNotificationLogs(): String { - return getString(PROMPT_FOR_NOTIFICATION_LOGS, "*") - } - - fun delayedNotificationsPromptConfig(): String { - return getString(PROMPT_FOR_NOTIFICATION_CONFIG, "") - } - - @JvmStatic - fun promptBatterySaver(): String { - return getString(PROMPT_BATTERY_SAVER, "*") - } - - /** Config object for what crashes to prompt about. */ - fun crashPromptConfig(): String { - return getString(CRASH_PROMPT_CONFIG, "") - } - - /** - * Whether or not SEPA debit payments for donations are enabled. - * WARNING: This feature is under heavy development and is *not* ready for wider use. - */ - fun sepaDebitDonations(): Boolean { - return getBoolean(SEPA_DEBIT_DONATIONS, false) - } - - fun idealDonations(): Boolean { - return getBoolean(IDEAL_DONATIONS, false) - } - - @JvmStatic - fun idealEnabledRegions(): String { - return getString(IDEAL_ENABLED_REGIONS, "") - } - - @JvmStatic - fun sepaEnabledRegions(): String { - return getString(SEPA_ENABLED_REGIONS, "") - } - - /** List of device products that are blocked from showing notification thumbnails. */ - fun notificationThumbnailProductBlocklist(): String { - return getString(NOTIFICATION_THUMBNAIL_BLOCKLIST, "") - } - - /** Whether or not to use active call manager instead of WebRtcCallService. */ - @JvmStatic - fun useActiveCallManager(): Boolean { - return getBoolean(USE_ACTIVE_CALL_MANAGER, false) - } - - /** Whether the in-app GIF search is available for use. */ - @JvmStatic - fun gifSearchAvailable(): Boolean { - return getBoolean(GIF_SEARCH, true) - } - - /** Allow media converters to remux audio instead of transcoding it. */ - @JvmStatic - fun allowAudioRemuxing(): Boolean { - return getBoolean(AUDIO_REMUXING, false) - } - - /** Get the default video zoom, expressed as 10x the actual Float value due to the service limiting us to whole numbers. */ - @JvmStatic - fun startVideoRecordAt1x(): Boolean { - return getBoolean(VIDEO_RECORD_1X_ZOOM, false) - } - - /** How often we allow a forced prekey refresh. */ - fun preKeyForceRefreshInterval(): Long { - return getLong(PREKEY_FORCE_REFRESH_INTERVAL, 1.hours.inWholeMilliseconds) - } - - /** Make CDSI lookups via libsignal-net instead of native websocket. */ - fun useLibsignalNetForCdsiLookup(): Boolean { - return getBoolean(CDSI_LIBSIGNAL_NET, false) - } - - /** Use Rx threading model to do sends. */ - @JvmStatic - fun useRxMessageSending(): Boolean { - return getBoolean(RX_MESSAGE_SEND, false) - } - - /** The lifespan of a linked device (i.e. the time it can be inactive for before it expires), in milliseconds. */ - @JvmStatic - fun linkedDeviceLifespan(): Long { - return getLong(LINKED_DEVICE_LIFESPAN_SECONDS, 30.days.inWholeSeconds).seconds.inWholeMilliseconds - } - - /** - * Enable Message Backups UI - * Note: This feature is in active development and is not intended to currently function. - */ - @JvmStatic - fun messageBackups(): Boolean { - return BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || getBoolean(MESSAGE_BACKUPS, false) - } - - /** Whether or not to use the custom CameraX controller class */ - @JvmStatic - fun customCameraXController(): Boolean { - return getBoolean(CAMERAX_CUSTOM_CONTROLLER, false) - } - - /** Whether or not to use the V2 refactor of registration. */ - @JvmStatic - fun registrationV2(): Boolean { - return getBoolean(REGISTRATION_V2, true) - } - - /** Whether unauthenticated chat web socket is backed by libsignal-net */ - @JvmStatic - fun libSignalWebSocketEnabled(): Boolean { - return getBoolean(LIBSIGNAL_WEB_SOCKET_ENABLED, false) - } - - /** Whether or not to launch the restore activity after registration is complete, rather than before. */ - @JvmStatic - fun restoreAfterRegistration(): Boolean { - return BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || getBoolean(RESTORE_POST_REGISTRATION, false) - } - - /** - * Percentage [0, 100] of web socket requests that will be "shadowed" by sending - * an unauthenticated keep-alive via libsignal-net. Default: 0 - */ - @JvmStatic - fun libSignalWebSocketShadowingPercentage(): Int { - val value = getInteger(LIBSIGNAL_WEB_SOCKET_SHADOW_PCT, 0) - return max(0.0, min(value.toDouble(), 100.0)).toInt() - } - - @JvmStatic - fun getBackgroundMessageProcessInterval(): Long { - val delayMinutes = getLong(MESSAGE_PROCESSOR_ALARM_INTERVAL, 6.hours.inWholeMinutes) - return delayMinutes.minutes.inWholeMilliseconds - } - - @JvmStatic - fun getBackgroundMessageProcessForegroundDelay(): Long { - return getInteger(MESSAGE_PROCESSOR_DELAY, 300).toLong() - } - - /** Whether or not to delete syncing is enabled. */ - @JvmStatic - fun deleteSyncEnabled(): Boolean { - return getBoolean(DELETE_SYNC_SEND_RECEIVE, false) - } - - /** Which phase we're in for the SVR3 migration */ - fun svr3MigrationPhase(): Int { - return getInteger(SVR3_MIGRATION_PHASE, 0) - } - /** Only for rendering debug info. */ @JvmStatic @get:Synchronized @@ -832,12 +124,6 @@ object FeatureFlags { val debugPendingDiskValues: Map get() = TreeMap(parseStoredConfig(SignalStore.remoteConfigValues().pendingConfig)) - /** Only for rendering debug info. */ - @JvmStatic - @get:Synchronized - val debugForcedValues: Map - get() = TreeMap(FORCED_VALUES) - @JvmStatic @VisibleForTesting fun updateInternal( @@ -873,7 +159,7 @@ object FeatureFlags { } if (sticky.contains(key) && (newValue is Boolean || diskValue is Boolean)) { - newValue = if (diskValue === java.lang.Boolean.TRUE) java.lang.Boolean.TRUE else newValue + newValue = if (diskValue == true) true else newValue } else if (sticky.contains(key)) { Log.w(TAG, "Tried to make a non-boolean sticky! Ignoring. (key: $key)") } @@ -883,6 +169,7 @@ object FeatureFlags { } else { newDisk.remove(key) } + if (hotSwap.contains(key)) { if (newValue != null) { newMemory[key] = newValue @@ -932,85 +219,6 @@ object FeatureFlags { return changes } - private fun getBoolean(key: String, defaultValue: Boolean): Boolean { - val forced = FORCED_VALUES[key] as? Boolean - if (forced != null) { - return forced - } - - val remote = REMOTE_VALUES[key] - if (remote is Boolean) { - return remote - } else if (remote is String) { - val stringValue = remote.lowercase(Locale.getDefault()) - if (stringValue == "true") { - return true - } else if (stringValue == "false") { - return false - } else { - Log.w(TAG, "Expected a boolean for key '$key', but got something else ($stringValue)! Falling back to the default.") - } - } else if (remote != null) { - Log.w(TAG, "Expected a boolean for key '$key', but got something else! Falling back to the default.") - } - - return defaultValue - } - - private fun getInteger(key: String, defaultValue: Int): Int { - val forced = FORCED_VALUES[key] as? Int - if (forced != null) { - return forced - } - - val remote = REMOTE_VALUES[key] - if (remote is String) { - try { - return remote.toInt() - } catch (e: NumberFormatException) { - Log.w(TAG, "Expected an int for key '$key', but got something else! Falling back to the default.") - } - } - - return defaultValue - } - - private fun getLong(key: String, defaultValue: Long): Long { - val forced = FORCED_VALUES[key] as? Long - if (forced != null) { - return forced - } - - val remote = REMOTE_VALUES[key] - if (remote is String) { - try { - return remote.toLong() - } catch (e: NumberFormatException) { - Log.w(TAG, "Expected a long for key '$key', but got something else! Falling back to the default.") - } - } - - return defaultValue - } - - private fun getString(key: String, defaultValue: String): String { - return getNullableString(key, defaultValue)!! - } - - private fun getNullableString(key: String, defaultValue: String?): String? { - val forced = FORCED_VALUES[key] as String? - if (forced != null) { - return forced - } - - val remote = REMOTE_VALUES[key] - if (remote is String) { - return remote - } - - return defaultValue - } - private fun parseStoredConfig(stored: String?): Map { val parsed: MutableMap = HashMap() @@ -1050,7 +258,7 @@ object FeatureFlags { private fun triggerFlagChangeListeners(changes: Map) { for ((key, value) in changes) { - val listener = FLAG_CHANGE_LISTENERS[key] + val listener = configsByKey[key]?.onChangeListener if (listener != null) { Log.i(TAG, "Triggering change listener for: $key") @@ -1067,11 +275,832 @@ object FeatureFlags { ) @VisibleForTesting - internal fun interface OnFlagChange { + fun interface OnFlagChange { fun onFlagChange(change: Change) } enum class Change { ENABLED, DISABLED, CHANGED, REMOVED } + + // endregion + + // region Conversion utilities + private fun Any?.asBoolean(defaultValue: Boolean): Boolean { + return when (this) { + is Boolean -> this + is String -> this.toBoolean() + else -> defaultValue + } + } + + private fun Any?.asInteger(defaultValue: Int): Int { + return when (this) { + is String -> this.toIntOrNull() ?: defaultValue + else -> defaultValue + } + } + + private fun Any?.asLong(defaultValue: Long): Long { + return when (this) { + is String -> this.toLongOrNull() ?: defaultValue + else -> defaultValue + } + } + + private fun Any?.asString(defaultValue: T): T { + @Suppress("UNCHECKED_CAST") + return when (this) { + is String -> this as T + else -> defaultValue + } + } + + // endregion + + // region Delegates + data class Config( + /** + * The key used to identify the remote config on the service. + */ + val key: String, + + /** + * By default, flags are only updated once at app start. This is to ensure that values don't + * change within an app session, simplifying logic. However, given that this can delay how often + * a flag is updated, you can put a flag in here to mark it as 'hot swappable'. Flags in this set + * will be updated arbitrarily at runtime. This will make values more responsive, but also places + * more burden on the reader to ensure that the app experience remains consistent. + */ + val hotSwappable: Boolean, + + /** + * Flags in this set will stay true forever once they receive a true value from a remote config. + */ + val sticky: Boolean, + + /** + * If this is false, the remote value of the flag will be ignored, and we'll only ever use the default value. + */ + val active: Boolean, + + /** + * Listeners that are called when the value in [REMOTE_VALUES] changes. That means that + * hot-swappable flags will have this invoked as soon as we know about that change, but otherwise + * these will only run during initialization. + * + * These can be called on any thread, including the main thread, so be careful! + */ + val onChangeListener: OnFlagChange? = null, + + /** + * Takes the remote value and coerces it to the value you want to read. If implementing this directly, + * consider using helpers like [asBoolean] or [asInteger] to make your life easier. + */ + val transformer: (Any?) -> T + ) { + operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + return transformer(REMOTE_VALUES[key]) + } + } + + private fun remoteBoolean( + key: String, + defaultValue: Boolean, + hotSwappable: Boolean, + sticky: Boolean = false, + active: Boolean = true, + onChangeListener: OnFlagChange? = null + ): Config { + return remoteValue( + key = key, + hotSwappable = hotSwappable, + sticky = sticky, + active = active, + onChangeListener = onChangeListener, + transformer = { it.asBoolean(defaultValue) } + ) + } + + private fun remoteInt( + key: String, + defaultValue: Int, + hotSwappable: Boolean, + active: Boolean = true, + onChangeListener: OnFlagChange? = null + ): Config { + return remoteValue( + key = key, + hotSwappable = hotSwappable, + sticky = false, + active = active, + onChangeListener = onChangeListener, + transformer = { it.asInteger(defaultValue) } + ) + } + + private fun remoteLong( + key: String, + defaultValue: Long, + hotSwappable: Boolean, + active: Boolean = true, + onChangeListener: OnFlagChange? = null + ): Config { + return remoteValue( + key = key, + hotSwappable = hotSwappable, + sticky = false, + active = active, + onChangeListener = onChangeListener, + transformer = { it.asLong(defaultValue) } + ) + } + + private fun remoteString( + key: String, + defaultValue: T, + hotSwappable: Boolean, + active: Boolean = true, + onChangeListener: OnFlagChange? = null + ): Config { + return remoteValue( + key = key, + hotSwappable = hotSwappable, + sticky = false, + active = active, + onChangeListener = onChangeListener, + transformer = { it.asString(defaultValue) } + ) + } + + private fun remoteValue( + key: String, + hotSwappable: Boolean, + sticky: Boolean = false, + active: Boolean = true, + onChangeListener: OnFlagChange? = null, + transformer: (Any?) -> T + ): Config { + val config = Config(key = key, active = active, hotSwappable = hotSwappable, sticky = sticky, onChangeListener = onChangeListener, transformer = transformer) + configsByKey[config.key] = config + return config + } + + // endregion + + // region Config definitions + + /** Payments support */ + val payments: Boolean by remoteValue( + key = "android.payments.kill", + hotSwappable = false + ) { value -> + !value.asBoolean(false) + } + + /** Whether or not to use the UUID in verification codes. */ + val verifyV2: Boolean by remoteBoolean( + key = "android.verifyV2", + defaultValue = false, + hotSwappable = true, + sticky = true + ) + + /** Maximum number of members allowed in a group. */ + @JvmStatic + @get:JvmName("groupLimits") + val groupLimits: SelectionLimits + get() = SelectionLimits(groupRecommendedLimit, groupHardLimit) + + private val groupRecommendedLimit: Int by remoteInt( + key = "global.groupsv2.maxGroupSize", + defaultValue = 151, + hotSwappable = true + ) + + private val groupHardLimit: Int by remoteInt( + key = "global.groupsv2.groupSizeHardLimit", + defaultValue = 1001, + hotSwappable = true + ) + + /** The maximum number of grapheme */ + @JvmStatic + val maxGroupNameGraphemeLength: Int by remoteValue( + key = "global.groupsv2.maxNameLength", + hotSwappable = true + ) { value -> + val remote = value.asInteger(-1) + max(32, remote) + } + + /** Whether or not the user is an 'internal' one, which activates certain developer tools. */ + @JvmStatic + @get:JvmName("internalUser") + val internalUser: Boolean by remoteValue( + key = "android.internalUser", + hotSwappable = true + ) { value -> + value.asBoolean(false) || Environment.IS_NIGHTLY || Environment.IS_STAGING + } + + /** The raw client expiration JSON string. */ + @JvmStatic + @get:JvmName("clientExpiration") + val clientExpiration: String? by remoteString( + key = "android.clientExpiration", + hotSwappable = true, + defaultValue = null + ) + + /** Whether to use the custom streaming muxer or built in android muxer. */ + @JvmStatic + @get:JvmName("useStreamingVideoMuxer") + val useStreamingVideoMuxer: Boolean by remoteBoolean( + key = "android.customVideoMuxer.1", + defaultValue = false, + hotSwappable = true + ) + + /** The time in between routine CDS refreshes, in seconds. */ + @JvmStatic + @get:JvmName("cdsRefreshIntervalSeconds") + val cdsRefreshIntervalSeconds: Int by remoteInt( + key = "cds.syncInterval.seconds", + defaultValue = 48.hours.inWholeSeconds.toInt(), + hotSwappable = true + ) + + /** The minimum time in between foreground CDS refreshes initiated via message requests, in milliseconds. */ + val cdsForegroundSyncInterval: Long by remoteValue( + key = "cds.foregroundSyncInterval.seconds", + hotSwappable = true + ) { value -> + val inSeconds = value.asLong(4.hours.inWholeSeconds) + inSeconds.seconds.inWholeMilliseconds + } + + val shareSelectionLimit: SelectionLimits by remoteValue( + key = "android.share.limit", + hotSwappable = true + ) { value -> + val limit = value.asInteger(5) + SelectionLimits(limit, limit) + } + + /** Whether or not to allow automatic session resets. */ + @JvmStatic + @get:JvmName("automaticSessionReset") + val automaticSessionReset: Boolean by remoteBoolean( + key = "android.automaticSessionReset.2", + defaultValue = true, + hotSwappable = true + ) + + /** How often we allow an automatic session reset. */ + @JvmStatic + @get:JvmName("automaticSessionResetIntervalSeconds") + val automaticSessionResetIntervalSeconds: Int by remoteInt( + key = "android.automaticSessionResetInterval", + defaultValue = 1.hours.inWholeSeconds.toInt(), + hotSwappable = true + ) + + @JvmStatic + val defaultMaxBackoff: Long by remoteValue( + key = "android.defaultMaxBackoff", + hotSwappable = true + ) { value -> + val inSeconds = value.asLong(60) + inSeconds.seconds.inWholeMilliseconds + } + + @JvmStatic + val serverErrorMaxBackoff: Long by remoteValue( + key = "android.serverErrorMaxBackoff", + hotSwappable = true + ) { value -> + val inSeconds = value.asLong(6.hours.inWholeSeconds) + inSeconds.seconds.inWholeMilliseconds + } + + /** Whether or not to allow automatic retries from OkHttp */ + @JvmStatic + @get:JvmName("okHttpAutomaticRetry") + val okHttpAutomaticRetry: Boolean by remoteBoolean( + key = "android.okhttpAutomaticRetry", + defaultValue = true, + hotSwappable = true + ) + + /** The minimum memory class required for rendering animated stickers in the keyboard and such */ + @JvmStatic + @get:JvmName("animatedStickerMinimumMemoryClass") + val animatedStickerMinimumMemoryClass: Int by remoteInt( + key = "android.animatedStickerMinMemory", + defaultValue = 193, + hotSwappable = true + ) + + /** The minimum total memory for rendering animated stickers in the keyboard and such */ + @JvmStatic + @get:JvmName("animatedStickerMinimumTotalMemoryMb") + val animatedStickerMinimumTotalMemoryMb: Int by remoteInt( + key = "android.animatedStickerMinTotalMemory", + defaultValue = 3.gibiBytes.inWholeMebiBytes.toInt(), + hotSwappable = true + ) + + @JvmStatic + val mediaQualityLevels: String by remoteString( + key = "android.mediaQuality.levels", + defaultValue = "", + hotSwappable = true + ) + + /** Whether or not sending or responding to retry receipts is enabled. */ + @JvmStatic + @get:JvmName("retryReceipts") + val retryReceipts: Boolean by remoteBoolean( + key = "android.retryReceipts", + defaultValue = true, + hotSwappable = true + ) + + /** How old a message is allowed to be while still resending in response to a retry receipt . */ + @JvmStatic + @get:JvmName("retryRespondMaxAge") + val retryRespondMaxAge: Long by remoteLong( + key = "android.retryRespondMaxAge", + defaultValue = 14.days.inWholeMilliseconds, + hotSwappable = true + ) + + /** The max number of retry receipts sends we allow (within [retryReceiptMaxCountResetAge]) before we consider the volume too large and stop responding. */ + @JvmStatic + @get:JvmName("retryReceiptMaxCount") + val retryReceiptMaxCount: Long by remoteLong( + key = "android.retryReceipt.maxCount", + defaultValue = 10, + hotSwappable = true + ) + + /** If the last retry receipt send was older than this, then we reset the retry receipt sent count. (For use with [retryReceiptMaxCount]) */ + @JvmStatic + @get:JvmName("retryReceiptMaxCountResetAge") + val retryReceiptMaxCountResetAge: Long by remoteLong( + key = "android.retryReceipt.maxCountResetAge", + defaultValue = 3.hours.inWholeMilliseconds, + hotSwappable = true + ) + + /** How long a sender key can live before it needs to be rotated. */ + @JvmStatic + @get:JvmName("senderKeyMaxAge") + val senderKeyMaxAge: Long by remoteValue( + key = "android.senderKeyMaxAge", + hotSwappable = true + ) { value -> + val remoteValue = value.asLong(14.days.inWholeMilliseconds) + min(remoteValue, 90.days.inWholeMilliseconds) + } + + /** Max group size that can be use group call ringing. */ + @JvmStatic + @get:JvmName("maxGroupCallRingSize") + val maxGroupCallRingSize: Long by remoteLong( + key = "global.calling.maxGroupCallRingSize", + defaultValue = 16, + hotSwappable = true + ) + + /** A comma-separated list of country codes where payments should be disabled. */ + @JvmStatic + @get:JvmName("paymentsCountryBlocklist") + val paymentsCountryBlocklist: String by remoteString( + key = "global.payments.disabledRegions", + defaultValue = "98,963,53,850,7", + hotSwappable = true + ) + + /** + * Whether users can apply alignment and scale to text posts + * + * NOTE: This feature is still under ongoing development, do not enable. + */ + val storiesTextFunctions: Boolean by remoteBoolean( + key = "android.stories.text.functions", + defaultValue = false, + hotSwappable = false + ) + + /** A comma-separated list of models that should *not* use hardware AEC for calling. */ + val hardwareAecBlocklistModels: String by remoteString( + key = "android.calling.hardwareAecBlockList", + defaultValue = "", + hotSwappable = true + ) + + /** A comma-separated list of models that should *not* use software AEC for calling. */ + val softwareAecBlocklistModels: String by remoteString( + key = "android.calling.softwareAecBlockList", + defaultValue = "", + hotSwappable = true + ) + + /** A comma-separated list of manufacturers that *should* use Telecom for calling. */ + val telecomManufacturerAllowList: String by remoteString( + key = "android.calling.telecomAllowList", + defaultValue = "", + hotSwappable = true + ) + + /** A comma-separated list of manufacturers that *should* use Telecom for calling. */ + val telecomModelBlocklist: String by remoteString( + key = "android.calling.telecomModelBlockList", + defaultValue = "", + hotSwappable = true + ) + + /** A comma-separated list of manufacturers that should *not* use CameraX. */ + val cameraXModelBlocklist: String by remoteString( + key = "android.cameraXModelBlockList", + defaultValue = "", + hotSwappable = true + ) + + /** A comma-separated list of manufacturers that should *not* use CameraX mixed mode. */ + val cameraXMixedModelBlocklist: String by remoteString( + key = "android.cameraXMixedModelBlockList", + defaultValue = "", + hotSwappable = false + ) + + /** Whether or not hardware AEC should be used for calling on devices older than API 29. */ + val useHardwareAecIfOlderThanApi29: Boolean by remoteBoolean( + key = "android.calling.useHardwareAecIfOlderThanApi29", + defaultValue = false, + hotSwappable = true + ) + + /** Prefetch count for stories from a given user. */ + val storiesAutoDownloadMaximum: Int by remoteInt( + key = "android.stories.autoDownloadMaximum", + defaultValue = 2, + hotSwappable = false + ) + + /** Whether client supports sending a request to another to activate payments */ + @JvmStatic + @get:JvmName("paymentsRequestActivateFlow") + val paymentsRequestActivateFlow: Boolean by remoteBoolean( + key = "android.payments.requestActivateFlow", + defaultValue = false, + hotSwappable = true + ) + + /** Serialized list of regions in which Google Pay is disabled for donations */ + @JvmStatic + @get:JvmName("googlePayDisabledRegions") + val googlePayDisabledRegions: String by remoteString( + key = "global.donations.gpayDisabledRegions", + defaultValue = "*", + hotSwappable = false + ) + + /** Serialized list of regions in which credit cards are disabled for donations */ + @JvmStatic + @get:JvmName("creditCardDisabledRegions") + val creditCardDisabledRegions: String by remoteString( + key = "global.donations.ccDisabledRegions", + defaultValue = "*", + hotSwappable = false + ) + + /** @return Serialized list of regions in which PayPal is disabled for donations */ + @JvmStatic + @get:JvmName("paypalDisabledRegions") + val paypalDisabledRegions: String by remoteString( + key = "global.donations.paypalDisabledRegions", + defaultValue = "*", + hotSwappable = false + ) + + /** If the user has more than this number of contacts, the CDS request will certainly be rejected, so we must fail. */ + val cdsHardLimit: Int by remoteInt( + key = "android.cds.hardLimit", + defaultValue = 50000, + hotSwappable = true + ) + + /** Whether or not we should allow PayPal payments for one-time donations */ + val paypalOneTimeDonations: Boolean by remoteBoolean( + key = "android.oneTimePayPalDonations.2", + defaultValue = Environment.IS_STAGING, + hotSwappable = false + ) + + /** Whether or not we should allow PayPal payments for recurring donations */ + val paypalRecurringDonations: Boolean by remoteBoolean( + key = "android.recurringPayPalDonations.3", + defaultValue = Environment.IS_STAGING, + hotSwappable = false + ) + + /** Enable/disable RingRTC field trial for "AnyAddressPortsKillSwitch" */ + @JvmStatic + @get:JvmName("callingFieldTrialAnyAddressPortsKillSwitch") + val callingFieldTrialAnyAddressPortsKillSwitch: Boolean by remoteBoolean( + key = "android.calling.fieldTrial.anyAddressPortsKillSwitch", + defaultValue = false, + hotSwappable = false + ) + + /** + * Enable/disable for notification when we cannot fetch messages despite receiving an urgent push. + */ + val fcmMayHaveMessagesNotificationKillSwitch: Boolean by remoteBoolean( + key = "android.fcmNotificationFallbackKillSwitch", + defaultValue = false, + hotSwappable = true, + sticky = true + ) + + /** + * Whether or not ad-hoc calling is enabled + */ + @JvmStatic + @get:JvmName("adHocCalling") + val adHocCalling: Boolean by remoteBoolean( + key = "android.calling.ad.hoc.3", + defaultValue = false, + hotSwappable = false + ) + + /** Maximum number of attachments allowed to be sent/received. */ + val maxAttachmentCount: Int by remoteInt( + key = "android.attachments.maxCount", + defaultValue = 32, + hotSwappable = true + ) + + /** Maximum attachment size for ciphertext in bytes. */ + val maxAttachmentReceiveSizeBytes: Long by remoteValue( + key = "global.attachments.maxReceiveBytes", + hotSwappable = true + ) { value -> + val maxAttachmentSize = maxAttachmentSizeBytes + val maxReceiveSize = value.asLong((maxAttachmentSize * 1.25).toInt().toLong()) + max(maxAttachmentSize, maxReceiveSize) + } + + /** Maximum attachment ciphertext size when sending in bytes */ + val maxAttachmentSizeBytes: Long by remoteLong( + key = "global.attachments.maxBytes", + defaultValue = 100.mebiBytes.inWholeBytes, + hotSwappable = true + ) + + const val PROMPT_FOR_NOTIFICATION_LOGS: String = "android.logs.promptNotifications" + + @JvmStatic + @get:JvmName("promptForDelayedNotificationLogs") + val promptForDelayedNotificationLogs: String by remoteString( + key = FeatureFlags.PROMPT_FOR_NOTIFICATION_LOGS, + defaultValue = "*", + hotSwappable = true + ) + + val delayedNotificationsPromptConfig: String by remoteString( + key = "android.logs.promptNotificationsConfig", + defaultValue = "", + hotSwappable = true + ) + + const val PROMPT_BATTERY_SAVER: String = "android.promptBatterySaver" + + @JvmStatic + @get:JvmName("promptBatterySaver") + val promptBatterySaver: String by remoteString( + key = PROMPT_BATTERY_SAVER, + defaultValue = "*", + hotSwappable = true + ) + + const val CRASH_PROMPT_CONFIG: String = "android.crashPromptConfig" + + /** Config object for what crashes to prompt about. */ + val crashPromptConfig: String by remoteString( + key = CRASH_PROMPT_CONFIG, + defaultValue = "", + hotSwappable = true + ) + + /** Whether or not SEPA debit payments for donations are enabled. */ + val sepaDebitDonations: Boolean by remoteBoolean( + key = "android.sepa.debit.donations.5", + defaultValue = false, + hotSwappable = false + ) + + val idealDonations: Boolean by remoteBoolean( + key = "android.ideal.donations.5", + defaultValue = false, + hotSwappable = false + ) + + @JvmStatic + @get:JvmName("idealEnabledRegions") + val idealEnabledRegions: String by remoteString( + key = "global.donations.idealEnabledRegions", + defaultValue = "", + hotSwappable = false + ) + + @JvmStatic + @get:JvmName("sepaEnabledRegions") + val sepaEnabledRegions: String by remoteString( + key = "global.donations.sepaEnabledRegions", + defaultValue = "", + hotSwappable = false + ) + + /** List of device products that are blocked from showing notification thumbnails. */ + val notificationThumbnailProductBlocklist: String by remoteString( + key = "android.notificationThumbnailProductBlocklist", + defaultValue = "", + hotSwappable = true + ) + + /** Whether or not to use active call manager instead of WebRtcCallService. */ + @JvmStatic + @get:JvmName("useActiveCallManager") + val useActiveCallManager: Boolean by remoteBoolean( + key = "android.calling.useActiveCallManager.5", + defaultValue = false, + hotSwappable = false + ) + + /** Whether the in-app GIF search is available for use. */ + @JvmStatic + @get:JvmName("gifSearchAvailable") + val gifSearchAvailable: Boolean by remoteBoolean( + key = "global.gifSearch", + defaultValue = true, + hotSwappable = true + ) + + /** Allow media converters to remux audio instead of transcoding it. */ + @JvmStatic + @get:JvmName("allowAudioRemuxing") + val allowAudioRemuxing: Boolean by remoteBoolean( + key = "android.media.audioRemux.1", + defaultValue = false, + hotSwappable = false + ) + + /** Get the default video zoom, expressed as 10x the actual Float value due to the service limiting us to whole numbers. */ + @JvmStatic + @get:JvmName("startVideoRecordAt1x") + val startVideoRecordAt1x: Boolean by remoteBoolean( + key = "android.media.videoCaptureDefaultZoom", + defaultValue = false, + hotSwappable = true + ) + + /** How often we allow a forced prekey refresh. */ + val preKeyForceRefreshInterval: Long by remoteLong( + key = "android.prekeyForceRefreshInterval", + defaultValue = 1.hours.inWholeMilliseconds, + hotSwappable = true + ) + + /** Make CDSI lookups via libsignal-net instead of native websocket. */ + val useLibsignalNetForCdsiLookup: Boolean by remoteBoolean( + key = "android.cds.libsignal.4", + defaultValue = false, + hotSwappable = true + ) + + /** Use Rx threading model to do sends. */ + @JvmStatic + @get:JvmName("useRxMessageSending") + val useRxMessageSending: Boolean by remoteBoolean( + key = "android.rxMessageSend.2", + defaultValue = false, + hotSwappable = true + ) + + /** The lifespan of a linked device (i.e. the time it can be inactive for before it expires), in milliseconds. */ + @JvmStatic + val linkedDeviceLifespan: Long by remoteValue( + key = "android.linkedDeviceLifespanSeconds", + hotSwappable = true + ) { value -> + val inSeconds = value.asLong(30.days.inWholeSeconds) + inSeconds.seconds.inWholeMilliseconds + } + + /** + * Enable Message Backups UI + * Note: This feature is in active development and is not intended to currently function. + */ + @JvmStatic + @get:JvmName("messageBackups") + val messageBackups: Boolean by remoteValue( + key = "android.messageBackups", + hotSwappable = false, + active = false + ) { value -> + BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false) + } + + /** Whether or not to use the custom CameraX controller class */ + @JvmStatic + @get:JvmName("customCameraXController") + val customCameraXController: Boolean by remoteBoolean( + key = "android.cameraXCustomController", + defaultValue = false, + hotSwappable = true + ) + + /** Whether or not to use the V2 refactor of registration. */ + @JvmStatic + @get:JvmName("registrationV2") + val registrationV2: Boolean by remoteBoolean( + key = "android.registration.v2", + defaultValue = true, + hotSwappable = false, + active = false + ) + + /** Whether unauthenticated chat web socket is backed by libsignal-net */ + @JvmStatic + @get:JvmName("libSignalWebSocketEnabled") + val libSignalWebSocketEnabled: Boolean by remoteBoolean( + key = "android.libsignalWebSocketEnabled", + defaultValue = false, + hotSwappable = false + ) + + /** Whether or not to launch the restore activity after registration is complete, rather than before. */ + @JvmStatic + @get:JvmName("restoreAfterRegistration") + val restoreAfterRegistration: Boolean by remoteValue( + key = "android.registration.restorePostRegistration", + hotSwappable = false, + active = false + ) { value -> + BuildConfig.MESSAGE_BACKUP_RESTORE_ENABLED || value.asBoolean(false) + } + + /** + * Percentage [0, 100] of web socket requests that will be "shadowed" by sending + * an unauthenticated keep-alive via libsignal-net. Default: 0 + */ + @JvmStatic + @get:JvmName("libSignalWebSocketShadowingPercentage") + val libSignalWebSocketShadowingPercentage: Int by remoteValue( + key = "android.libsignalWebSocketShadowingPercentage", + hotSwappable = false + ) { value -> + val remote = value.asInteger(0) + remote.coerceIn(0, 100) + } + + @JvmStatic + val backgroundMessageProcessInterval: Long by remoteValue( + key = "android.messageProcessor.alarmIntervalMins", + hotSwappable = true, + onChangeListener = { RoutineMessageFetchReceiver.startOrUpdateAlarm(application) } + ) { value -> + val inMinutes = value.asLong(6.hours.inWholeMinutes) + inMinutes.minutes.inWholeMilliseconds + } + + @JvmStatic + val backgroundMessageProcessForegroundDelay: Int by remoteInt( + key = "android.messageProcessor.foregroundDelayMs", + defaultValue = 300, + hotSwappable = true + ) + + /** Whether or not to delete syncing is enabled. */ + @JvmStatic + @get:JvmName("deleteSyncEnabled") + val deleteSyncEnabled: Boolean by remoteBoolean( + key = "android.deleteSyncSendReceive", + defaultValue = false, + hotSwappable = true + ) + + /** Which phase we're in for the SVR3 migration */ + val svr3MigrationPhase: Int by remoteInt( + key = "global.svr3.phase", + defaultValue = 0, + hotSwappable = true + ) + // TODO [svr3] on change listener to enqueue migration + + // endregion } diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt index d2e732d8dc..9b0e734bd4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt @@ -116,7 +116,7 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener { if (fingerprints.isEmpty()) { val resolved = viewModel.recipient.resolve() - Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.serviceId.isPresent, resolved.e164.isPresent)) + Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2, resolved.serviceId.isPresent, resolved.e164.isPresent)) MaterialAlertDialogBuilder(requireContext()) .setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext()))) .setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> requireActivity().finish() } diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt index 166496b7b0..499e22e43f 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/SpinnerApplicationContext.kt @@ -86,7 +86,7 @@ class SpinnerApplicationContext : ApplicationContext() { ) ) - Log.initialize({ FeatureFlags.internalUser() }, AndroidLogger(), PersistentLogger(this), SpinnerLogger()) + Log.initialize({ FeatureFlags.internalUser }, AndroidLogger(), PersistentLogger(this), SpinnerLogger()) DatabaseMonitor.initialize(object : QueryMonitor { override fun onSql(sql: String, args: Array?) { diff --git a/app/src/test/java/org/thoughtcrime/securesms/crash/CrashConfigTest.kt b/app/src/test/java/org/thoughtcrime/securesms/crash/CrashConfigTest.kt index 427d52595c..ffc086641e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/crash/CrashConfigTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/crash/CrashConfigTest.kt @@ -45,31 +45,31 @@ class CrashConfigTest { @Test fun `simple name pattern`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "name": "test", "percent": 100 } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "name": "test", "percent": 100 } ]""" CrashConfig.computePatterns() assertIs listOf(CrashConfig.CrashPattern(namePattern = "test")) } @Test fun `simple message pattern`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "message": "test", "percent": 100 } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "message": "test", "percent": 100 } ]""" CrashConfig.computePatterns() assertIs listOf(CrashConfig.CrashPattern(messagePattern = "test")) } @Test fun `simple stackTrace pattern`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "stackTrace": "test", "percent": 100 } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "stackTrace": "test", "percent": 100 } ]""" CrashConfig.computePatterns() assertIs listOf(CrashConfig.CrashPattern(stackTracePattern = "test")) } @Test fun `all fields set`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "name": "test1", "message": "test2", "stackTrace": "test3", "percent": 100 } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "name": "test1", "message": "test2", "stackTrace": "test3", "percent": 100 } ]""" CrashConfig.computePatterns() assertIs listOf(CrashConfig.CrashPattern(namePattern = "test1", messagePattern = "test2", stackTracePattern = "test3")) } @Test fun `multiple configs`() { - every { FeatureFlags.crashPromptConfig() } returns + every { FeatureFlags.crashPromptConfig } returns """ [ { "name": "test1", "percent": 100 }, @@ -87,7 +87,7 @@ class CrashConfigTest { @Test fun `empty fields are considered null`() { - every { FeatureFlags.crashPromptConfig() } returns + every { FeatureFlags.crashPromptConfig } returns """ [ { "name": "", "percent": 100 }, @@ -104,31 +104,31 @@ class CrashConfigTest { @Test fun `ignore zero percent`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "name": "test", "percent": 0 } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "name": "test", "percent": 0 } ]""" CrashConfig.computePatterns() assertIs emptyList() } @Test fun `not setting percent is the same as zero percent`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "name": "test" } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "name": "test" } ]""" CrashConfig.computePatterns() assertIs emptyList() } @Test fun `ignore configs without a pattern`() { - every { FeatureFlags.crashPromptConfig() } returns """[ { "percent": 100 } ]""" + every { FeatureFlags.crashPromptConfig } returns """[ { "percent": 100 } ]""" CrashConfig.computePatterns() assertIs emptyList() } @Test fun `ignore invalid json`() { - every { FeatureFlags.crashPromptConfig() } returns "asdf" + every { FeatureFlags.crashPromptConfig } returns "asdf" CrashConfig.computePatterns() assertIs emptyList() } @Test fun `ignore empty json`() { - every { FeatureFlags.crashPromptConfig() } returns "" + every { FeatureFlags.crashPromptConfig } returns "" CrashConfig.computePatterns() assertIs emptyList() } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/keyvalue/PaymentsValuesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/keyvalue/PaymentsValuesTest.kt index 1e8adc3320..35b0af42b4 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/keyvalue/PaymentsValuesTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/keyvalue/PaymentsValuesTest.kt @@ -48,8 +48,8 @@ class PaymentsValuesTest { } ) - every { FeatureFlags.payments() } returns false - every { FeatureFlags.paymentsCountryBlocklist() } returns "" + every { FeatureFlags.payments } returns false + every { FeatureFlags.paymentsCountryBlocklist } returns "" assertEquals(PaymentsAvailability.DISABLED_REMOTELY, SignalStore.paymentsValues().paymentsAvailability) } @@ -64,8 +64,8 @@ class PaymentsValuesTest { } ) - every { FeatureFlags.payments() } returns false - every { FeatureFlags.paymentsCountryBlocklist() } returns "" + every { FeatureFlags.payments } returns false + every { FeatureFlags.paymentsCountryBlocklist } returns "" assertEquals(PaymentsAvailability.WITHDRAW_ONLY, SignalStore.paymentsValues().paymentsAvailability) } @@ -80,8 +80,8 @@ class PaymentsValuesTest { } ) - every { FeatureFlags.payments() } returns true - every { FeatureFlags.paymentsCountryBlocklist() } returns "" + every { FeatureFlags.payments } returns true + every { FeatureFlags.paymentsCountryBlocklist } returns "" assertEquals(PaymentsAvailability.REGISTRATION_AVAILABLE, SignalStore.paymentsValues().paymentsAvailability) } @@ -96,8 +96,8 @@ class PaymentsValuesTest { } ) - every { FeatureFlags.payments() } returns true - every { FeatureFlags.paymentsCountryBlocklist() } returns "" + every { FeatureFlags.payments } returns true + every { FeatureFlags.paymentsCountryBlocklist } returns "" assertEquals(PaymentsAvailability.WITHDRAW_AND_SEND, SignalStore.paymentsValues().paymentsAvailability) } @@ -112,8 +112,8 @@ class PaymentsValuesTest { } ) - every { FeatureFlags.payments() } returns true - every { FeatureFlags.paymentsCountryBlocklist() } returns "1" + every { FeatureFlags.payments } returns true + every { FeatureFlags.paymentsCountryBlocklist } returns "1" assertEquals(PaymentsAvailability.NOT_IN_REGION, SignalStore.paymentsValues().paymentsAvailability) } @@ -128,8 +128,8 @@ class PaymentsValuesTest { } ) - every { FeatureFlags.payments() } returns true - every { FeatureFlags.paymentsCountryBlocklist() } returns "1" + every { FeatureFlags.payments } returns true + every { FeatureFlags.paymentsCountryBlocklist } returns "1" assertEquals(PaymentsAvailability.WITHDRAW_ONLY, SignalStore.paymentsValues().paymentsAvailability) } diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/FeatureFlags_ConsistencyTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/FeatureFlags_ConsistencyTest.java deleted file mode 100644 index 50caa9e347..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/util/FeatureFlags_ConsistencyTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; - -import org.junit.Test; -import org.signal.core.util.SetUtil; - -import java.util.Set; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public final class FeatureFlags_ConsistencyTest { - - /** - * Ensures developer makes decision on whether a flag should or should not be remote capable. - */ - @Test - public void no_flags_are_in_both_lists() { - Set intersection = SetUtil.intersection(FeatureFlags.REMOTE_CAPABLE, - FeatureFlags.NOT_REMOTE_CAPABLE); - - assertTrue(intersection.isEmpty()); - } - - /** - * Ensures developer makes decision on whether a flag should or should not be remote capable. - */ - @Test - public void all_flags_are_in_one_list_or_another() { - Set flagsByReflection = Stream.of(FeatureFlags.class.getDeclaredFields()) - .filter(f -> f.getType() == String.class) - .filter(f -> !f.getName().equals("TAG")) - .map(f -> { - try { - f.setAccessible(true); - return (String) f.get(null); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - }) - .collect(Collectors.toSet()); - - Set flagsInBothSets = SetUtil.union(FeatureFlags.REMOTE_CAPABLE, - FeatureFlags.NOT_REMOTE_CAPABLE); - - assertEquals(flagsInBothSets, flagsByReflection); - } - - /** - * Ensures we don't leave old feature flag values in the hot swap list. - */ - @Test - public void all_hot_swap_values_are_defined_capable_or_not() { - Set flagsInBothSets = SetUtil.union(FeatureFlags.REMOTE_CAPABLE, - FeatureFlags.NOT_REMOTE_CAPABLE); - - assertTrue(flagsInBothSets.containsAll(FeatureFlags.HOT_SWAPPABLE)); - } - - /** - * Ensures we don't leave old feature flag values in the sticky list. - */ - @Test - public void all_sticky_values_are_defined_capable_or_not() { - Set flagsInBothSets = SetUtil.union(FeatureFlags.REMOTE_CAPABLE, - FeatureFlags.NOT_REMOTE_CAPABLE); - - assertTrue(flagsInBothSets.containsAll(FeatureFlags.STICKY)); - } - - /** - * Ensures we don't release with forced values which is intended for local development only. - */ - @Test - public void no_values_are_forced() { - assertTrue(FeatureFlags.FORCED_VALUES.isEmpty()); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/FeatureFlags_StaticValuesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/FeatureFlags_StaticValuesTest.kt new file mode 100644 index 0000000000..fed7b8d355 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/FeatureFlags_StaticValuesTest.kt @@ -0,0 +1,79 @@ +package org.thoughtcrime.securesms.util + +import org.junit.Test +import kotlin.reflect.KProperty1 +import kotlin.reflect.KVisibility +import kotlin.reflect.full.memberProperties + +/** + * Ensures we don't release with forced values which is intended for local development only. + */ +class FeatureFlags_StaticValuesTest { + + /** + * This test cycles the REMOTE_VALUES through a bunch of different inputs, then looks at all of the public getters and checks to see if they return different + * values when the inputs change. If they don't, then it's likely that the getter is returning a static value, which was likely introduced during testing + * and not something we actually want to commit. + */ + @Test + fun `Ensure there's no static values`() { + // A list of inputs we'll cycle the remote config values to in order to see if it changes the outputs of the getters + val remoteTestInputs = listOf( + true, + false, + "true", + "false", + "cat", + "dog", + "1", + "100", + "12345678910111213141516", + "*" + ) + + val configKeys = FeatureFlags.configsByKey.keys + + val ignoreList = setOf( + "REMOTE_VALUES", + "configsByKey", + "debugMemoryValues", + "debugDiskValues", + "debugPendingDiskValues", + "CRASH_PROMPT_CONFIG", + "PROMPT_BATTERY_SAVER", + "PROMPT_FOR_NOTIFICATION_LOGS" + ) + + val publicVals: List> = FeatureFlags::class.memberProperties + .filter { it.visibility == KVisibility.PUBLIC } + .filterNot { ignoreList.contains(it.name) } + + val publicValOutputs: MutableMap> = mutableMapOf() + + for (input in remoteTestInputs) { + for (key in configKeys) { + FeatureFlags.REMOTE_VALUES[key] = input + } + + for (publicVal in publicVals) { + val output: Any? = publicVal.getter.call(FeatureFlags) + val existingOutputs: MutableSet = publicValOutputs.getOrDefault(publicVal.name, mutableSetOf()) + existingOutputs.add(output) + publicValOutputs[publicVal.name] = existingOutputs + } + } + + for (entry in publicValOutputs) { + val getter = entry.key + val outputs = entry.value + + if (outputs.size == 0) { + throw AssertionError("Getter $getter has no outputs! Something is wrong.") + } + + if (outputs.size == 1) { + throw AssertionError("Getter '$getter' had the same output every time (value = ${outputs.first()})! Did you accidentally set it to a constant? Or, if you think this is a mistake, add a value to the inputs of this test that would case the value to change.") + } + } + } +}