Merge pull request #1265 from vector-im/feature/deactivate

Deactivate account using password
This commit is contained in:
Benoit Marty 2020-04-23 17:30:08 +02:00 committed by GitHub
commit e8a91eab88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 856 additions and 118 deletions

View file

@ -7,6 +7,7 @@ Features ✨:
- Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
- Save media files to Gallery (#973)
- Account deactivation (with password only) (#35)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)

View file

@ -38,10 +38,10 @@ When the client receives the new information, it immediately sends another reque
This effectively emulates a server push feature.
The HTTP long Polling can be fine tuned in the **SDK** using two parameters:
* timout (Sync request timeout)
* timeout (Sync request timeout)
* delay (Delay between each sync)
**timeout** is a server paramter, defined by:
**timeout** is a server parameter, defined by:
```
The maximum time to wait, in milliseconds, before returning this request.`
If no events (or other data) become available before this time elapses, the server will return a response with empty fields.

View file

@ -57,7 +57,7 @@ We get credential (200)
```json
{
"user_id": "@benoit0816:matrix.org",
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
@ -128,6 +128,8 @@ We get the credentials (200)
}
```
It's worth noting that the response from the homeserver contains the userId of Alice.
### Login with Msisdn
Not supported yet in RiotX

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -183,9 +183,9 @@ class CommonTestHelper(context: Context) {
* @param testParams test params about the session
* @return the session associated with the existing account
*/
private fun logIntoAccount(userId: String,
password: String,
testParams: SessionTestParams): Session {
fun logIntoAccount(userId: String,
password: String,
testParams: SessionTestParams): Session {
val session = logAccountAndSync(userId, password, testParams)
assertNotNull(session)
return session
@ -260,14 +260,45 @@ class CommonTestHelper(context: Context) {
return session
}
/**
* Log into the account and expect an error
*
* @param userName the account username
* @param password the password
*/
fun logAccountWithError(userName: String,
password: String): Throwable {
val hs = createHomeServerConfig()
doSync<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
*
* @param latch
* @throws InterruptedException
*/
fun await(latch: CountDownLatch, timout: Long? = TestConstants.timeOutMillis) {
assertTrue(latch.await(timout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
assertTrue(latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS))
}
fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
@ -282,10 +313,10 @@ class CommonTestHelper(context: Context) {
}
}
fun waitWithLatch(timout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, block: (CountDownLatch) -> Unit) {
val latch = CountDownLatch(1)
block(latch)
await(latch, timout)
await(latch, timeout)
}
// Transform a method with a MatrixCallback to a synchronous method

View file

@ -23,11 +23,28 @@ import im.vector.matrix.android.api.util.Cancelable
* This interface defines methods to manage the account. It's implemented at the session level.
*/
interface AccountService {
/**
* Ask the homeserver to change the password.
* @param password Current password.
* @param newPassword New password
*/
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<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
}

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
@ -30,4 +29,12 @@ internal interface AccountAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
fun changePassword(@Body params: ChangePasswordParams): Call<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>
}

View file

@ -39,6 +39,9 @@ internal abstract class AccountModule {
@Binds
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
@Binds
abstract fun bindDeactivateAccountTask(task: DefaultDeactivateAccountTask): DeactivateAccountTask
@Binds
abstract fun bindAccountService(service: DefaultAccountService): AccountService
}

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.api.session.account.model
package im.vector.matrix.android.internal.session.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.session.account
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId

View file

@ -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
)
}
}
}

View file

@ -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()
}
}

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
private val deactivateAccountTask: DeactivateAccountTask,
private val taskExecutor: TaskExecutor) : AccountService {
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
@ -33,4 +34,12 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
}
.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)
}
}

View file

@ -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)") }
}
}
}

View file

@ -16,58 +16,31 @@
package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.cleanup.CleanupSession
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.io.File
import java.net.HttpURLConnection
import javax.inject.Inject
internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params(
val sigOutFromHomeserver: Boolean
val signOutFromHomeserver: Boolean
)
}
internal class DefaultSignOutTask @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
@SessionId private val sessionId: String,
private val signOutAPI: SignOutAPI,
private val sessionManager: SessionManager,
private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@SessionFilesDirectory private val sessionFiles: File,
@SessionCacheDirectory private val sessionCache: File,
private val realmKeysUtils: RealmKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String,
private val eventBus: EventBus
private val eventBus: EventBus,
private val cleanupSession: CleanupSession
) : SignOutTask {
override suspend fun execute(params: SignOutTask.Params) {
// It should be done even after a soft logout, to be sure the deviceId is deleted on the
if (params.sigOutFromHomeserver) {
if (params.signOutFromHomeserver) {
Timber.d("SignOut: send request...")
try {
executeRequest<Unit>(eventBus) {
@ -87,37 +60,7 @@ internal class DefaultSignOutTask @Inject constructor(
}
}
Timber.d("SignOut: release session...")
sessionManager.releaseSession(sessionId)
Timber.d("SignOut: cancel pending works...")
workManagerProvider.cancelAllWorks()
Timber.d("SignOut: delete session params...")
sessionParamsStore.delete(sessionId)
Timber.d("SignOut: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("SignOut: clear crypto data...")
clearCryptoDataTask.execute(Unit)
Timber.d("SignOut: clear file system")
sessionFiles.deleteRecursively()
sessionCache.deleteRecursively()
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
// Sanity check
if (BuildConfig.DEBUG) {
Realm.getGlobalInstanceCount(realmSessionConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for session has not been closed ($it)") }
Realm.getGlobalInstanceCount(realmCryptoConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
}
Timber.d("SignOut: cleanup session...")
cleanupSession.handle()
}
}

View file

@ -81,6 +81,7 @@ import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFra
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.account.deactivation.DeactivateAccountFragment
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.devtools.AccountDataFragment
@ -445,8 +446,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BootstrapMigrateBackupFragment::class)
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
@Binds
@IntoMap
@FragmentKey(DeactivateAccountFragment::class)
fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
}

View file

@ -49,6 +49,7 @@ data class MainActivityArgs(
val clearCache: Boolean = false,
val clearCredentials: Boolean = false,
val isUserLoggedOut: Boolean = false,
val isAccountDeactivated: Boolean = false,
val isSoftLogout: Boolean = false
) : Parcelable
@ -110,6 +111,7 @@ class MainActivity : VectorBaseActivity() {
clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
isAccountDeactivated = argsFromIntent?.isAccountDeactivated ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
)
}
@ -121,7 +123,14 @@ class MainActivity : VectorBaseActivity() {
return
}
when {
args.clearCredentials -> session.signOut(
args.isAccountDeactivated -> {
// Just do the local cleanup
Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession()
doLocalCleanup()
startNextActivityAndFinish()
}
args.clearCredentials -> session.signOut(
!args.isUserLoggedOut,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
@ -135,7 +144,7 @@ class MainActivity : VectorBaseActivity() {
displayError(failure)
}
})
args.clearCache -> session.clearCache(
args.clearCache -> session.clearCache(
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanup()
@ -182,16 +191,16 @@ class MainActivity : VectorBaseActivity() {
private fun startNextActivityAndFinish() {
val intent = when {
args.clearCredentials
&& !args.isUserLoggedOut ->
// User has explicitly asked to log out
&& (!args.isUserLoggedOut || args.isAccountDeactivated) ->
// User has explicitly asked to log out or deactivated his account
LoginActivity.newIntent(this, null)
args.isSoftLogout ->
args.isSoftLogout ->
// The homeserver has invalidated the token, with a soft logout
SoftLogoutActivity.newIntent(this)
args.isUserLoggedOut ->
args.isUserLoggedOut ->
// the homeserver has invalidated the token (password changed, device deleted, other security reasons)
SignedOutActivity.newIntent(this)
sessionHolder.hasActiveSession() ->
sessionHolder.hasActiveSession() ->
// We have a session.
// Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) {
@ -200,7 +209,7 @@ class MainActivity : VectorBaseActivity() {
// The token is still invalid
SoftLogoutActivity.newIntent(this)
}
else ->
else ->
// First start, or no active session
LoginActivity.newIntent(this, null)
}

View file

@ -35,6 +35,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
@ -92,6 +93,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.subscribe { sharedAction ->
when (sharedAction) {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
@ -99,7 +101,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
BootstrapBottomSheet.show(supportFragmentManager, true)
}
}
}.exhaustive
}
.disposeOnDestroy()

View file

@ -23,6 +23,7 @@ import im.vector.riotx.core.platform.VectorSharedAction
*/
sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
object CloseDrawer : HomeActivitySharedAction()
object OpenGroup : HomeActivitySharedAction()
object PromptForSecurityBootstrap : HomeActivitySharedAction()
}

View file

@ -33,10 +33,15 @@ class HomeDrawerFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
override fun getLayoutResId() = R.layout.fragment_home_drawer
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
if (savedInstanceState == null) {
replaceChildFragment(R.id.homeDrawerGroupListContainer, GroupListFragment::class.java)
}
@ -49,11 +54,13 @@ class HomeDrawerFragment @Inject constructor(
}
}
homeDrawerHeaderSettingsView.setOnClickListener {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openSettings(requireActivity())
}
// Debug menu
homeDrawerHeaderDebugView.setOnClickListener {
sharedActionViewModel.post(HomeActivitySharedAction.CloseDrawer)
navigator.openDebug(requireActivity())
}
}

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.login
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.autofill.HintConstants
import androidx.core.view.isVisible
import butterknife.OnClick
@ -40,7 +41,8 @@ import kotlinx.android.synthetic.main.fragment_login.*
import javax.inject.Inject
/**
* In this screen, in signin mode:
* In this screen:
* In signin mode:
* - the user is asked for login (or email) and password to sign in to a homeserver.
* - He also can reset his password
* In signup mode:
@ -49,6 +51,7 @@ import javax.inject.Inject
class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false
private var isSignupMode = false
override fun getLayoutResId() = R.layout.fragment_login
@ -57,6 +60,14 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
setupSubmitButton()
setupPasswordReveal()
passwordField.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
}
private fun setupAutoFill(state: LoginViewState) {
@ -82,7 +93,20 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
val login = loginField.text.toString()
val password = passwordField.text.toString()
loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx)))
// This can be called by the IME action, so deal with empty cases
var error = 0
if (login.isEmpty()) {
loginFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_user_name else R.string.error_empty_field_enter_user_name)
error++
}
if (password.isEmpty()) {
passwordFieldTil.error = getString(if (isSignupMode) R.string.error_empty_field_choose_password else R.string.error_empty_field_your_password)
error++
}
if (error == 0) {
loginViewModel.handle(LoginAction.LoginOrRegister(login, password, getString(R.string.login_mobile_device_riotx)))
}
}
private fun cleanupUi() {
@ -190,6 +214,8 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
}
override fun updateWithState(state: LoginViewState) {
isSignupMode = state.signMode == SignMode.SignUp
setupUi(state)
setupAutoFill(state)
setupButtons(state)

View file

@ -191,7 +191,12 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
backgroundHandler.removeCallbacksAndMessages(null)
backgroundHandler.postDelayed(
{
refreshNotificationDrawerBg()
try {
refreshNotificationDrawerBg()
} catch (throwable: Throwable) {
// It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer
Timber.w(throwable, "refreshNotificationDrawerBg failure")
}
}, 200)
}

View file

@ -159,7 +159,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY"
private const val DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK"
private const val DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY"
const val SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY"
private const val SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY"
private const val MEDIA_SAVING_3_DAYS = 0

View file

@ -20,6 +20,7 @@ import android.content.Intent
import androidx.fragment.app.FragmentManager
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@ -43,6 +44,8 @@ class VectorSettingsActivity : VectorBaseActivity(),
private var keyToHighlight: String? = null
var ignoreInvalidTokenError = false
@Inject lateinit var session: Session
override fun injectWith(injector: ScreenComponent) {
@ -57,7 +60,7 @@ class VectorSettingsActivity : VectorBaseActivity(),
when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY ->
replaceFragment(R.id.vector_settings_page, VectorSettingsSecurityPrivacyFragment::class.java, null, FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
@ -110,6 +113,14 @@ class VectorSettingsActivity : VectorBaseActivity(),
return keyToHighlight
}
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
if (ignoreInvalidTokenError) {
Timber.w("Ignoring invalid token global error")
} else {
super.handleInvalidToken(globalError)
}
}
companion object {
fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }

View file

@ -234,19 +234,6 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
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) {

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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)
}
}
}

View 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>

View file

@ -82,6 +82,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
@ -104,19 +105,18 @@
</FrameLayout>
<RelativeLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:orientation="horizontal">
android:layout_marginTop="22dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/forgetPasswordButton"
style="@style/Style.Vector.Login.Button.Text"
android:layout_width="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
android:id="@+id/loginSubmit"
@ -124,12 +124,12 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:text="@string/auth_login"
app:layout_constraintEnd_toEndOf="parent"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View file

@ -11,7 +11,8 @@
<!-- 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 -->

View file

@ -97,14 +97,13 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory
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.VectorPreferenceCategory android:title="@string/settings_deactivate_account_section">
<im.vector.riotx.core.preference.VectorPreference
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>