Start conversion from Fragment Nav Framework to utilizing a centralized AppSettingsRouter.

This commit is contained in:
Alex Hart
2025-09-08 16:07:11 -03:00
committed by Greyson Parrelli
parent 909ea6b925
commit 23b5a3dcb0
4 changed files with 379 additions and 128 deletions

View File

@@ -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)
}
}
}

View File

@@ -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<AppSettingsRouter>()
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")
}

View File

@@ -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<RecipientId>? = 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
}
}

View File

@@ -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<AppSettingsRoute>()
fun navigateTo(route: AppSettingsRoute) {
viewModelScope.launch {
currentRoute.emit(route)
}
}
}