From 23b5a3dcb0080076e55d05beade5ef2bc41a157b Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 8 Sep 2025 16:07:11 -0300 Subject: [PATCH] Start conversion from Fragment Nav Framework to utilizing a centralized AppSettingsRouter. --- .../settings/app/AppSettingsActivity.kt | 159 +++++------- .../settings/app/AppSettingsFragment.kt | 88 ++++--- .../settings/app/routes/AppSettingsRoute.kt | 231 ++++++++++++++++++ .../settings/app/routes/AppSettingsRouter.kt | 29 +++ 4 files changed, 379 insertions(+), 128 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRouter.kt 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 fb63a2b81a..afc11566bd 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 @@ -7,17 +7,18 @@ import androidx.navigation.NavDirections import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.Subject +import org.signal.core.util.getParcelableExtraCompat import org.signal.donations.InAppPaymentType import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity -import org.thoughtcrime.securesms.components.settings.app.chats.folders.CreateFoldersFragmentArgs -import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs +import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.profiles.manage.UsernameEditMode import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.util.CachedInflater @@ -25,8 +26,7 @@ import org.thoughtcrime.securesms.util.DynamicTheme import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.navigation.safeNavigate -private const val START_LOCATION = "app.settings.start.location" -private const val START_ARGUMENTS = "app.settings.start.arguments" +private const val START_ROUTE = "app.settings.args.START_ROUTE" private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES" private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated" private const val EXTRA_PERFORM_ACTION_ON_CREATE = "extra_perform_action_on_create" @@ -48,42 +48,41 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent { val startingAction: NavDirections? = if (intent?.categories?.contains(NOTIFICATION_CATEGORY) == true) { AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment() } else { - when (StartLocation.fromCode(intent?.getIntExtra(START_LOCATION, StartLocation.HOME.code))) { - StartLocation.HOME -> null - StartLocation.BACKUPS -> AppSettingsFragmentDirections.actionDirectToBackupsPreferenceFragment() - StartLocation.HELP -> AppSettingsFragmentDirections.actionDirectToHelpFragment() - .setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0)) - - StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment() - StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment() - StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment() - StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(InAppPaymentType.RECURRING_DONATION) - StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(InAppPaymentType.ONE_TIME_DONATION) - StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations() - StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles() - StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles() - StartLocation.NOTIFICATION_PROFILE_DETAILS -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails( - EditNotificationProfileScheduleFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).profileId + val appSettingsRoute: AppSettingsRoute? = intent?.getParcelableExtraCompat(START_ROUTE, AppSettingsRoute::class.java) + when (appSettingsRoute) { + AppSettingsRoute.Empty -> null + AppSettingsRoute.BackupsRoute.Local -> AppSettingsFragmentDirections.actionDirectToBackupsPreferenceFragment() + is AppSettingsRoute.HelpRoute.Settings -> AppSettingsFragmentDirections.actionDirectToHelpFragment() + .setStartCategoryIndex(appSettingsRoute.startCategoryIndex) + AppSettingsRoute.DataAndStorageRoute.Proxy -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment() + AppSettingsRoute.NotificationsRoute.Notifications -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment() + AppSettingsRoute.ChangeNumberRoute.Start -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment() + is AppSettingsRoute.DonationsRoute.Donations -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(appSettingsRoute.directToCheckoutType) + AppSettingsRoute.NotificationsRoute.NotificationProfiles -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles() + is AppSettingsRoute.NotificationsRoute.EditProfile -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles() + is AppSettingsRoute.NotificationsRoute.ProfileDetails -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails( + appSettingsRoute.profileId ) - StartLocation.PRIVACY -> AppSettingsFragmentDirections.actionDirectToPrivacy() - StartLocation.LINKED_DEVICES -> AppSettingsFragmentDirections.actionDirectToDevices() - StartLocation.USERNAME_LINK -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings() - StartLocation.RECOVER_USERNAME -> AppSettingsFragmentDirections.actionDirectToUsernameRecovery() - StartLocation.REMOTE_BACKUPS -> AppSettingsFragmentDirections.actionDirectToRemoteBackupsSettingsFragment() - StartLocation.CHAT_FOLDERS -> AppSettingsFragmentDirections.actionDirectToChatFoldersFragment() - StartLocation.CREATE_CHAT_FOLDER -> AppSettingsFragmentDirections.actionDirectToCreateFoldersFragment( - CreateFoldersFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).folderId, - CreateFoldersFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).threadIds + AppSettingsRoute.PrivacyRoute.Privacy -> AppSettingsFragmentDirections.actionDirectToPrivacy() + AppSettingsRoute.LinkDeviceRoute.LinkDevice -> AppSettingsFragmentDirections.actionDirectToDevices() + AppSettingsRoute.UsernameLinkRoute.UsernameLink -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings() + is AppSettingsRoute.AccountRoute.Username -> AppSettingsFragmentDirections.actionDirectToUsernameRecovery() + is AppSettingsRoute.BackupsRoute.Remote -> AppSettingsFragmentDirections.actionDirectToRemoteBackupsSettingsFragment() + AppSettingsRoute.ChatFoldersRoute.ChatFolders -> AppSettingsFragmentDirections.actionDirectToChatFoldersFragment() + is AppSettingsRoute.ChatFoldersRoute.CreateChatFolders -> AppSettingsFragmentDirections.actionDirectToCreateFoldersFragment( + appSettingsRoute.folderId, + appSettingsRoute.threadIds ) - StartLocation.BACKUPS_SETTINGS -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment() - StartLocation.INVITE -> AppSettingsFragmentDirections.actionDirectToInviteFragment() - StartLocation.MANAGE_STORAGE -> AppSettingsFragmentDirections.actionDirectToStoragePreferenceFragment() + AppSettingsRoute.BackupsRoute.Backups -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment() + AppSettingsRoute.Invite -> AppSettingsFragmentDirections.actionDirectToInviteFragment() + AppSettingsRoute.DataAndStorageRoute.DataAndStorage -> AppSettingsFragmentDirections.actionDirectToStoragePreferenceFragment() + else -> error("Unsupported start location: ${appSettingsRoute?.javaClass?.name}") } } - intent = intent.putExtra(START_LOCATION, StartLocation.HOME) + intent = intent.putExtra(START_ROUTE, AppSettingsRoute.Empty) if (startingAction == null && savedInstanceState != null) { wasConfigurationUpdated = savedInstanceState.getBoolean(STATE_WAS_CONFIGURATION_UPDATED) @@ -148,123 +147,89 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent { @JvmStatic @JvmOverloads fun home(context: Context, action: String? = null): Intent { - return getIntentForStartLocation(context, StartLocation.HOME) + return getIntentForStartLocation(context, AppSettingsRoute.Empty) .putExtra(EXTRA_PERFORM_ACTION_ON_CREATE, action) } @JvmStatic - fun backups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS) + fun backups(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Local) @JvmStatic fun help(context: Context, startCategoryIndex: Int = 0): Intent { - return getIntentForStartLocation(context, StartLocation.HELP) + return getIntentForStartLocation(context, AppSettingsRoute.HelpRoute.Settings(startCategoryIndex = startCategoryIndex)) .putExtra(HelpFragment.START_CATEGORY_INDEX, startCategoryIndex) } @JvmStatic - fun proxy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.PROXY) + fun proxy(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DataAndStorageRoute.Proxy) @JvmStatic - fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS) + fun notifications(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.Notifications) @JvmStatic - fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER) + fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.ChangeNumberRoute.Start) @JvmStatic - fun subscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.SUBSCRIPTIONS) + fun subscriptions(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DonationsRoute.Donations(directToCheckoutType = InAppPaymentType.RECURRING_DONATION)) @JvmStatic - fun boost(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BOOST) + fun boost(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DonationsRoute.Donations(directToCheckoutType = InAppPaymentType.ONE_TIME_DONATION)) @JvmStatic - fun manageSubscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.MANAGE_SUBSCRIPTIONS) + fun manageSubscriptions(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DonationsRoute.Donations()) - fun manageStorage(context: Context): Intent = getIntentForStartLocation(context, StartLocation.MANAGE_STORAGE) + fun manageStorage(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DataAndStorageRoute.DataAndStorage) @JvmStatic - fun notificationProfiles(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATION_PROFILES) + fun notificationProfiles(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.NotificationProfiles) @JvmStatic - fun createNotificationProfile(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CREATE_NOTIFICATION_PROFILE) + fun createNotificationProfile(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.EditProfile()) @JvmStatic - fun privacy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.PRIVACY) + fun privacy(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.PrivacyRoute.Privacy) @JvmStatic fun notificationProfileDetails(context: Context, profileId: Long): Intent { - val arguments = EditNotificationProfileScheduleFragmentArgs.Builder(profileId, false) - .build() - .toBundle() - - return getIntentForStartLocation(context, StartLocation.NOTIFICATION_PROFILE_DETAILS) - .putExtra(START_ARGUMENTS, arguments) + return getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.ProfileDetails(profileId = profileId)) } @JvmStatic - fun linkedDevices(context: Context): Intent = getIntentForStartLocation(context, StartLocation.LINKED_DEVICES) + fun linkedDevices(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.LinkDeviceRoute.LinkDevice) @JvmStatic - fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.USERNAME_LINK) + fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.UsernameLinkRoute.UsernameLink) @JvmStatic - fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, StartLocation.RECOVER_USERNAME) + fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.AccountRoute.Username(mode = UsernameEditMode.RECOVERY)) @JvmStatic - fun remoteBackups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.REMOTE_BACKUPS) + fun remoteBackups(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Remote()) @JvmStatic - fun chatFolders(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHAT_FOLDERS) + fun chatFolders(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.ChatFoldersRoute.ChatFolders) @JvmStatic fun createChatFolder(context: Context, id: Long = -1, threadIds: LongArray?): Intent { - val arguments = CreateFoldersFragmentArgs.Builder(id, threadIds ?: longArrayOf()) - .build() - .toBundle() - - return getIntentForStartLocation(context, StartLocation.CREATE_CHAT_FOLDER).putExtra(START_ARGUMENTS, arguments) + return getIntentForStartLocation( + context, + AppSettingsRoute.ChatFoldersRoute.CreateChatFolders( + folderId = id, + threadIds = threadIds ?: longArrayOf() + ) + ) } @JvmStatic - fun backupsSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS_SETTINGS) + fun backupsSettings(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Backups) @JvmStatic - fun invite(context: Context): Intent = getIntentForStartLocation(context, StartLocation.INVITE) + fun invite(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.Invite) - private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent { + private fun getIntentForStartLocation(context: Context, startRoute: AppSettingsRoute): Intent { return Intent(context, AppSettingsActivity::class.java) .putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number) - .putExtra(START_LOCATION, startLocation.code) - } - } - - private enum class StartLocation(val code: Int) { - HOME(0), - BACKUPS(1), - HELP(2), - PROXY(3), - NOTIFICATIONS(4), - CHANGE_NUMBER(5), - SUBSCRIPTIONS(6), - BOOST(7), - MANAGE_SUBSCRIPTIONS(8), - NOTIFICATION_PROFILES(9), - CREATE_NOTIFICATION_PROFILE(10), - NOTIFICATION_PROFILE_DETAILS(11), - PRIVACY(12), - LINKED_DEVICES(13), - USERNAME_LINK(14), - RECOVER_USERNAME(15), - REMOTE_BACKUPS(16), - CHAT_FOLDERS(17), - CREATE_CHAT_FOLDER(18), - BACKUPS_SETTINGS(19), - INVITE(20), - MANAGE_STORAGE(21); - - companion object { - fun fromCode(code: Int?): StartLocation { - return entries.find { code == it.code } ?: HOME - } + .putExtra(START_ROUTE, startRoute) } } } 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 25a77b1448..706e234789 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 @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.settings.app import android.os.Bundle import android.view.View import android.widget.Toast -import androidx.annotation.IdRes import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -39,8 +38,9 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavDirections +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -67,6 +67,8 @@ import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner import org.thoughtcrime.securesms.banner.ui.compose.Importance import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel import org.thoughtcrime.securesms.components.emoji.Emojifier +import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute +import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRouter import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate @@ -83,9 +85,38 @@ import org.thoughtcrime.securesms.util.navigation.safeNavigate class AppSettingsFragment : ComposeFragment(), Callbacks { private val viewModel: AppSettingsViewModel by viewModels() + private val appSettingsRouter by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner)) + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + appSettingsRouter.currentRoute.collect { route -> + when (route) { + is AppSettingsRoute.BackupsRoute.Remote -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment) + is AppSettingsRoute.AccountRoute.Account -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_accountSettingsFragment) + is AppSettingsRoute.LinkDeviceRoute.LinkDevice -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkDeviceFragment) + is AppSettingsRoute.DonationsRoute.Donations -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageDonationsFragment) + is AppSettingsRoute.AppearanceRoute.Appearance -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment) + is AppSettingsRoute.ChatsRoute.Chats -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment) + is AppSettingsRoute.StoriesRoute.Privacy -> findNavController().safeNavigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToStoryPrivacySettings(route.titleId)) + is AppSettingsRoute.NotificationsRoute.Notifications -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment) + is AppSettingsRoute.PrivacyRoute.Privacy -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_privacySettingsFragment) + is AppSettingsRoute.BackupsRoute.Backups -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment) + is AppSettingsRoute.DataAndStorageRoute.DataAndStorage -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment) + is AppSettingsRoute.AppUpdates -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_appUpdatesSettingsFragment) + is AppSettingsRoute.Payments -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_paymentsActivity) + is AppSettingsRoute.HelpRoute.Settings -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_helpSettingsFragment) + is AppSettingsRoute.Invite -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_inviteFragment) + is AppSettingsRoute.InternalRoute.Internal -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_internalSettingsFragment) + is AppSettingsRoute.AccountRoute.ManageProfile -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_manageProfileActivity) + is AppSettingsRoute.UsernameLinkRoute.UsernameLink -> findNavController().safeNavigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment) + else -> error("Unsupported route: ${route.javaClass.name}") + } + } + } + } } @Composable @@ -118,12 +149,8 @@ class AppSettingsFragment : ComposeFragment(), Callbacks { requireActivity().finishAfterTransition() } - override fun navigate(actionId: Int) { - findNavController().safeNavigate(actionId) - } - - override fun navigate(directions: NavDirections) { - findNavController().safeNavigate(directions) + override fun navigate(route: AppSettingsRoute) { + appSettingsRouter.navigateTo(route) } override fun onResume() { @@ -203,7 +230,7 @@ private fun AppSettingsContent( BackupsWarningRow( text = stringResource(R.string.AppSettingsFragment__renew_your_signal_backups_subscription), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment) + callbacks.navigate(AppSettingsRoute.BackupsRoute.Remote()) } ) @@ -219,7 +246,7 @@ private fun AppSettingsContent( text = stringResource(R.string.AppSettingsFragment__couldnt_complete_backup), onClick = { BackupRepository.markBackupFailedIndicatorClicked() - callbacks.navigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment) + callbacks.navigate(AppSettingsRoute.BackupsRoute.Remote()) } ) @@ -235,7 +262,7 @@ private fun AppSettingsContent( text = stringResource(R.string.AppSettingsFragment__couldnt_redeem_your_backups_subscription), onClick = { BackupRepository.markBackupAlreadyRedeemedIndicatorClicked() - callbacks.navigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment) + callbacks.navigate(AppSettingsRoute.BackupsRoute.Remote()) } ) @@ -252,7 +279,7 @@ private fun AppSettingsContent( icon = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24), iconTint = MaterialTheme.colorScheme.error, onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment) + callbacks.navigate(AppSettingsRoute.BackupsRoute.Remote()) } ) @@ -268,7 +295,7 @@ private fun AppSettingsContent( text = stringResource(R.string.AccountSettingsFragment__account), icon = painterResource(R.drawable.symbol_person_circle_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_accountSettingsFragment) + callbacks.navigate(AppSettingsRoute.AccountRoute.Account) } ) } @@ -278,7 +305,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__linked_devices), icon = painterResource(R.drawable.symbol_devices_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_linkDeviceFragment) + callbacks.navigate(AppSettingsRoute.LinkDeviceRoute.LinkDevice) }, enabled = isRegisteredAndUpToDate ) @@ -312,7 +339,7 @@ private fun AppSettingsContent( }, onClick = { if (state.allowUserToGoToDonationManagementScreen) { - callbacks.navigate(R.id.action_appSettingsFragment_to_manageDonationsFragment) + callbacks.navigate(AppSettingsRoute.DonationsRoute.Donations()) } else { CommunicationActions.openBrowserLink(context, donateUrl) } @@ -332,7 +359,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__appearance), icon = painterResource(R.drawable.symbol_appearance_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_appearanceSettingsFragment) + callbacks.navigate(AppSettingsRoute.AppearanceRoute.Appearance) } ) } @@ -342,7 +369,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences_chats__chats), icon = painterResource(R.drawable.symbol_chat_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment) + callbacks.navigate(AppSettingsRoute.ChatsRoute.Chats) }, enabled = state.legacyLocalBackupsEnabled || isRegisteredAndUpToDate ) @@ -353,7 +380,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__stories), icon = painterResource(R.drawable.symbol_stories_24), onClick = { - callbacks.navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToStoryPrivacySettings(R.string.preferences__stories)) + callbacks.navigate(AppSettingsRoute.StoriesRoute.Privacy(titleId = R.string.preferences__stories)) }, enabled = isRegisteredAndUpToDate ) @@ -364,7 +391,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__notifications), icon = painterResource(R.drawable.symbol_bell_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment) + callbacks.navigate(AppSettingsRoute.NotificationsRoute.Notifications) }, enabled = isRegisteredAndUpToDate ) @@ -375,7 +402,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__privacy), icon = painterResource(R.drawable.symbol_lock_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment) + callbacks.navigate(AppSettingsRoute.PrivacyRoute.Privacy) }, enabled = isRegisteredAndUpToDate ) @@ -398,7 +425,7 @@ private fun AppSettingsContent( ) }, onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment) + callbacks.navigate(AppSettingsRoute.BackupsRoute.Backups) }, onLongClick = { callbacks.copyRemoteBackupsSubscriberIdToClipboard() @@ -413,7 +440,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__data_and_storage), icon = painterResource(R.drawable.symbol_data_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment) + callbacks.navigate(AppSettingsRoute.DataAndStorageRoute.DataAndStorage) } ) } @@ -424,7 +451,7 @@ private fun AppSettingsContent( text = "App updates", icon = painterResource(R.drawable.symbol_calendar_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_appUpdatesSettingsFragment) + callbacks.navigate(AppSettingsRoute.AppUpdates) } ) } @@ -467,7 +494,7 @@ private fun AppSettingsContent( ) }, onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_paymentsActivity) + callbacks.navigate(AppSettingsRoute.Payments) } ) } @@ -482,7 +509,7 @@ private fun AppSettingsContent( text = stringResource(R.string.preferences__help), icon = painterResource(R.drawable.symbol_help_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_helpSettingsFragment) + callbacks.navigate(AppSettingsRoute.HelpRoute.Settings()) } ) } @@ -492,7 +519,7 @@ private fun AppSettingsContent( text = stringResource(R.string.AppSettingsFragment__invite_your_friends), icon = painterResource(R.drawable.symbol_invite_24), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_inviteFragment) + callbacks.navigate(AppSettingsRoute.Invite) } ) } @@ -506,7 +533,7 @@ private fun AppSettingsContent( Rows.TextRow( text = stringResource(R.string.preferences__internal_preferences), onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_internalSettingsFragment) + callbacks.navigate(AppSettingsRoute.InternalRoute.Internal) } ) } @@ -558,7 +585,7 @@ private fun BioRow( modifier = Modifier .clickable( onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_manageProfileActivity) + callbacks.navigate(AppSettingsRoute.AccountRoute.ManageProfile) } ) .horizontalGutters() @@ -632,7 +659,7 @@ private fun BioRow( if (hasUsername) { IconButtons.IconButton( onClick = { - callbacks.navigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment) + callbacks.navigate(AppSettingsRoute.UsernameLinkRoute.UsernameLink) }, size = 36.dp, colors = IconButtons.iconButtonColors( @@ -711,8 +738,7 @@ private fun BioRowPreview() { private interface Callbacks { fun onNavigationClick(): Unit = error("Not implemented.") - fun navigate(@IdRes actionId: Int): Unit = error("Not implemented") - fun navigate(directions: NavDirections): Unit = error("Not implemented") + fun navigate(route: AppSettingsRoute): Unit = error("Not implemented") fun copyDonorBadgeSubscriberIdToClipboard(): Unit = error("Not implemented") fun copyRemoteBackupsSubscriberIdToClipboard(): Unit = error("Not implemented") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt new file mode 100644 index 0000000000..0c4461c42e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.routes + +import android.os.Parcelable +import androidx.annotation.StringRes +import kotlinx.parcelize.Parcelize +import org.signal.donations.InAppPaymentType +import org.thoughtcrime.securesms.database.model.DistributionListId +import org.thoughtcrime.securesms.groups.ParcelableGroupId +import org.thoughtcrime.securesms.profiles.manage.UsernameEditMode +import org.thoughtcrime.securesms.recipients.RecipientId + +/** + * Describes a route that the AppSettings screen can open. Every route listed here is displayed in + * the PRIMARY (detail) pane of the AppSettingsScreen. + */ +@Parcelize +sealed interface AppSettingsRoute : Parcelable { + /** + * Empty state, displayed when there is no current route. In this case, the "top" of our + * scaffold navigator should be the SECONDARY (list) pane. + */ + data object Empty : AppSettingsRoute + + @Parcelize + sealed interface AccountRoute : AppSettingsRoute { + data object Account : AccountRoute + data object ManageProfile : AccountRoute + data object AdvancedPinSettings : AccountRoute + data object DeleteAccount : AccountRoute + data object ExportAccountData : AccountRoute + data object OldDeviceTransfer : AccountRoute + data class Username(val mode: UsernameEditMode = UsernameEditMode.NORMAL) : AccountRoute + } + + data object Payments : AppSettingsRoute + data object Invite : AppSettingsRoute + data object AppUpdates : AppSettingsRoute + + @Parcelize + sealed interface StoriesRoute : AppSettingsRoute { + data class Privacy(@StringRes val titleId: Int) : StoriesRoute + data object MyStory : StoriesRoute + data class PrivateStory(val distributionListId: DistributionListId) : StoriesRoute + data class GroupStory(val groupId: ParcelableGroupId) : StoriesRoute + data object OnlyShareWith : StoriesRoute + data object AllExcept : StoriesRoute + data object SignalConnections : StoriesRoute + data class EditName(val distributionListId: DistributionListId, val name: String) : StoriesRoute + data class AddViewers(val distributionListId: DistributionListId) : StoriesRoute + } + + @Parcelize + sealed interface UsernameLinkRoute : AppSettingsRoute { + data object UsernameLink : UsernameLinkRoute + data object QRColorPicker : UsernameLinkRoute + data object Share : UsernameLinkRoute + } + + @Parcelize + sealed interface BackupsRoute : AppSettingsRoute { + data object Backups : BackupsRoute + data object Local : BackupsRoute + data class Remote(val backupLaterSelected: Boolean = false) : BackupsRoute + data object DisplayKey : BackupsRoute + } + + @Parcelize + sealed interface NotificationsRoute : AppSettingsRoute { + data object Notifications : NotificationsRoute + data object NotificationProfiles : NotificationsRoute + data class EditProfile(val profileId: Long = -1L) : NotificationsRoute + data class ProfileDetails(val profileId: Long) : NotificationsRoute + data class AddAllowedMembers(val profileId: Long) : NotificationsRoute + + data class SelectRecipients(val profileId: Long, val currentSelection: Array? = null) : NotificationsRoute { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SelectRecipients + + if (profileId != other.profileId) return false + if (!currentSelection.contentEquals(other.currentSelection)) return false + + return true + } + + override fun hashCode(): Int { + var result = profileId.hashCode() + result = 31 * result + (currentSelection?.contentHashCode() ?: 0) + return result + } + } + + data class EditSchedule(val profileId: Long, val createMode: Boolean) : NotificationsRoute + data class Created(val profileId: Long) : NotificationsRoute + } + + @Parcelize + sealed interface DonationsRoute : AppSettingsRoute { + data class Donations( + val directToCheckoutType: InAppPaymentType = InAppPaymentType.UNKNOWN + ) : DonationsRoute + + data object Badges : DonationsRoute + data object Receipts : DonationsRoute + data class Receipt(val id: Long) : DonationsRoute + data object LearnMore : DonationsRoute + data object Featured : DonationsRoute + } + + @Parcelize + sealed interface InternalRoute : AppSettingsRoute { + data object Internal : InternalRoute + data object DonorErrorConfiguration : InternalRoute + data object StoryDialogs : InternalRoute + data object Search : InternalRoute + data object SvrPlayground : InternalRoute + data object ChatSpringboard : InternalRoute + data object OneTimeDonationConfiguration : InternalRoute + data object TerminalDonationConfiguration : InternalRoute + data object BackupPlayground : InternalRoute + data object StorageServicePlayground : InternalRoute + data object SqlitePlayground : InternalRoute + data object ConversationTestFragment : InternalRoute + } + + @Parcelize + sealed interface PrivacyRoute : AppSettingsRoute { + data object Privacy : PrivacyRoute + data object BlockedUsers : PrivacyRoute + data object Advanced : PrivacyRoute + data object ExpiringMessages : PrivacyRoute + data object PhoneNumberPrivacy : PrivacyRoute + data object ScreenLock : PrivacyRoute + } + + @Parcelize + sealed interface DataAndStorageRoute : AppSettingsRoute { + data object DataAndStorage : DataAndStorageRoute + data object Storage : DataAndStorageRoute + data object Proxy : DataAndStorageRoute + } + + @Parcelize + sealed interface HelpRoute : AppSettingsRoute { + data class Settings( + val startCategoryIndex: Int = 0 + ) : HelpRoute + + data object Help : HelpRoute + data object DebugLog : HelpRoute + data object Licenses : HelpRoute + } + + @Parcelize + sealed interface AppearanceRoute : AppSettingsRoute { + data object Appearance : AppearanceRoute + data object Wallpaper : AppearanceRoute + data object AppIconSelection : AppearanceRoute + data object AppIconTutorial : AppearanceRoute + } + + @Parcelize + sealed interface ChatsRoute : AppSettingsRoute { + data object Chats : ChatsRoute + data object Reactions : ChatsRoute + } + + @Parcelize + sealed interface ChatFoldersRoute : AppSettingsRoute { + data object ChatFolders : ChatFoldersRoute + + data class CreateChatFolders( + val folderId: Long, + val threadIds: LongArray + ) : ChatFoldersRoute { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CreateChatFolders + + if (folderId != other.folderId) return false + if (!threadIds.contentEquals(other.threadIds)) return false + + return true + } + + override fun hashCode(): Int { + var result = folderId.hashCode() + result = 31 * result + threadIds.contentHashCode() + return result + } + } + + data object Education : ChatFoldersRoute + data object ChooseChats : ChatFoldersRoute + } + + @Parcelize + sealed interface LinkDeviceRoute : AppSettingsRoute { + data object LinkDevice : LinkDeviceRoute + data object Finished : LinkDeviceRoute + data object LearnMore : LinkDeviceRoute + data object Education : LinkDeviceRoute + data object EditName : LinkDeviceRoute + data object Add : LinkDeviceRoute + data object Intro : LinkDeviceRoute + data object Sync : LinkDeviceRoute + } + + @Parcelize + sealed interface ChangeNumberRoute : AppSettingsRoute { + data object Start : ChangeNumberRoute + data object EnterPhoneNumber : ChangeNumberRoute + data object Confirm : ChangeNumberRoute + data object CountryPicker : ChangeNumberRoute + data object Verify : ChangeNumberRoute + data object Captcha : ChangeNumberRoute + data object EnterCode : ChangeNumberRoute + data object RegistrationLock : ChangeNumberRoute + data object AccountLocked : ChangeNumberRoute + data object PinDiffers : ChangeNumberRoute + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRouter.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRouter.kt new file mode 100644 index 0000000000..7268fdc040 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRouter.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.routes + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +/** + * Router which manages what screen we are displaying in app settings. Underneath, this is a ViewModel + * that is tied to the top-level parent, so that all screens throughout the app settings can access it. + * + * This gives a single point to navigate to a new page, but assumes that the actual backstack of routes + * will be handled elsewhere. This just emits routing requests. + */ +class AppSettingsRouter() : ViewModel() { + + val currentRoute = MutableSharedFlow() + + fun navigateTo(route: AppSettingsRoute) { + viewModelScope.launch { + currentRoute.emit(route) + } + } +}