Skip to content

Commit 175bada

Browse files
committed
Improve existing APIs
1 parent abb9959 commit 175bada

File tree

7 files changed

+140
-14
lines changed

7 files changed

+140
-14
lines changed

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowManager.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
package io.element.android.features.securebackup.impl.reset
1818

1919
import io.element.android.libraries.architecture.AsyncData
20-
import io.element.android.libraries.di.SessionScope
21-
import io.element.android.libraries.di.SingleIn
2220
import io.element.android.libraries.di.annotations.SessionCoroutineScope
2321
import io.element.android.libraries.matrix.api.MatrixClient
2422
import io.element.android.libraries.matrix.api.core.SessionId
@@ -28,11 +26,8 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
2826
import kotlinx.coroutines.CoroutineScope
2927
import kotlinx.coroutines.flow.MutableStateFlow
3028
import kotlinx.coroutines.flow.StateFlow
31-
import kotlinx.coroutines.flow.distinctUntilChanged
32-
import kotlinx.coroutines.flow.filter
3329
import kotlinx.coroutines.flow.filterIsInstance
3430
import kotlinx.coroutines.flow.first
35-
import kotlinx.coroutines.flow.map
3631
import kotlinx.coroutines.launch
3732
import javax.inject.Inject
3833

@@ -76,4 +71,9 @@ class ResetIdentityFlowManager @Inject constructor(
7671
resetHandleFlow
7772
}
7873
}
74+
75+
suspend fun cancel() {
76+
currentHandleFlow.value.dataOrNull()?.cancel()
77+
resetHandleFlow.value = AsyncData.Uninitialized
78+
}
7979
}

features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/ResetIdentityFlowNode.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ package io.element.android.features.securebackup.impl.reset
1919
import android.app.Activity
2020
import android.os.Parcelable
2121
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.collectAsState
23+
import androidx.compose.runtime.getValue
2224
import androidx.compose.ui.Modifier
2325
import androidx.compose.ui.platform.LocalContext
26+
import androidx.lifecycle.DefaultLifecycleObserver
27+
import androidx.lifecycle.LifecycleOwner
2428
import com.bumble.appyx.core.modality.BuildContext
2529
import com.bumble.appyx.core.node.Node
2630
import com.bumble.appyx.core.plugin.Plugin
@@ -37,12 +41,14 @@ import io.element.android.libraries.architecture.AsyncData
3741
import io.element.android.libraries.architecture.BackstackView
3842
import io.element.android.libraries.architecture.BaseFlowNode
3943
import io.element.android.libraries.architecture.createNode
44+
import io.element.android.libraries.designsystem.components.ProgressDialog
4045
import io.element.android.libraries.di.SessionScope
4146
import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle
4247
import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle
4348
import io.element.android.libraries.matrix.api.encryption.IdentityResetHandle
4449
import io.element.android.libraries.oidc.api.OidcEntryPoint
4550
import kotlinx.coroutines.CoroutineScope
51+
import kotlinx.coroutines.Job
4652
import kotlinx.coroutines.flow.filterIsInstance
4753
import kotlinx.coroutines.flow.first
4854
import kotlinx.coroutines.launch
@@ -76,13 +82,27 @@ class ResetIdentityFlowNode @AssistedInject constructor(
7682
}
7783

7884
private lateinit var activity: Activity
85+
private var resetJob: Job? = null
7986

8087
override fun onBuilt() {
8188
super.onBuilt()
8289

8390
resetIdentityFlowManager.whenResetIsDone {
8491
plugins<Callback>().forEach { it.onDone() }
8592
}
93+
94+
lifecycle.addObserver(object : DefaultLifecycleObserver {
95+
override fun onStart(owner: LifecycleOwner) {
96+
// If the custom tab was opened, we need to cancel the reset job
97+
// when we come back to the node if it the reset wasn't successful
98+
cancelResetJob()
99+
}
100+
101+
override fun onDestroy(owner: LifecycleOwner) {
102+
// Make sure we cancel the reset job when the node is destroyed, just in case
103+
cancelResetJob()
104+
}
105+
})
86106
}
87107

88108
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -121,15 +141,29 @@ class ResetIdentityFlowNode @AssistedInject constructor(
121141
} else {
122142
backstack.push(NavTarget.ResetOidc(handle.url))
123143
}
124-
handle.resetOidc()
144+
resetJob = launch { handle.resetOidc() }
125145
}
126146
is IdentityPasswordResetHandle -> backstack.push(NavTarget.ResetPassword)
127147
}
128148
}
129149

150+
private fun cancelResetJob() {
151+
resetJob?.cancel()
152+
resetJob = null
153+
coroutineScope.launch { resetIdentityFlowManager.cancel() }
154+
}
155+
130156
@Composable
131157
override fun View(modifier: Modifier) {
132-
(LocalContext.current as? Activity)?.let { activity = it }
158+
// Workaround to get the current activity
159+
if (!this::activity.isInitialized) {
160+
activity = LocalContext.current as Activity
161+
}
162+
163+
val startResetState by resetIdentityFlowManager.currentHandleFlow.collectAsState()
164+
if (startResetState.isLoading()) {
165+
ProgressDialog()
166+
}
133167

134168
BackstackView(modifier)
135169
}

libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,19 @@ fun Activity.openUrlInChromeCustomTab(
4747
true -> CustomTabsIntent.COLOR_SCHEME_DARK
4848
}
4949
)
50+
.setShareIdentityEnabled(false)
5051
// Note: setting close button icon does not work
5152
// .setCloseButtonIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_back_24dp))
5253
// .setStartAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
5354
// .setExitAnimations(context, R.anim.enter_fade_in, R.anim.exit_fade_out)
5455
.apply { session?.let { setSession(it) } }
5556
.build()
57+
.apply {
58+
// Disable download button
59+
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true)
60+
// Disable bookmark button
61+
intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_START_BUTTON", true)
62+
}
5663
.launchUrl(this, Uri.parse(url))
5764
} catch (activityNotFoundException: ActivityNotFoundException) {
5865
// TODO context.toast(R.string.error_no_external_application_found)

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/encryption/EncryptionService.kt

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,52 @@ interface EncryptionService {
6464
*/
6565
suspend fun deviceEd25519(): String?
6666

67+
/**
68+
* Starts the identity reset process. This will return a handle that can be used to reset the identity.
69+
*/
6770
suspend fun startIdentityReset(): Result<IdentityResetHandle?>
6871
}
6972

70-
interface IdentityResetHandle
73+
/**
74+
* A handle to reset the user's identity.
75+
*/
76+
interface IdentityResetHandle {
77+
/**
78+
* Cancel the reset process and drops the existing handle in the SDK.
79+
*/
80+
suspend fun cancel()
81+
}
7182

83+
/**
84+
* A handle to reset the user's identity with a password login type.
85+
*/
7286
interface IdentityPasswordResetHandle : IdentityResetHandle {
87+
/**
88+
* Reset the password of the user.
89+
*
90+
* This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
91+
* called, or the identity is reset.
92+
*
93+
* @param userId the user id of the user to reset the password for.
94+
* @param password the current password, which will be validated before the process takes place.
95+
*/
7396
suspend fun resetPassword(userId: UserId, password: String): Result<Unit>
7497
}
7598

99+
/**
100+
* A handle to reset the user's identity with an OIDC login type.
101+
*/
76102
interface IdentityOidcResetHandle : IdentityResetHandle {
103+
/**
104+
* The URL to open in a webview/custom tab to reset the identity.
105+
*/
77106
val url: String
107+
108+
/**
109+
* Reset the identity using the OIDC flow.
110+
*
111+
* This method will block the coroutine it's running on and keep polling indefinitely until either the coroutine is cancelled, the [cancel] method is
112+
* called, or the identity is reset.
113+
*/
78114
suspend fun resetOidc(): Result<Unit>
79115
}

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ class RustPasswordIdentityResetHandle(
4242
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
4343
return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) }
4444
}
45+
46+
override suspend fun cancel() {
47+
identityResetHandle.cancelAndDestroy()
48+
}
4549
}
4650

4751
class RustOidcIdentityResetHandle(
@@ -51,4 +55,13 @@ class RustOidcIdentityResetHandle(
5155
override suspend fun resetOidc(): Result<Unit> {
5256
return runCatching { identityResetHandle.reset(null) }
5357
}
58+
59+
override suspend fun cancel() {
60+
identityResetHandle.cancelAndDestroy()
61+
}
62+
}
63+
64+
private suspend fun org.matrix.rustcomponents.sdk.IdentityResetHandle.cancelAndDestroy() {
65+
cancel()
66+
destroy()
5467
}

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/encryption/FakeIdentityResetHandle.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,27 @@ import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetH
2222

2323
class FakeIdentityOidcResetHandle(
2424
override val url: String = "",
25-
var resetOidcLambda: () -> Result<Unit> = { error("Not implemented") }
25+
var resetOidcLambda: () -> Result<Unit> = { error("Not implemented") },
26+
var cancelLambda: () -> Unit = { error("Not implemented") },
2627
) : IdentityOidcResetHandle {
2728
override suspend fun resetOidc(): Result<Unit> {
2829
return resetOidcLambda()
2930
}
31+
32+
override suspend fun cancel() {
33+
cancelLambda()
34+
}
3035
}
3136

3237
class FakeIdentityPasswordResetHandle(
33-
var resetPasswordLambda: (UserId, String) -> Result<Unit> = { _, _ -> error("Not implemented") }
38+
var resetPasswordLambda: (UserId, String) -> Result<Unit> = { _, _ -> error("Not implemented") },
39+
var cancelLambda: () -> Unit = { error("Not implemented") },
3440
) : IdentityPasswordResetHandle {
3541
override suspend fun resetPassword(userId: UserId, password: String): Result<Unit> {
3642
return resetPasswordLambda(userId, password)
3743
}
44+
45+
override suspend fun cancel() {
46+
cancelLambda()
47+
}
3848
}

libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
package io.element.android.libraries.oidc.impl.webview
1818

19+
import android.annotation.SuppressLint
1920
import android.webkit.WebView
2021
import androidx.activity.compose.BackHandler
21-
import androidx.compose.foundation.layout.Box
22-
import androidx.compose.foundation.layout.statusBarsPadding
22+
import androidx.compose.foundation.layout.padding
23+
import androidx.compose.material3.ExperimentalMaterial3Api
2324
import androidx.compose.runtime.Composable
2425
import androidx.compose.runtime.getValue
2526
import androidx.compose.runtime.mutableStateOf
@@ -30,10 +31,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
3031
import androidx.compose.ui.viewinterop.AndroidView
3132
import io.element.android.libraries.core.bool.orFalse
3233
import io.element.android.libraries.designsystem.components.async.AsyncActionView
34+
import io.element.android.libraries.designsystem.components.button.BackButton
3335
import io.element.android.libraries.designsystem.preview.ElementPreview
3436
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
37+
import io.element.android.libraries.designsystem.theme.components.Scaffold
38+
import io.element.android.libraries.designsystem.theme.components.TopAppBar
3539
import io.element.android.libraries.oidc.impl.OidcUrlParser
3640

41+
@OptIn(ExperimentalMaterial3Api::class)
3742
@Composable
3843
fun OidcView(
3944
state: OidcState,
@@ -55,7 +60,7 @@ fun OidcView(
5560
OidcWebViewClient(::shouldOverrideUrl)
5661
}
5762

58-
BackHandler {
63+
fun onBack() {
5964
if (webView?.canGoBack().orFalse()) {
6065
webView?.goBack()
6166
} else {
@@ -64,11 +69,32 @@ fun OidcView(
6469
}
6570
}
6671

67-
Box(modifier = modifier.statusBarsPadding()) {
72+
BackHandler { onBack() }
73+
74+
Scaffold(
75+
modifier = modifier,
76+
topBar = {
77+
TopAppBar(
78+
title = {},
79+
navigationIcon = {
80+
BackButton(onClick = ::onBack)
81+
},
82+
)
83+
}
84+
) { contentPadding ->
6885
AndroidView(
86+
modifier = Modifier.padding(contentPadding),
6987
factory = { context ->
7088
WebView(context).apply {
7189
webViewClient = oidcWebViewClient
90+
settings.apply {
91+
@SuppressLint("SetJavaScriptEnabled")
92+
javaScriptEnabled = true
93+
allowContentAccess = true
94+
allowFileAccess = true
95+
databaseEnabled = true
96+
domStorageEnabled = true
97+
}
7298
loadUrl(state.oidcDetails.url)
7399
}.also {
74100
webView = it

0 commit comments

Comments
 (0)