Improve existing APIs

This commit is contained in:
Jorge Martín
2024-08-09 09:34:04 +02:00
parent 2283e0e361
commit 252b3e11a0
7 changed files with 140 additions and 14 deletions

View File

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

View File

@@ -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<Callback>().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)
}

View File

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

View File

@@ -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<IdentityResetHandle?>
}
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<Unit>
}
/**
* 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<Unit>
}

View File

@@ -42,6 +42,10 @@ class RustPasswordIdentityResetHandle(
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
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<Unit> {
return runCatching { identityResetHandle.reset(null) }
}
override suspend fun cancel() {
identityResetHandle.cancelAndDestroy()
}
}
private suspend fun org.matrix.rustcomponents.sdk.IdentityResetHandle.cancelAndDestroy() {
cancel()
destroy()
}

View File

@@ -22,17 +22,27 @@ import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetH
class FakeIdentityOidcResetHandle(
override val url: String = "",
var resetOidcLambda: () -> Result<Unit> = { error("Not implemented") }
var resetOidcLambda: () -> Result<Unit> = { error("Not implemented") },
var cancelLambda: () -> Unit = { error("Not implemented") },
) : IdentityOidcResetHandle {
override suspend fun resetOidc(): Result<Unit> {
return resetOidcLambda()
}
override suspend fun cancel() {
cancelLambda()
}
}
class FakeIdentityPasswordResetHandle(
var resetPasswordLambda: (UserId, String) -> Result<Unit> = { _, _ -> error("Not implemented") }
var resetPasswordLambda: (UserId, String) -> Result<Unit> = { _, _ -> error("Not implemented") },
var cancelLambda: () -> Unit = { error("Not implemented") },
) : IdentityPasswordResetHandle {
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
return resetPasswordLambda(userId, password)
}
override suspend fun cancel() {
cancelLambda()
}
}

View File

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