Improve existing APIs
This commit is contained in:
parent
2283e0e361
commit
252b3e11a0
7 changed files with 140 additions and 14 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue