From 76995604585218e5bd421f6050ebbacc68a5a808 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Dec 2019 18:33:16 +0100 Subject: [PATCH] Soft Logout - WIP --- .idea/dictionaries/bmarty.xml | 1 + CHANGES.md | 2 +- .../matrix/android/api/session/Session.kt | 2 +- .../api/session/signout/SignOutService.kt | 10 +- .../android/api/session/sync/SyncState.kt | 1 + .../internal/auth/SessionParamsStore.kt | 3 + .../auth/db/RealmSessionParamsStore.kt | 28 +++ .../network/AccessTokenInterceptor.kt | 22 ++- .../session/signout/DefaultSignOutService.kt | 11 ++ .../session/signout/SignInAgainTask.kt | 55 ++++++ .../internal/session/signout/SignOutAPI.kt | 15 ++ .../internal/session/signout/SignOutModule.kt | 7 +- .../internal/session/sync/job/SyncThread.kt | 13 +- vector/src/main/AndroidManifest.xml | 3 + .../im/vector/riotx/core/di/FragmentModule.kt | 6 + .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../riotx/core/platform/VectorBaseActivity.kt | 2 +- .../im/vector/riotx/features/MainActivity.kt | 34 ++-- .../features/signout/SoftLogoutAction.kt | 24 +++ .../features/signout/SoftLogoutActivity.kt | 82 +++++++++ .../features/signout/SoftLogoutFragment.kt | 165 ++++++++++++++++++ .../features/signout/SoftLogoutViewModel.kt | 112 ++++++++++++ .../features/signout/SoftLogoutViewState.kt | 28 +++ .../main/res/layout/fragment_soft_logout.xml | 145 +++++++++++++++ vector/src/main/res/values/strings_riotX.xml | 14 ++ .../src/main/res/values/text_appearances.xml | 4 + 26 files changed, 765 insertions(+), 27 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt create mode 100644 vector/src/main/res/layout/fragment_soft_logout.xml diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml index 00c6f6c865..a34f4219d9 100644 --- a/.idea/dictionaries/bmarty.xml +++ b/.idea/dictionaries/bmarty.xml @@ -18,6 +18,7 @@ pbkdf pkcs signin + signout signup diff --git a/CHANGES.md b/CHANGES.md index d2d1902205..b0d98f94ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,7 @@ Changes in RiotX 0.11.0 (2019-XX-XX) =================================================== Features ✨: - - + - Implement soft logout (#281) Improvements 🙌: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 257924e6b4..13b9fe07d6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -81,7 +81,7 @@ interface Session : /** * Launches infinite periodic background syncs - * THis does not work in doze mode :/ + * This does not work in doze mode :/ * If battery optimization is on it can work in app standby but that's all :/ */ fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt index 1ee176c9e4..2c2e0f0d61 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt @@ -20,10 +20,18 @@ import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.util.Cancelable /** - * This interface defines a method to sign out. It's implemented at the session level. + * This interface defines a method to sign out, or to renew the token. It's implemented at the session level. */ interface SignOutService { + /** + * Ask the homeserver for a new access token. + * The same deviceId will be used + */ + fun signInAgain(password: String, + deviceName: String, + callback: MatrixCallback): Cancelable + /** * Sign out, and release the session, clear all the session data, including crypto data * @param sigOutFromHomeserver true if the sign out request has to be done diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt index 4db40b2c55..6656d52e5e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt @@ -23,4 +23,5 @@ sealed class SyncState { object KILLING : SyncState() object KILLED : SyncState() object NO_NETWORK : SyncState() + object INVALID_TOKEN : SyncState() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt index 17bcb9dc81..e57883f731 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.SessionParams internal interface SessionParamsStore { @@ -28,6 +29,8 @@ internal interface SessionParamsStore { suspend fun save(sessionParams: SessionParams) + suspend fun updateCredentials(newCredentials: Credentials) + suspend fun delete(userId: String) suspend fun deleteAll() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt index 1b15995ae6..6d0430da05 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.auth.db +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.database.awaitTransaction @@ -75,6 +76,33 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S } } + override suspend fun updateCredentials(newCredentials: Credentials) { + awaitTransaction(realmConfiguration) { realm -> + val currentSessionParams = realm + .where(SessionParamsEntity::class.java) + .equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId) + .findAll() + .map { mapper.map(it) } + .firstOrNull() + + if (currentSessionParams == null) { + // Should not happen + "Session param not found for user ${newCredentials.userId}" + .let { Timber.w(it) } + .also { error(it) } + } else { + val newSessionParams = currentSessionParams.copy( + credentials = newCredentials + ) + + val entity = mapper.map(newSessionParams) + if (entity != null) { + realm.insertOrUpdate(entity) + } + } + } + } + override suspend fun delete(userId: String) { awaitTransaction(realmConfiguration) { it.where(SessionParamsEntity::class.java) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt index 2630560e45..e0257bfc83 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt @@ -16,19 +16,29 @@ package im.vector.matrix.android.internal.network -import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.di.UserId import okhttp3.Interceptor import okhttp3.Response import javax.inject.Inject -internal class AccessTokenInterceptor @Inject constructor(private val credentials: Credentials) : Interceptor { +internal class AccessTokenInterceptor @Inject constructor( + @UserId private val userId: String, + private val sessionParamsStore: SessionParamsStore) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { var request = chain.request() - val newRequestBuilder = request.newBuilder() - // Add the access token to all requests if it is set - newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken) - request = newRequestBuilder.build() + + accessToken?.let { + val newRequestBuilder = request.newBuilder() + // Add the access token to all requests if it is set + newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it") + request = newRequestBuilder.build() + } + return chain.proceed(request) } + + private val accessToken + get() = sessionParamsStore.get(userId)?.credentials?.accessToken } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt index 40a408889d..a992e67dec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt @@ -24,8 +24,19 @@ import im.vector.matrix.android.internal.task.configureWith import javax.inject.Inject internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, + private val signInAgainTask: SignInAgainTask, private val taskExecutor: TaskExecutor) : SignOutService { + override fun signInAgain(password: String, + deviceName: String, + callback: MatrixCallback): Cancelable { + return signInAgainTask + .configureWith(SignInAgainTask.Params(password, deviceName)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun signOut(sigOutFromHomeserver: Boolean, callback: MatrixCallback): Cancelable { return signOutTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt new file mode 100644 index 0000000000..2bdac6664f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 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.signout + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.internal.auth.SessionParamsStore +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface SignInAgainTask : Task { + data class Params( + val password: String, + val deviceName: String + ) +} + +internal class DefaultSignInAgainTask @Inject constructor( + private val signOutAPI: SignOutAPI, + private val sessionParams: SessionParams, + private val sessionParamsStore: SessionParamsStore) : SignInAgainTask { + + override suspend fun execute(params: SignInAgainTask.Params) { + val newCredentials = executeRequest { + apiCall = signOutAPI.loginAgain( + PasswordLoginParams.userIdentifier( + // Reuse the same userId + sessionParams.credentials.userId, + params.password, + params.deviceName, + // Reuse the same deviceId + sessionParams.credentials.deviceId + ) + ) + } + + sessionParamsStore.updateCredentials(newCredentials) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt index 2f19fee847..9db7c7d915 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt @@ -16,12 +16,27 @@ package im.vector.matrix.android.internal.session.signout +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call +import retrofit2.http.Body +import retrofit2.http.Headers import retrofit2.http.POST internal interface SignOutAPI { + /** + * Attempt to login again to the same account. + * Set all the timeouts to 1 minute + * It is similar to [AuthAPI.login] + * + * @param loginParams the login parameters + */ + @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000") + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login") + fun loginAgain(@Body loginParams: PasswordLoginParams): Call + /** * Invalidate the access token, so that it can no longer be used for authorization. */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt index c55c82274d..590729837b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt @@ -37,8 +37,11 @@ internal abstract class SignOutModule { } @Binds - abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask + abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask @Binds - abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService + abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask + + @Binds + abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index 8b149f57d5..19cde7d597 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -50,6 +50,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private var cancelableTask: Cancelable? = null private var isStarted = false + private var isTokenValid = true init { updateStateTo(SyncState.IDLE) @@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, if (!isStarted) { Timber.v("Resume sync...") isStarted = true + // Check again the token validity + isTokenValid = true lock.notify() } } @@ -113,6 +116,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, updateStateTo(SyncState.PAUSED) synchronized(lock) { lock.wait() } Timber.v("...unlocked") + } else if (!isTokenValid) { + Timber.v("Token is invalid. Waiting...") + updateStateTo(SyncState.INVALID_TOKEN) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") } else { if (state !is SyncState.RUNNING) { updateStateTo(SyncState.RUNNING(afterPause = true)) @@ -142,9 +150,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, Timber.v("Cancelled") } else if (failure is Failure.ServerError && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) { - // No token or invalid token, stop the thread + // No token or invalid token Timber.w(failure) - updateStateTo(SyncState.KILLING) + isTokenValid = false + isStarted = false } else { Timber.e(failure) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index a922af665c..cf7a39e761 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -99,6 +99,9 @@ + doCleanUp() } - .setNegativeButton(R.string.cancel) { _, _ -> start() } + .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() } .setCancelable(false) .show() } - private fun start() { - val intent = if (sessionHolder.hasActiveSession()) { - HomeActivity.newIntent(this) - } else { - // Check if we've been signed out - if (args.isUserLoggedOut) { - // TODO Soft logout - SignedOutActivity.newIntent(this) - } else { + private fun startNextActivityAndFinish() { + val intent = when { + args.clearCredentials -> + // User has explicitly asked to log out + LoginActivity.newIntent(this, null) + args.isSoftLogout -> + // The homeserver has invalidated the token, with a soft logout + SoftLogoutActivity.newIntent(this) + args.isUserLoggedOut -> + // the homeserver has invalidated the token (password changed, device deleted, other security reason + SignedOutActivity.newIntent(this) + sessionHolder.hasActiveSession() -> + // We have a session. In case of soft logout (i.e. restart of the app after a soft logout) + // the app will try to sync and will reenter the soft logout use case + HomeActivity.newIntent(this) + else -> + // First start, or no active session LoginActivity.newIntent(this, null) - } } startActivity(intent) finish() diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt new file mode 100644 index 0000000000..da1d4a3adb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutAction.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2019 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.signout + +import im.vector.riotx.core.platform.VectorViewModelAction + +sealed class SoftLogoutAction : VectorViewModelAction { + data class SignInAgain(val password: String) : SoftLogoutAction() + // TODO Add reset pwd... +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutActivity.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutActivity.kt new file mode 100644 index 0000000000..d028406a1b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutActivity.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2019 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.signout + +import android.content.Context +import android.content.Intent +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.viewModel +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 +import im.vector.riotx.core.extensions.replaceFragment +import im.vector.riotx.core.platform.VectorBaseActivity +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs +import timber.log.Timber +import javax.inject.Inject + +/** + * In this screen, the user is viewing a message informing that he has been logged out + */ +class SoftLogoutActivity : VectorBaseActivity() { + + private val softLogoutViewModel: SoftLogoutViewModel by viewModel() + // TODO For forgotten pwd + // private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel + + @Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory + @Inject lateinit var session: Session + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutRes() = R.layout.activity_simple + + override fun initUiAndData() { + super.initUiAndData() + + if (isFirstCreation()) { + replaceFragment(R.id.simpleFragmentContainer, SoftLogoutFragment::class.java) + } + + softLogoutViewModel + .subscribe(this) { + updateWithState(it) + } + } + + private fun updateWithState(softLogoutViewState: SoftLogoutViewState) { + if (softLogoutViewState.asyncLoginAction is Success) { + MainActivity.restartApp(this, MainActivityArgs()) + } + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SoftLogoutActivity::class.java) + } + } + + override fun handleInvalidToken(globalError: GlobalError.InvalidToken) { + // No op here + Timber.w("Ignoring invalid token global error") + } +} + diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt new file mode 100644 index 0000000000..99082527ed --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutFragment.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2019 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.signout + +import android.content.DialogInterface +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.autofill.HintConstants +import butterknife.OnClick +import com.airbnb.mvrx.* +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.dialogs.withColoredButton +import im.vector.riotx.core.error.ErrorFormatter +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.features.MainActivity +import im.vector.riotx.features.MainActivityArgs +import io.reactivex.rxkotlin.subscribeBy +import kotlinx.android.synthetic.main.fragment_soft_logout.* +import javax.inject.Inject + +/** + * In this screen: + * - the user is asked to enter a password to sign in again to a homeserver. + * - or to cleanup all the data + */ +class SoftLogoutFragment @Inject constructor( + private val errorFormatter: ErrorFormatter +) : VectorBaseFragment() { + + private var passwordShown = false + + private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel() + + override fun getLayoutResId() = R.layout.fragment_soft_logout + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupSubmitButton() + setupPasswordReveal() + setupAutoFill() + } + + private fun setupAutoFill() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + softLogoutPasswordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD) + } + } + + @OnClick(R.id.softLogoutSubmit) + fun submit() { + cleanupUi() + + val password = softLogoutPasswordField.text.toString() + softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password)) + } + + @OnClick(R.id.softLogoutClearDataSubmit) + fun clearData() { + cleanupUi() + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.soft_logout_clear_data_dialog_title) + .setMessage(R.string.soft_logout_clear_data_dialog_content) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ -> + MainActivity.restartApp(requireActivity(), MainActivityArgs( + clearCache = true, + clearCredentials = true, + isUserLoggedOut = true + )) + } + .show() + .withColoredButton(DialogInterface.BUTTON_POSITIVE) + } + + private fun cleanupUi() { + softLogoutSubmit.hideKeyboard() + softLogoutPasswordFieldTil.error = null + } + + private fun setupUi(state: SoftLogoutViewState) { + softLogoutNotice.text = getString(R.string.soft_logout_signin_notice, + state.homeServerUrl, + state.userDisplayName, + state.userId) + } + + private fun setupSubmitButton() { + softLogoutPasswordField.textChanges() + .map { it.trim().isNotEmpty() } + .subscribeBy { + softLogoutPasswordFieldTil.error = null + softLogoutSubmit.isEnabled = it + } + .disposeOnDestroyView() + } + + @OnClick(R.id.softLogoutForgetPasswordButton) + fun forgetPasswordClicked() { + // TODO + // loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked) + } + + private fun setupPasswordReveal() { + passwordShown = false + + softLogoutPasswordReveal.setOnClickListener { + passwordShown = !passwordShown + + renderPasswordField() + } + + renderPasswordField() + } + + private fun renderPasswordField() { + softLogoutPasswordField.showPassword(passwordShown) + + if (passwordShown) { + softLogoutPasswordReveal.setImageResource(R.drawable.ic_eye_closed_black) + softLogoutPasswordReveal.contentDescription = getString(R.string.a11y_hide_password) + } else { + softLogoutPasswordReveal.setImageResource(R.drawable.ic_eye_black) + softLogoutPasswordReveal.contentDescription = getString(R.string.a11y_show_password) + } + } + + override fun invalidate() = withState(softLogoutViewModel) { state -> + setupUi(state) + setupAutoFill() + + when (state.asyncLoginAction) { + is Loading -> { + // Ensure password is hidden + passwordShown = false + renderPasswordField() + } + is Fail -> { + softLogoutPasswordFieldTil.error = errorFormatter.toHumanReadable(state.asyncLoginAction.error) + } + // Success is handled by the SoftLogoutActivity + is Success -> Unit + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt new file mode 100644 index 0000000000..ed23f4660a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewModel.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019 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.signout + +import com.airbnb.mvrx.* +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.session.Session +import im.vector.matrix.android.api.util.Cancelable +import im.vector.riotx.R +import im.vector.riotx.core.di.ActiveSessionHolder +import im.vector.riotx.core.extensions.toReducedUrl +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.resources.StringProvider + +/** + * + */ +class SoftLogoutViewModel @AssistedInject constructor( + @Assisted initialState: SoftLogoutViewState, + private val session: Session, + private val stringProvider: StringProvider, + private val activeSessionHolder: ActiveSessionHolder) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SoftLogoutViewState): SoftLogoutViewModel + } + + companion object : MvRxViewModelFactory { + + override fun initialState(viewModelContext: ViewModelContext): SoftLogoutViewState? { + val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() + val userId = activity.session.myUserId + return SoftLogoutViewState( + homeServerUrl = activity.session.sessionParams.homeServerConnectionConfig.homeServerUri.toString().toReducedUrl(), + userId = userId, + userDisplayName = activity.session.getUser(userId)?.displayName ?: userId + ) + } + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SoftLogoutViewState): SoftLogoutViewModel? { + val activity: SoftLogoutActivity = (viewModelContext as ActivityViewModelContext).activity() + return activity.softLogoutViewModelFactory.create(state) + } + } + + private var currentTask: Cancelable? = null + + // TODO Cleanup + // private val _viewEvents = PublishDataSource() + // val viewEvents: DataSource = _viewEvents + + override fun handle(action: SoftLogoutAction) { + when (action) { + is SoftLogoutAction.SignInAgain -> handleSignInAgain(action) + } + } + + private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) { + setState { copy(asyncLoginAction = Loading()) } + currentTask = session.signInAgain(action.password, + // TODO We should use the previous device name (we have to provide it for the homeserver + stringProvider.getString(R.string.login_mobile_device), + object : MatrixCallback { + override fun onFailure(failure: Throwable) { + setState { + copy( + asyncLoginAction = Fail(failure) + ) + } + } + + override fun onSuccess(data: Unit) { + activeSessionHolder.setActiveSession(session) + // Start the sync + session.startSync(true) + + // TODO Configure and start ? Check that the push still works... + setState { + copy( + asyncLoginAction = Success(Unit) + ) + } + } + } + ) + } + + override fun onCleared() { + super.onCleared() + + currentTask?.cancel() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt new file mode 100644 index 0000000000..0c2503ce06 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/signout/SoftLogoutViewState.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 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.signout + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized + +data class SoftLogoutViewState( + val asyncLoginAction: Async = Uninitialized, + val homeServerUrl: String, + val userId: String, + val userDisplayName: String +) : MvRxState diff --git a/vector/src/main/res/layout/fragment_soft_logout.xml b/vector/src/main/res/layout/fragment_soft_logout.xml new file mode 100644 index 0000000000..b534541ecc --- /dev/null +++ b/vector/src/main/res/layout/fragment_soft_logout.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 58ccb02f38..5ef3cad535 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -144,4 +144,18 @@ It can be due to various reasons:\n\n• You’ve changed your password on another device.\n\n• You have deleted this device from another device.\n\n• The administrator of your server has invalidated your access for security reason. Sign in again + You’re signed out + Sign in + + Your homeserver (%1$s) admin has signed you out of your account %2$s (%3$s). + Sign in + Password + Clear personal data + Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account. + Clear all data + + Clear data + Clear all data currently stored on this device?\nSign in again to access your account data and messages. + Clear data + diff --git a/vector/src/main/res/values/text_appearances.xml b/vector/src/main/res/values/text_appearances.xml index 6c2a71631d..c43cb7e69e 100644 --- a/vector/src/main/res/values/text_appearances.xml +++ b/vector/src/main/res/values/text_appearances.xml @@ -44,6 +44,10 @@ ?riotx_text_primary + +