diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt index 76674cd87d..fa4f693cd3 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt @@ -17,8 +17,6 @@ package io.element.android.features.securebackup.impl.reset import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId @@ -28,11 +26,8 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -76,4 +71,9 @@ class ResetIdentityFlowManager @Inject constructor( resetHandleFlow } } + + suspend fun cancel() { + currentHandleFlow.value.dataOrNull()?.cancel() + resetHandleFlow.value = AsyncData.Uninitialized + } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt index 11dfb71c22..e144a52432 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt @@ -19,8 +19,12 @@ package io.element.android.features.securebackup.impl.reset import android.app.Activity import android.os.Parcelable import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -37,12 +41,14 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.oidc.api.OidcEntryPoint import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -76,6 +82,7 @@ class ResetIdentityFlowNode @AssistedInject constructor( } private lateinit var activity: Activity + private var resetJob: Job? = null override fun onBuilt() { super.onBuilt() @@ -83,6 +90,19 @@ class ResetIdentityFlowNode @AssistedInject constructor( resetIdentityFlowManager.whenResetIsDone { plugins().forEach { it.onDone() } } + + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + // If the custom tab was opened, we need to cancel the reset job + // when we come back to the node if it the reset wasn't successful + cancelResetJob() + } + + override fun onDestroy(owner: LifecycleOwner) { + // Make sure we cancel the reset job when the node is destroyed, just in case + cancelResetJob() + } + }) } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -121,15 +141,29 @@ class ResetIdentityFlowNode @AssistedInject constructor( } else { backstack.push(NavTarget.ResetOidc(handle.url)) } - handle.resetOidc() + resetJob = launch { handle.resetOidc() } } is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) } } + private fun cancelResetJob() { + resetJob?.cancel() + resetJob = null + coroutineScope.launch { resetIdentityFlowManager.cancel() } + } + @Composable override fun View(modifier: Modifier) { - (LocalContext.current as? Activity)?.let { activity = it } + // Workaround to get the current activity + if (!this::activity.isInitialized) { + activity = LocalContext.current as Activity + } + + val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState() + if (startResetState.isLoading()) { + ProgressDialog() + } BackstackView(modifier) } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index ec0d9662c7..425133b797 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -47,12 +47,19 @@ fun Activity.openUrlInChromeCustomTab( true -> CustomTabsIntent.COLOR_SCHEME_DARK } ) + .setShareIdentityEnabled(false) // Note: setting close button icon does not work // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) // .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) .apply { session?.let { setSession(it) } } .build() + .apply { + // Disable download button + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true) + // Disable bookmark button + intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true) + } .launchUrl(this, Uri.parse(url)) } catch (activityNotFoundException: ActivityNotFoundException) { // TODO context.toast(R.string.error_no_external_application_found) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt index fdaebc9c97..420940aefe 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt @@ -64,16 +64,52 @@ interface EncryptionService { */ suspend fun deviceEd25519(): String? + /** + * Starts the identity reset process. This will return a handle that can be used to reset the identity. + */ suspend fun startIdentityReset(): Result } -interface IdentityResetHandle +/** + * A handle to reset the user's identity. + */ +interface IdentityResetHandle { + /** + * Cancel the reset process and drops the existing handle in the SDK. + */ + suspend fun cancel() +} +/** + * A handle to reset the user's identity with a password login type. + */ interface IdentityPasswordResetHandle : IdentityResetHandle { + /** + * Reset the password of the user. + * + * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is + * called, or the identity is reset. + * + * @param userId the user id of the user to reset the password for. + * @param password the current password, which will be validated before the process takes place. + */ suspend fun resetPassword(userId: UserId, password: String): Result } +/** + * A handle to reset the user's identity with an OIDC login type. + */ interface IdentityOidcResetHandle : IdentityResetHandle { + /** + * The URL to open in a webview/custom tab to reset the identity. + */ val url: String + + /** + * Reset the identity using the OIDC flow. + * + * This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is + * called, or the identity is reset. + */ suspend fun resetOidc(): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt index 69c7ecdd31..0d51d7c4c2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt @@ -42,6 +42,10 @@ class RustPasswordIdentityResetHandle( override suspend fun resetPassword(userId: UserId, password: String): Result { return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } } + + override suspend fun cancel() { + identityResetHandle.cancelAndDestroy() + } } class RustOidcIdentityResetHandle( @@ -51,4 +55,13 @@ class RustOidcIdentityResetHandle( override suspend fun resetOidc(): Result { return runCatching { identityResetHandle.reset(null) } } + + override suspend fun cancel() { + identityResetHandle.cancelAndDestroy() + } +} + +private suspend fun org.matrix.rustcomponents.sdk.IdentityResetHandle.cancelAndDestroy() { + cancel() + destroy() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt index bf0aa312be..6b3eabd102 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt @@ -22,17 +22,27 @@ import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetH class FakeIdentityOidcResetHandle( override val url: String = "", - var resetOidcLambda: () -> Result = { error("Not implemented") } + var resetOidcLambda: () -> Result = { error("Not implemented") }, + var cancelLambda: () -> Unit = { error("Not implemented") }, ) : IdentityOidcResetHandle { override suspend fun resetOidc(): Result { return resetOidcLambda() } + + override suspend fun cancel() { + cancelLambda() + } } class FakeIdentityPasswordResetHandle( - var resetPasswordLambda: (UserId, String) -> Result = { _, _ -> error("Not implemented") } + var resetPasswordLambda: (UserId, String) -> Result = { _, _ -> error("Not implemented") }, + var cancelLambda: () -> Unit = { error("Not implemented") }, ) : IdentityPasswordResetHandle { override suspend fun resetPassword(userId: UserId, password: String): Result { return resetPasswordLambda(userId, password) } + + override suspend fun cancel() { + cancelLambda() + } } diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt index e8ddcd03c3..56a04cb0ea 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt @@ -16,10 +16,11 @@ package io.element.android.libraries.oidc.impl.webview +import android.annotation.SuppressLint import android.webkit.WebView import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -30,10 +31,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.viewinterop.AndroidView import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.components.async.AsyncActionView +import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.oidc.impl.OidcUrlParser +@OptIn(ExperimentalMaterial3Api::class) @Composable fun OidcView( state: OidcState, @@ -55,7 +60,7 @@ fun OidcView( OidcWebViewClient(::shouldOverrideUrl) } - BackHandler { + fun onBack() { if (webView?.canGoBack().orFalse()) { webView?.goBack() } else { @@ -64,11 +69,32 @@ fun OidcView( } } - Box(modifier = modifier.statusBarsPadding()) { + BackHandler { onBack() } + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { + BackButton(onClick = ::onBack) + }, + ) + } + ) { contentPadding -> AndroidView( + modifier = Modifier.padding(contentPadding), factory = { context -> WebView(context).apply { webViewClient = oidcWebViewClient + settings.apply { + @SuppressLint("SetJavaScriptEnabled") + javaScriptEnabled = true + allowContentAccess = true + allowFileAccess = true + databaseEnabled = true + domStorageEnabled = true + } loadUrl(state.oidcDetails.url) }.also { webView = it