Soft Logout - WIP

This commit is contained in:
Benoit Marty 2019-12-11 18:33:16 +01:00
parent a193b2659d
commit 7699560458
26 changed files with 765 additions and 27 deletions

View file

@ -18,6 +18,7 @@
<w>pbkdf</w>
<w>pkcs</w>
<w>signin</w>
<w>signout</w>
<w>signup</w>
</words>
</dictionary>

View file

@ -2,7 +2,7 @@ Changes in RiotX 0.11.0 (2019-XX-XX)
===================================================
Features ✨:
-
- Implement soft logout (#281)
Improvements 🙌:
-

View file

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

View file

@ -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<Unit>): 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

View file

@ -23,4 +23,5 @@ sealed class SyncState {
object KILLING : SyncState()
object KILLED : SyncState()
object NO_NETWORK : SyncState()
object INVALID_TOKEN : SyncState()
}

View file

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

View file

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

View file

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

View file

@ -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<Unit>): Cancelable {
return signInAgainTask
.configureWith(SignInAgainTask.Params(password, deviceName)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask

View file

@ -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<SignInAgainTask.Params, Unit> {
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<Credentials> {
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)
}
}

View file

@ -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<Credentials>
/**
* Invalidate the access token, so that it can no longer be used for authorization.
*/

View file

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

View file

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

View file

@ -99,6 +99,9 @@
</intent-filter>
</activity>
<activity android:name=".features.signout.SignedOutActivity" />
<activity
android:name=".features.signout.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" />
<!-- Services -->
<service

View file

@ -47,6 +47,7 @@ import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFr
import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.signout.SoftLogoutFragment
@Module
interface FragmentModule {
@ -261,4 +262,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(EmojiChooserFragment::class)
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SoftLogoutFragment::class)
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
}

View file

@ -49,6 +49,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.signout.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository
@Component(
@ -126,6 +127,8 @@ interface ScreenComponent {
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
fun inject(activity: SoftLogoutActivity)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View file

@ -205,7 +205,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
MainActivity.restartApp(this,
MainActivityArgs(
clearCache = true,
clearCache = false,
clearCredentials = !globalError.softLogout,
isUserLoggedOut = true,
isSoftLogout = globalError.softLogout

View file

@ -33,6 +33,7 @@ import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.signout.SignedOutActivity
import im.vector.riotx.features.signout.SoftLogoutActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -81,7 +82,7 @@ class MainActivity : VectorBaseActivity() {
if (args.clearCache || args.clearCredentials) {
doCleanUp()
} else {
start()
startNextActivityAndFinish()
}
}
@ -143,7 +144,7 @@ class MainActivity : VectorBaseActivity() {
}
}
start()
startNextActivityAndFinish()
}
private fun displayError(failure: Throwable) {
@ -151,22 +152,29 @@ class MainActivity : VectorBaseActivity() {
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> 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()

View file

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

View file

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

View file

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

View file

@ -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<SoftLogoutViewState, SoftLogoutAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: SoftLogoutViewState): SoftLogoutViewModel
}
companion object : MvRxViewModelFactory<SoftLogoutViewModel, SoftLogoutViewState> {
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<LoginViewEvents>()
// val viewEvents: DataSource<LoginViewEvents> = _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<Unit> {
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()
}
}

View file

@ -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<Unit> = Uninitialized,
val homeServerUrl: String,
val userId: String,
val userDisplayName: String
) : MvRxState

View file

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/softLogout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background">
<!-- Missing attributes are in the style -->
<ImageView
style="@style/LoginLogo"
tools:ignore="ContentDescription,MissingConstraints" />
<!-- Missing attributes are in the style -->
<androidx.core.widget.NestedScrollView
style="@style/LoginFormScrollView"
tools:ignore="MissingConstraints">
<LinearLayout
style="@style/LoginFormContainer"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/soft_logout_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/soft_logout_signin_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<TextView
android:id="@+id/softLogoutNotice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="@string/soft_logout_signin_notice" />
<FrameLayout
android:id="@+id/softLogoutPasswordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/softLogoutPasswordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/soft_logout_signin_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/softLogoutPasswordField"
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/softLogoutPasswordReveal"
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>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutForgetPasswordButton"
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" />
<com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_gravity="end"
android:text="@string/soft_logout_signin_submit"
tools:enabled="false"
tools:ignore="RelativeOverlap" />
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/soft_logout_clear_data_title"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
android:text="@string/soft_logout_clear_data_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/softLogoutClearDataSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:text="@string/soft_logout_clear_data_submit"
app:backgroundTint="@color/vector_error_color" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -144,4 +144,18 @@
<string name="signed_out_notice">It can be due to various reasons:\n\n• Youve 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.</string>
<string name="signed_out_submit">Sign in again</string>
<string name="soft_logout_title">Youre signed out</string>
<string name="soft_logout_signin_title">Sign in</string>
<!-- Replacement: homeserver url, user display name and userId -->
<string name="soft_logout_signin_notice">Your homeserver (%1$s) admin has signed you out of your account %2$s (%3$s).</string>
<string name="soft_logout_signin_submit">Sign in</string>
<string name="soft_logout_signin_password_hint">Password</string>
<string name="soft_logout_clear_data_title">Clear personal data</string>
<string name="soft_logout_clear_data_notice">Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if youre finished using this device, or want to sign in to another account.</string>
<string name="soft_logout_clear_data_submit">Clear all data</string>
<string name="soft_logout_clear_data_dialog_title">Clear data</string>
<string name="soft_logout_clear_data_dialog_content">Clear all data currently stored on this device?\nSign in again to access your account data and messages.</string>
<string name="soft_logout_clear_data_dialog_submit">Clear data</string>
</resources>

View file

@ -44,6 +44,10 @@
<item name="android:textColor">?riotx_text_primary</item>
</style>
<style name="TextAppearance.Vector.Login.Title.Small">
<item name="android:textSize">15sp</item>
</style>
<style name="TextAppearance.Vector.Login.Text" parent="TextAppearance.AppCompat">
<item name="android:textSize">16sp</item>
<item name="android:fontFamily">sans-serif</item>