mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:05:28 +00:00
Use only ClipboardHelper to copy to the clipboard, and do not render feedback on Android 13+
Since Android 13, the system displays its own confirmation when content is copied to the clipboard, so the application does not render its own Toast or Snackbar anymore on those versions. Context.copyToClipboard, CopyToClipboardUseCase and the direct ClipboardManager usage in EventDebugInfoView are removed. ClipboardHelper.copyPlainText(text, isSensitive, onSuccessOnOldDevice) is now the only entry point, and AndroidClipboardHelper is the only class using ClipboardManager. Copy actions from the views are moved to their Presenter through new events, or to the Node when there is no Presenter. A ToastHelper is introduced so that Toasts are kept where they were used before. Snackbars are kept where they were already used. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
@@ -11,6 +11,7 @@ package io.element.android.features.securebackup.impl.setup
|
||||
sealed interface SecureBackupSetupEvent {
|
||||
data object CreateRecoveryKey : SecureBackupSetupEvent
|
||||
data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvent
|
||||
data object CopyRecoveryKey : SecureBackupSetupEvent
|
||||
data object Done : SecureBackupSetupEvent
|
||||
data object DismissDialog : SecureBackupSetupEvent
|
||||
}
|
||||
|
||||
+12
@@ -22,9 +22,12 @@ import com.freeletics.flowredux.compose.rememberStateAndDispatch
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.loggerTagSetup
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
@@ -38,6 +41,8 @@ class SecureBackupSetupPresenter(
|
||||
@Assisted private val isChangeRecoveryKeyUserStory: Boolean,
|
||||
private val stateMachine: SecureBackupSetupStateMachine,
|
||||
private val encryptionService: EncryptionService,
|
||||
private val clipboardHelper: ClipboardHelper,
|
||||
private val toastHelper: ToastHelper,
|
||||
) : Presenter<SecureBackupSetupState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
@@ -60,6 +65,13 @@ class SecureBackupSetupPresenter(
|
||||
}
|
||||
SecureBackupSetupEvent.RecoveryKeyHasBeenSaved ->
|
||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey)
|
||||
SecureBackupSetupEvent.CopyRecoveryKey -> {
|
||||
val recoveryKey = setupState.recoveryKey() ?: return
|
||||
clipboardHelper.copyPlainText(recoveryKey, isSensitive = true) {
|
||||
toastHelper.show(R.string.screen_recovery_key_copied_to_clipboard)
|
||||
}
|
||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey)
|
||||
}
|
||||
SecureBackupSetupEvent.DismissDialog -> {
|
||||
showSaveConfirmationDialog = false
|
||||
stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError)
|
||||
|
||||
+1
-9
@@ -20,7 +20,6 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView
|
||||
import io.element.android.libraries.androidutils.system.copyToClipboard
|
||||
import io.element.android.libraries.androidutils.system.startSharePlainTextIntent
|
||||
import io.element.android.libraries.designsystem.atomic.pages.FlowStepPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
@@ -114,17 +113,10 @@ private fun subtitle(state: SecureBackupSetupState): String {
|
||||
private fun Content(
|
||||
state: SecureBackupSetupState,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val formattedRecoveryKey = state.recoveryKeyViewState.formattedRecoveryKey
|
||||
val toastMessage = stringResource(R.string.screen_recovery_key_copied_to_clipboard)
|
||||
val clickLambda = if (formattedRecoveryKey != null) {
|
||||
{
|
||||
context.copyToClipboard(
|
||||
text = formattedRecoveryKey,
|
||||
toastMessage = toastMessage,
|
||||
isSensitive = true,
|
||||
)
|
||||
state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved)
|
||||
state.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||
}
|
||||
} else {
|
||||
if (!state.recoveryKeyViewState.inProgress) {
|
||||
|
||||
+4
@@ -15,6 +15,8 @@ import io.element.android.features.securebackup.api.SecureBackupSetupEntryPoint
|
||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupNode
|
||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupPresenter
|
||||
import io.element.android.features.securebackup.impl.setup.SecureBackupSetupStateMachine
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.FakeToastHelper
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
import io.element.android.tests.testutils.node.TestParentNode
|
||||
@@ -40,6 +42,8 @@ class DefaultSecureBackupSetupEntryPointTest {
|
||||
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
||||
stateMachine = SecureBackupSetupStateMachine(),
|
||||
encryptionService = FakeEncryptionService(),
|
||||
clipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper = FakeToastHelper(),
|
||||
)
|
||||
},
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
|
||||
+59
@@ -12,8 +12,13 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.securebackup.impl.R
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUserStory
|
||||
import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState
|
||||
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
|
||||
import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper
|
||||
import io.element.android.libraries.androidutils.toast.FakeToastHelper
|
||||
import io.element.android.libraries.androidutils.toast.ToastHelper
|
||||
import io.element.android.libraries.matrix.api.encryption.EnableRecoveryProgress
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.test.A_RECOVERY_KEY
|
||||
@@ -186,16 +191,70 @@ class SecureBackupSetupPresenterTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy recovery key on old device shows a toast`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = true)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createSecureBackupSetupPresenter(
|
||||
encryptionService = encryptionService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey)
|
||||
skipItems(1)
|
||||
encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY))
|
||||
val createdState = awaitItem()
|
||||
createdState.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(A_RECOVERY_KEY)
|
||||
assertThat(clipboardHelper.isSensitive).isTrue()
|
||||
assertThat(toastHelper.shownToasts).containsExactly(R.string.screen_recovery_key_copied_to_clipboard)
|
||||
assertThat(awaitItem().setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - copy recovery key on new device does not show a toast`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val clipboardHelper = FakeClipboardHelper(isOldDevice = false)
|
||||
val toastHelper = FakeToastHelper()
|
||||
val presenter = createSecureBackupSetupPresenter(
|
||||
encryptionService = encryptionService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem().eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey)
|
||||
skipItems(1)
|
||||
encryptionService.emitEnableRecoveryProgress(EnableRecoveryProgress.Done(A_RECOVERY_KEY))
|
||||
val createdState = awaitItem()
|
||||
createdState.eventSink.invoke(SecureBackupSetupEvent.CopyRecoveryKey)
|
||||
assertThat(clipboardHelper.clipboardContents).isEqualTo(A_RECOVERY_KEY)
|
||||
assertThat(clipboardHelper.isSensitive).isTrue()
|
||||
assertThat(toastHelper.shownToasts).isEmpty()
|
||||
assertThat(awaitItem().setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSecureBackupSetupPresenter(
|
||||
isChangeRecoveryKeyUserStory: Boolean = false,
|
||||
encryptionService: EncryptionService = FakeEncryptionService(
|
||||
enableRecoveryLambda = { _, _ -> Result.success("") },
|
||||
),
|
||||
clipboardHelper: ClipboardHelper = FakeClipboardHelper(),
|
||||
toastHelper: ToastHelper = FakeToastHelper(),
|
||||
): SecureBackupSetupPresenter {
|
||||
return SecureBackupSetupPresenter(
|
||||
isChangeRecoveryKeyUserStory = isChangeRecoveryKeyUserStory,
|
||||
stateMachine = SecureBackupSetupStateMachine(),
|
||||
encryptionService = encryptionService,
|
||||
clipboardHelper = clipboardHelper,
|
||||
toastHelper = toastHelper,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user