mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-29 06:28:45 +03:00
Merge branch 'develop' into feature/unwedging
This commit is contained in:
commit
4ee13b6fa1
47 changed files with 1214 additions and 309 deletions
|
@ -7,6 +7,7 @@ Features ✨:
|
||||||
- Cross-Signing | Verify new session from existing session (#1134)
|
- Cross-Signing | Verify new session from existing session (#1134)
|
||||||
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||||
- Save media files to Gallery (#973)
|
- Save media files to Gallery (#973)
|
||||||
|
- Account deactivation (with password only) (#35)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Verification DM / Handle concurrent .start after .ready (#794)
|
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||||
|
@ -23,6 +24,7 @@ Improvements 🙌:
|
||||||
- Cross-Signing | Composer decoration: shields (#1077)
|
- Cross-Signing | Composer decoration: shields (#1077)
|
||||||
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
|
- 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))
|
- 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 🐛:
|
Bugfix 🐛:
|
||||||
- Fix summary notification staying after "mark as read"
|
- Fix summary notification staying after "mark as read"
|
||||||
|
@ -35,7 +37,9 @@ Bugfix 🐛:
|
||||||
- Fix crash when trying to download file without internet connection (#1229)
|
- Fix crash when trying to download file without internet connection (#1229)
|
||||||
- Local echo are not updated in timeline (for failed & encrypted states)
|
- Local echo are not updated in timeline (for failed & encrypted states)
|
||||||
- Render image event even if thumbnail_info does not have mimetype defined (#1209)
|
- 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)
|
- Fix issue with media path (#1227)
|
||||||
|
- Add user to direct chat by user id (#1065)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
@ -456,6 +460,7 @@ Bugfix:
|
||||||
- Fix messages with empty `in_reply_to` not rendering (#447)
|
- Fix messages with empty `in_reply_to` not rendering (#447)
|
||||||
- Fix clear cache (#408) and Logout (#205)
|
- Fix clear cache (#408) and Logout (#205)
|
||||||
- Fix `(edited)` link can be copied to clipboard (#402)
|
- Fix `(edited)` link can be copied to clipboard (#402)
|
||||||
|
- KeyBackup / SSSS | Should get the key from SSSS instead of asking recovery Key (#1163)
|
||||||
|
|
||||||
Build:
|
Build:
|
||||||
- Split APK: generate one APK per arch, to reduce APK size of about 30%
|
- Split APK: generate one APK per arch, to reduce APK size of about 30%
|
||||||
|
|
|
@ -38,10 +38,10 @@ When the client receives the new information, it immediately sends another reque
|
||||||
This effectively emulates a server push feature.
|
This effectively emulates a server push feature.
|
||||||
|
|
||||||
The HTTP long Polling can be fine tuned in the **SDK** using two parameters:
|
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)
|
* 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.`
|
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.
|
If no events (or other data) become available before this time elapses, the server will return a response with empty fields.
|
||||||
|
|
|
@ -57,7 +57,7 @@ We get credential (200)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"user_id": "@benoit0816:matrix.org",
|
"user_id": "@alice:matrix.org",
|
||||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
|
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
|
||||||
"home_server": "matrix.org",
|
"home_server": "matrix.org",
|
||||||
"device_id": "GTVREDALBF",
|
"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
|
### Login with Msisdn
|
||||||
|
|
||||||
Not supported yet in RiotX
|
Not supported yet in RiotX
|
||||||
|
|
|
@ -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<Unit> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Unit> {
|
||||||
|
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<LoginFlowResult> {
|
||||||
|
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<RegistrationResult>(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
|
||||||
|
}
|
||||||
|
}
|
|
@ -186,9 +186,9 @@ class CommonTestHelper(context: Context) {
|
||||||
* @param testParams test params about the session
|
* @param testParams test params about the session
|
||||||
* @return the session associated with the existing account
|
* @return the session associated with the existing account
|
||||||
*/
|
*/
|
||||||
private fun logIntoAccount(userId: String,
|
fun logIntoAccount(userId: String,
|
||||||
password: String,
|
password: String,
|
||||||
testParams: SessionTestParams): Session {
|
testParams: SessionTestParams): Session {
|
||||||
val session = logAccountAndSync(userId, password, testParams)
|
val session = logAccountAndSync(userId, password, testParams)
|
||||||
assertNotNull(session)
|
assertNotNull(session)
|
||||||
return session
|
return session
|
||||||
|
@ -263,14 +263,45 @@ class CommonTestHelper(context: Context) {
|
||||||
return session
|
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<LoginFlowResult> {
|
||||||
|
matrix.authenticationService
|
||||||
|
.getLoginFlow(hs, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestFailure: Throwable? = null
|
||||||
|
waitWithLatch { latch ->
|
||||||
|
matrix.authenticationService
|
||||||
|
.getLoginWizard()
|
||||||
|
.login(userName, password, "myDevice", object : TestMatrixCallback<Session>(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
|
* Await for a latch and ensure the result is true
|
||||||
*
|
*
|
||||||
* @param latch
|
* @param latch
|
||||||
* @throws InterruptedException
|
* @throws InterruptedException
|
||||||
*/
|
*/
|
||||||
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
|
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
|
||||||
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
|
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)
|
val latch = CountDownLatch(1)
|
||||||
block(latch)
|
block(latch)
|
||||||
await(latch, timout)
|
await(latch, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform a method with a MatrixCallback to a synchronous method
|
// Transform a method with a MatrixCallback to a synchronous method
|
||||||
|
|
|
@ -33,6 +33,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||||
import org.matrix.olm.OlmManager
|
import org.matrix.olm.OlmManager
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class MatrixConfiguration(
|
data class MatrixConfiguration(
|
||||||
|
@ -61,7 +62,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
||||||
Monarchy.init(context)
|
Monarchy.init(context)
|
||||||
DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
|
DaggerMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
|
||||||
if (context.applicationContext !is Configuration.Provider) {
|
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)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
* This interface defines methods to manage the account. It's implemented at the session level.
|
||||||
*/
|
*/
|
||||||
interface AccountService {
|
interface AccountService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ask the homeserver to change the password.
|
* Ask the homeserver to change the password.
|
||||||
* @param password Current password.
|
* @param password Current password.
|
||||||
* @param newPassword New password
|
* @param newPassword New password
|
||||||
*/
|
*/
|
||||||
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
|
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): 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. <b>This action is irreversible</b>.\n\nDeactivating your account <b>does not by default
|
||||||
|
* cause us to forget messages you have sent</b>. 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<Unit>): Cancelable
|
||||||
}
|
}
|
||||||
|
|
|
@ -728,7 +728,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
if (backUp) {
|
if (backUp) {
|
||||||
maybeBackupKeys()
|
maybeBackupKeys()
|
||||||
}
|
}
|
||||||
|
// Save for next time and for gossiping
|
||||||
|
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(callback)
|
||||||
|
|
|
@ -419,7 +419,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
|
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
|
||||||
|
|
||||||
if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2
|
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
|
// Unsupported algorithm
|
||||||
return IntegrityResult.Error(
|
return IntegrityResult.Error(
|
||||||
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")
|
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.account
|
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 im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
|
@ -30,4 +29,12 @@ internal interface AccountAPI {
|
||||||
*/
|
*/
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
|
||||||
fun changePassword(@Body params: ChangePasswordParams): Call<Unit>
|
fun changePassword(@Body params: ChangePasswordParams): Call<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ internal abstract class AccountModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
|
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindAccountService(service: DefaultAccountService): AccountService
|
abstract fun bindAccountService(service: DefaultAccountService): AccountService
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* limitations under the License.
|
* 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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
|
@ -17,7 +17,6 @@
|
||||||
package im.vector.matrix.android.internal.session.account
|
package im.vector.matrix.android.internal.session.account
|
||||||
|
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
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.auth.registration.RegistrationFlowResponse
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<DeactivateAccountTask.Params, Unit> {
|
||||||
|
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<Unit>(eventBus) {
|
||||||
|
apiCall = accountAPI.deactivate(deactivateAccountParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupSession.handle()
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
|
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
|
||||||
|
private val deactivateAccountTask: DeactivateAccountTask,
|
||||||
private val taskExecutor: TaskExecutor) : AccountService {
|
private val taskExecutor: TaskExecutor) : AccountService {
|
||||||
|
|
||||||
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
|
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deactivateAccount(password: String, eraseAllData: Boolean, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
return deactivateAccountTask
|
||||||
|
.configureWith(DeactivateAccountTask.Params(password, eraseAllData)) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,58 +16,31 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.signout
|
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.Failure
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
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.network.executeRequest
|
||||||
import im.vector.matrix.android.internal.session.SessionModule
|
import im.vector.matrix.android.internal.session.cleanup.CleanupSession
|
||||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import io.realm.Realm
|
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
|
internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
|
||||||
data class Params(
|
data class Params(
|
||||||
val sigOutFromHomeserver: Boolean
|
val signOutFromHomeserver: Boolean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultSignOutTask @Inject constructor(
|
internal class DefaultSignOutTask @Inject constructor(
|
||||||
private val workManagerProvider: WorkManagerProvider,
|
|
||||||
@SessionId private val sessionId: String,
|
|
||||||
private val signOutAPI: SignOutAPI,
|
private val signOutAPI: SignOutAPI,
|
||||||
private val sessionManager: SessionManager,
|
private val eventBus: EventBus,
|
||||||
private val sessionParamsStore: SessionParamsStore,
|
private val cleanupSession: CleanupSession
|
||||||
@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
|
|
||||||
) : SignOutTask {
|
) : SignOutTask {
|
||||||
|
|
||||||
override suspend fun execute(params: SignOutTask.Params) {
|
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
|
// 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...")
|
Timber.d("SignOut: send request...")
|
||||||
try {
|
try {
|
||||||
executeRequest<Unit>(eventBus) {
|
executeRequest<Unit>(eventBus) {
|
||||||
|
@ -87,37 +60,7 @@ internal class DefaultSignOutTask @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("SignOut: release session...")
|
Timber.d("SignOut: cleanup session...")
|
||||||
sessionManager.releaseSession(sessionId)
|
cleanupSession.handle()
|
||||||
|
|
||||||
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)") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ import im.vector.riotx.features.version.VersionProvider
|
||||||
import im.vector.riotx.push.fcm.FcmHelper
|
import im.vector.riotx.push.fcm.FcmHelper
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.concurrent.Executors
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -146,7 +147,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
|
|
||||||
override fun providesMatrixConfiguration() = MatrixConfiguration(BuildConfig.FLAVOR_DESCRIPTION)
|
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 {
|
override fun injector(): VectorComponent {
|
||||||
return vectorComponent
|
return vectorComponent
|
||||||
|
|
|
@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
|
||||||
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
||||||
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
|
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
|
||||||
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
|
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.crosssigning.CrossSigningSettingsFragment
|
||||||
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||||
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
||||||
|
@ -445,8 +446,14 @@ interface FragmentModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
||||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapMigrateBackupFragment::class)
|
@FragmentKey(BootstrapMigrateBackupFragment::class)
|
||||||
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
|
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(DeactivateAccountFragment::class)
|
||||||
|
fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ data class MainActivityArgs(
|
||||||
val clearCache: Boolean = false,
|
val clearCache: Boolean = false,
|
||||||
val clearCredentials: Boolean = false,
|
val clearCredentials: Boolean = false,
|
||||||
val isUserLoggedOut: Boolean = false,
|
val isUserLoggedOut: Boolean = false,
|
||||||
|
val isAccountDeactivated: Boolean = false,
|
||||||
val isSoftLogout: Boolean = false
|
val isSoftLogout: Boolean = false
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() {
|
||||||
clearCache = argsFromIntent?.clearCache ?: false,
|
clearCache = argsFromIntent?.clearCache ?: false,
|
||||||
clearCredentials = argsFromIntent?.clearCredentials ?: false,
|
clearCredentials = argsFromIntent?.clearCredentials ?: false,
|
||||||
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
|
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
|
||||||
|
isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false,
|
||||||
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
|
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -121,7 +123,14 @@ class MainActivity : VectorBaseActivity() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
when {
|
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,
|
!args.isUserLoggedOut,
|
||||||
object : MatrixCallback<Unit> {
|
object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
|
@ -135,7 +144,7 @@ class MainActivity : VectorBaseActivity() {
|
||||||
displayError(failure)
|
displayError(failure)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
args.clearCache -> session.clearCache(
|
args.clearCache -> session.clearCache(
|
||||||
object : MatrixCallback<Unit> {
|
object : MatrixCallback<Unit> {
|
||||||
override fun onSuccess(data: Unit) {
|
override fun onSuccess(data: Unit) {
|
||||||
doLocalCleanup()
|
doLocalCleanup()
|
||||||
|
@ -182,16 +191,16 @@ class MainActivity : VectorBaseActivity() {
|
||||||
private fun startNextActivityAndFinish() {
|
private fun startNextActivityAndFinish() {
|
||||||
val intent = when {
|
val intent = when {
|
||||||
args.clearCredentials
|
args.clearCredentials
|
||||||
&& !args.isUserLoggedOut ->
|
&& (!args.isUserLoggedOut || args.isAccountDeactivated) ->
|
||||||
// User has explicitly asked to log out
|
// User has explicitly asked to log out or deactivated his account
|
||||||
LoginActivity.newIntent(this, null)
|
LoginActivity.newIntent(this, null)
|
||||||
args.isSoftLogout ->
|
args.isSoftLogout ->
|
||||||
// The homeserver has invalidated the token, with a soft logout
|
// The homeserver has invalidated the token, with a soft logout
|
||||||
SoftLogoutActivity.newIntent(this)
|
SoftLogoutActivity.newIntent(this)
|
||||||
args.isUserLoggedOut ->
|
args.isUserLoggedOut ->
|
||||||
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
|
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
|
||||||
SignedOutActivity.newIntent(this)
|
SignedOutActivity.newIntent(this)
|
||||||
sessionHolder.hasActiveSession() ->
|
sessionHolder.hasActiveSession() ->
|
||||||
// We have a session.
|
// We have a session.
|
||||||
// Check it can be opened
|
// Check it can be opened
|
||||||
if (sessionHolder.getActiveSession().isOpenable) {
|
if (sessionHolder.getActiveSession().isOpenable) {
|
||||||
|
@ -200,7 +209,7 @@ class MainActivity : VectorBaseActivity() {
|
||||||
// The token is still invalid
|
// The token is still invalid
|
||||||
SoftLogoutActivity.newIntent(this)
|
SoftLogoutActivity.newIntent(this)
|
||||||
}
|
}
|
||||||
else ->
|
else ->
|
||||||
// First start, or no active session
|
// First start, or no active session
|
||||||
LoginActivity.newIntent(this, null)
|
LoginActivity.newIntent(this, null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.viewModel
|
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.matrix.android.api.session.room.failure.CreateRoomFailure
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.SimpleFragmentActivity
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import kotlinx.android.synthetic.main.activity.*
|
import kotlinx.android.synthetic.main.activity.*
|
||||||
|
import java.net.HttpURLConnection
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
|
@ -91,8 +93,14 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||||
if (error is CreateRoomFailure.CreatedWithTimeout) {
|
if (error is CreateRoomFailure.CreatedWithTimeout) {
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} 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)
|
AlertDialog.Builder(this)
|
||||||
.setMessage(errorFormatter.toHumanReadable(error))
|
.setMessage(message)
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.airbnb.mvrx.Fail
|
||||||
import com.airbnb.mvrx.Loading
|
import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.Uninitialized
|
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.Session
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
@ -56,15 +57,29 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
|
||||||
override fun buildModels() {
|
override fun buildModels() {
|
||||||
val currentState = state ?: return
|
val currentState = state ?: return
|
||||||
val hasSearch = currentState.directorySearchTerm.isNotBlank()
|
val hasSearch = currentState.directorySearchTerm.isNotBlank()
|
||||||
val asyncUsers = currentState.directoryUsers
|
when (val asyncUsers = currentState.directoryUsers) {
|
||||||
when (asyncUsers) {
|
|
||||||
is Uninitialized -> renderEmptyState(false)
|
is Uninitialized -> renderEmptyState(false)
|
||||||
is Loading -> renderLoading()
|
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)
|
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<User>, searchTerms: String): List<User> {
|
||||||
|
return directoryUsers +
|
||||||
|
searchTerms
|
||||||
|
.takeIf { terms -> MatrixPatterns.isUserId(terms) && !directoryUsers.any { it.userId == terms } }
|
||||||
|
?.let { listOf(User(it)) }
|
||||||
|
.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderLoading() {
|
private fun renderLoading() {
|
||||||
loadingItem {
|
loadingItem {
|
||||||
id("loading")
|
id("loading")
|
||||||
|
|
|
@ -20,16 +20,22 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.Observer
|
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.R
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.riotx.core.extensions.observeEvent
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.extensions.replaceFragment
|
import im.vector.riotx.core.extensions.replaceFragment
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
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() {
|
class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private const val REQUEST_4S_SECRET = 100
|
||||||
|
const val SECRET_ALIAS = SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
|
||||||
|
|
||||||
fun intent(context: Context): Intent {
|
fun intent(context: Context): Intent {
|
||||||
return Intent(context, KeysBackupRestoreActivity::class.java)
|
return Intent(context, KeysBackupRestoreActivity::class.java)
|
||||||
}
|
}
|
||||||
|
@ -39,14 +45,20 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
|
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
hideWaitingView()
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
override fun initUiAndData() {
|
override fun initUiAndData() {
|
||||||
super.initUiAndData()
|
super.initUiAndData()
|
||||||
viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java)
|
viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java)
|
||||||
viewModel.initSession(session)
|
viewModel.initSession(session)
|
||||||
viewModel.keyVersionResult.observe(this, Observer { keyVersion ->
|
|
||||||
|
|
||||||
if (keyVersion != null && supportFragmentManager.fragments.isEmpty()) {
|
viewModel.keySourceModel.observe(this, Observer { keySource ->
|
||||||
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
if (keySource != null && !keySource.isInQuadS && supportFragmentManager.fragments.isEmpty()) {
|
||||||
|
val isBackupCreatedFromPassphrase =
|
||||||
|
viewModel.keyVersionResult.value?.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||||
if (isBackupCreatedFromPassphrase) {
|
if (isBackupCreatedFromPassphrase) {
|
||||||
replaceFragment(R.id.container, KeysBackupRestoreFromPassphraseFragment::class.java)
|
replaceFragment(R.id.container, KeysBackupRestoreFromPassphraseFragment::class.java)
|
||||||
} else {
|
} else {
|
||||||
|
@ -69,7 +81,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
if (viewModel.keyVersionResult.value == null) {
|
if (viewModel.keyVersionResult.value == null) {
|
||||||
// We need to fetch from API
|
// We need to fetch from API
|
||||||
viewModel.getLatestVersion(this)
|
viewModel.getLatestVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
|
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
|
||||||
|
@ -78,8 +90,25 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java)
|
addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java)
|
||||||
}
|
}
|
||||||
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
|
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
|
||||||
|
viewModel.keyVersionResult.value?.version?.let {
|
||||||
|
KeysBackupBanner.onRecoverDoneForVersion(this, it)
|
||||||
|
}
|
||||||
replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java)
|
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()
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ class KeysBackupRestoreFromKeyFragment @Inject constructor()
|
||||||
if (value.isNullOrBlank()) {
|
if (value.isNullOrBlank()) {
|
||||||
viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message)
|
viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message)
|
||||||
} else {
|
} else {
|
||||||
viewModel.recoverKeys(requireContext(), sharedViewModel)
|
viewModel.recoverKeys(sharedViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,21 +15,19 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.crypto.keysbackup.restore
|
package im.vector.riotx.features.crypto.keysbackup.restore
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import androidx.lifecycle.viewModelScope
|
||||||
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 im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
|
class KeysBackupRestoreFromKeyViewModel @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
var recoveryCode: MutableLiveData<String> = MutableLiveData()
|
var recoveryCode: MutableLiveData<String> = MutableLiveData()
|
||||||
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
|
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
|
||||||
|
@ -45,66 +43,16 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
|
||||||
recoveryCodeErrorText.value = null
|
recoveryCodeErrorText.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
fun recoverKeys(sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
||||||
val session = sharedViewModel.session
|
sharedViewModel.loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading)))
|
||||||
val keysBackup = session.cryptoService().keysBackupService()
|
|
||||||
|
|
||||||
recoveryCodeErrorText.value = null
|
recoveryCodeErrorText.value = null
|
||||||
val recoveryKey = recoveryCode.value!!
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val recoveryKey = recoveryCode.value!!
|
||||||
val keysVersionResult = sharedViewModel.keyVersionResult.value!!
|
try {
|
||||||
|
sharedViewModel.recoverUsingBackupPass(recoveryKey)
|
||||||
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
|
} catch (failure: Throwable) {
|
||||||
recoveryKey,
|
recoveryCodeErrorText.value = stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt)
|
||||||
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<ImportRoomKeysResult> {
|
|
||||||
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<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.v("##### trustKeysBackupVersion onSuccess")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ import im.vector.riotx.core.extensions.showPassword
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import javax.inject.Inject
|
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
|
override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseF
|
||||||
if (value.isNullOrBlank()) {
|
if (value.isNullOrBlank()) {
|
||||||
viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message)
|
viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message)
|
||||||
} else {
|
} else {
|
||||||
viewModel.recoverKeys(context!!, sharedViewModel)
|
viewModel.recoverKeys(sharedViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,21 +15,18 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.crypto.keysbackup.restore
|
package im.vector.riotx.features.crypto.keysbackup.restore
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import androidx.lifecycle.viewModelScope
|
||||||
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 im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel() {
|
class KeysBackupRestoreFromPassphraseViewModel @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
var passphrase: MutableLiveData<String> = MutableLiveData()
|
var passphrase: MutableLiveData<String> = MutableLiveData()
|
||||||
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
|
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
|
||||||
|
@ -48,71 +45,14 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
|
||||||
passphraseErrorText.value = null
|
passphraseErrorText.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
fun recoverKeys(sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
||||||
val keysBackup = sharedViewModel.session.cryptoService().keysBackupService()
|
|
||||||
|
|
||||||
passphraseErrorText.value = null
|
passphraseErrorText.value = null
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val keysVersionResult = sharedViewModel.keyVersionResult.value!!
|
try {
|
||||||
|
sharedViewModel.recoverUsingBackupPass(passphrase.value!!)
|
||||||
keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
|
} catch (failure: Throwable) {
|
||||||
passphrase.value!!,
|
passphraseErrorText.value = stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)
|
||||||
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<ImportRoomKeysResult> {
|
|
||||||
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<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.v("##### trustKeysBackupVersion onSuccess")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,30 +15,52 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.crypto.keysbackup.restore
|
package im.vector.riotx.features.crypto.keysbackup.restore
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
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.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.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.crypto.model.ImportRoomKeysResult
|
||||||
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.core.utils.LiveEvent
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
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 {
|
companion object {
|
||||||
const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY"
|
const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY"
|
||||||
const val NAVIGATE_TO_SUCCESS = "NAVIGATE_TO_SUCCESS"
|
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
|
lateinit var session: Session
|
||||||
|
|
||||||
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
|
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
|
||||||
|
|
||||||
|
var keySourceModel: MutableLiveData<KeySource> = MutableLiveData()
|
||||||
|
|
||||||
private var _keyVersionResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
|
private var _keyVersionResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
|
||||||
val keyVersionResultError: LiveData<LiveEvent<String>>
|
val keyVersionResultError: LiveData<LiveEvent<String>>
|
||||||
get() = _keyVersionResultError
|
get() = _keyVersionResultError
|
||||||
|
@ -62,30 +84,192 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() {
|
||||||
this.session = session
|
this.session = session
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLatestVersion(context: Context) {
|
val progressObserver = object : StepProgressListener {
|
||||||
val keysBackup = session.cryptoService().keysBackupService()
|
override fun onStepProgress(step: StepProgressListener.Step) {
|
||||||
|
when (step) {
|
||||||
loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restore_is_getting_backup_version))
|
is StepProgressListener.Step.ComputingKey -> {
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message)
|
||||||
keysBackup.getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
|
+ "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
|
||||||
override fun onSuccess(data: KeysVersionResult?) {
|
step.progress,
|
||||||
loadingEvent.value = null
|
step.total))
|
||||||
if (data?.version.isNullOrBlank()) {
|
}
|
||||||
// should not happen
|
is StepProgressListener.Step.DownloadingKey -> {
|
||||||
_keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, ""))
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message)
|
||||||
} else {
|
+ "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
|
||||||
keyVersionResult.value = data
|
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) {
|
fun getLatestVersion() {
|
||||||
loadingEvent.value = null
|
val keysBackup = session.cryptoService().keysBackupService()
|
||||||
_keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage))
|
|
||||||
|
|
||||||
// TODO For network error
|
loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version))
|
||||||
// _keyVersionResultError.value = LiveEvent(context.getString(R.string.network_error_please_check_and_retry))
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val version = awaitCallback<KeysVersionResult?> {
|
||||||
|
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<Map<String, String>>(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<ImportRoomKeysResult> {
|
||||||
|
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<ImportRoomKeysResult> {
|
||||||
|
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<Unit> {
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
Timber.v("##### trustKeysBackupVersion onSuccess")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToRecoverWithKey() {
|
fun moveToRecoverWithKey() {
|
||||||
|
@ -94,6 +278,6 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
fun didRecoverSucceed(result: ImportRoomKeysResult) {
|
fun didRecoverSucceed(result: ImportRoomKeysResult) {
|
||||||
importKeyResult = result
|
importKeyResult = result
|
||||||
_navigateEvent.value = LiveEvent(NAVIGATE_TO_SUCCESS)
|
_navigateEvent.postValue(LiveEvent(NAVIGATE_TO_SUCCESS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
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.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.MatrixItem
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
|
@ -73,7 +74,8 @@ data class VerificationBottomSheetViewState(
|
||||||
val isMe: Boolean = false,
|
val isMe: Boolean = false,
|
||||||
val currentDeviceCanCrossSign: Boolean = false,
|
val currentDeviceCanCrossSign: Boolean = false,
|
||||||
val userWantsToCancel: Boolean = false,
|
val userWantsToCancel: Boolean = false,
|
||||||
val userThinkItsNotHim: Boolean = false
|
val userThinkItsNotHim: Boolean = false,
|
||||||
|
val quadSContainsSecrets: Boolean = true
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
class VerificationBottomSheetViewModel @AssistedInject constructor(
|
class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
|
@ -116,6 +118,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
|
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 {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
otherUserMxItem = userItem?.toMatrixItem(),
|
otherUserMxItem = userItem?.toMatrixItem(),
|
||||||
|
@ -126,7 +132,8 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
selfVerificationMode = selfVerificationMode,
|
selfVerificationMode = selfVerificationMode,
|
||||||
roomId = args.roomId,
|
roomId = args.roomId,
|
||||||
isMe = args.otherUserId == session.myUserId,
|
isMe = args.otherUserId == session.myUserId,
|
||||||
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign()
|
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
|
||||||
|
quadSContainsSecrets = ssssOk
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,14 +65,16 @@ class VerificationRequestController @Inject constructor(
|
||||||
title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
|
title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
|
||||||
}
|
}
|
||||||
|
|
||||||
bottomSheetVerificationActionItem {
|
if (state.quadSContainsSecrets) {
|
||||||
id("passphrase")
|
bottomSheetVerificationActionItem {
|
||||||
title(stringProvider.getString(R.string.verification_cannot_access_other_session))
|
id("passphrase")
|
||||||
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
title(stringProvider.getString(R.string.verification_cannot_access_other_session))
|
||||||
subTitle(stringProvider.getString(R.string.verification_use_passphrase))
|
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
iconRes(R.drawable.ic_arrow_right)
|
subTitle(stringProvider.getString(R.string.verification_use_passphrase))
|
||||||
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
listener { listener?.onClickRecoverFromPassphrase() }
|
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
|
||||||
|
listener { listener?.onClickRecoverFromPassphrase() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val styledText =
|
val styledText =
|
||||||
|
|
|
@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
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.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.replaceFragment
|
import im.vector.riotx.core.extensions.replaceFragment
|
||||||
import im.vector.riotx.core.platform.ToolbarConfigurable
|
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||||
|
@ -92,6 +93,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
.subscribe { sharedAction ->
|
.subscribe { sharedAction ->
|
||||||
when (sharedAction) {
|
when (sharedAction) {
|
||||||
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
||||||
|
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
is HomeActivitySharedAction.OpenGroup -> {
|
is HomeActivitySharedAction.OpenGroup -> {
|
||||||
drawerLayout.closeDrawer(GravityCompat.START)
|
drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||||
|
@ -99,7 +101,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
||||||
BootstrapBottomSheet.show(supportFragmentManager, true)
|
BootstrapBottomSheet.show(supportFragmentManager, true)
|
||||||
}
|
}
|
||||||
}
|
}.exhaustive
|
||||||
}
|
}
|
||||||
.disposeOnDestroy()
|
.disposeOnDestroy()
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import im.vector.riotx.core.platform.VectorSharedAction
|
||||||
*/
|
*/
|
||||||
sealed class HomeActivitySharedAction : VectorSharedAction {
|
sealed class HomeActivitySharedAction : VectorSharedAction {
|
||||||
object OpenDrawer : HomeActivitySharedAction()
|
object OpenDrawer : HomeActivitySharedAction()
|
||||||
|
object CloseDrawer : HomeActivitySharedAction()
|
||||||
object OpenGroup : HomeActivitySharedAction()
|
object OpenGroup : HomeActivitySharedAction()
|
||||||
object PromptForSecurityBootstrap : HomeActivitySharedAction()
|
object PromptForSecurityBootstrap : HomeActivitySharedAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,15 @@ class HomeDrawerFragment @Inject constructor(
|
||||||
private val avatarRenderer: AvatarRenderer
|
private val avatarRenderer: AvatarRenderer
|
||||||
) : VectorBaseFragment() {
|
) : VectorBaseFragment() {
|
||||||
|
|
||||||
|
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_home_drawer
|
override fun getLayoutResId() = R.layout.fragment_home_drawer
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
|
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
|
||||||
}
|
}
|
||||||
|
@ -49,11 +54,13 @@ class HomeDrawerFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
homeDrawerHeaderSettingsView.setOnClickListener {
|
homeDrawerHeaderSettingsView.setOnClickListener {
|
||||||
|
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
|
||||||
navigator.openSettings(requireActivity())
|
navigator.openSettings(requireActivity())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug menu
|
// Debug menu
|
||||||
homeDrawerHeaderDebugView.setOnClickListener {
|
homeDrawerHeaderDebugView.setOnClickListener {
|
||||||
|
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
|
||||||
navigator.openDebug(requireActivity())
|
navigator.openDebug(requireActivity())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.login
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import androidx.autofill.HintConstants
|
import androidx.autofill.HintConstants
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import butterknife.OnClick
|
import butterknife.OnClick
|
||||||
|
@ -40,7 +41,8 @@ import kotlinx.android.synthetic.main.fragment_login.*
|
||||||
import javax.inject.Inject
|
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.
|
* - the user is asked for login (or email) and password to sign in to a homeserver.
|
||||||
* - He also can reset his password
|
* - He also can reset his password
|
||||||
* In signup mode:
|
* In signup mode:
|
||||||
|
@ -49,6 +51,7 @@ import javax.inject.Inject
|
||||||
class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||||
|
|
||||||
private var passwordShown = false
|
private var passwordShown = false
|
||||||
|
private var isSignupMode = false
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_login
|
override fun getLayoutResId() = R.layout.fragment_login
|
||||||
|
|
||||||
|
@ -57,6 +60,14 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||||
|
|
||||||
setupSubmitButton()
|
setupSubmitButton()
|
||||||
setupPasswordReveal()
|
setupPasswordReveal()
|
||||||
|
|
||||||
|
passwordField.setOnEditorActionListener { _, actionId, _ ->
|
||||||
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
submit()
|
||||||
|
return@setOnEditorActionListener true
|
||||||
|
}
|
||||||
|
return@setOnEditorActionListener false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAutoFill(state: LoginViewState) {
|
private fun setupAutoFill(state: LoginViewState) {
|
||||||
|
@ -82,7 +93,20 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||||
val login = loginField.text.toString()
|
val login = loginField.text.toString()
|
||||||
val password = passwordField.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() {
|
private fun cleanupUi() {
|
||||||
|
@ -190,6 +214,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateWithState(state: LoginViewState) {
|
override fun updateWithState(state: LoginViewState) {
|
||||||
|
isSignupMode = state.signMode == SignMode.SignUp
|
||||||
|
|
||||||
setupUi(state)
|
setupUi(state)
|
||||||
setupAutoFill(state)
|
setupAutoFill(state)
|
||||||
setupButtons(state)
|
setupButtons(state)
|
||||||
|
|
|
@ -191,7 +191,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
||||||
backgroundHandler.removeCallbacksAndMessages(null)
|
backgroundHandler.removeCallbacksAndMessages(null)
|
||||||
backgroundHandler.postDelayed(
|
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)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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_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_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"
|
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 SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"
|
||||||
|
|
||||||
private const val MEDIA_SAVING_3_DAYS = 0
|
private const val MEDIA_SAVING_3_DAYS = 0
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Intent
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import im.vector.matrix.android.api.failure.GlobalError
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
|
|
||||||
private var keyToHighlight: String? = null
|
private var keyToHighlight: String? = null
|
||||||
|
|
||||||
|
var ignoreInvalidTokenError = false
|
||||||
|
|
||||||
@Inject lateinit var session: Session
|
@Inject lateinit var session: Session
|
||||||
|
|
||||||
override fun injectWith(injector: ScreenComponent) {
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
|
||||||
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
|
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)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
|
||||||
else ->
|
else ->
|
||||||
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
|
||||||
|
@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
|
||||||
return keyToHighlight
|
return keyToHighlight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
|
||||||
|
if (ignoreInvalidTokenError) {
|
||||||
|
Timber.w("Ignoring invalid token global error")
|
||||||
|
} else {
|
||||||
|
super.handleInvalidToken(globalError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
|
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
|
||||||
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
|
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
|
||||||
|
|
|
@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deactivate account section
|
|
||||||
|
|
||||||
// deactivate account
|
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_DEACTIVATE_ACCOUNT_KEY)!!
|
|
||||||
.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
activity?.let {
|
|
||||||
notImplemented()
|
|
||||||
// TODO startActivity(DeactivateAccountActivity.getIntent(it))
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(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<Unit> {
|
||||||
|
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<DeactivateAccountViewModel, DeactivateAccountViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: DeactivateAccountViewState): DeactivateAccountViewModel? {
|
||||||
|
val fragment: DeactivateAccountFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.viewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
vector/src/main/res/layout/fragment_deactivate_account.xml
Normal file
106
vector/src/main/res/layout/fragment_deactivate_account.xml
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deactivateAccountContent"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/deactivate_account_content"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/deactivateAccountEraseCheckbox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/deactivate_account_delete_checkbox"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/deactivateAccountPromptPassword"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/deactivate_account_prompt_password"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/deactivateAccountPasswordContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/auth_password_placeholder"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:nextFocusDown="@+id/login_password"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPromptPassword">
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/deactivateAccountPasswordTil"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/login_signup_password_hint"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:errorIconDrawable="@null">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/deactivateAccountPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingEnd="48dp"
|
||||||
|
android:paddingRight="48dp"
|
||||||
|
tools:ignore="RtlSymmetry" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/deactivateAccountPasswordReveal"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_eye_black"
|
||||||
|
android:tint="?attr/colorAccent"
|
||||||
|
tools:contentDescription="@string/a11y_show_password" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/deactivateAccountSubmit"
|
||||||
|
style="@style/VectorButtonStyleDestructive"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/deactivate_account_submit"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
|
@ -82,6 +82,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ems="10"
|
android:ems="10"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:paddingEnd="48dp"
|
android:paddingEnd="48dp"
|
||||||
|
@ -104,19 +105,18 @@
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="22dp"
|
android:layout_marginTop="22dp">
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/forgetPasswordButton"
|
android:id="@+id/forgetPasswordButton"
|
||||||
style="@style/Style.Vector.Login.Button.Text"
|
style="@style/Style.Vector.Login.Button.Text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="start"
|
android:text="@string/auth_forgot_password"
|
||||||
android:text="@string/auth_forgot_password" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/loginSubmit"
|
android:id="@+id/loginSubmit"
|
||||||
|
@ -124,12 +124,12 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_gravity="end"
|
|
||||||
android:text="@string/auth_login"
|
android:text="@string/auth_login"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:enabled="false"
|
tools:enabled="false"
|
||||||
tools:ignore="RelativeOverlap" />
|
tools:ignore="RelativeOverlap" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@
|
||||||
|
|
||||||
|
|
||||||
<!-- BEGIN Strings added by Benoit -->
|
<!-- BEGIN Strings added by Benoit -->
|
||||||
|
<string name="error_empty_field_choose_user_name">Please choose a username.</string>
|
||||||
|
<string name="error_empty_field_choose_password">Please choose a password.</string>
|
||||||
<!-- END Strings added by Benoit -->
|
<!-- END Strings added by Benoit -->
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,4 +31,5 @@
|
||||||
|
|
||||||
<!-- END Strings added by Others -->
|
<!-- END Strings added by Others -->
|
||||||
|
|
||||||
|
<string name="create_room_dm_failure">"We couldn't create your DM. Please check the users you want to invite and try again."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -97,14 +97,13 @@
|
||||||
|
|
||||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_deactivate_account_section">
|
||||||
android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY"
|
|
||||||
android:title="@string/settings_deactivate_account_section"
|
|
||||||
app:isPreferenceVisible="@bool/false_not_implemented">
|
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreference
|
<im.vector.riotx.core.preference.VectorPreference
|
||||||
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"
|
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"
|
||||||
android:title="@string/settings_deactivate_my_account" />
|
android:persistent="false"
|
||||||
|
android:title="@string/settings_deactivate_my_account"
|
||||||
|
app:fragment="im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment" />
|
||||||
|
|
||||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue