Soft Logout - WIP
This commit is contained in:
parent
a193b2659d
commit
7699560458
|
@ -18,6 +18,7 @@
|
||||||
<w>pbkdf</w>
|
<w>pbkdf</w>
|
||||||
<w>pkcs</w>
|
<w>pkcs</w>
|
||||||
<w>signin</w>
|
<w>signin</w>
|
||||||
|
<w>signout</w>
|
||||||
<w>signup</w>
|
<w>signup</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
|
|
|
@ -2,7 +2,7 @@ Changes in RiotX 0.11.0 (2019-XX-XX)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- Implement soft logout (#281)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
-
|
-
|
||||||
|
|
|
@ -81,7 +81,7 @@ interface Session :
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches infinite periodic background syncs
|
* 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 :/
|
* If battery optimization is on it can work in app standby but that's all :/
|
||||||
*/
|
*/
|
||||||
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
|
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
|
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 {
|
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
|
* 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
|
* @param sigOutFromHomeserver true if the sign out request has to be done
|
||||||
|
|
|
@ -23,4 +23,5 @@ sealed class SyncState {
|
||||||
object KILLING : SyncState()
|
object KILLING : SyncState()
|
||||||
object KILLED : SyncState()
|
object KILLED : SyncState()
|
||||||
object NO_NETWORK : SyncState()
|
object NO_NETWORK : SyncState()
|
||||||
|
object INVALID_TOKEN : SyncState()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.auth
|
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
|
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||||
|
|
||||||
internal interface SessionParamsStore {
|
internal interface SessionParamsStore {
|
||||||
|
@ -28,6 +29,8 @@ internal interface SessionParamsStore {
|
||||||
|
|
||||||
suspend fun save(sessionParams: SessionParams)
|
suspend fun save(sessionParams: SessionParams)
|
||||||
|
|
||||||
|
suspend fun updateCredentials(newCredentials: Credentials)
|
||||||
|
|
||||||
suspend fun delete(userId: String)
|
suspend fun delete(userId: String)
|
||||||
|
|
||||||
suspend fun deleteAll()
|
suspend fun deleteAll()
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.auth.db
|
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.api.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
import im.vector.matrix.android.internal.database.awaitTransaction
|
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) {
|
override suspend fun delete(userId: String) {
|
||||||
awaitTransaction(realmConfiguration) {
|
awaitTransaction(realmConfiguration) {
|
||||||
it.where(SessionParamsEntity::class.java)
|
it.where(SessionParamsEntity::class.java)
|
||||||
|
|
|
@ -16,19 +16,29 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.network
|
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.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import javax.inject.Inject
|
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 {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
var request = chain.request()
|
var request = chain.request()
|
||||||
val newRequestBuilder = request.newBuilder()
|
|
||||||
// Add the access token to all requests if it is set
|
accessToken?.let {
|
||||||
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken)
|
val newRequestBuilder = request.newBuilder()
|
||||||
request = newRequestBuilder.build()
|
// Add the access token to all requests if it is set
|
||||||
|
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
|
||||||
|
request = newRequestBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
return chain.proceed(request)
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
|
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
|
||||||
|
private val signInAgainTask: SignInAgainTask,
|
||||||
private val taskExecutor: TaskExecutor) : SignOutService {
|
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,
|
override fun signOut(sigOutFromHomeserver: Boolean,
|
||||||
callback: MatrixCallback<Unit>): Cancelable {
|
callback: MatrixCallback<Unit>): Cancelable {
|
||||||
return signOutTask
|
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
|
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 im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.Headers
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
||||||
internal interface SignOutAPI {
|
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.
|
* Invalidate the access token, so that it can no longer be used for authorization.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -37,8 +37,11 @@ internal abstract class SignOutModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask
|
abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
|
||||||
|
|
||||||
@Binds
|
@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 cancelableTask: Cancelable? = null
|
||||||
|
|
||||||
private var isStarted = false
|
private var isStarted = false
|
||||||
|
private var isTokenValid = true
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updateStateTo(SyncState.IDLE)
|
updateStateTo(SyncState.IDLE)
|
||||||
|
@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
if (!isStarted) {
|
if (!isStarted) {
|
||||||
Timber.v("Resume sync...")
|
Timber.v("Resume sync...")
|
||||||
isStarted = true
|
isStarted = true
|
||||||
|
// Check again the token validity
|
||||||
|
isTokenValid = true
|
||||||
lock.notify()
|
lock.notify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,6 +116,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
updateStateTo(SyncState.PAUSED)
|
updateStateTo(SyncState.PAUSED)
|
||||||
synchronized(lock) { lock.wait() }
|
synchronized(lock) { lock.wait() }
|
||||||
Timber.v("...unlocked")
|
Timber.v("...unlocked")
|
||||||
|
} else if (!isTokenValid) {
|
||||||
|
Timber.v("Token is invalid. Waiting...")
|
||||||
|
updateStateTo(SyncState.INVALID_TOKEN)
|
||||||
|
synchronized(lock) { lock.wait() }
|
||||||
|
Timber.v("...unlocked")
|
||||||
} else {
|
} else {
|
||||||
if (state !is SyncState.RUNNING) {
|
if (state !is SyncState.RUNNING) {
|
||||||
updateStateTo(SyncState.RUNNING(afterPause = true))
|
updateStateTo(SyncState.RUNNING(afterPause = true))
|
||||||
|
@ -142,9 +150,10 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
|
||||||
Timber.v("Cancelled")
|
Timber.v("Cancelled")
|
||||||
} else if (failure is Failure.ServerError
|
} else if (failure is Failure.ServerError
|
||||||
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
|
&& (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)
|
Timber.w(failure)
|
||||||
updateStateTo(SyncState.KILLING)
|
isTokenValid = false
|
||||||
|
isStarted = false
|
||||||
} else {
|
} else {
|
||||||
Timber.e(failure)
|
Timber.e(failure)
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,9 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".features.signout.SignedOutActivity" />
|
<activity android:name=".features.signout.SignedOutActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".features.signout.SoftLogoutActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize" />
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
<service
|
<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.*
|
||||||
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
||||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||||
|
import im.vector.riotx.features.signout.SoftLogoutFragment
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
interface FragmentModule {
|
interface FragmentModule {
|
||||||
|
@ -261,4 +262,9 @@ interface FragmentModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(EmojiChooserFragment::class)
|
@FragmentKey(EmojiChooserFragment::class)
|
||||||
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
|
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.roomdirectory.createroom.CreateRoomActivity
|
||||||
import im.vector.riotx.features.settings.VectorSettingsActivity
|
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||||
import im.vector.riotx.features.share.IncomingShareActivity
|
import im.vector.riotx.features.share.IncomingShareActivity
|
||||||
|
import im.vector.riotx.features.signout.SoftLogoutActivity
|
||||||
import im.vector.riotx.features.ui.UiStateRepository
|
import im.vector.riotx.features.ui.UiStateRepository
|
||||||
|
|
||||||
@Component(
|
@Component(
|
||||||
|
@ -126,6 +127,8 @@ interface ScreenComponent {
|
||||||
|
|
||||||
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
|
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
|
||||||
|
|
||||||
|
fun inject(activity: SoftLogoutActivity)
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(vectorComponent: VectorComponent,
|
fun create(vectorComponent: VectorComponent,
|
||||||
|
|
|
@ -205,7 +205,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
||||||
|
|
||||||
MainActivity.restartApp(this,
|
MainActivity.restartApp(this,
|
||||||
MainActivityArgs(
|
MainActivityArgs(
|
||||||
clearCache = true,
|
clearCache = false,
|
||||||
clearCredentials = !globalError.softLogout,
|
clearCredentials = !globalError.softLogout,
|
||||||
isUserLoggedOut = true,
|
isUserLoggedOut = true,
|
||||||
isSoftLogout = globalError.softLogout
|
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.home.HomeActivity
|
||||||
import im.vector.riotx.features.login.LoginActivity
|
import im.vector.riotx.features.login.LoginActivity
|
||||||
import im.vector.riotx.features.signout.SignedOutActivity
|
import im.vector.riotx.features.signout.SignedOutActivity
|
||||||
|
import im.vector.riotx.features.signout.SoftLogoutActivity
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
@ -81,7 +82,7 @@ class MainActivity : VectorBaseActivity() {
|
||||||
if (args.clearCache || args.clearCredentials) {
|
if (args.clearCache || args.clearCredentials) {
|
||||||
doCleanUp()
|
doCleanUp()
|
||||||
} else {
|
} else {
|
||||||
start()
|
startNextActivityAndFinish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +144,7 @@ class MainActivity : VectorBaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start()
|
startNextActivityAndFinish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayError(failure: Throwable) {
|
private fun displayError(failure: Throwable) {
|
||||||
|
@ -151,22 +152,29 @@ class MainActivity : VectorBaseActivity() {
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(R.string.dialog_title_error)
|
||||||
.setMessage(errorFormatter.toHumanReadable(failure))
|
.setMessage(errorFormatter.toHumanReadable(failure))
|
||||||
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
|
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
|
||||||
.setNegativeButton(R.string.cancel) { _, _ -> start() }
|
.setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun start() {
|
private fun startNextActivityAndFinish() {
|
||||||
val intent = if (sessionHolder.hasActiveSession()) {
|
val intent = when {
|
||||||
HomeActivity.newIntent(this)
|
args.clearCredentials ->
|
||||||
} else {
|
// User has explicitly asked to log out
|
||||||
// Check if we've been signed out
|
LoginActivity.newIntent(this, null)
|
||||||
if (args.isUserLoggedOut) {
|
args.isSoftLogout ->
|
||||||
// TODO Soft logout
|
// The homeserver has invalidated the token, with a soft logout
|
||||||
SignedOutActivity.newIntent(this)
|
SoftLogoutActivity.newIntent(this)
|
||||||
} else {
|
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)
|
LoginActivity.newIntent(this, null)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
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
|
|
@ -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_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="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>
|
</resources>
|
||||||
|
|
|
@ -44,6 +44,10 @@
|
||||||
<item name="android:textColor">?riotx_text_primary</item>
|
<item name="android:textColor">?riotx_text_primary</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.Vector.Login.Title.Small">
|
||||||
|
<item name="android:textSize">15sp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="TextAppearance.Vector.Login.Text" parent="TextAppearance.AppCompat">
|
<style name="TextAppearance.Vector.Login.Text" parent="TextAppearance.AppCompat">
|
||||||
<item name="android:textSize">16sp</item>
|
<item name="android:textSize">16sp</item>
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
<item name="android:fontFamily">sans-serif</item>
|
||||||
|
|
Loading…
Reference in New Issue