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 package io.element.android.features.securebackup.impl.reset
import io.element.android.libraries.architecture.AsyncData 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.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId 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.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -76,4 +71,9 @@ class ResetIdentityFlowManager @Inject constructor(
resetHandleFlow 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.app.Activity
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin 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.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode 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.di.SessionScope
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle 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.IdentityPasswordResetHandle
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
import io.element.android.libraries.oidc.api.OidcEntryPoint import io.element.android.libraries.oidc.api.OidcEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -76,6 +82,7 @@ class ResetIdentityFlowNode @AssistedInject constructor(
} }
private lateinit var activity: Activity private lateinit var activity: Activity
private var resetJob: Job? = null
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
@ -83,6 +90,19 @@ class ResetIdentityFlowNode @AssistedInject constructor(
resetIdentityFlowManager.whenResetIsDone { resetIdentityFlowManager.whenResetIsDone {
plugins<Callback>().forEach { it.onDone() } 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -121,15 +141,29 @@ class ResetIdentityFlowNode @AssistedInject constructor(
} else { } else {
backstack.push(NavTarget.ResetOidc(handle.url)) backstack.push(NavTarget.ResetOidc(handle.url))
} }
handle.resetOidc() resetJob = launch { handle.resetOidc() }
} }
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword) is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
} }
} }
private fun cancelResetJob() {
resetJob?.cancel()
resetJob = null
coroutineScope.launch { resetIdentityFlowManager.cancel() }
}
@Composable @Composable
override fun View(modifier: Modifier) { 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) BackstackView(modifier)
} }

View file

@ -47,12 +47,19 @@ fun Activity.openUrlInChromeCustomTab(
true -> CustomTabsIntent.COLOR_SCHEME_DARK true -> CustomTabsIntent.COLOR_SCHEME_DARK
} }
) )
.setShareIdentityEnabled(false)
// Note: setting close button icon does not work // Note: setting close button icon does not work
// .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp)) // .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
// .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out) // .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
// .setExitAnimations(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) } } .apply { session?.let { setSession(it) } }
.build() .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)) .launchUrl(this, Uri.parse(url))
} catch (activityNotFoundException: ActivityNotFoundException) { } catch (activityNotFoundException: ActivityNotFoundException) {
// TODO context.toast(R.string.error_no_external_application_found) // TODO context.toast(R.string.error_no_external_application_found)

View file

@ -64,16 +64,52 @@ interface EncryptionService {
*/ */
suspend fun deviceEd25519(): String? 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?> 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 { 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> 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 { interface IdentityOidcResetHandle : IdentityResetHandle {
/**
* The URL to open in a webview/custom tab to reset the identity.
*/
val url: String 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> suspend fun resetOidc(): Result<Unit>
} }

View file

@ -42,6 +42,10 @@ class RustPasswordIdentityResetHandle(
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> { override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) }
} }
override suspend fun cancel() {
identityResetHandle.cancelAndDestroy()
}
} }
class RustOidcIdentityResetHandle( class RustOidcIdentityResetHandle(
@ -51,4 +55,13 @@ class RustOidcIdentityResetHandle(
override suspend fun resetOidc(): Result<Unit> { override suspend fun resetOidc(): Result<Unit> {
return runCatching { identityResetHandle.reset(null) } 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( class FakeIdentityOidcResetHandle(
override val url: String = "", 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 { ) : IdentityOidcResetHandle {
override suspend fun resetOidc(): Result<Unit> { override suspend fun resetOidc(): Result<Unit> {
return resetOidcLambda() return resetOidcLambda()
} }
override suspend fun cancel() {
cancelLambda()
}
} }
class FakeIdentityPasswordResetHandle( 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 { ) : IdentityPasswordResetHandle {
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> { override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
return resetPasswordLambda(userId, password) return resetPasswordLambda(userId, password)
} }
override suspend fun cancel() {
cancelLambda()
}
} }

View file

@ -16,10 +16,11 @@
package io.element.android.libraries.oidc.impl.webview package io.element.android.libraries.oidc.impl.webview
import android.annotation.SuppressLint
import android.webkit.WebView import android.webkit.WebView
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -30,10 +31,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.components.async.AsyncActionView 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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 import io.element.android.libraries.oidc.impl.OidcUrlParser
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun OidcView( fun OidcView(
state: OidcState, state: OidcState,
@ -55,7 +60,7 @@ fun OidcView(
OidcWebViewClient(::shouldOverrideUrl) OidcWebViewClient(::shouldOverrideUrl)
} }
BackHandler { fun onBack() {
if (webView?.canGoBack().orFalse()) { if (webView?.canGoBack().orFalse()) {
webView?.goBack() webView?.goBack()
} else { } 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( AndroidView(
modifier = Modifier.padding(contentPadding),
factory = { context -> factory = { context ->
WebView(context).apply { WebView(context).apply {
webViewClient = oidcWebViewClient webViewClient = oidcWebViewClient
settings.apply {
@SuppressLint("SetJavaScriptEnabled")
javaScriptEnabled = true
allowContentAccess = true
allowFileAccess = true
databaseEnabled = true
domStorageEnabled = true
}
loadUrl(state.oidcDetails.url) loadUrl(state.oidcDetails.url)
}.also { }.also {
webView = it webView = it