diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeFeature.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeFeature.kt new file mode 100644 index 0000000000..da608ad624 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeFeature.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp + +/** + * Represents a "Feature" included for a specify tier of message backups + */ +data class MessageBackupsTypeFeature( + val iconResourceId: Int, + val label: String +) + +/** + * Renders a "feature row" for a given feature. + */ +@Composable +fun MessageBackupsTypeFeatureRow( + messageBackupsTypeFeature: MessageBackupsTypeFeature, + iconTint: Color = LocalContentColor.current, + modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(id = messageBackupsTypeFeature.iconResourceId), + contentDescription = null, + tint = iconTint, + modifier = Modifier.padding(end = 8.dp) + ) + + Text( + text = messageBackupsTypeFeature.label, + style = MaterialTheme.typography.bodyLarge + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeSelectionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeSelectionScreen.kt index 7f91bed99c..e69242525f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeSelectionScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/MessageBackupsTypeSelectionScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,7 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -271,32 +269,8 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String { } } -@Composable -private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - painter = painterResource(id = messageBackupsTypeFeature.iconResourceId), - contentDescription = null, - modifier = Modifier.padding(end = 8.dp) - ) - - Text( - text = messageBackupsTypeFeature.label, - style = MaterialTheme.typography.bodyLarge - ) - } -} - data class MessageBackupsType( val pricePerMonth: FiatMoney, val title: String, val features: ImmutableList ) - -data class MessageBackupsTypeFeature( - val iconResourceId: Int, - val label: String -) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragment.kt new file mode 100644 index 0000000000..c44e63e45e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/restore/RestoreFromBackupFragment.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.ui.restore + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.signal.core.ui.Buttons +import org.signal.core.ui.Previews +import org.signal.core.ui.theme.SignalTheme +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeature +import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeatureRow +import org.thoughtcrime.securesms.compose.ComposeFragment +import org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Fragment which facilitates restoring from a backup during + * registration. + */ +class RestoreFromBackupFragment : ComposeFragment() { + + private val navArgs: RestoreFromBackupFragmentArgs by navArgs() + + @Composable + override fun FragmentContent() { + RestoreFromBackupContent( + features = persistentListOf(), + onRestoreBackupClick = { + // TODO [message-backups] Restore backup. + }, + onCancelClick = { + findNavController() + .popBackStack() + }, + onMoreOptionsClick = { + findNavController() + .safeNavigate(RestoreFromBackupFragmentDirections.actionRestoreFromBacakupFragmentToMoreOptions(MoreTransferOrRestoreOptionsMode.SELECTION)) + }, + cancelable = navArgs.cancelable + ) + } +} + +@Preview +@Composable +private fun RestoreFromBackupContentPreview() { + Previews.Preview { + RestoreFromBackupContent( + features = persistentListOf( + MessageBackupsTypeFeature( + iconResourceId = R.drawable.symbol_thread_compact_bold_16, + label = "Your last 30 days of media" + ), + MessageBackupsTypeFeature( + iconResourceId = R.drawable.symbol_recent_compact_bold_16, + label = "All of your text messages" + ) + ), + onRestoreBackupClick = {}, + onCancelClick = {}, + onMoreOptionsClick = {}, + true + ) + } +} + +@Composable +private fun RestoreFromBackupContent( + features: ImmutableList, + onRestoreBackupClick: () -> Unit, + onCancelClick: () -> Unit, + onMoreOptionsClick: () -> Unit, + cancelable: Boolean +) { + Column( + modifier = Modifier + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + .padding(top = 40.dp, bottom = 24.dp) + ) { + Text( + text = "Restore from backup", // TODO [message-backups] Finalized copy. + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 12.dp) + ) + + val yourLastBackupText = buildAnnotatedString { + append("Your last backup was made on March 5, 2024 at 9:00am.") // TODO [message-backups] Finalized copy. + append(" ") + withStyle(SpanStyle(fontWeight = FontWeight.SemiBold)) { + append("Only media sent or received in the past 30 days is included.") // TODO [message-backups] Finalized copy. + } + } + + Text( + text = yourLastBackupText, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(bottom = 28.dp) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(18.dp)) + .padding(horizontal = 20.dp) + .padding(top = 20.dp, bottom = 18.dp) + ) { + Text( + text = "Your backup includes:", // TODO [message-backups] Finalized copy. + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 6.dp) + ) + + features.forEach { + MessageBackupsTypeFeatureRow( + messageBackupsTypeFeature = it, + iconTint = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 16.dp, top = 6.dp) + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Buttons.LargeTonal( + onClick = onRestoreBackupClick, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Restore backup" // TODO [message-backups] Finalized copy. + ) + } + + if (cancelable) { + TextButton( + onClick = onCancelClick, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = android.R.string.cancel) + ) + } + } else { + TextButton( + onClick = onMoreOptionsClick, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.TransferOrRestoreFragment__more_options) + ) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsMode.kt b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsMode.kt new file mode 100644 index 0000000000..4619dd41b7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsMode.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.devicetransfer.moreoptions + +/** + * Allows component opening sheet to specify mode + */ +enum class MoreTransferOrRestoreOptionsMode { + /** + * Only display the option to log in without transferring. Selection + * will be disabled. + */ + SKIP_ONLY, + + /** + * Display transfer/restore local/skip as well as a next and cancel button + */ + SELECTION +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsSheet.kt new file mode 100644 index 0000000000..d04ca5c2cc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/moreoptions/MoreTransferOrRestoreOptionsSheet.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.devicetransfer.moreoptions + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import org.signal.core.ui.BottomSheets +import org.signal.core.ui.Buttons +import org.signal.core.ui.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment +import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType + +/** + * Lists a set of options the user can choose from for restoring backup or skipping restoration + */ +class MoreTransferOrRestoreOptionsSheet : ComposeBottomSheetDialogFragment() { + + private val args by navArgs() + + @Composable + override fun SheetContent() { + var selectedOption by remember { + mutableStateOf(null) + } + + MoreOptionsSheetContent( + mode = args.mode, + selectedOption = selectedOption, + onOptionSelected = { selectedOption = it }, + onCancelClick = { findNavController().popBackStack() }, + onNextClick = { + this.onNextClicked(selectedOption ?: BackupRestorationType.NONE) + } + ) + } + + private fun onNextClicked(selectedOption: BackupRestorationType) { + // TODO [message-requests] -- Launch next screen based off user choice + } +} + +@Preview +@Composable +private fun MoreOptionsSheetContentPreview() { + Previews.BottomSheetPreview { + MoreOptionsSheetContent( + mode = MoreTransferOrRestoreOptionsMode.SKIP_ONLY, + selectedOption = null, + onOptionSelected = {}, + onCancelClick = {}, + onNextClick = {} + ) + } +} + +@Preview +@Composable +private fun MoreOptionsSheetSelectableContentPreview() { + Previews.BottomSheetPreview { + MoreOptionsSheetContent( + mode = MoreTransferOrRestoreOptionsMode.SELECTION, + selectedOption = null, + onOptionSelected = {}, + onCancelClick = {}, + onNextClick = {} + ) + } +} + +@Composable +private fun MoreOptionsSheetContent( + mode: MoreTransferOrRestoreOptionsMode, + selectedOption: BackupRestorationType?, + onOptionSelected: (BackupRestorationType) -> Unit, + onCancelClick: () -> Unit, + onNextClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter)) + ) { + BottomSheets.Handle() + + Spacer(modifier = Modifier.size(42.dp)) + + if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) { + TransferFromAndroidDeviceOption( + selectedOption = selectedOption, + onOptionSelected = onOptionSelected + ) + Spacer(modifier = Modifier.size(16.dp)) + RestoreLocalBackupOption( + selectedOption = selectedOption, + onOptionSelected = onOptionSelected + ) + Spacer(modifier = Modifier.size(16.dp)) + } + + LogInWithoutTransferringOption( + selectedOption = selectedOption, + onOptionSelected = when (mode) { + MoreTransferOrRestoreOptionsMode.SKIP_ONLY -> { _ -> onNextClick() } + MoreTransferOrRestoreOptionsMode.SELECTION -> onOptionSelected + } + ) + + if (mode == MoreTransferOrRestoreOptionsMode.SELECTION) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp, bottom = 24.dp) + ) { + TextButton( + onClick = onCancelClick + ) { + Text(text = stringResource(id = android.R.string.cancel)) + } + + Spacer(modifier = Modifier.weight(1f)) + + Buttons.LargeTonal( + enabled = selectedOption != null, + onClick = onNextClick + ) { + Text(text = stringResource(id = R.string.RegistrationActivity_next)) + } + } + } else { + Spacer(modifier = Modifier.size(45.dp)) + } + } +} + +@Preview +@Composable +private fun LogInWithoutTransferringOptionPreview() { + Previews.BottomSheetPreview { + LogInWithoutTransferringOption( + selectedOption = null, + onOptionSelected = {} + ) + } +} + +@Composable +private fun LogInWithoutTransferringOption( + selectedOption: BackupRestorationType?, + onOptionSelected: (BackupRestorationType) -> Unit +) { + Option( + icon = { + Box( + modifier = Modifier.padding(horizontal = 18.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset. + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(36.dp) + ) + } + }, + isSelected = selectedOption == BackupRestorationType.NONE, + title = "Log in without transferring", // TODO [message-backups] Finalized copy. + subtitle = "Continue without transferring your messages and media", // TODO [message-backups] Finalized copy. + onClick = { onOptionSelected(BackupRestorationType.NONE) } + ) +} + +@Preview +@Composable +private fun TransferFromAndroidDeviceOptionPreview() { + Previews.BottomSheetPreview { + TransferFromAndroidDeviceOption( + selectedOption = null, + onOptionSelected = {} + ) + } +} + +@Composable +private fun TransferFromAndroidDeviceOption( + selectedOption: BackupRestorationType?, + onOptionSelected: (BackupRestorationType) -> Unit +) { + Option( + icon = { + Box( + modifier = Modifier.padding(horizontal = 18.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset. + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(36.dp) + ) + } + }, + isSelected = selectedOption == BackupRestorationType.DEVICE_TRANSFER, + title = "Transfer from Android device", // TODO [message-backups] Finalized copy. + subtitle = "Transfer your account and messages from your old device.", // TODO [message-backups] Finalized copy. + onClick = { onOptionSelected(BackupRestorationType.DEVICE_TRANSFER) } + ) +} + +@Preview +@Composable +private fun RestoreLocalBackupOptionPreview() { + Previews.BottomSheetPreview { + RestoreLocalBackupOption( + selectedOption = null, + onOptionSelected = {} + ) + } +} + +@Composable +private fun RestoreLocalBackupOption( + selectedOption: BackupRestorationType?, + onOptionSelected: (BackupRestorationType) -> Unit +) { + Option( + icon = { + Box( + modifier = Modifier.padding(horizontal = 18.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset. + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(36.dp) + ) + } + }, + isSelected = selectedOption == BackupRestorationType.LOCAL_BACKUP, + title = "Restore local backup", // TODO [message-backups] Finalized copy. + subtitle = "Restore your messages from a backup file you saved on your device.", // TODO [message-backups] Finalized copy. + onClick = { onOptionSelected(BackupRestorationType.LOCAL_BACKUP) } + ) +} + +@Preview +@Composable +private fun OptionPreview() { + Previews.BottomSheetPreview { + Option( + icon = { + Box( + modifier = Modifier.padding(horizontal = 18.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] Finalized asset. + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(36.dp) + ) + } + }, + isSelected = false, + title = "Log in without transferring", // TODO [message-backups] Finalized copy. + subtitle = "Continue without transferring your messages and media", // TODO [message-backups] Finalized copy. + onClick = {} + ) + } +} + +@Composable +private fun Option( + icon: @Composable () -> Unit, + isSelected: Boolean, + title: String, + subtitle: String, + onClick: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(12.dp) + ) + .border( + width = if (isSelected) 2.dp else 0.dp, + color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent + ) + .clip(RoundedCornerShape(12.dp)) + .clickable { onClick() } + .padding(vertical = 21.dp) + ) { + icon() + Column { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/BackupRestorationType.kt b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/BackupRestorationType.kt new file mode 100644 index 0000000000..ea34c70d1a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/BackupRestorationType.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.devicetransfer.newdevice + +/** + * What kind of backup restore the user wishes to perform. + */ +enum class BackupRestorationType { + DEVICE_TRANSFER, + LOCAL_BACKUP, + REMOTE_BACKUP, + NONE +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java index e29b7b8673..257990f890 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreFragment.java @@ -12,6 +12,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreBinding; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.navigation.SafeNavigation; @@ -36,7 +37,13 @@ public final class TransferOrRestoreFragment extends LoggingFragment { binding.transferOrRestoreFragmentTransfer.setOnClickListener(v -> viewModel.onTransferFromAndroidDeviceSelected()); binding.transferOrRestoreFragmentRestore.setOnClickListener(v -> viewModel.onRestoreFromLocalBackupSelected()); + binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener(v -> viewModel.onRestoreFromRemoteBackupSelected()); binding.transferOrRestoreFragmentNext.setOnClickListener(v -> launchSelection(viewModel.getStateSnapshot())); + binding.transferOrRestoreFragmentMoreOptions.setOnClickListener(v -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_transferOrRestore_to_moreOptions)); + + int visibility = FeatureFlags.messageBackups() ? View.VISIBLE : View.GONE; + binding.transferOrRestoreFragmentRestoreRemoteCard.setVisibility(visibility); + binding.transferOrRestoreFragmentMoreOptions.setVisibility(visibility); String description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device); String toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device); @@ -47,15 +54,18 @@ public final class TransferOrRestoreFragment extends LoggingFragment { lifecycleDisposable.add(viewModel.getState().subscribe(this::updateSelection)); } - private void updateSelection(TransferOrRestoreViewModel.RestorationType restorationType) { - binding.transferOrRestoreFragmentTransferCard.setSelected(restorationType == TransferOrRestoreViewModel.RestorationType.DEVICE_TRANSFER); - binding.transferOrRestoreFragmentRestoreCard.setSelected(restorationType == TransferOrRestoreViewModel.RestorationType.LOCAL_BACKUP); + private void updateSelection(BackupRestorationType restorationType) { + binding.transferOrRestoreFragmentTransferCard.setSelected(restorationType == BackupRestorationType.DEVICE_TRANSFER); + binding.transferOrRestoreFragmentRestoreCard.setSelected(restorationType == BackupRestorationType.LOCAL_BACKUP); + binding.transferOrRestoreFragmentRestoreRemoteCard.setSelected(restorationType == BackupRestorationType.REMOTE_BACKUP); } - private void launchSelection(TransferOrRestoreViewModel.RestorationType restorationType) { + private void launchSelection(BackupRestorationType restorationType) { switch (restorationType) { case DEVICE_TRANSFER -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_new_device_transfer_instructions); case LOCAL_BACKUP -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_choose_backup); + case REMOTE_BACKUP -> {} + default -> throw new IllegalArgumentException(); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreViewModel.kt index e6efdddbe9..7848ff2304 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/devicetransfer/newdevice/TransferOrRestoreViewModel.kt @@ -15,21 +15,20 @@ import io.reactivex.rxjava3.processors.BehaviorProcessor */ class TransferOrRestoreViewModel : ViewModel() { - private val internalState = BehaviorProcessor.createDefault(RestorationType.DEVICE_TRANSFER) + private val internalState = BehaviorProcessor.createDefault(BackupRestorationType.DEVICE_TRANSFER) - val state: Flowable = internalState.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()) - val stateSnapshot: RestorationType get() = internalState.value!! + val state: Flowable = internalState.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()) + val stateSnapshot: BackupRestorationType get() = internalState.value!! fun onTransferFromAndroidDeviceSelected() { - internalState.onNext(RestorationType.DEVICE_TRANSFER) + internalState.onNext(BackupRestorationType.DEVICE_TRANSFER) } fun onRestoreFromLocalBackupSelected() { - internalState.onNext(RestorationType.LOCAL_BACKUP) + internalState.onNext(BackupRestorationType.LOCAL_BACKUP) } - enum class RestorationType { - DEVICE_TRANSFER, - LOCAL_BACKUP + fun onRestoreFromRemoteBackupSelected() { + internalState.onNext(BackupRestorationType.REMOTE_BACKUP) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index b5ae59104d..3d01d1637b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -126,6 +126,7 @@ public final class FeatureFlags { private static final String CDSI_LIBSIGNAL_NET = "android.cds.libsignal.2"; private static final String RX_MESSAGE_SEND = "android.rxMessageSend"; private static final String LINKED_DEVICE_LIFESPAN_SECONDS = "android.linkedDeviceLifespanSeconds"; + private static final String MESSAGE_BACKUPS = "android.messageBackups"; /** * We will only store remote values for flags in this set. If you want a flag to be controllable @@ -208,7 +209,7 @@ public final class FeatureFlags { ); @VisibleForTesting - static final Set NOT_REMOTE_CAPABLE = SetUtil.newHashSet(); + static final Set NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS); /** * Values in this map will take precedence over any value. This should only be used for local @@ -731,6 +732,14 @@ public final class FeatureFlags { return TimeUnit.SECONDS.toMillis(seconds); } + /** + * Enable Message Backups UI + * Note: This feature is in active development and is not intended to currently function. + */ + public static boolean messageBackups() { + return getBoolean(MESSAGE_BACKUPS, false); + } + /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/layout/fragment_transfer_restore.xml b/app/src/main/res/layout/fragment_transfer_restore.xml index 9aea5a37eb..efee469134 100644 --- a/app/src/main/res/layout/fragment_transfer_restore.xml +++ b/app/src/main/res/layout/fragment_transfer_restore.xml @@ -92,6 +92,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + You need access to your old device. Restore from backup Restore your messages from a local backup. If you don’t restore now, you won\'t be able to restore later. + + More options Open Signal on your old Android phone diff --git a/core-ui/src/main/java/org/signal/core/ui/Previews.kt b/core-ui/src/main/java/org/signal/core/ui/Previews.kt index 7394b3528c..8adaa4cf0a 100644 --- a/core-ui/src/main/java/org/signal/core/ui/Previews.kt +++ b/core-ui/src/main/java/org/signal/core/ui/Previews.kt @@ -5,8 +5,11 @@ package org.signal.core.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import org.signal.core.ui.theme.SignalTheme object Previews { @@ -20,4 +23,17 @@ object Previews { } } } + + @Composable + fun BottomSheetPreview( + content: @Composable () -> Unit + ) { + SignalTheme { + Surface { + Box(modifier = Modifier.background(color = SignalTheme.colors.colorSurface1)) { + content() + } + } + } + } }