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 com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.getParcelableExtraCompat
import org.signal.donations.InAppPaymentType import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.chats.folders.CreateFoldersFragmentArgs import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileScheduleFragmentArgs
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePayRepository
import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.manage.UsernameEditMode
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.util.CachedInflater 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.SignalE164Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
private const val START_LOCATION = "app.settings.start.location" private const val START_ROUTE = "app.settings.args.START_ROUTE"
private const val START_ARGUMENTS = "app.settings.start.arguments"
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES" 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 STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
private const val EXTRA_PERFORM_ACTION_ON_CREATE = "extra_perform_action_on_create" 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) { val startingAction: NavDirections? = if (intent?.categories?.contains(NOTIFICATION_CATEGORY) == true) {
AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment() AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
} else { } else {
when (StartLocation.fromCode(intent?.getIntExtra(START_LOCATION, StartLocation.HOME.code))) { val appSettingsRoute: AppSettingsRoute? = intent?.getParcelableExtraCompat(START_ROUTE, AppSettingsRoute::class.java)
StartLocation.HOME -> null when (appSettingsRoute) {
StartLocation.BACKUPS -> AppSettingsFragmentDirections.actionDirectToBackupsPreferenceFragment() AppSettingsRoute.Empty -> null
StartLocation.HELP -> AppSettingsFragmentDirections.actionDirectToHelpFragment() AppSettingsRoute.BackupsRoute.Local -> AppSettingsFragmentDirections.actionDirectToBackupsPreferenceFragment()
.setStartCategoryIndex(intent.getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0)) is AppSettingsRoute.HelpRoute.Settings -> AppSettingsFragmentDirections.actionDirectToHelpFragment()
.setStartCategoryIndex(appSettingsRoute.startCategoryIndex)
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment() AppSettingsRoute.DataAndStorageRoute.Proxy -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment() AppSettingsRoute.NotificationsRoute.Notifications -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment() AppSettingsRoute.ChangeNumberRoute.Start -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(InAppPaymentType.RECURRING_DONATION) is AppSettingsRoute.DonationsRoute.Donations -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(appSettingsRoute.directToCheckoutType)
StartLocation.BOOST -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(InAppPaymentType.ONE_TIME_DONATION) AppSettingsRoute.NotificationsRoute.NotificationProfiles -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles()
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations() is AppSettingsRoute.NotificationsRoute.EditProfile -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles()
StartLocation.NOTIFICATION_PROFILES -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles() is AppSettingsRoute.NotificationsRoute.ProfileDetails -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails(
StartLocation.CREATE_NOTIFICATION_PROFILE -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles() appSettingsRoute.profileId
StartLocation.NOTIFICATION_PROFILE_DETAILS -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails(
EditNotificationProfileScheduleFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).profileId
) )
StartLocation.PRIVACY -> AppSettingsFragmentDirections.actionDirectToPrivacy() AppSettingsRoute.PrivacyRoute.Privacy -> AppSettingsFragmentDirections.actionDirectToPrivacy()
StartLocation.LINKED_DEVICES -> AppSettingsFragmentDirections.actionDirectToDevices() AppSettingsRoute.LinkDeviceRoute.LinkDevice -> AppSettingsFragmentDirections.actionDirectToDevices()
StartLocation.USERNAME_LINK -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings() AppSettingsRoute.UsernameLinkRoute.UsernameLink -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings()
StartLocation.RECOVER_USERNAME -> AppSettingsFragmentDirections.actionDirectToUsernameRecovery() is AppSettingsRoute.AccountRoute.Username -> AppSettingsFragmentDirections.actionDirectToUsernameRecovery()
StartLocation.REMOTE_BACKUPS -> AppSettingsFragmentDirections.actionDirectToRemoteBackupsSettingsFragment() is AppSettingsRoute.BackupsRoute.Remote -> AppSettingsFragmentDirections.actionDirectToRemoteBackupsSettingsFragment()
StartLocation.CHAT_FOLDERS -> AppSettingsFragmentDirections.actionDirectToChatFoldersFragment() AppSettingsRoute.ChatFoldersRoute.ChatFolders -> AppSettingsFragmentDirections.actionDirectToChatFoldersFragment()
StartLocation.CREATE_CHAT_FOLDER -> AppSettingsFragmentDirections.actionDirectToCreateFoldersFragment( is AppSettingsRoute.ChatFoldersRoute.CreateChatFolders -> AppSettingsFragmentDirections.actionDirectToCreateFoldersFragment(
CreateFoldersFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).folderId, appSettingsRoute.folderId,
CreateFoldersFragmentArgs.fromBundle(intent.getBundleExtra(START_ARGUMENTS)!!).threadIds appSettingsRoute.threadIds
) )
StartLocation.BACKUPS_SETTINGS -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment() AppSettingsRoute.BackupsRoute.Backups -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment()
StartLocation.INVITE -> AppSettingsFragmentDirections.actionDirectToInviteFragment() AppSettingsRoute.Invite -> AppSettingsFragmentDirections.actionDirectToInviteFragment()
StartLocation.MANAGE_STORAGE -> AppSettingsFragmentDirections.actionDirectToStoragePreferenceFragment() 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) { if (startingAction == null && savedInstanceState != null) {
wasConfigurationUpdated = savedInstanceState.getBoolean(STATE_WAS_CONFIGURATION_UPDATED) wasConfigurationUpdated = savedInstanceState.getBoolean(STATE_WAS_CONFIGURATION_UPDATED)
@@ -148,123 +147,89 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun home(context: Context, action: String? = null): Intent { 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) .putExtra(EXTRA_PERFORM_ACTION_ON_CREATE, action)
} }
@JvmStatic @JvmStatic
fun backups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS) fun backups(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Local)
@JvmStatic @JvmStatic
fun help(context: Context, startCategoryIndex: Int = 0): Intent { 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) .putExtra(HelpFragment.START_CATEGORY_INDEX, startCategoryIndex)
} }
@JvmStatic @JvmStatic
fun proxy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.PROXY) fun proxy(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.DataAndStorageRoute.Proxy)
@JvmStatic @JvmStatic
fun notifications(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATIONS) fun notifications(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.Notifications)
@JvmStatic @JvmStatic
fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHANGE_NUMBER) fun changeNumber(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.ChangeNumberRoute.Start)
@JvmStatic @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 @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 @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 @JvmStatic
fun notificationProfiles(context: Context): Intent = getIntentForStartLocation(context, StartLocation.NOTIFICATION_PROFILES) fun notificationProfiles(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.NotificationProfiles)
@JvmStatic @JvmStatic
fun createNotificationProfile(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CREATE_NOTIFICATION_PROFILE) fun createNotificationProfile(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.EditProfile())
@JvmStatic @JvmStatic
fun privacy(context: Context): Intent = getIntentForStartLocation(context, StartLocation.PRIVACY) fun privacy(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.PrivacyRoute.Privacy)
@JvmStatic @JvmStatic
fun notificationProfileDetails(context: Context, profileId: Long): Intent { fun notificationProfileDetails(context: Context, profileId: Long): Intent {
val arguments = EditNotificationProfileScheduleFragmentArgs.Builder(profileId, false) return getIntentForStartLocation(context, AppSettingsRoute.NotificationsRoute.ProfileDetails(profileId = profileId))
.build()
.toBundle()
return getIntentForStartLocation(context, StartLocation.NOTIFICATION_PROFILE_DETAILS)
.putExtra(START_ARGUMENTS, arguments)
} }
@JvmStatic @JvmStatic
fun linkedDevices(context: Context): Intent = getIntentForStartLocation(context, StartLocation.LINKED_DEVICES) fun linkedDevices(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.LinkDeviceRoute.LinkDevice)
@JvmStatic @JvmStatic
fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.USERNAME_LINK) fun usernameLinkSettings(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.UsernameLinkRoute.UsernameLink)
@JvmStatic @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 @JvmStatic
fun remoteBackups(context: Context): Intent = getIntentForStartLocation(context, StartLocation.REMOTE_BACKUPS) fun remoteBackups(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Remote())
@JvmStatic @JvmStatic
fun chatFolders(context: Context): Intent = getIntentForStartLocation(context, StartLocation.CHAT_FOLDERS) fun chatFolders(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.ChatFoldersRoute.ChatFolders)
@JvmStatic @JvmStatic
fun createChatFolder(context: Context, id: Long = -1, threadIds: LongArray?): Intent { fun createChatFolder(context: Context, id: Long = -1, threadIds: LongArray?): Intent {
val arguments = CreateFoldersFragmentArgs.Builder(id, threadIds ?: longArrayOf()) return getIntentForStartLocation(
.build() context,
.toBundle() AppSettingsRoute.ChatFoldersRoute.CreateChatFolders(
folderId = id,
return getIntentForStartLocation(context, StartLocation.CREATE_CHAT_FOLDER).putExtra(START_ARGUMENTS, arguments) threadIds = threadIds ?: longArrayOf()
)
)
} }
@JvmStatic @JvmStatic
fun backupsSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS_SETTINGS) fun backupsSettings(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Backups)
@JvmStatic @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) return Intent(context, AppSettingsActivity::class.java)
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number) .putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number)
.putExtra(START_LOCATION, startLocation.code) .putExtra(START_ROUTE, startRoute)
}
}
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
}
} }
} }
} }

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.settings.app
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.annotation.IdRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow 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.banner.ui.compose.Importance
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
import org.thoughtcrime.securesms.components.emoji.Emojifier 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.BadgeImageMedium
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.completed.InAppPaymentsBottomSheetDelegate 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 { class AppSettingsFragment : ComposeFragment(), Callbacks {
private val viewModel: AppSettingsViewModel by viewModels() private val viewModel: AppSettingsViewModel by viewModels()
private val appSettingsRouter by viewModels<AppSettingsRouter>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner)) 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 @Composable
@@ -118,12 +149,8 @@ class AppSettingsFragment : ComposeFragment(), Callbacks {
requireActivity().finishAfterTransition() requireActivity().finishAfterTransition()
} }
override fun navigate(actionId: Int) { override fun navigate(route: AppSettingsRoute) {
findNavController().safeNavigate(actionId) appSettingsRouter.navigateTo(route)
}
override fun navigate(directions: NavDirections) {
findNavController().safeNavigate(directions)
} }
override fun onResume() { override fun onResume() {
@@ -203,7 +230,7 @@ private fun AppSettingsContent(
BackupsWarningRow( BackupsWarningRow(
text = stringResource(R.string.AppSettingsFragment__renew_your_signal_backups_subscription), text = stringResource(R.string.AppSettingsFragment__renew_your_signal_backups_subscription),
onClick = { 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), text = stringResource(R.string.AppSettingsFragment__couldnt_complete_backup),
onClick = { onClick = {
BackupRepository.markBackupFailedIndicatorClicked() 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), text = stringResource(R.string.AppSettingsFragment__couldnt_redeem_your_backups_subscription),
onClick = { onClick = {
BackupRepository.markBackupAlreadyRedeemedIndicatorClicked() 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), icon = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24),
iconTint = MaterialTheme.colorScheme.error, iconTint = MaterialTheme.colorScheme.error,
onClick = { 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), text = stringResource(R.string.AccountSettingsFragment__account),
icon = painterResource(R.drawable.symbol_person_circle_24), icon = painterResource(R.drawable.symbol_person_circle_24),
onClick = { 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), text = stringResource(R.string.preferences__linked_devices),
icon = painterResource(R.drawable.symbol_devices_24), icon = painterResource(R.drawable.symbol_devices_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_linkDeviceFragment) callbacks.navigate(AppSettingsRoute.LinkDeviceRoute.LinkDevice)
}, },
enabled = isRegisteredAndUpToDate enabled = isRegisteredAndUpToDate
) )
@@ -312,7 +339,7 @@ private fun AppSettingsContent(
}, },
onClick = { onClick = {
if (state.allowUserToGoToDonationManagementScreen) { if (state.allowUserToGoToDonationManagementScreen) {
callbacks.navigate(R.id.action_appSettingsFragment_to_manageDonationsFragment) callbacks.navigate(AppSettingsRoute.DonationsRoute.Donations())
} else { } else {
CommunicationActions.openBrowserLink(context, donateUrl) CommunicationActions.openBrowserLink(context, donateUrl)
} }
@@ -332,7 +359,7 @@ private fun AppSettingsContent(
text = stringResource(R.string.preferences__appearance), text = stringResource(R.string.preferences__appearance),
icon = painterResource(R.drawable.symbol_appearance_24), icon = painterResource(R.drawable.symbol_appearance_24),
onClick = { 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), text = stringResource(R.string.preferences_chats__chats),
icon = painterResource(R.drawable.symbol_chat_24), icon = painterResource(R.drawable.symbol_chat_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_chatsSettingsFragment) callbacks.navigate(AppSettingsRoute.ChatsRoute.Chats)
}, },
enabled = state.legacyLocalBackupsEnabled || isRegisteredAndUpToDate enabled = state.legacyLocalBackupsEnabled || isRegisteredAndUpToDate
) )
@@ -353,7 +380,7 @@ private fun AppSettingsContent(
text = stringResource(R.string.preferences__stories), text = stringResource(R.string.preferences__stories),
icon = painterResource(R.drawable.symbol_stories_24), icon = painterResource(R.drawable.symbol_stories_24),
onClick = { onClick = {
callbacks.navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToStoryPrivacySettings(R.string.preferences__stories)) callbacks.navigate(AppSettingsRoute.StoriesRoute.Privacy(titleId = R.string.preferences__stories))
}, },
enabled = isRegisteredAndUpToDate enabled = isRegisteredAndUpToDate
) )
@@ -364,7 +391,7 @@ private fun AppSettingsContent(
text = stringResource(R.string.preferences__notifications), text = stringResource(R.string.preferences__notifications),
icon = painterResource(R.drawable.symbol_bell_24), icon = painterResource(R.drawable.symbol_bell_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_notificationsSettingsFragment) callbacks.navigate(AppSettingsRoute.NotificationsRoute.Notifications)
}, },
enabled = isRegisteredAndUpToDate enabled = isRegisteredAndUpToDate
) )
@@ -375,7 +402,7 @@ private fun AppSettingsContent(
text = stringResource(R.string.preferences__privacy), text = stringResource(R.string.preferences__privacy),
icon = painterResource(R.drawable.symbol_lock_24), icon = painterResource(R.drawable.symbol_lock_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_privacySettingsFragment) callbacks.navigate(AppSettingsRoute.PrivacyRoute.Privacy)
}, },
enabled = isRegisteredAndUpToDate enabled = isRegisteredAndUpToDate
) )
@@ -398,7 +425,7 @@ private fun AppSettingsContent(
) )
}, },
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment) callbacks.navigate(AppSettingsRoute.BackupsRoute.Backups)
}, },
onLongClick = { onLongClick = {
callbacks.copyRemoteBackupsSubscriberIdToClipboard() callbacks.copyRemoteBackupsSubscriberIdToClipboard()
@@ -413,7 +440,7 @@ private fun AppSettingsContent(
text = stringResource(R.string.preferences__data_and_storage), text = stringResource(R.string.preferences__data_and_storage),
icon = painterResource(R.drawable.symbol_data_24), icon = painterResource(R.drawable.symbol_data_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_dataAndStorageSettingsFragment) callbacks.navigate(AppSettingsRoute.DataAndStorageRoute.DataAndStorage)
} }
) )
} }
@@ -424,7 +451,7 @@ private fun AppSettingsContent(
text = "App updates", text = "App updates",
icon = painterResource(R.drawable.symbol_calendar_24), icon = painterResource(R.drawable.symbol_calendar_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_appUpdatesSettingsFragment) callbacks.navigate(AppSettingsRoute.AppUpdates)
} }
) )
} }
@@ -467,7 +494,7 @@ private fun AppSettingsContent(
) )
}, },
onClick = { 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), text = stringResource(R.string.preferences__help),
icon = painterResource(R.drawable.symbol_help_24), icon = painterResource(R.drawable.symbol_help_24),
onClick = { 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), text = stringResource(R.string.AppSettingsFragment__invite_your_friends),
icon = painterResource(R.drawable.symbol_invite_24), icon = painterResource(R.drawable.symbol_invite_24),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_inviteFragment) callbacks.navigate(AppSettingsRoute.Invite)
} }
) )
} }
@@ -506,7 +533,7 @@ private fun AppSettingsContent(
Rows.TextRow( Rows.TextRow(
text = stringResource(R.string.preferences__internal_preferences), text = stringResource(R.string.preferences__internal_preferences),
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_internalSettingsFragment) callbacks.navigate(AppSettingsRoute.InternalRoute.Internal)
} }
) )
} }
@@ -558,7 +585,7 @@ private fun BioRow(
modifier = Modifier modifier = Modifier
.clickable( .clickable(
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_manageProfileActivity) callbacks.navigate(AppSettingsRoute.AccountRoute.ManageProfile)
} }
) )
.horizontalGutters() .horizontalGutters()
@@ -632,7 +659,7 @@ private fun BioRow(
if (hasUsername) { if (hasUsername) {
IconButtons.IconButton( IconButtons.IconButton(
onClick = { onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_usernameLinkSettingsFragment) callbacks.navigate(AppSettingsRoute.UsernameLinkRoute.UsernameLink)
}, },
size = 36.dp, size = 36.dp,
colors = IconButtons.iconButtonColors( colors = IconButtons.iconButtonColors(
@@ -711,8 +738,7 @@ private fun BioRowPreview() {
private interface Callbacks { private interface Callbacks {
fun onNavigationClick(): Unit = error("Not implemented.") fun onNavigationClick(): Unit = error("Not implemented.")
fun navigate(@IdRes actionId: Int): Unit = error("Not implemented") fun navigate(route: AppSettingsRoute): Unit = error("Not implemented")
fun navigate(directions: NavDirections): Unit = error("Not implemented")
fun copyDonorBadgeSubscriberIdToClipboard(): Unit = error("Not implemented") fun copyDonorBadgeSubscriberIdToClipboard(): Unit = error("Not implemented")
fun copyRemoteBackupsSubscriberIdToClipboard(): 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)
}
}
}