diff --git a/CHANGES.md b/CHANGES.md index dc5d6f4a96..04f14e151a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: - Cross-Signing | Verify new session from existing session (#1134) - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) - Save media files to Gallery (#973) + - Account deactivation (with password only) (#35) Improvements 🙌: - Verification DM / Handle concurrent .start after .ready (#794) @@ -23,6 +24,7 @@ Improvements 🙌: - Cross-Signing | Composer decoration: shields (#1077) - Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197) - Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719)) + - Cross-Signing | Hide Use recovery key when 4S is not setup (#1007) Bugfix 🐛: - Fix summary notification staying after "mark as read" @@ -35,7 +37,9 @@ Bugfix 🐛: - Fix crash when trying to download file without internet connection (#1229) - Local echo are not updated in timeline (for failed & encrypted states) - Render image event even if thumbnail_info does not have mimetype defined (#1209) + - RiotX now uses as many threads as it needs to do work and send messages (#1221) - Fix issue with media path (#1227) + - Add user to direct chat by user id (#1065) Translations 🗣: - @@ -456,6 +460,7 @@ Bugfix: - Fix messages with empty `in_reply_to` not rendering (#447) - Fix clear cache (#408) and Logout (#205) - Fix `(edited)` link can be copied to clipboard (#402) + - KeyBackup / SSSS | Should get the key from SSSS instead of asking recovery Key (#1163) Build: - Split APK: generate one APK per arch, to reduce APK size of about 30% diff --git a/docs/notifications.md b/docs/notifications.md index 328eb86954..8efcb87bf3 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -38,10 +38,10 @@ When the client receives the new information, it immediately sends another reque This effectively emulates a server push feature. The HTTP long Polling can be fine tuned in the **SDK** using two parameters: -* timout (Sync request timeout) +* timeout (Sync request timeout) * delay (Delay between each sync) -**timeout** is a server paramter, defined by: +**timeout** is a server parameter, defined by: ``` The maximum time to wait, in milliseconds, before returning this request.` If no events (or other data) become available before this time elapses, the server will return a response with empty fields. diff --git a/docs/signin.md b/docs/signin.md index 245ea444f6..e7368137ae 100644 --- a/docs/signin.md +++ b/docs/signin.md @@ -57,7 +57,7 @@ We get credential (200) ```json { - "user_id": "@benoit0816:matrix.org", + "user_id": "@alice:matrix.org", "access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg", "home_server": "matrix.org", "device_id": "GTVREDALBF", @@ -128,6 +128,8 @@ We get the credentials (200) } ``` +It's worth noting that the response from the homeserver contains the userId of Alice. + ### Login with Msisdn Not supported yet in RiotX diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/ChangePasswordTest.kt new file mode 100644 index 0000000000..2574700d49 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/ChangePasswordTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.account + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.failure.isInvalidPassword +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import org.amshove.kluent.shouldBeTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class ChangePasswordTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + companion object { + private const val NEW_PASSWORD = "this is a new password" + } + + @Test + fun changePasswordTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + + // Change password + commonTestHelper.doSync { + session.changePassword(TestConstants.PASSWORD, NEW_PASSWORD, it) + } + + // Try to login with the previous password, it will fail + val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) + throwable.isInvalidPassword().shouldBeTrue() + + // Try to login with the new password, should work + val session2 = commonTestHelper.logIntoAccount(session.myUserId, NEW_PASSWORD, SessionTestParams(withInitialSync = false)) + + commonTestHelper.signOutAndClose(session) + commonTestHelper.signOutAndClose(session2) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/DeactivateAccountTest.kt new file mode 100644 index 0000000000..17ff984bc8 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/account/DeactivateAccountTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.account + +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.auth.data.LoginFlowResult +import im.vector.matrix.android.api.auth.registration.RegistrationResult +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.SessionTestParams +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.common.TestMatrixCallback +import org.junit.Assert.assertTrue +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class DeactivateAccountTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + + @Test + fun deactivateAccountTest() { + val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false)) + + // Deactivate the account + commonTestHelper.doSync { + session.deactivateAccount(TestConstants.PASSWORD, false, it) + } + + // Try to login on the previous account, it will fail (M_USER_DEACTIVATED) + val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD) + + // Test the error + assertTrue(throwable is Failure.ServerError + && throwable.error.code == MatrixError.M_USER_DEACTIVATED + && throwable.error.message == "This account has been deactivated") + + // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE) + val hs = commonTestHelper.createHomeServerConfig() + + commonTestHelper.doSync { + commonTestHelper.matrix.authenticationService.getLoginFlow(hs, it) + } + + var accountCreationError: Throwable? = null + commonTestHelper.waitWithLatch { + commonTestHelper.matrix.authenticationService + .getRegistrationWizard() + .createAccount(session.myUserId.substringAfter("@").substringBefore(":"), + TestConstants.PASSWORD, + null, + object : TestMatrixCallback(it, false) { + override fun onFailure(failure: Throwable) { + accountCreationError = failure + super.onFailure(failure) + } + }) + } + + // Test the error + accountCreationError.let { + assertTrue(it is Failure.ServerError + && it.error.code == MatrixError.M_USER_IN_USE) + } + + // No need to close the session, it has been deactivated + } +} diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt index 965255e045..d168fe8ea6 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/common/CommonTestHelper.kt @@ -186,9 +186,9 @@ class CommonTestHelper(context: Context) { * @param testParams test params about the session * @return the session associated with the existing account */ - private fun logIntoAccount(userId: String, - password: String, - testParams: SessionTestParams): Session { + fun logIntoAccount(userId: String, + password: String, + testParams: SessionTestParams): Session { val session = logAccountAndSync(userId, password, testParams) assertNotNull(session) return session @@ -263,14 +263,45 @@ class CommonTestHelper(context: Context) { return session } + /** + * Log into the account and expect an error + * + * @param userName the account username + * @param password the password + */ + fun logAccountWithError(userName: String, + password: String): Throwable { + val hs = createHomeServerConfig() + + doSync { + matrix.authenticationService + .getLoginFlow(hs, it) + } + + var requestFailure: Throwable? = null + waitWithLatch { latch -> + matrix.authenticationService + .getLoginWizard() + .login(userName, password, "myDevice", object : TestMatrixCallback(latch, onlySuccessful = false) { + override fun onFailure(failure: Throwable) { + requestFailure = failure + super.onFailure(failure) + } + }) + } + + assertNotNull(requestFailure) + return requestFailure!! + } + /** * Await for a latch and ensure the result is true * * @param latch * @throws InterruptedException */ - fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) { - assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) + fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) { + assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)) } fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) { @@ -285,10 +316,10 @@ class CommonTestHelper(context: Context) { } } - fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) { + fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) { val latch = CountDownLatch(1) block(latch) - await(latch, timout) + await(latch, timeout) } // Transform a method with a MatrixCallback to a synchronous method diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 22ac0324cf..4c6e3ea3bd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager import java.io.InputStream import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.Executors import javax.inject.Inject data class MatrixConfiguration( @@ -61,7 +62,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo Monarchy.init(context) DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this) if (context.applicationContext !is Configuration.Provider) { - WorkManager.initialize(context, Configuration.Builder().build()) + WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build()) } ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt index 68643ff723..ddbaaea6ef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt @@ -23,11 +23,28 @@ import im.vector.matrix.android.api.util.Cancelable * This interface defines methods to manage the account. It's implemented at the session level. */ interface AccountService { - /** * Ask the homeserver to change the password. * @param password Current password. * @param newPassword New password */ fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable + + /** + * Deactivate the account. + * + * This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register + * the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account + * details from your identity server. This action is irreversible.\n\nDeactivating your account does not by default + * cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below. + * + * Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not + * be shared with any new or unregistered users, but registered users who already have access to these messages will still + * have access to their copy. + * + * @param password the account password + * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see + * an incomplete view of conversations + */ + fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 9245f77317..ebef751925 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -728,7 +728,8 @@ internal class DefaultKeysBackupService @Inject constructor( if (backUp) { maybeBackupKeys() } - + // Save for next time and for gossiping + saveBackupRecoveryKey(recoveryKey, keysVersionResult.version) result } }.foldToCallback(callback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 649a5a118f..7db3d6ead3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -419,7 +419,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor( ?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2 - || keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { + && keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) { // Unsupported algorithm return IntegrityResult.Error( SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt index 23d8210e89..62aded3f03 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt @@ -16,7 +16,6 @@ package im.vector.matrix.android.internal.session.account -import im.vector.matrix.android.api.session.account.model.ChangePasswordParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body @@ -30,4 +29,12 @@ internal interface AccountAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") fun changePassword(@Body params: ChangePasswordParams): Call + + /** + * Deactivate the user account + * + * @param params the deactivate account params + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/deactivate") + fun deactivate(@Body params: DeactivateAccountParams): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt index 87e003b0d3..032139ce5d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt @@ -39,6 +39,9 @@ internal abstract class AccountModule { @Binds abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask + @Binds + abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask + @Binds abstract fun bindAccountService(service: DefaultAccountService): AccountService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordParams.kt similarity index 95% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordParams.kt index 83ce8eb0aa..8aa1f9834c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordParams.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.api.session.account.model +package im.vector.matrix.android.internal.session.account import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt index ecd4b309d8..6400253d9f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt @@ -17,7 +17,6 @@ package im.vector.matrix.android.internal.session.account import im.vector.matrix.android.api.failure.Failure -import im.vector.matrix.android.api.session.account.model.ChangePasswordParams import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.UserId diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt new file mode 100644 index 0000000000..9960f61dbc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountParams.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth + +@JsonClass(generateAdapter = true) +internal data class DeactivateAccountParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + // Set to true to erase all data of the account + @Json(name = "erase") + val erase: Boolean +) { + companion object { + fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { + return DeactivateAccountParams( + auth = UserPasswordAuth(user = userId, password = password), + erase = erase + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt new file mode 100644 index 0000000000..f5b105cfee --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DeactivateAccountTask.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.account + +import im.vector.matrix.android.internal.di.UserId +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.cleanup.CleanupSession +import im.vector.matrix.android.internal.task.Task +import org.greenrobot.eventbus.EventBus +import javax.inject.Inject + +internal interface DeactivateAccountTask : Task { + data class Params( + val password: String, + val eraseAllData: Boolean + ) +} + +internal class DefaultDeactivateAccountTask @Inject constructor( + private val accountAPI: AccountAPI, + private val eventBus: EventBus, + @UserId private val userId: String, + private val cleanupSession: CleanupSession +) : DeactivateAccountTask { + + override suspend fun execute(params: DeactivateAccountTask.Params) { + val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) + + executeRequest(eventBus) { + apiCall = accountAPI.deactivate(deactivateAccountParams) + } + + cleanupSession.handle() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt index fce01994d3..f6db1dd3db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith import javax.inject.Inject internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask, + private val deactivateAccountTask: DeactivateAccountTask, private val taskExecutor: TaskExecutor) : AccountService { override fun changePassword(password: String, newPassword: String, callback: MatrixCallback): Cancelable { @@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw } .executeBy(taskExecutor) } + + override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback): Cancelable { + return deactivateAccountTask + .configureWith(DeactivateAccountTask.Params(password, eraseAllData)) { + this.callback = callback + } + .executeBy(taskExecutor) + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt new file mode 100644 index 0000000000..ebd0fad39c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cleanup/CleanupSession.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.cleanup + +import im.vector.matrix.android.BuildConfig +import im.vector.matrix.android.internal.SessionManager +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.crypto.CryptoModule +import im.vector.matrix.android.internal.database.RealmKeysUtils +import im.vector.matrix.android.internal.di.CryptoDatabase +import im.vector.matrix.android.internal.di.SessionCacheDirectory +import im.vector.matrix.android.internal.di.SessionDatabase +import im.vector.matrix.android.internal.di.SessionFilesDirectory +import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.di.UserMd5 +import im.vector.matrix.android.internal.di.WorkManagerProvider +import im.vector.matrix.android.internal.session.SessionModule +import im.vector.matrix.android.internal.session.cache.ClearCacheTask +import io.realm.Realm +import io.realm.RealmConfiguration +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +internal class CleanupSession @Inject constructor( + private val workManagerProvider: WorkManagerProvider, + @SessionId private val sessionId: String, + private val sessionManager: SessionManager, + private val sessionParamsStore: SessionParamsStore, + @SessionDatabase private val clearSessionDataTask: ClearCacheTask, + @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, + @SessionFilesDirectory private val sessionFiles: File, + @SessionCacheDirectory private val sessionCache: File, + private val realmKeysUtils: RealmKeysUtils, + @SessionDatabase private val realmSessionConfiguration: RealmConfiguration, + @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, + @UserMd5 private val userMd5: String +) { + suspend fun handle() { + Timber.d("Cleanup: release session...") + sessionManager.releaseSession(sessionId) + + Timber.d("Cleanup: cancel pending works...") + workManagerProvider.cancelAllWorks() + + Timber.d("Cleanup: delete session params...") + sessionParamsStore.delete(sessionId) + + Timber.d("Cleanup: clear session data...") + clearSessionDataTask.execute(Unit) + + Timber.d("Cleanup: clear crypto data...") + clearCryptoDataTask.execute(Unit) + + Timber.d("Cleanup: clear file system") + sessionFiles.deleteRecursively() + sessionCache.deleteRecursively() + + Timber.d("Cleanup: clear the database keys") + realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) + realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) + + // Sanity check + if (BuildConfig.DEBUG) { + Realm.getGlobalInstanceCount(realmSessionConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for session has not been closed ($it)") } + Realm.getGlobalInstanceCount(realmCryptoConfiguration) + .takeIf { it > 0 } + ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index b14a7758c5..610ade5744 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -16,58 +16,31 @@ package im.vector.matrix.android.internal.session.signout -import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.MatrixError -import im.vector.matrix.android.internal.SessionManager -import im.vector.matrix.android.internal.auth.SessionParamsStore -import im.vector.matrix.android.internal.crypto.CryptoModule -import im.vector.matrix.android.internal.database.RealmKeysUtils -import im.vector.matrix.android.internal.di.CryptoDatabase -import im.vector.matrix.android.internal.di.SessionCacheDirectory -import im.vector.matrix.android.internal.di.SessionDatabase -import im.vector.matrix.android.internal.di.SessionFilesDirectory -import im.vector.matrix.android.internal.di.SessionId -import im.vector.matrix.android.internal.di.UserMd5 -import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.network.executeRequest -import im.vector.matrix.android.internal.session.SessionModule -import im.vector.matrix.android.internal.session.cache.ClearCacheTask +import im.vector.matrix.android.internal.session.cleanup.CleanupSession import im.vector.matrix.android.internal.task.Task -import io.realm.Realm -import io.realm.RealmConfiguration import org.greenrobot.eventbus.EventBus import timber.log.Timber -import java.io.File import java.net.HttpURLConnection import javax.inject.Inject internal interface SignOutTask : Task { data class Params( - val sigOutFromHomeserver: Boolean + val signOutFromHomeserver: Boolean ) } internal class DefaultSignOutTask @Inject constructor( - private val workManagerProvider: WorkManagerProvider, - @SessionId private val sessionId: String, private val signOutAPI: SignOutAPI, - private val sessionManager: SessionManager, - private val sessionParamsStore: SessionParamsStore, - @SessionDatabase private val clearSessionDataTask: ClearCacheTask, - @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask, - @SessionFilesDirectory private val sessionFiles: File, - @SessionCacheDirectory private val sessionCache: File, - private val realmKeysUtils: RealmKeysUtils, - @SessionDatabase private val realmSessionConfiguration: RealmConfiguration, - @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, - @UserMd5 private val userMd5: String, - private val eventBus: EventBus + private val eventBus: EventBus, + private val cleanupSession: CleanupSession ) : SignOutTask { override suspend fun execute(params: SignOutTask.Params) { // It should be done even after a soft logout, to be sure the deviceId is deleted on the - if (params.sigOutFromHomeserver) { + if (params.signOutFromHomeserver) { Timber.d("SignOut: send request...") try { executeRequest(eventBus) { @@ -87,37 +60,7 @@ internal class DefaultSignOutTask @Inject constructor( } } - Timber.d("SignOut: release session...") - sessionManager.releaseSession(sessionId) - - Timber.d("SignOut: cancel pending works...") - workManagerProvider.cancelAllWorks() - - Timber.d("SignOut: delete session params...") - sessionParamsStore.delete(sessionId) - - Timber.d("SignOut: clear session data...") - clearSessionDataTask.execute(Unit) - - Timber.d("SignOut: clear crypto data...") - clearCryptoDataTask.execute(Unit) - - Timber.d("SignOut: clear file system") - sessionFiles.deleteRecursively() - sessionCache.deleteRecursively() - - Timber.d("SignOut: clear the database keys") - realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5)) - realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5)) - - // Sanity check - if (BuildConfig.DEBUG) { - Realm.getGlobalInstanceCount(realmSessionConfiguration) - .takeIf { it > 0 } - ?.let { Timber.e("All realm instance for session has not been closed ($it)") } - Realm.getGlobalInstanceCount(realmCryptoConfiguration) - .takeIf { it > 0 } - ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") } - } + Timber.d("SignOut: cleanup session...") + cleanupSession.handle() } } diff --git a/vector/src/main/java/im/vector/riotx/VectorApplication.kt b/vector/src/main/java/im/vector/riotx/VectorApplication.kt index 680550e818..2bceb38b75 100644 --- a/vector/src/main/java/im/vector/riotx/VectorApplication.kt +++ b/vector/src/main/java/im/vector/riotx/VectorApplication.kt @@ -56,6 +56,7 @@ import im.vector.riotx.features.version.VersionProvider import im.vector.riotx.push.fcm.FcmHelper import timber.log.Timber import java.text.SimpleDateFormat +import java.util.concurrent.Executors import java.util.Date import java.util.Locale import javax.inject.Inject @@ -146,7 +147,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration. override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION) - override fun getWorkManagerConfiguration() = androidx.work.Configuration.Builder().build() + override fun getWorkManagerConfiguration() = androidx.work.Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build() override fun injector(): VectorComponent { return vectorComponent diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c2f2959bd7..d22d80c4b3 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment +import im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devtools.AccountDataFragment @@ -445,8 +446,14 @@ interface FragmentModule { @IntoMap @FragmentKey(BootstrapAccountPasswordFragment::class) fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment + @Binds @IntoMap @FragmentKey(BootstrapMigrateBackupFragment::class) fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(DeactivateAccountFragment::class) + fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index bc5a1aff95..a5ec0591f5 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -49,6 +49,7 @@ data class MainActivityArgs( val clearCache: Boolean = false, val clearCredentials: Boolean = false, val isUserLoggedOut: Boolean = false, + val isAccountDeactivated: Boolean = false, val isSoftLogout: Boolean = false ) : Parcelable @@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() { clearCache = argsFromIntent?.clearCache ?: false, clearCredentials = argsFromIntent?.clearCredentials ?: false, isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false, + isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false, isSoftLogout = argsFromIntent?.isSoftLogout ?: false ) } @@ -121,7 +123,14 @@ class MainActivity : VectorBaseActivity() { return } when { - args.clearCredentials -> session.signOut( + args.isAccountDeactivated -> { + // Just do the local cleanup + Timber.w("Account deactivated, start app") + sessionHolder.clearActiveSession() + doLocalCleanup() + startNextActivityAndFinish() + } + args.clearCredentials -> session.signOut( !args.isUserLoggedOut, object : MatrixCallback { override fun onSuccess(data: Unit) { @@ -135,7 +144,7 @@ class MainActivity : VectorBaseActivity() { displayError(failure) } }) - args.clearCache -> session.clearCache( + args.clearCache -> session.clearCache( object : MatrixCallback { override fun onSuccess(data: Unit) { doLocalCleanup() @@ -182,16 +191,16 @@ class MainActivity : VectorBaseActivity() { private fun startNextActivityAndFinish() { val intent = when { args.clearCredentials - && !args.isUserLoggedOut -> - // User has explicitly asked to log out + && (!args.isUserLoggedOut || args.isAccountDeactivated) -> + // User has explicitly asked to log out or deactivated his account LoginActivity.newIntent(this, null) - args.isSoftLogout -> + args.isSoftLogout -> // The homeserver has invalidated the token, with a soft logout SoftLogoutActivity.newIntent(this) - args.isUserLoggedOut -> + args.isUserLoggedOut -> // the homeserver has invalidated the token (password changed, device deleted, other security reasons) SignedOutActivity.newIntent(this) - sessionHolder.hasActiveSession() -> + sessionHolder.hasActiveSession() -> // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { @@ -200,7 +209,7 @@ class MainActivity : VectorBaseActivity() { // The token is still invalid SoftLogoutActivity.newIntent(this) } - else -> + else -> // First start, or no active session LoginActivity.newIntent(this, null) } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt index 12674e5cd2..3ae206cd21 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/CreateDirectRoomActivity.kt @@ -28,6 +28,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.viewModel +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.room.failure.CreateRoomFailure import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -37,6 +38,7 @@ import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.platform.SimpleFragmentActivity import im.vector.riotx.core.platform.WaitingViewData import kotlinx.android.synthetic.main.activity.* +import java.net.HttpURLConnection import javax.inject.Inject class CreateDirectRoomActivity : SimpleFragmentActivity() { @@ -91,8 +93,14 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() { if (error is CreateRoomFailure.CreatedWithTimeout) { finish() } else { + val message = if (error is Failure.ServerError && error.httpCode == HttpURLConnection.HTTP_INTERNAL_ERROR /*500*/) { + // This error happen if the invited userId does not exist. + getString(R.string.create_room_dm_failure) + } else { + errorFormatter.toHumanReadable(error) + } AlertDialog.Builder(this) - .setMessage(errorFormatter.toHumanReadable(error)) + .setMessage(message) .setPositiveButton(R.string.ok, null) .show() } diff --git a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt index 016806f319..1c38e6f723 100644 --- a/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt +++ b/vector/src/main/java/im/vector/riotx/features/createdirect/DirectoryUsersController.kt @@ -23,6 +23,7 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized +import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.toMatrixItem @@ -56,15 +57,29 @@ class DirectoryUsersController @Inject constructor(private val session: Session, override fun buildModels() { val currentState = state ?: return val hasSearch = currentState.directorySearchTerm.isNotBlank() - val asyncUsers = currentState.directoryUsers - when (asyncUsers) { + when (val asyncUsers = currentState.directoryUsers) { is Uninitialized -> renderEmptyState(false) is Loading -> renderLoading() - is Success -> renderSuccess(asyncUsers(), currentState.selectedUsers.map { it.userId }, hasSearch) + is Success -> renderSuccess( + computeUsersList(asyncUsers(), currentState.directorySearchTerm), + currentState.selectedUsers.map { it.userId }, + hasSearch + ) is Fail -> renderFailure(asyncUsers.error) } } + /** + * Eventually add the searched terms, if it is a userId, and if not already present in the result + */ + private fun computeUsersList(directoryUsers: List, searchTerms: String): List { + return directoryUsers + + searchTerms + .takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } } + ?.let { listOf(User(it)) } + .orEmpty() + } + private fun renderLoading() { loadingItem { id("loading") diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt index e6d303b3aa..2b4e372166 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt @@ -20,16 +20,22 @@ import android.content.Context import android.content.Intent import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Observer +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import im.vector.riotx.R import im.vector.riotx.core.extensions.addFragmentToBackstack import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.SimpleFragmentActivity +import im.vector.riotx.core.ui.views.KeysBackupBanner +import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity class KeysBackupRestoreActivity : SimpleFragmentActivity() { companion object { + private const val REQUEST_4S_SECRET = 100 + const val SECRET_ALIAS = SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS + fun intent(context: Context): Intent { return Intent(context, KeysBackupRestoreActivity::class.java) } @@ -39,14 +45,20 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { private lateinit var viewModel: KeysBackupRestoreSharedViewModel + override fun onBackPressed() { + hideWaitingView() + super.onBackPressed() + } + override fun initUiAndData() { super.initUiAndData() viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java) viewModel.initSession(session) - viewModel.keyVersionResult.observe(this, Observer { keyVersion -> - if (keyVersion != null && supportFragmentManager.fragments.isEmpty()) { - val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null + viewModel.keySourceModel.observe(this, Observer { keySource -> + if (keySource != null && !keySource.isInQuadS && supportFragmentManager.fragments.isEmpty()) { + val isBackupCreatedFromPassphrase = + viewModel.keyVersionResult.value?.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null if (isBackupCreatedFromPassphrase) { replaceFragment(R.id.container, KeysBackupRestoreFromPassphraseFragment::class.java) } else { @@ -69,7 +81,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { if (viewModel.keyVersionResult.value == null) { // We need to fetch from API - viewModel.getLatestVersion(this) + viewModel.getLatestVersion() } viewModel.navigateEvent.observeEvent(this) { uxStateEvent -> @@ -78,8 +90,25 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java) } KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { + viewModel.keyVersionResult.value?.version?.let { + KeysBackupBanner.onRecoverDoneForVersion(this, it) + } replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java) } + KeysBackupRestoreSharedViewModel.NAVIGATE_TO_4S -> { + launch4SActivity() + } + KeysBackupRestoreSharedViewModel.NAVIGATE_FAILED_TO_LOAD_4S -> { + AlertDialog.Builder(this) + .setTitle(R.string.unknown_error) + .setMessage(R.string.error_failed_to_import_keys) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> + // nop + launch4SActivity() + } + .show() + } } } @@ -93,4 +122,30 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() { finish() } } + + private fun launch4SActivity() { + SharedSecureStorageActivity.newIntent( + context = this, + keyId = null, // default key + requestedSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME), + resultKeyStoreAlias = SECRET_ALIAS + ).let { + startActivityForResult(it, REQUEST_4S_SECRET) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_4S_SECRET) { + val extraResult = data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT) + if (resultCode == Activity.RESULT_OK && extraResult != null) { + viewModel.handleGotSecretFromSSSS( + extraResult, + SECRET_ALIAS + ) + } else { + finish() + } + } + super.onActivityResult(requestCode, resultCode, data) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt index 730c92a319..9a6e65a885 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt @@ -82,7 +82,7 @@ class KeysBackupRestoreFromKeyFragment @Inject constructor() if (value.isNullOrBlank()) { viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message) } else { - viewModel.recoverKeys(requireContext(), sharedViewModel) + viewModel.recoverKeys(sharedViewModel) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt index 0cf297f7f1..c8406570d3 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt @@ -15,21 +15,19 @@ */ package im.vector.riotx.features.crypto.keysbackup.restore -import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.listeners.StepProgressListener -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult -import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import androidx.lifecycle.viewModelScope import im.vector.riotx.R import im.vector.riotx.core.platform.WaitingViewData -import im.vector.riotx.core.ui.views.KeysBackupBanner -import timber.log.Timber +import im.vector.riotx.core.resources.StringProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import javax.inject.Inject -class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() { +class KeysBackupRestoreFromKeyViewModel @Inject constructor( + private val stringProvider: StringProvider +) : ViewModel() { var recoveryCode: MutableLiveData = MutableLiveData() var recoveryCodeErrorText: MutableLiveData = MutableLiveData() @@ -45,66 +43,16 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() { recoveryCodeErrorText.value = null } - fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) { - val session = sharedViewModel.session - val keysBackup = session.cryptoService().keysBackupService() - + fun recoverKeys(sharedViewModel: KeysBackupRestoreSharedViewModel) { + sharedViewModel.loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading))) recoveryCodeErrorText.value = null - val recoveryKey = recoveryCode.value!! - - val keysVersionResult = sharedViewModel.keyVersionResult.value!! - - keysBackup.restoreKeysWithRecoveryKey(keysVersionResult, - recoveryKey, - null, - session.myUserId, - object : StepProgressListener { - override fun onStepProgress(step: StepProgressListener.Step) { - when (step) { - is StepProgressListener.Step.DownloadingKey -> { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), - isIndeterminate = true)) - } - is StepProgressListener.Step.ImportingKey -> { - // Progress 0 can take a while, display an indeterminate progress in this case - if (step.progress == 0) { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), - isIndeterminate = true)) - } else { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), - step.progress, - step.total)) - } - } - } - } - }, - object : MatrixCallback { - override fun onSuccess(data: ImportRoomKeysResult) { - sharedViewModel.loadingEvent.value = null - sharedViewModel.didRecoverSucceed(data) - - KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!) - trustOnDecrypt(keysBackup, keysVersionResult) - } - - override fun onFailure(failure: Throwable) { - sharedViewModel.loadingEvent.value = null - recoveryCodeErrorText.value = context.getString(R.string.keys_backup_recovery_code_error_decrypt) - Timber.e(failure, "## onUnexpectedError") - } - }) - } - - private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) { - keysBackup.trustKeysBackupVersion(keysVersionResult, true, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("##### trustKeysBackupVersion onSuccess") - } - }) + viewModelScope.launch(Dispatchers.IO) { + val recoveryKey = recoveryCode.value!! + try { + sharedViewModel.recoverUsingBackupPass(recoveryKey) + } catch (failure: Throwable) { + recoveryCodeErrorText.value = stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt index 8dc8855583..0947c144d8 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -36,7 +36,7 @@ import im.vector.riotx.core.extensions.showPassword import im.vector.riotx.core.platform.VectorBaseFragment import javax.inject.Inject -class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseFragment() { +class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBaseFragment() { override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase @@ -119,7 +119,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseF if (value.isNullOrBlank()) { viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message) } else { - viewModel.recoverKeys(context!!, sharedViewModel) + viewModel.recoverKeys(sharedViewModel) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt index 69c5e70740..46e8d5fa18 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt @@ -15,21 +15,18 @@ */ package im.vector.riotx.features.crypto.keysbackup.restore -import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.listeners.StepProgressListener -import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService -import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult -import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import androidx.lifecycle.viewModelScope import im.vector.riotx.R -import im.vector.riotx.core.platform.WaitingViewData -import im.vector.riotx.core.ui.views.KeysBackupBanner -import timber.log.Timber +import im.vector.riotx.core.resources.StringProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import javax.inject.Inject -class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel() { +class KeysBackupRestoreFromPassphraseViewModel @Inject constructor( + private val stringProvider: StringProvider +) : ViewModel() { var passphrase: MutableLiveData = MutableLiveData() var passphraseErrorText: MutableLiveData = MutableLiveData() @@ -48,71 +45,14 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel passphraseErrorText.value = null } - fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) { - val keysBackup = sharedViewModel.session.cryptoService().keysBackupService() - + fun recoverKeys(sharedViewModel: KeysBackupRestoreSharedViewModel) { passphraseErrorText.value = null - - val keysVersionResult = sharedViewModel.keyVersionResult.value!! - - keysBackup.restoreKeyBackupWithPassword(keysVersionResult, - passphrase.value!!, - null, - sharedViewModel.session.myUserId, - object : StepProgressListener { - override fun onStepProgress(step: StepProgressListener.Step) { - when (step) { - is StepProgressListener.Step.ComputingKey -> { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_computing_key_waiting_message), - step.progress, - step.total)) - } - is StepProgressListener.Step.DownloadingKey -> { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), - isIndeterminate = true)) - } - is StepProgressListener.Step.ImportingKey -> { - Timber.d("backupKeys.ImportingKey.progress: ${step.progress}") - // Progress 0 can take a while, display an indeterminate progress in this case - if (step.progress == 0) { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), - isIndeterminate = true)) - } else { - sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) - + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), - step.progress, - step.total)) - } - } - } - } - }, - object : MatrixCallback { - override fun onSuccess(data: ImportRoomKeysResult) { - sharedViewModel.loadingEvent.value = null - sharedViewModel.didRecoverSucceed(data) - - KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!) - trustOnDecrypt(keysBackup, keysVersionResult) - } - - override fun onFailure(failure: Throwable) { - sharedViewModel.loadingEvent.value = null - passphraseErrorText.value = context.getString(R.string.keys_backup_passphrase_error_decrypt) - Timber.e(failure, "## onUnexpectedError") - } - }) - } - - private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) { - keysBackup.trustKeysBackupVersion(keysVersionResult, true, - object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.v("##### trustKeysBackupVersion onSuccess") - } - }) + viewModelScope.launch(Dispatchers.IO) { + try { + sharedViewModel.recoverUsingBackupPass(passphrase.value!!) + } catch (failure: Throwable) { + passphraseErrorText.value = stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt) + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt index 5586d0cf05..24aa37b95c 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt @@ -15,30 +15,52 @@ */ package im.vector.riotx.features.crypto.keysbackup.restore -import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.StepProgressListener import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.securestorage.KeyInfoResult +import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.riotx.R import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.utils.LiveEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject -class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() { +class KeysBackupRestoreSharedViewModel @Inject constructor( + private val stringProvider: StringProvider +) : ViewModel() { + + data class KeySource( + val isInMemory: Boolean, + val isInQuadS: Boolean + ) companion object { const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY" const val NAVIGATE_TO_SUCCESS = "NAVIGATE_TO_SUCCESS" + const val NAVIGATE_TO_4S = "NAVIGATE_TO_4S" + const val NAVIGATE_FAILED_TO_LOAD_4S = "NAVIGATE_FAILED_TO_LOAD_4S" } lateinit var session: Session var keyVersionResult: MutableLiveData = MutableLiveData() + var keySourceModel: MutableLiveData = MutableLiveData() + private var _keyVersionResultError: MutableLiveData> = MutableLiveData() val keyVersionResultError: LiveData> get() = _keyVersionResultError @@ -62,30 +84,192 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() { this.session = session } - fun getLatestVersion(context: Context) { - val keysBackup = session.cryptoService().keysBackupService() - - loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restore_is_getting_backup_version)) - - keysBackup.getCurrentVersion(object : MatrixCallback { - override fun onSuccess(data: KeysVersionResult?) { - loadingEvent.value = null - if (data?.version.isNullOrBlank()) { - // should not happen - _keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, "")) - } else { - keyVersionResult.value = data + val progressObserver = object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + when (step) { + is StepProgressListener.Step.ComputingKey -> { + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), + step.progress, + step.total)) + } + is StepProgressListener.Step.DownloadingKey -> { + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), + isIndeterminate = true)) + } + is StepProgressListener.Step.ImportingKey -> { + Timber.d("backupKeys.ImportingKey.progress: ${step.progress}") + // Progress 0 can take a while, display an indeterminate progress in this case + if (step.progress == 0) { + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + isIndeterminate = true)) + } else { + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + step.progress, + step.total)) + } } } + } + } - override fun onFailure(failure: Throwable) { - loadingEvent.value = null - _keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage)) + fun getLatestVersion() { + val keysBackup = session.cryptoService().keysBackupService() - // TODO For network error - // _keyVersionResultError.value = LiveEvent(context.getString(R.string.network_error_please_check_and_retry)) + loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version)) + + viewModelScope.launch(Dispatchers.IO) { + try { + val version = awaitCallback { + keysBackup.getCurrentVersion(it) + } + if (version?.version == null) { + loadingEvent.postValue(null) + _keyVersionResultError.postValue(LiveEvent(stringProvider.getString(R.string.keys_backup_get_version_error, ""))) + return@launch + } + + keyVersionResult.postValue(version) + // Let's check if there is quads + val isBackupKeyInQuadS = isBackupKeyInQuadS() + + val savedSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() + if (savedSecret != null && savedSecret.version == version.version) { + // key is in memory! + keySourceModel.postValue( + KeySource(isInMemory = true, isInQuadS = true) + ) + // Go and use it!! + try { + recoverUsingBackupRecoveryKey(savedSecret.recoveryKey) + } catch (failure: Throwable) { + keySourceModel.postValue( + KeySource(isInMemory = false, isInQuadS = true) + ) + } + } else if (isBackupKeyInQuadS) { + // key is in QuadS! + keySourceModel.postValue( + KeySource(isInMemory = false, isInQuadS = true) + ) + _navigateEvent.postValue(LiveEvent(NAVIGATE_TO_4S)) + } else { + // we need to restore directly + keySourceModel.postValue( + KeySource(isInMemory = false, isInQuadS = false) + ) + } + + loadingEvent.postValue(null) + } catch (failure: Throwable) { + loadingEvent.postValue(null) + _keyVersionResultError.postValue(LiveEvent(stringProvider.getString(R.string.keys_backup_get_version_error, failure.localizedMessage))) } - }) + } + } + + fun handleGotSecretFromSSSS(cipherData: String, alias: String) { + try { + cipherData.fromBase64().inputStream().use { ins -> + val res = session.loadSecureSecret>(ins, alias) + val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) + if (secret == null) { + _navigateEvent.postValue( + LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S) + ) + return + } + loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version)) + + viewModelScope.launch(Dispatchers.IO) { + try { + recoverUsingBackupRecoveryKey(computeRecoveryKey(secret.fromBase64())) + } catch (failure: Throwable) { + _navigateEvent.postValue( + LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S) + ) + } + } + } + } catch (failure: Throwable) { + _navigateEvent.postValue( + LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S) + ) + } + } + + suspend fun recoverUsingBackupPass(passphrase: String) { + val keysBackup = session.cryptoService().keysBackupService() + val keyVersion = keyVersionResult.value ?: return + + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading))) + + try { + val result = awaitCallback { + keysBackup.restoreKeyBackupWithPassword(keyVersion, + passphrase, + null, + session.myUserId, + progressObserver, + it + ) + } + loadingEvent.postValue(null) + didRecoverSucceed(result) + trustOnDecrypt(keysBackup, keyVersion) + } catch (failure: Throwable) { + loadingEvent.postValue(null) + throw failure + } + } + + suspend fun recoverUsingBackupRecoveryKey(recoveryKey: String) { + val keysBackup = session.cryptoService().keysBackupService() + val keyVersion = keyVersionResult.value ?: return + + loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading))) + + try { + val result = awaitCallback { + keysBackup.restoreKeysWithRecoveryKey(keyVersion, + recoveryKey, + null, + session.myUserId, + progressObserver, + it + ) + } + loadingEvent.postValue(null) + didRecoverSucceed(result) + trustOnDecrypt(keysBackup, keyVersion) + } catch (failure: Throwable) { + loadingEvent.postValue(null) + throw failure + } + } + + private fun isBackupKeyInQuadS(): Boolean { + val sssBackupSecret = session.getAccountDataEvent(KEYBACKUP_SECRET_SSSS_NAME) + ?: return false + + // Some sanity ? + val defaultKeyResult = session.sharedSecretStorageService.getDefaultKey() + val keyInfo = (defaultKeyResult as? KeyInfoResult.Success)?.keyInfo + ?: return false + + return (sssBackupSecret.content["encrypted"] as? Map<*, *>)?.containsKey(keyInfo.id) == true + } + + private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) { + keysBackup.trustKeysBackupVersion(keysVersionResult, true, + object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.v("##### trustKeysBackupVersion onSuccess") + } + }) } fun moveToRecoverWithKey() { @@ -94,6 +278,6 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() { fun didRecoverSucceed(result: ImportRoomKeysResult) { importKeyResult = result - _navigateEvent.value = LiveEvent(NAVIGATE_TO_SUCCESS) + _navigateEvent.postValue(LiveEvent(NAVIGATE_TO_SUCCESS)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index f5f92c381d..6a2b7825ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -44,6 +44,7 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams +import im.vector.matrix.android.api.session.securestorage.IntegrityResult import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64 @@ -73,7 +74,8 @@ data class VerificationBottomSheetViewState( val isMe: Boolean = false, val currentDeviceCanCrossSign: Boolean = false, val userWantsToCancel: Boolean = false, - val userThinkItsNotHim: Boolean = false + val userThinkItsNotHim: Boolean = false, + val quadSContainsSecrets: Boolean = true ) : MvRxState class VerificationBottomSheetViewModel @AssistedInject constructor( @@ -116,6 +118,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction } + val ssssOk = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets( + listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME), + null // default key + ) is IntegrityResult.Success setState { copy( otherUserMxItem = userItem?.toMatrixItem(), @@ -126,7 +132,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( selfVerificationMode = selfVerificationMode, roomId = args.roomId, isMe = args.otherUserId == session.myUserId, - currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign() + currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(), + quadSContainsSecrets = ssssOk ) } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt index 56c76bc2b0..a1b55832d5 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestController.kt @@ -65,14 +65,16 @@ class VerificationRequestController @Inject constructor( title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName())) } - bottomSheetVerificationActionItem { - id("passphrase") - title(stringProvider.getString(R.string.verification_cannot_access_other_session)) - titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - subTitle(stringProvider.getString(R.string.verification_use_passphrase)) - iconRes(R.drawable.ic_arrow_right) - iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) - listener { listener?.onClickRecoverFromPassphrase() } + if (state.quadSContainsSecrets) { + bottomSheetVerificationActionItem { + id("passphrase") + title(stringProvider.getString(R.string.verification_cannot_access_other_session)) + titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + subTitle(stringProvider.getString(R.string.verification_use_passphrase)) + iconRes(R.drawable.ic_arrow_right) + iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)) + listener { listener?.onClickRecoverFromPassphrase() } + } } } else { val styledText = diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 2449c635e4..ac2e2b7fa9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.riotx.R import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable @@ -92,6 +93,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { .subscribe { sharedAction -> when (sharedAction) { is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START) is HomeActivitySharedAction.OpenGroup -> { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) @@ -99,7 +101,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { is HomeActivitySharedAction.PromptForSecurityBootstrap -> { BootstrapBottomSheet.show(supportFragmentManager, true) } - } + }.exhaustive } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt index 902ea93588..a074c0e879 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt @@ -23,6 +23,7 @@ import im.vector.riotx.core.platform.VectorSharedAction */ sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() + object CloseDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() object PromptForSecurityBootstrap : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt index 9aa9313ad2..a8373797c6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt @@ -33,10 +33,15 @@ class HomeDrawerFragment @Inject constructor( private val avatarRenderer: AvatarRenderer ) : VectorBaseFragment() { + private lateinit var sharedActionViewModel: HomeSharedActionViewModel + override fun getLayoutResId() = R.layout.fragment_home_drawer override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) + if (savedInstanceState == null) { replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java) } @@ -49,11 +54,13 @@ class HomeDrawerFragment @Inject constructor( } } homeDrawerHeaderSettingsView.setOnClickListener { + sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) navigator.openSettings(requireActivity()) } // Debug menu homeDrawerHeaderDebugView.setOnClickListener { + sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer) navigator.openDebug(requireActivity()) } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 57f914118b..8b89aeda2a 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.login import android.os.Build import android.os.Bundle import android.view.View +import android.view.inputmethod.EditorInfo import androidx.autofill.HintConstants import androidx.core.view.isVisible import butterknife.OnClick @@ -40,7 +41,8 @@ import kotlinx.android.synthetic.main.fragment_login.* import javax.inject.Inject /** - * In this screen, in signin mode: + * In this screen: + * In signin mode: * - the user is asked for login (or email) and password to sign in to a homeserver. * - He also can reset his password * In signup mode: @@ -49,6 +51,7 @@ import javax.inject.Inject class LoginFragment @Inject constructor() : AbstractLoginFragment() { private var passwordShown = false + private var isSignupMode = false override fun getLayoutResId() = R.layout.fragment_login @@ -57,6 +60,14 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { setupSubmitButton() setupPasswordReveal() + + passwordField.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } } private fun setupAutoFill(state: LoginViewState) { @@ -82,7 +93,20 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { val login = loginField.text.toString() val password = passwordField.text.toString() - loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx))) + // This can be called by the IME action, so deal with empty cases + var error = 0 + if (login.isEmpty()) { + loginFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_user_name else R.string.error_empty_field_enter_user_name) + error++ + } + if (password.isEmpty()) { + passwordFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_password else R.string.error_empty_field_your_password) + error++ + } + + if (error == 0) { + loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx))) + } } private fun cleanupUi() { @@ -190,6 +214,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() { } override fun updateWithState(state: LoginViewState) { + isSignupMode = state.signMode == SignMode.SignUp + setupUi(state) setupAutoFill(state) setupButtons(state) diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 2984cc3889..ae8e7f23fc 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -191,7 +191,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context backgroundHandler.removeCallbacksAndMessages(null) backgroundHandler.postDelayed( { - refreshNotificationDrawerBg() + try { + refreshNotificationDrawerBg() + } catch (throwable: Throwable) { + // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer + Timber.w(throwable, "refreshNotificationDrawerBg failure") + } }, 200) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index f0a5a8ace8..e765f961dd 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -159,7 +159,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY" private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK" private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY" - const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY" private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY" private const val MEDIA_SAVING_3_DAYS = 0 diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 5db14fdbd2..6d00f02c97 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -20,6 +20,7 @@ import android.content.Intent import androidx.fragment.app.FragmentManager import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import im.vector.matrix.android.api.failure.GlobalError import im.vector.matrix.android.api.session.Session import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent @@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(), private var keyToHighlight: String? = null + var ignoreInvalidTokenError = false + @Inject lateinit var session: Session override fun injectWith(injector: ScreenComponent) { @@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(), when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS -> replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG) - EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> + EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY -> replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG) else -> replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) @@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(), return keyToHighlight } + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + if (ignoreInvalidTokenError) { + Timber.w("Ignoring invalid token global error") + } else { + super.handleInvalidToken(globalError) + } + } + companion object { fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java) .apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index f754064fbc..802cf7b33f 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { false } - - // Deactivate account section - - // deactivate account - findPreference(VectorPreferences.SETTINGS_DEACTIVATE_ACCOUNT_KEY)!! - .onPreferenceClickListener = Preference.OnPreferenceClickListener { - activity?.let { - notImplemented() - // TODO startActivity(DeactivateAccountActivity.getIntent(it)) - } - - false - } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt new file mode 100644 index 0000000000..f5130d5e00 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.account.deactivation + +import android.content.Context +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs +import im.vector.riotx.features.settings.VectorSettingsActivity +import kotlinx.android.synthetic.main.fragment_deactivate_account.* +import javax.inject.Inject + +class DeactivateAccountFragment @Inject constructor( + val viewModelFactory: DeactivateAccountViewModel.Factory +) : VectorBaseFragment() { + + private val viewModel: DeactivateAccountViewModel by fragmentViewModel() + + override fun getLayoutResId() = R.layout.fragment_deactivate_account + + override fun onResume() { + super.onResume() + (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) + } + + private var settingsActivity: VectorSettingsActivity? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + settingsActivity = context as? VectorSettingsActivity + } + + override fun onDetach() { + super.onDetach() + settingsActivity = null + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUi() + setupViewListeners() + observeViewEvents() + } + + private fun setupUi() { + deactivateAccountPassword.textChanges() + .subscribe { + deactivateAccountPasswordTil.error = null + deactivateAccountSubmit.isEnabled = it.isNotEmpty() + } + .disposeOnDestroyView() + } + + private fun setupViewListeners() { + deactivateAccountPasswordReveal.setOnClickListener { + viewModel.handle(DeactivateAccountAction.TogglePassword) + } + + deactivateAccountSubmit.setOnClickListener { + viewModel.handle(DeactivateAccountAction.DeactivateAccount( + deactivateAccountPassword.text.toString(), + deactivateAccountEraseCheckbox.isChecked)) + } + } + + private fun observeViewEvents() { + viewModel.observeViewEvents { + when (it) { + is DeactivateAccountViewEvents.Loading -> { + settingsActivity?.ignoreInvalidTokenError = true + showLoadingDialog(it.message) + } + DeactivateAccountViewEvents.EmptyPassword -> { + settingsActivity?.ignoreInvalidTokenError = false + deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) + } + DeactivateAccountViewEvents.InvalidPassword -> { + settingsActivity?.ignoreInvalidTokenError = false + deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password) + } + is DeactivateAccountViewEvents.OtherFailure -> { + settingsActivity?.ignoreInvalidTokenError = false + displayErrorDialog(it.throwable) + } + DeactivateAccountViewEvents.Done -> + MainActivity.restartApp(activity!!, MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) + }.exhaustive + } + } + + override fun invalidate() = withState(viewModel) { state -> + deactivateAccountPassword.showPassword(state.passwordShown) + deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt new file mode 100644 index 0000000000..4e7f7252e2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewEvents.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.settings.account.deactivation + +import im.vector.riotx.core.platform.VectorViewEvents + +/** + * Transient events for deactivate account settings screen + */ +sealed class DeactivateAccountViewEvents : VectorViewEvents { + data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() + object EmptyPassword : DeactivateAccountViewEvents() + object InvalidPassword : DeactivateAccountViewEvents() + data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() + object Done : DeactivateAccountViewEvents() +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt new file mode 100644 index 0000000000..adfc9ff5ae --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotx.features.settings.account.deactivation + +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.failure.isInvalidPassword +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.VectorViewModelAction + +data class DeactivateAccountViewState( + val passwordShown: Boolean = false +) : MvRxState + +sealed class DeactivateAccountAction : VectorViewModelAction { + object TogglePassword : DeactivateAccountAction() + data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction() +} + +class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel + } + + override fun handle(action: DeactivateAccountAction) { + when (action) { + DeactivateAccountAction.TogglePassword -> handleTogglePassword() + is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) + }.exhaustive + } + + private fun handleTogglePassword() = withState { + setState { + copy(passwordShown = !passwordShown) + } + } + + private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { + if (action.password.isEmpty()) { + _viewEvents.post(DeactivateAccountViewEvents.EmptyPassword) + return + } + + _viewEvents.post(DeactivateAccountViewEvents.Loading()) + + session.deactivateAccount(action.password, action.eraseAllData, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _viewEvents.post(DeactivateAccountViewEvents.Done) + } + + override fun onFailure(failure: Throwable) { + if (failure.isInvalidPassword()) { + _viewEvents.post(DeactivateAccountViewEvents.InvalidPassword) + } else { + _viewEvents.post(DeactivateAccountViewEvents.OtherFailure(failure)) + } + } + }) + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? { + val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.viewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml new file mode 100644 index 0000000000..1bf04ba81e --- /dev/null +++ b/vector/src/main/res/layout/fragment_deactivate_account.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + +