mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Soft Logout - WIP
This commit is contained in:
parent
a193b2659d
commit
7699560458
26 changed files with 765 additions and 27 deletions
|
@ -18,6 +18,7 @@
|
|||
<w>pbkdf</w>
|
||||
<w>pkcs</w>
|
||||
<w>signin</w>
|
||||
<w>signout</w>
|
||||
<w>signup</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
|
|
@ -2,7 +2,7 @@ Changes in RiotX 0.11.0 (2019-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Implement soft logout (#281)
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,4 +23,5 @@ sealed class SyncState {
|
|||
object KILLING : SyncState()
|
||||
object KILLED : SyncState()
|
||||
object NO_NETWORK : SyncState()
|
||||
object INVALID_TOKEN : SyncState()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -99,6 +99,9 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".features.signout.SignedOutActivity" />
|
||||
<activity
|
||||
android:name=".features.signout.SoftLogoutActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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...
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
145
vector/src/main/res/layout/fragment_soft_logout.xml
Normal file
145
vector/src/main/res/layout/fragment_soft_logout.xml
Normal 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>
|
||||
|
|
@ -144,4 +144,18 @@
|
|||
<string name="signed_out_notice">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.</string>
|
||||
<string name="signed_out_submit">Sign in again</string>
|
||||
|
||||
<string name="soft_logout_title">You’re 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 you’re 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue