Merge pull request #768 from vector-im/feature/soft_logout

Handle invalid tokens gracefully
This commit is contained in:
Benoit Marty 2019-12-16 14:57:11 +01:00 committed by GitHub
commit 3feb2d8980
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 2254 additions and 397 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

@ -22,5 +22,6 @@ package im.vector.matrix.android.api.auth.data
*/
data class SessionParams(
val credentials: Credentials,
val homeServerConnectionConfig: HomeServerConnectionConfig
val homeServerConnectionConfig: HomeServerConnectionConfig,
val isTokenValid: Boolean
)

View File

@ -16,7 +16,8 @@
package im.vector.matrix.android.api.failure
// This data class will be sent to the bus
data class ConsentNotGivenError(
val consentUri: String
)
// This class will be sent to the bus
sealed class GlobalError {
data class InvalidToken(val softLogout: Boolean) : GlobalError()
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
}

View File

@ -22,45 +22,112 @@ import com.squareup.moshi.JsonClass
/**
* This data class holds the error defined by the matrix specifications.
* You shouldn't have to instantiate it.
* Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
*/
@JsonClass(generateAdapter = true)
data class MatrixError(
/** unique string which can be used to handle an error message */
@Json(name = "errcode") val code: String,
/** human-readable error message */
@Json(name = "error") val message: String,
// For M_CONSENT_NOT_GIVEN
@Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data
// For M_RESOURCE_LIMIT_EXCEEDED
@Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null,
// For LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
// For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
) {
companion object {
const val FORBIDDEN = "M_FORBIDDEN"
const val UNKNOWN = "M_UNKNOWN"
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
const val MISSING_TOKEN = "M_MISSING_TOKEN"
const val BAD_JSON = "M_BAD_JSON"
const val NOT_JSON = "M_NOT_JSON"
const val NOT_FOUND = "M_NOT_FOUND"
const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
const val USER_IN_USE = "M_USER_IN_USE"
const val ROOM_IN_USE = "M_ROOM_IN_USE"
const val BAD_PAGINATION = "M_BAD_PAGINATION"
const val UNAUTHORIZED = "M_UNAUTHORIZED"
const val OLD_VERSION = "M_OLD_VERSION"
const val UNRECOGNIZED = "M_UNRECOGNIZED"
/** Forbidden access, e.g. joining a room without permission, failed login. */
const val M_FORBIDDEN = "M_FORBIDDEN"
/** An unknown error has occurred. */
const val M_UNKNOWN = "M_UNKNOWN"
/** The access token specified was not recognised. */
const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
/** No access token was specified for the request. */
const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
/** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
const val M_BAD_JSON = "M_BAD_JSON"
/** Request did not contain valid JSON. */
const val M_NOT_JSON = "M_NOT_JSON"
/** No resource was found for this request. */
const val M_NOT_FOUND = "M_NOT_FOUND"
/** Too many requests have been sent in a short period of time. Wait a while then try again. */
const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
const val LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
const val THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
// Error code returned by the server when no account matches the given 3pid
const val THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
const val THREEPID_IN_USE = "M_THREEPID_IN_USE"
const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
const val TOO_LARGE = "M_TOO_LARGE"
/* ==========================================================================================
* Other error codes the client might encounter are
* ========================================================================================== */
/** Encountered when trying to register a user ID which has been taken. */
const val M_USER_IN_USE = "M_USER_IN_USE"
/** Sent when the room alias given to the createRoom API is already in use. */
const val M_ROOM_IN_USE = "M_ROOM_IN_USE"
/** (Not documented yet) */
const val M_BAD_PAGINATION = "M_BAD_PAGINATION"
/** The request was not correctly authorized. Usually due to login failures. */
const val M_UNAUTHORIZED = "M_UNAUTHORIZED"
/** (Not documented yet) */
const val M_OLD_VERSION = "M_OLD_VERSION"
/** The server did not understand the request. */
const val M_UNRECOGNIZED = "M_UNRECOGNIZED"
/** (Not documented yet) */
const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
/** Authentication could not be performed on the third party identifier. */
const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
/** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */
const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
/** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */
const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE"
/** The client's request used a third party server, eg. identity server, that this server does not trust. */
const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
/** The request or entity was too large. */
const val M_TOO_LARGE = "M_TOO_LARGE"
/** (Not documented yet) */
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
/** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
* a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory
* or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach
* out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account
* data, etc) and not routes which only read state (eg: /sync, get account data, etc). */
const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
/** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */
const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED"
/** Encountered when trying to register a user ID which is not valid. */
const val M_INVALID_USERNAME = "M_INVALID_USERNAME"
/** Sent when the initial state given to the createRoom API is invalid. */
const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"
/** The server does not permit this third party identifier. This may happen if the server only permits,
* for example, email addresses from a particular domain. */
const val M_THREEPID_DENIED = "M_THREEPID_DENIED"
/** The client's request to create a room used a room version that the server does not support. */
const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
/** The client attempted to join a room that has a version the server does not support.
* Inspect the room_version property of the error response for the room's version. */
const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
/** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */
const val M_BAD_STATE = "M_BAD_STATE"
/** The room or resource does not permit guests to access it. */
const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
/** A Captcha is required to complete the request. */
const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
/** The Captcha provided did not match what was expected. */
const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
/** A required parameter was missing from the request. */
const val M_MISSING_PARAM = "M_MISSING_PARAM"
/** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */
const val M_INVALID_PARAM = "M_INVALID_PARAM"
/** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */
const val M_EXCLUSIVE = "M_EXCLUSIVE"
/** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
/** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -62,6 +62,11 @@ interface Session :
*/
val sessionParams: SessionParams
/**
* The session is valid, i.e. it has a valid token so far
*/
val isOpenable: Boolean
/**
* Useful shortcut to get access to the userId
*/
@ -81,7 +86,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)
@ -136,13 +141,10 @@ interface Session :
*/
interface Listener {
/**
* The access token is not valid anymore
* Possible cases:
* - The access token is not valid anymore,
* - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
fun onInvalidToken()
/**
* A M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
fun onGlobalError(globalError: GlobalError)
}
}

View File

@ -17,14 +17,31 @@
package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
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 {
/**
* Sign out
* Ask the homeserver for a new access token.
* The same deviceId will be used
*/
fun signOut(callback: MatrixCallback<Unit>)
fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Update the session with credentials received after SSO
*/
fun updateCredentials(credentials: Credentials,
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
*/
fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable
}

View File

@ -17,10 +17,11 @@
package im.vector.matrix.android.api.session.sync
sealed class SyncState {
object IDLE : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()
object NO_NETWORK : SyncState()
object Idle : SyncState()
data class Running(val afterPause: Boolean) : SyncState()
object Paused : SyncState()
object Killing : SyncState()
object Killed : SyncState()
object NoNetwork : SyncState()
object InvalidToken : SyncState()
}

View File

@ -53,7 +53,7 @@ internal abstract class AuthModule {
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration())
.migration(AuthRealmMigration)
.build()
}
}

View File

@ -60,7 +60,8 @@ internal class DefaultSessionCreator @Inject constructor(
?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri
))
),
isTokenValid = true)
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)

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,10 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams)
suspend fun setTokenInvalid(userId: String)
suspend fun updateCredentials(newCredentials: Credentials)
suspend fun delete(userId: String)
suspend fun deleteAll()

View File

@ -20,12 +20,10 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
internal class AuthRealmMigration : RealmMigration {
internal object AuthRealmMigration : RealmMigration {
companion object {
// Current schema version
const val SCHEMA_VERSION = 1L
}
// Current schema version
const val SCHEMA_VERSION = 2L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
@ -46,5 +44,14 @@ internal class AuthRealmMigration : RealmMigration {
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
if (oldVersion <= 1) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
}
}

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,53 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
}
}
override suspend fun setTokenInvalid(userId: String) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user $userId"
.let { Timber.w(it) }
.also { error(it) }
} else {
currentSessionParams.isTokenValid = false
}
}
}
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,
isTokenValid = true
)
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

@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "",
var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = ""
var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
// In case of hard logout, this object is deleted from DB
var isTokenValid: Boolean = true
) : RealmObject()

View File

@ -36,7 +36,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentials == null || homeServerConnectionConfig == null) {
return null
}
return SessionParams(credentials, homeServerConnectionConfig)
return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
}
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@ -48,6 +48,10 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null
}
return SessionParamsEntity(sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson)
return SessionParamsEntity(
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid)
}
}

View File

@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) {
&& failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null)
} else {
@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) {
&& failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null)
} else {
@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) {
MatrixError.NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
MatrixError.M_NOT_FOUND,
MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)

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

@ -20,8 +20,8 @@ package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
@ -32,6 +32,7 @@ import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
import java.io.IOException
import java.net.HttpURLConnection
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@ -99,7 +100,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri))
EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
}
return Failure.ServerError(matrixError, httpCode)

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private var syncThread: SyncThread? = null
override val isOpenable: Boolean
get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
@MainThread
override fun open() {
assertMainThread()
@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
sessionListeners.dispatchConsentNotGiven(consentNotGivenError)
fun onGlobalError(globalError: GlobalError) {
if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(myUserId)
}
}
sessionListeners.dispatchGlobalError(globalError)
}
override fun contentUrlResolver() = contentUrlResolver

View File

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session
import javax.inject.Inject
@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
}
}
fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) {
fun dispatchGlobalError(globalError: GlobalError) {
synchronized(listeners) {
listeners.forEach {
it.onConsentNotGivenError(consentNotGivenError)
it.onGlobalError(globalError)
}
}
}

View File

@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
private fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection
|| (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED)
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
}
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {

View File

@ -17,17 +17,43 @@
package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService {
override fun signOut(callback: MatrixCallback<Unit>) {
signOutTask
.configureWith {
override fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable {
return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
}
override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask
.configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
this.callback = callback
}
.executeBy(taskExecutor)

View File

@ -0,0 +1,56 @@
/*
* 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
)
}
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,
// The spec says the initial device name will be ignored
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
// but https://github.com/matrix-org/synapse/issues/6525
// Reuse the same deviceId
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

@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.signout
import android.content.Context
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
@ -32,9 +34,14 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import java.net.HttpURLConnection
import javax.inject.Inject
internal interface SignOutTask : Task<Unit, Unit>
internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params(
val sigOutFromHomeserver: Boolean
)
}
internal class DefaultSignOutTask @Inject constructor(private val context: Context,
@UserId private val userId: String,
@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) {
Timber.d("SignOut: send request...")
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
override suspend fun execute(params: SignOutTask.Params) {
// It should be done even after a soft logout, to be sure the deviceId is deleted on the
if (params.sigOutFromHomeserver) {
Timber.d("SignOut: send request...")
try {
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}
} catch (throwable: Throwable) {
// Maybe due to https://github.com/matrix-org/synapse/issues/5755
if (throwable is Failure.ServerError
&& throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also throwable.error.isSoftLogout should be true
// Ignore
Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
} else {
throw throwable
}
}
}
Timber.d("SignOut: release session...")

View File

@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
}
val syncResponse = try {
executeRequest<SyncResponse> {
apiCall = syncAPI.sync(requestParams)
}
} catch (throwable: Throwable) {
// Intercept 401
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
sessionParamsStore.delete(userId)
}
throw throwable
val syncResponse = executeRequest<SyncResponse> {
apiCall = syncAPI.sync(requestParams)
}
syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch)

View File

@ -147,7 +147,7 @@ open class SyncService : Service() {
}
if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread
stopSelf()
}

View File

@ -44,19 +44,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.IDLE
private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>()
private val lock = Object()
private var cancelableTask: Cancelable? = null
private var isStarted = false
private var isTokenValid = true
init {
updateStateTo(SyncState.IDLE)
updateStateTo(SyncState.Idle)
}
fun setInitialForeground(initialForeground: Boolean) {
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED
val newState = if (initialForeground) SyncState.Idle else SyncState.Paused
updateStateTo(newState)
}
@ -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()
}
}
@ -78,7 +81,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
fun kill() = synchronized(lock) {
Timber.v("Kill sync...")
updateStateTo(SyncState.KILLING)
updateStateTo(SyncState.Killing)
cancelableTask?.cancel()
lock.notify()
}
@ -100,26 +103,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) {
while (state != SyncState.Killing) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK)
updateStateTo(SyncState.NoNetwork)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED)
updateStateTo(SyncState.Paused)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isTokenValid) {
Timber.v("Token is invalid. Waiting...")
updateStateTo(SyncState.InvalidToken)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else {
if (state !is SyncState.RUNNING) {
updateStateTo(SyncState.RUNNING(afterPause = true))
if (state !is SyncState.Running) {
updateStateTo(SyncState.Running(afterPause = true))
}
// No timeout after a pause
val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1)
@ -141,10 +149,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} else if (failure is Failure.Cancelled) {
Timber.v("Cancelled")
} else if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// No token or invalid token, stop the thread
&& (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token
Timber.w(failure)
updateStateTo(SyncState.KILLING)
isTokenValid = false
isStarted = false
} else {
Timber.e(failure)
@ -163,8 +172,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
latch.await()
state.let {
if (it is SyncState.RUNNING && it.afterPause) {
updateStateTo(SyncState.RUNNING(afterPause = false))
if (it is SyncState.Running && it.afterPause) {
updateStateTo(SyncState.Running(afterPause = false))
}
}
@ -172,7 +181,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
}
Timber.v("Sync killed")
updateStateTo(SyncState.KILLED)
updateStateTo(SyncState.Killed)
backgroundDetectionObserver.unregister(this)
networkConnectivityChecker.unregister(this)
}

View File

@ -98,6 +98,10 @@
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</activity>
<activity android:name=".features.signout.hard.SignedOutActivity" />
<activity
android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" />
<!-- Services -->
<service

View File

@ -37,7 +37,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
fun setActiveSession(session: Session) {
activeSession.set(session)
sessionObservableStore.post(Option.fromNullable(session))
sessionObservableStore.post(Option.just(session))
keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session)
}

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.soft.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

@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory
import androidx.lifecycle.ViewModelProvider
import dagger.BindsInstance
import dagger.Component
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
@ -49,6 +50,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.soft.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository
@Component(
@ -78,6 +80,8 @@ interface ScreenComponent {
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun uiStateRepository(): UiStateRepository
fun inject(activity: HomeActivity)
@ -126,6 +130,8 @@ interface ScreenComponent {
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
fun inject(activity: SoftLogoutActivity)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.core.utils.DimensionConverter
@ -88,6 +89,8 @@ interface VectorComponent {
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun homeRoomListObservableStore(): HomeRoomListDataSource
fun shareRoomListObservableStore(): ShareRoomListDataSource

View File

@ -26,6 +26,8 @@ import dagger.Provides
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.error.DefaultErrorFormatter
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
@ -72,6 +74,9 @@ abstract class VectorModule {
@Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter
@Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository
}

View File

@ -25,14 +25,15 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) {
interface ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String
}
fun toHumanReadable(failure: Failure): String {
// Default
return failure.localizedMessage
}
class DefaultErrorFormatter @Inject constructor(
private val stringProvider: StringProvider
) : ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String {
override fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) {
null -> null
is Failure.NetworkConnection -> {
@ -41,6 +42,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException ->
// Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host)
else ->
stringProvider.getString(R.string.error_no_network)
@ -52,23 +54,23 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
// Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted)
}
throwable.error.code == MatrixError.FORBIDDEN
throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message == "Invalid password" -> {
stringProvider.getString(R.string.auth_invalid_login_param)
}
throwable.error.code == MatrixError.USER_IN_USE -> {
throwable.error.code == MatrixError.M_USER_IN_USE -> {
stringProvider.getString(R.string.login_signup_error_user_in_use)
}
throwable.error.code == MatrixError.BAD_JSON -> {
throwable.error.code == MatrixError.M_BAD_JSON -> {
stringProvider.getString(R.string.login_error_bad_json)
}
throwable.error.code == MatrixError.NOT_JSON -> {
throwable.error.code == MatrixError.M_NOT_JSON -> {
stringProvider.getString(R.string.login_error_not_json)
}
throwable.error.code == MatrixError.LIMIT_EXCEEDED -> {
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
limitExceededError(throwable.error)
}
throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> {
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found)
}
else -> {

View File

@ -21,6 +21,6 @@ import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean {
return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& this.error.code == MatrixError.UNAUTHORIZED)
return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& error.code == MatrixError.M_UNAUTHORIZED)
}

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.extensions
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
@ -40,3 +41,11 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
}
/**
* Tell is the session has unsaved e2e keys in the backup
*/
fun Session.hasUnsavedKeys(): Boolean {
return inboundGroupSessionsCount(false) > 0
&& getKeysBackupService().state != KeysBackupState.ReadyToBackUp
}

View File

@ -35,3 +35,12 @@ fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder
return this
}
/**
* Ex: "https://matrix.org/" -> "matrix.org"
*/
fun String?.toReducedUrl(): String {
return (this ?: "")
.substringAfter("://")
.trim { it == '/' }
}

View File

@ -38,12 +38,15 @@ import butterknife.Unbinder
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.*
import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.consent.ConsentNotGivenHelper
import im.vector.riotx.features.navigation.Navigator
@ -89,6 +92,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder
// Filter for multiple invalid token error
private var mainActivityStarted = false
private var unBinder: Unbinder? = null
private var savedInstanceState: Bundle? = null
@ -153,9 +159,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
})
sessionListener = getVectorComponent().sessionListener()
sessionListener.consentNotGivenLiveData.observeEvent(this) {
consentNotGivenHelper.displayDialog(it.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
sessionListener.globalErrorLiveData.observeEvent(this) {
handleGlobalError(it)
}
doBeforeSetContentView()
@ -180,6 +185,33 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
}
private fun handleGlobalError(globalError: GlobalError) {
when (globalError) {
is GlobalError.InvalidToken ->
handleInvalidToken(globalError)
is GlobalError.ConsentNotGivenError ->
consentNotGivenHelper.displayDialog(globalError.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
}
}
protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
Timber.w("Invalid token event received")
if (mainActivityStarted) {
return
}
mainActivityStarted = true
MainActivity.restartApp(this,
MainActivityArgs(
clearCredentials = !globalError.softLogout,
isUserLoggedOut = true,
isSoftLogout = globalError.softLogout
)
)
}
override fun onDestroy() {
super.onDestroy()
unBinder?.unbind()

View File

@ -34,6 +34,7 @@ import com.bumptech.glide.util.Util.assertMainThread
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
@ -49,12 +50,14 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
}
/* ==========================================================================================
* Navigator
* Navigator and other common objects
* ========================================================================================== */
protected lateinit var navigator: Navigator
private lateinit var screenComponent: ScreenComponent
protected lateinit var navigator: Navigator
protected lateinit var errorFormatter: ErrorFormatter
/* ==========================================================================================
* View model
* ========================================================================================== */
@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = screenComponent.navigator()
errorFormatter = screenComponent.errorFormatter()
viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector())

View File

@ -19,9 +19,11 @@ package im.vector.riotx.features
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@ -30,6 +32,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity
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.notifications.NotificationDrawerManager
import im.vector.riotx.features.signout.hard.SignedOutActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -37,23 +43,37 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@Parcelize
data class MainActivityArgs(
val clearCache: Boolean = false,
val clearCredentials: Boolean = false,
val isUserLoggedOut: Boolean = false,
val isSoftLogout: Boolean = false
) : Parcelable
/**
* This is the entry point of RiotX
* This Activity, when started with argument, is also doing some cleanup when user disconnects,
* clears cache, is logged out, or is soft logged out
*/
class MainActivity : VectorBaseActivity() {
companion object {
private const val EXTRA_CLEAR_CACHE = "EXTRA_CLEAR_CACHE"
private const val EXTRA_CLEAR_CREDENTIALS = "EXTRA_CLEAR_CREDENTIALS"
private const val EXTRA_ARGS = "EXTRA_ARGS"
// Special action to clear cache and/or clear credentials
fun restartApp(activity: Activity, clearCache: Boolean = false, clearCredentials: Boolean = false) {
fun restartApp(activity: Activity, args: MainActivityArgs) {
val intent = Intent(activity, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra(EXTRA_CLEAR_CACHE, clearCache)
intent.putExtra(EXTRA_CLEAR_CREDENTIALS, clearCredentials)
intent.putExtra(EXTRA_ARGS, args)
activity.startActivity(intent)
}
}
private lateinit var args: MainActivityArgs
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
@ -63,42 +83,71 @@ class MainActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val clearCache = intent.getBooleanExtra(EXTRA_CLEAR_CACHE, false)
val clearCredentials = intent.getBooleanExtra(EXTRA_CLEAR_CREDENTIALS, false)
args = parseArgs()
if (args.clearCredentials || args.isUserLoggedOut) {
clearNotifications()
}
// Handle some wanted cleanup
if (clearCache || clearCredentials) {
doCleanUp(clearCache, clearCredentials)
if (args.clearCache || args.clearCredentials) {
doCleanUp()
} else {
start()
startNextActivityAndFinish()
}
}
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) {
private fun clearNotifications() {
// Dismiss all notifications
notificationDrawerManager.clearAllEvents()
notificationDrawerManager.persistInfo()
}
private fun parseArgs(): MainActivityArgs {
val argsFromIntent: MainActivityArgs? = intent.getParcelableExtra(EXTRA_ARGS)
Timber.w("Starting MainActivity with $argsFromIntent")
return MainActivityArgs(
clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
)
}
private fun doCleanUp() {
when {
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
}
args.clearCredentials -> sessionHolder.getActiveSession().signOut(
!args.isUserLoggedOut,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
args.clearCache -> sessionHolder.getActiveSession().clearCache(
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
}
}
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here
Timber.w("Ignoring invalid token global error")
}
private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
@ -112,24 +161,43 @@ class MainActivity : VectorBaseActivity() {
}
}
start()
startNextActivityAndFinish()
}
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) {
private fun displayError(failure: Throwable) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) }
.setNegativeButton(R.string.cancel) { _, _ -> start() }
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
.setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
.setCancelable(false)
.show()
}
private fun start() {
val intent = if (sessionHolder.hasActiveSession()) {
HomeActivity.newIntent(this)
} else {
LoginActivity.newIntent(this, null)
private fun startNextActivityAndFinish() {
val intent = when {
args.clearCredentials
&& !args.isUserLoggedOut ->
// 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.
// Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) {
HomeActivity.newIntent(this)
} else {
// The token is still invalid
SoftLogoutActivity.newIntent(this)
}
else ->
// First start, or no active session
LoginActivity.newIntent(this, null)
}
startActivity(intent)
finish()

View File

@ -34,5 +34,5 @@ data class HomeDetailViewState(
val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false,
val syncState: SyncState = SyncState.IDLE
val syncState: SyncState = SyncState.Idle
) : MvRxState

View File

@ -48,6 +48,7 @@ class BreadcrumbsFragment @Inject constructor(
override fun onDestroyView() {
breadcrumbsRecyclerView.cleanup()
breadcrumbsController.listener = null
super.onDestroyView()
}
@ -56,6 +57,7 @@ class BreadcrumbsFragment @Inject constructor(
breadcrumbsController.listener = this
}
// TODO Use invalidate() ?
private fun renderState(state: BreadcrumbsViewState) {
breadcrumbsController.update(state)
}

View File

@ -69,7 +69,6 @@ import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
@ -141,7 +140,6 @@ class RoomDetailFragment @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
val textComposerViewModelFactory: TextComposerViewModel.Factory,
private val errorFormatter: ErrorFormatter,
private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences
) :

View File

@ -58,7 +58,7 @@ data class RoomDetailViewState(
val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE,
val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true

View File

@ -35,7 +35,6 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.StateView
@ -61,7 +60,6 @@ data class RoomListParams(
class RoomListFragment @Inject constructor(
private val roomController: RoomSummaryController,
val roomListViewModelFactory: RoomListViewModel.Factory,
private val errorFormatter: ErrorFormatter,
private val notificationDrawerManager: NotificationDrawerManager
) : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {

View File

@ -87,7 +87,7 @@ class LinkHandlerActivity : VectorBaseActivity() {
.setMessage(R.string.error_user_already_logged_in)
.setCancelable(false)
.setPositiveButton(R.string.logout) { _, _ ->
sessionHolder.getSafeActiveSession()?.signOut(object : MatrixCallback<Unit> {
sessionHolder.getSafeActiveSession()?.signOut(true, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
displayError(failure)
}

View File

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment
import io.reactivex.android.schedulers.AndroidSchedulers
import javax.net.ssl.HttpsURLConnection
/**
@ -60,6 +61,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
loginViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
handleLoginViewEvents(it)
}
@ -78,7 +80,7 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
private fun showError(throwable: Throwable) {
when (throwable) {
is Failure.ServerError -> {
if (throwable.error.code == MatrixError.FORBIDDEN
if (throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.httpCode == HttpsURLConnection.HTTP_FORBIDDEN /* 403 */) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
@ -93,7 +95,13 @@ abstract class AbstractLoginFragment : VectorBaseFragment(), OnBackPressed {
}
}
abstract fun onError(throwable: Throwable)
open fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
return when {

View File

@ -55,4 +55,7 @@ sealed class LoginAction : VectorViewModelAction {
object ResetSignMode : ResetAction()
object ResetLogin : ResetAction()
object ResetResetPassword : ResetAction()
// For the soft logout case
data class SetupSsoForSessionRecovery(val homeServerUrl: String, val deviceId: String) : LoginAction()
}

View File

@ -20,6 +20,7 @@ import android.content.Context
import android.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
@ -43,19 +44,21 @@ import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.terms.LoginTermsFragment
import im.vector.riotx.features.login.terms.LoginTermsFragmentArgument
import im.vector.riotx.features.login.terms.toLocalizedLoginTerms
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
/**
* The LoginActivity manages the fragment navigation and also display the loading View
*/
class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
private val loginViewModel: LoginViewModel by viewModel()
private lateinit var loginSharedActionViewModel: LoginSharedActionViewModel
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
@CallSuper
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
@ -75,17 +78,17 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
// Find findViewById does not work, I do not know why
// findViewById<View?>(R.id.loginLogo)
?.children
?.first { it.id == R.id.loginLogo }
?.firstOrNull { it.id == R.id.loginLogo }
?.let { ft.addSharedElement(it, ViewCompat.getTransitionName(it) ?: "") }
// TODO
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
override fun getLayoutRes() = R.layout.activity_login
final override fun getLayoutRes() = R.layout.activity_login
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java)
addFirstFragment()
}
// Get config extra
@ -96,7 +99,8 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
}
loginSharedActionViewModel = viewModelProvider.get(LoginSharedActionViewModel::class.java)
loginSharedActionViewModel.observe()
loginSharedActionViewModel
.observe()
.subscribe {
handleLoginNavigation(it)
}
@ -106,16 +110,20 @@ class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
.subscribe(this) {
updateWithState(it)
}
.disposeOnDestroy()
loginViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
handleLoginViewEvents(it)
}
.disposeOnDestroy()
}
protected open fun addFirstFragment() {
addFragment(R.id.loginFragmentContainer, LoginSplashFragment::class.java)
}
private fun handleLoginNavigation(loginNavigation: LoginNavigation) {
// Assigning to dummy make sure we do not forget a case
@Suppress("UNUSED_VARIABLE")

View File

@ -29,7 +29,6 @@ import androidx.core.view.isVisible
import com.airbnb.mvrx.args
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.AssetReader
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_captcha.*
@ -47,8 +46,7 @@ data class LoginCaptchaFragmentArgument(
* In this screen, the user is asked to confirm he is not a robot
*/
class LoginCaptchaFragment @Inject constructor(
private val assetReader: AssetReader,
private val errorFormatter: ErrorFormatter
private val assetReader: AssetReader
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_captcha
@ -172,14 +170,6 @@ class LoginCaptchaFragment @Inject constructor(
}
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}

View File

@ -29,9 +29,9 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.riotx.R
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.extensions.toReducedUrl
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
@ -45,9 +45,7 @@ import javax.inject.Inject
* In signup mode:
* - the user is asked for login and password
*/
class LoginFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false
@ -103,7 +101,7 @@ class LoginFragment @Inject constructor(
ServerType.MatrixOrg -> {
loginServerIcon.isVisible = true
loginServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
loginTitle.text = getString(resId, state.homeServerUrlSimple)
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
loginNotice.text = getString(R.string.login_server_matrix_org_text)
}
ServerType.Modular -> {
@ -114,7 +112,7 @@ class LoginFragment @Inject constructor(
}
ServerType.Other -> {
loginServerIcon.isVisible = false
loginTitle.text = getString(resId, state.homeServerUrlSimple)
loginTitle.text = getString(resId, state.homeServerUrl.toReducedUrl())
loginNotice.text = getString(R.string.login_server_other_text)
}
}
@ -134,7 +132,7 @@ class LoginFragment @Inject constructor(
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.isNotEmpty() },
BiFunction<Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty
}
@ -198,7 +196,7 @@ class LoginFragment @Inject constructor(
is Fail -> {
val error = state.asyncLoginAction.error
if (error is Failure.ServerError
&& error.error.code == MatrixError.FORBIDDEN
&& error.error.code == MatrixError.M_FORBIDDEN
&& error.error.message.isEmpty()) {
// Login with email, but email unknown
loginFieldTil.error = getString(R.string.login_login_with_email_error)

View File

@ -31,7 +31,6 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.failure.Failure
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.error.is401
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.isEmail
@ -56,7 +55,7 @@ data class LoginGenericTextInputFormFragmentArgument(
/**
* In this screen, the user is asked for a text input
*/
class LoginGenericTextInputFormFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() {
class LoginGenericTextInputFormFragment @Inject constructor() : AbstractLoginFragment() {
private val params: LoginGenericTextInputFormFragmentArgument by args()

View File

@ -25,10 +25,10 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.isEmail
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.extensions.toReducedUrl
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
@ -38,9 +38,7 @@ import javax.inject.Inject
/**
* In this screen, the user is asked for email and new password to reset his password
*/
class LoginResetPasswordFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginResetPasswordFragment @Inject constructor() : AbstractLoginFragment() {
private var passwordShown = false
@ -57,7 +55,7 @@ class LoginResetPasswordFragment @Inject constructor(
}
private fun setupUi(state: LoginViewState) {
resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrlSimple)
resetPasswordTitle.text = getString(R.string.login_reset_password_on, state.homeServerUrl.toReducedUrl())
}
private fun setupSubmitButton() {
@ -138,14 +136,6 @@ class LoginResetPasswordFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetResetPassword)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun updateWithState(state: LoginViewState) {
setupUi(state)

View File

@ -21,7 +21,6 @@ import butterknife.OnClick
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.error.is401
import kotlinx.android.synthetic.main.fragment_login_reset_password_mail_confirmation.*
import javax.inject.Inject
@ -29,9 +28,7 @@ import javax.inject.Inject
/**
* In this screen, the user is asked to check his email and to click on a button once it's done
*/
class LoginResetPasswordMailConfirmationFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginResetPasswordMailConfirmationFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_reset_password_mail_confirmation
@ -44,14 +41,6 @@ class LoginResetPasswordMailConfirmationFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetPasswordMailConfirmed)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetResetPassword)
}

View File

@ -16,18 +16,14 @@
package im.vector.riotx.features.login
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import javax.inject.Inject
/**
* In this screen, the user is asked for email and new password to reset his password
* In this screen, we confirm to the user that his password has been reset
*/
class LoginResetPasswordSuccessFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginResetPasswordSuccessFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_reset_password_success
@ -36,14 +32,6 @@ class LoginResetPasswordSuccessFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OnResetPasswordMailConfirmationSuccessDone)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetResetPassword)
}

View File

@ -18,11 +18,9 @@ package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import kotlinx.android.synthetic.main.fragment_login_server_selection.*
import me.gujun.android.span.span
@ -31,9 +29,7 @@ import javax.inject.Inject
/**
* In this screen, the user will choose between matrix.org, modular or other type of homeserver
*/
class LoginServerSelectionFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginServerSelectionFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_server_selection
@ -107,14 +103,6 @@ class LoginServerSelectionFragment @Inject constructor(
loginViewModel.handle(LoginAction.ResetHomeServerType)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun updateWithState(state: LoginViewState) {
updateSelectedChoice(state)

View File

@ -24,7 +24,6 @@ import androidx.core.view.isVisible
import butterknife.OnClick
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import kotlinx.android.synthetic.main.fragment_login_server_url_form.*
@ -33,9 +32,7 @@ import javax.inject.Inject
/**
* In this screen, the user is prompted to enter a homeserver url
*/
class LoginServerUrlFormFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginServerUrlFormFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_server_url_form

View File

@ -16,20 +16,17 @@
package im.vector.riotx.features.login
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import butterknife.OnClick
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.toReducedUrl
import kotlinx.android.synthetic.main.fragment_login_signup_signin_selection.*
import javax.inject.Inject
/**
* In this screen, the user is asked to sign up or to sign in to the homeserver
*/
class LoginSignUpSignInSelectionFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_signup_signin_selection
@ -40,19 +37,19 @@ class LoginSignUpSignInSelectionFragment @Inject constructor(
ServerType.MatrixOrg -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_matrix_org)
loginSignupSigninServerIcon.isVisible = true
loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrlSimple)
loginSignupSigninTitle.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
loginSignupSigninText.text = getString(R.string.login_server_matrix_org_text)
}
ServerType.Modular -> {
loginSignupSigninServerIcon.setImageResource(R.drawable.ic_logo_modular)
loginSignupSigninServerIcon.isVisible = true
loginSignupSigninTitle.text = getString(R.string.login_connect_to_modular)
loginSignupSigninText.text = state.homeServerUrlSimple
loginSignupSigninText.text = state.homeServerUrl.toReducedUrl()
}
ServerType.Other -> {
loginSignupSigninServerIcon.isVisible = false
loginSignupSigninTitle.text = getString(R.string.login_server_other_title)
loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrlSimple)
loginSignupSigninText.text = getString(R.string.login_connect_to, state.homeServerUrl.toReducedUrl())
}
}
}
@ -84,14 +81,6 @@ class LoginSignUpSignInSelectionFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetSignMode)
}

View File

@ -16,18 +16,14 @@
package im.vector.riotx.features.login
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import javax.inject.Inject
/**
* In this screen, the user is viewing an introduction to what he can do with this application
*/
class LoginSplashFragment @Inject constructor(
private val errorFormatter: ErrorFormatter
) : AbstractLoginFragment() {
class LoginSplashFragment @Inject constructor() : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_splash
@ -36,14 +32,6 @@ class LoginSplashFragment @Inject constructor(
loginSharedActionViewModel.post(LoginNavigation.OpenServerSelection)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun resetViewModel() {
// Nothing to do
}

View File

@ -16,6 +16,7 @@
package im.vector.riotx.features.login
import androidx.fragment.app.FragmentActivity
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -37,6 +38,7 @@ import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import timber.log.Timber
import java.util.concurrent.CancellationException
@ -60,8 +62,11 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? {
val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.loginViewModelFactory.create(state)
return when (val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity()) {
is LoginActivity -> activity.loginViewModelFactory.create(state)
is SoftLogoutActivity -> activity.loginViewModelFactory.create(state)
else -> error("Invalid Activity")
}
}
}
@ -97,6 +102,18 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
is LoginAction.ResetPasswordMailConfirmed -> handleResetPasswordMailConfirmed()
is LoginAction.RegisterAction -> handleRegisterAction(action)
is LoginAction.ResetAction -> handleResetAction(action)
is LoginAction.SetupSsoForSessionRecovery -> handleSetupSsoForSessionRecovery(action)
}
}
private fun handleSetupSsoForSessionRecovery(action: LoginAction.SetupSsoForSessionRecovery) {
setState {
copy(
signMode = SignMode.SignIn,
loginMode = LoginMode.Sso,
homeServerUrl = action.homeServerUrl,
deviceId = action.deviceId
)
}
}
@ -452,7 +469,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
}
private fun startAuthenticationFlow() {
// No op
// Ensure Wizard is ready
loginWizard
}

View File

@ -34,6 +34,9 @@ data class LoginViewState(
val resetPasswordEmail: String? = null,
@PersistState
val homeServerUrl: String? = null,
// For SSO session recovery
@PersistState
val deviceId: String? = null,
// Network result
@PersistState
@ -49,17 +52,11 @@ data class LoginViewState(
|| asyncResetPassword is Loading
|| asyncResetMailConfirmed is Loading
|| asyncRegistration is Loading
// Keep loading when it is success because of the delay to switch to the next Activity
|| asyncLoginAction is Success
}
fun isUserLogged(): Boolean {
return asyncLoginAction is Success
}
/**
* Ex: "https://matrix.org/" -> "matrix.org"
*/
val homeServerUrlSimple: String
get() = (homeServerUrl ?: "")
.substringAfter("://")
.trim { it == '/' }
}

View File

@ -19,10 +19,8 @@ package im.vector.riotx.features.login
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.args
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.error.is401
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_login_wait_for_email.*
@ -36,7 +34,7 @@ data class LoginWaitForEmailFragmentArgument(
/**
* In this screen, the user is asked to check his emails
*/
class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter: ErrorFormatter) : AbstractLoginFragment() {
class LoginWaitForEmailFragment @Inject constructor() : AbstractLoginFragment() {
private val params: LoginWaitForEmailFragmentArgument by args()
@ -69,11 +67,7 @@ class LoginWaitForEmailFragment @Inject constructor(private val errorFormatter:
// Try again, with a delay
loginViewModel.handle(LoginAction.CheckIfEmailHasBeenValidated(10_000))
} else {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
super.onError(throwable)
}
}

View File

@ -30,10 +30,13 @@ import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.features.signout.soft.SoftLogoutAction
import im.vector.riotx.features.signout.soft.SoftLogoutViewModel
import kotlinx.android.synthetic.main.fragment_login_web.*
import timber.log.Timber
import java.net.URLDecoder
@ -44,13 +47,13 @@ import javax.inject.Inject
* of the homeserver, as a fallback to login or to create an account
*/
class LoginWebFragment @Inject constructor(
private val assetReader: AssetReader,
private val errorFormatter: ErrorFormatter
private val assetReader: AssetReader
) : AbstractLoginFragment() {
override fun getLayoutResId() = R.layout.fragment_login_web
private var isWebViewLoaded = false
private var isForSessionRecovery = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -60,6 +63,9 @@ class LoginWebFragment @Inject constructor(
override fun updateWithState(state: LoginViewState) {
setupTitle(state)
isForSessionRecovery = state.deviceId?.isNotBlank() == true
if (!isWebViewLoaded) {
setupWebView(state)
isWebViewLoaded = true
@ -110,13 +116,22 @@ class LoginWebFragment @Inject constructor(
}
private fun launchWebView(state: LoginViewState) {
if (state.signMode == SignMode.SignIn) {
loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/login/")
} else {
// MODE_REGISTER
loginWebWebView.loadUrl(state.homeServerUrl?.trim { it == '/' } + "/_matrix/static/client/register/")
val url = buildString {
append(state.homeServerUrl?.trim { it == '/' })
if (state.signMode == SignMode.SignIn) {
append("/_matrix/static/client/login/")
state.deviceId?.takeIf { it.isNotBlank() }?.let {
// But https://github.com/matrix-org/synapse/issues/5755
append("?device_id=$it")
}
} else {
// MODE_REGISTER
append("/_matrix/static/client/register/")
}
}
loginWebWebView.loadUrl(url)
loginWebWebView.webViewClient = object : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
error: SslError) {
@ -212,10 +227,7 @@ class LoginWebFragment @Inject constructor(
if (state.signMode == SignMode.SignIn) {
try {
if (action == "onLogin") {
val credentials = javascriptResponse.credentials
if (credentials != null) {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
javascriptResponse.credentials?.let { notifyViewModel(it) }
}
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : failed")
@ -224,10 +236,7 @@ class LoginWebFragment @Inject constructor(
// MODE_REGISTER
// check the required parameters
if (action == "onRegistered") {
val credentials = javascriptResponse.credentials
if (credentials != null) {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
javascriptResponse.credentials?.let { notifyViewModel(it) }
}
}
}
@ -239,16 +248,17 @@ class LoginWebFragment @Inject constructor(
}
}
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
private fun notifyViewModel(credentials: Credentials) {
if (isForSessionRecovery) {
val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
softLogoutViewModel.handle(SoftLogoutAction.WebLoginSuccess(credentials))
} else {
loginViewModel.handle(LoginAction.WebLoginSuccess(credentials))
}
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
override fun resetViewModel() {
loginViewModel.handle(LoginAction.ResetLogin)
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {

View File

@ -19,13 +19,12 @@ package im.vector.riotx.features.login.terms
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AlertDialog
import butterknife.OnClick
import com.airbnb.mvrx.args
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.toReducedUrl
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction
@ -44,8 +43,7 @@ data class LoginTermsFragmentArgument(
* LoginTermsFragment displays the list of policies the user has to accept
*/
class LoginTermsFragment @Inject constructor(
private val policyController: PolicyController,
private val errorFormatter: ErrorFormatter
private val policyController: PolicyController
) : AbstractLoginFragment(),
PolicyController.PolicyControllerListener {
@ -106,16 +104,8 @@ class LoginTermsFragment @Inject constructor(
loginViewModel.handle(LoginAction.AcceptTerms)
}
override fun onError(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun updateWithState(state: LoginViewState) {
policyController.homeServer = state.homeServerUrlSimple
policyController.homeServer = state.homeServerUrl.toReducedUrl()
renderState()
}

View File

@ -26,7 +26,6 @@ import com.google.android.material.snackbar.Snackbar
import com.jakewharton.rxbinding3.appcompat.queryTextChanges
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
@ -42,8 +41,7 @@ import javax.inject.Inject
* - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect
*/
class PublicRoomsFragment @Inject constructor(
private val publicRoomsController: PublicRoomsController,
private val errorFormatter: ErrorFormatter
private val publicRoomsController: PublicRoomsController
) : VectorBaseFragment(), PublicRoomsController.Callback {
private val viewModel: RoomDirectoryViewModel by activityViewModel()

View File

@ -24,7 +24,6 @@ import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.platform.ButtonStateView
import im.vector.riotx.core.platform.VectorBaseFragment
@ -37,7 +36,6 @@ import javax.inject.Inject
* Note: this Fragment is also used for world readable room for the moment
*/
class RoomPreviewNoPreviewFragment @Inject constructor(
private val errorFormatter: ErrorFormatter,
val roomPreviewViewModelFactory: RoomPreviewViewModel.Factory,
private val avatarRenderer: AvatarRenderer
) : VectorBaseFragment() {

View File

@ -18,27 +18,21 @@ package im.vector.riotx.features.session
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SessionListener @Inject constructor() : Session.Listener {
private val _consentNotGivenLiveData = MutableLiveData<LiveEvent<ConsentNotGivenError>>()
val consentNotGivenLiveData: LiveData<LiveEvent<ConsentNotGivenError>>
get() = _consentNotGivenLiveData
private val _globalErrorLiveData = MutableLiveData<LiveEvent<GlobalError>>()
val globalErrorLiveData: LiveData<LiveEvent<GlobalError>>
get() = _globalErrorLiveData
override fun onInvalidToken() {
// TODO Handle this error
Timber.e("Token is not valid anymore: handle this properly")
}
override fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
_consentNotGivenLiveData.postLiveEvent(consentNotGivenError)
override fun onGlobalError(globalError: GlobalError) {
_globalErrorLiveData.postLiveEvent(globalError)
}
}

View File

@ -43,6 +43,7 @@ import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.features.workers.signout.SignOutUiWorker
import kotlinx.coroutines.Dispatchers
@ -176,7 +177,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayLoadingView()
MainActivity.restartApp(activity!!, clearCache = true, clearCredentials = false)
MainActivity.restartApp(activity!!, MainActivityArgs(clearCache = true))
false
}
}

View File

@ -25,7 +25,6 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
@ -37,8 +36,7 @@ import javax.inject.Inject
class VectorSettingsIgnoredUsersFragment @Inject constructor(
val ignoredUsersViewModelFactory: IgnoredUsersViewModel.Factory,
private val ignoredUsersController: IgnoredUsersController,
private val errorFormatter: ErrorFormatter
private val ignoredUsersController: IgnoredUsersController
) : VectorBaseFragment(), IgnoredUsersController.Callback {
override fun getLayoutResId() = R.layout.fragment_generic_recycler

View File

@ -0,0 +1,52 @@
/*
* 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.hard
import android.content.Context
import android.content.Intent
import butterknife.OnClick
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import timber.log.Timber
/**
* In this screen, the user is viewing a message informing that he has been logged out
*/
class SignedOutActivity : VectorBaseActivity() {
override fun getLayoutRes() = R.layout.activity_signed_out
@OnClick(R.id.signedOutSubmit)
fun submit() {
// All is already cleared when we are here
MainActivity.restartApp(this, MainActivityArgs())
}
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, SignedOutActivity::class.java)
}
}
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here
Timber.w("Ignoring invalid token global error")
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.soft
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class SoftLogoutAction : VectorViewModelAction {
// In case of failure to get the login flow
object RetryLoginFlow : SoftLogoutAction()
// For password entering management
data class PasswordChanged(val password: String) : SoftLogoutAction()
object TogglePassword : SoftLogoutAction()
data class SignInAgain(val password: String) : SoftLogoutAction()
// For signing again with SSO
data class WebLoginSuccess(val credentials: Credentials) : SoftLogoutAction()
// To clear the current session
object ClearData : SoftLogoutAction()
}

View File

@ -0,0 +1,125 @@
/*
* 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.soft
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
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.error.ErrorFormatter
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.login.LoginActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_login.*
import timber.log.Timber
import javax.inject.Inject
/**
* In this screen, the user is viewing a message informing that he has been logged out
* Extends LoginActivity to get the login with SSO and forget password functionality for (nearly) free
*/
class SoftLogoutActivity : LoginActivity() {
private val softLogoutViewModel: SoftLogoutViewModel by viewModel()
@Inject lateinit var softLogoutViewModelFactory: SoftLogoutViewModel.Factory
@Inject lateinit var session: Session
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun initUiAndData() {
super.initUiAndData()
softLogoutViewModel
.subscribe(this) {
updateWithState(it)
}
softLogoutViewModel.viewEvents
.observe()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
handleSoftLogoutViewEvents(it)
}
.disposeOnDestroy()
}
private fun handleSoftLogoutViewEvents(softLogoutViewEvents: SoftLogoutViewEvents) {
when (softLogoutViewEvents) {
is SoftLogoutViewEvents.Error ->
showError(errorFormatter.toHumanReadable(softLogoutViewEvents.throwable))
is SoftLogoutViewEvents.ErrorNotSameUser -> {
// Pop the backstack
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// And inform the user
showError(getString(
R.string.soft_logout_sso_not_same_user_error,
softLogoutViewEvents.currentUserId,
softLogoutViewEvents.newUserId)
)
}
is SoftLogoutViewEvents.ClearData -> {
MainActivity.restartApp(this, MainActivityArgs(clearCredentials = true))
}
}
}
private fun showError(message: String) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
override fun addFirstFragment() {
replaceFragment(R.id.loginFragmentContainer, SoftLogoutFragment::class.java)
}
private fun updateWithState(softLogoutViewState: SoftLogoutViewState) {
if (softLogoutViewState.asyncLoginAction is Success) {
MainActivity.restartApp(this, MainActivityArgs())
}
loginLoading.isVisible = softLogoutViewState.isLoading()
}
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,161 @@
/*
* 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.soft
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.toReducedUrl
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.login.LoginMode
import im.vector.riotx.features.signout.soft.epoxy.*
import javax.inject.Inject
class SoftLogoutController @Inject constructor(
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter
) : EpoxyController() {
var listener: Listener? = null
private var viewState: SoftLogoutViewState? = null
init {
// We are requesting a model build directly as the first build of epoxy is on the main thread.
// It avoids to build the whole list of breadcrumbs on the main thread.
requestModelBuild()
}
fun update(viewState: SoftLogoutViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val safeViewState = viewState ?: return
buildHeader(safeViewState)
buildForm(safeViewState)
buildClearDataSection()
}
private fun buildHeader(state: SoftLogoutViewState) {
loginHeaderItem {
id("header")
}
loginTitleItem {
id("title")
text(stringProvider.getString(R.string.soft_logout_title))
}
loginTitleSmallItem {
id("signTitle")
text(stringProvider.getString(R.string.soft_logout_signin_title))
}
loginTextItem {
id("signText1")
text(stringProvider.getString(R.string.soft_logout_signin_notice,
state.homeServerUrl.toReducedUrl(),
state.userDisplayName,
state.userId))
}
if (state.hasUnsavedKeys) {
loginTextItem {
id("signText2")
text(stringProvider.getString(R.string.soft_logout_signin_e2e_warning_notice))
}
}
}
private fun buildForm(state: SoftLogoutViewState) {
when (state.asyncHomeServerLoginFlowRequest) {
is Incomplete -> {
loadingItem {
id("loading")
}
}
is Fail -> {
loginErrorWithRetryItem {
id("errorRetry")
text(errorFormatter.toHumanReadable(state.asyncHomeServerLoginFlowRequest.error))
listener { listener?.retry() }
}
}
is Success -> {
when (state.asyncHomeServerLoginFlowRequest.invoke()) {
LoginMode.Password -> {
loginPasswordFormItem {
id("passwordForm")
stringProvider(stringProvider)
passwordShown(state.passwordShown)
submitEnabled(state.submitEnabled)
onPasswordEdited { listener?.passwordEdited(it) }
errorText((state.asyncLoginAction as? Fail)?.error?.let { errorFormatter.toHumanReadable(it) })
passwordRevealClickListener { listener?.revealPasswordClicked() }
forgetPasswordClickListener { listener?.forgetPasswordClicked() }
submitClickListener { password -> listener?.signinSubmit(password) }
}
}
LoginMode.Sso -> {
loginCenterButtonItem {
id("sso")
text(stringProvider.getString(R.string.login_signin_sso))
listener { listener?.signinFallbackSubmit() }
}
}
LoginMode.Unsupported -> {
loginCenterButtonItem {
id("fallback")
text(stringProvider.getString(R.string.login_signin))
listener { listener?.signinFallbackSubmit() }
}
}
LoginMode.Unknown -> Unit // Should not happen
}
}
}
}
private fun buildClearDataSection() {
loginTitleSmallItem {
id("clearDataTitle")
text(stringProvider.getString(R.string.soft_logout_clear_data_title))
}
loginTextItem {
id("clearDataText")
text(stringProvider.getString(R.string.soft_logout_clear_data_notice))
}
loginRedButtonItem {
id("clearDataSubmit")
text(stringProvider.getString(R.string.soft_logout_clear_data_submit))
listener { listener?.clearData() }
}
}
interface Listener {
fun retry()
fun passwordEdited(password: String)
fun signinSubmit(password: String)
fun signinFallbackSubmit()
fun clearData()
fun forgetPasswordClicked()
fun revealPasswordClicked()
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.soft
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.login.AbstractLoginFragment
import im.vector.riotx.features.login.LoginAction
import im.vector.riotx.features.login.LoginMode
import im.vector.riotx.features.login.LoginNavigation
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
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 softLogoutController: SoftLogoutController
) : AbstractLoginFragment(), SoftLogoutController.Listener {
private val softLogoutViewModel: SoftLogoutViewModel by activityViewModel()
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
softLogoutViewModel.subscribe(this) { softLogoutViewState ->
softLogoutController.update(softLogoutViewState)
when (softLogoutViewState.asyncHomeServerLoginFlowRequest.invoke()) {
LoginMode.Sso,
LoginMode.Unsupported -> {
// Prepare the loginViewModel for a SSO/login fallback recovery
loginViewModel.handle(LoginAction.SetupSsoForSessionRecovery(
softLogoutViewState.homeServerUrl,
softLogoutViewState.deviceId
))
}
else -> Unit
}
}
}
private fun setupRecyclerView() {
recyclerView.configureWith(softLogoutController)
softLogoutController.listener = this
}
override fun onDestroyView() {
recyclerView.cleanup()
softLogoutController.listener = null
super.onDestroyView()
}
override fun retry() {
softLogoutViewModel.handle(SoftLogoutAction.RetryLoginFlow)
}
override fun passwordEdited(password: String) {
softLogoutViewModel.handle(SoftLogoutAction.PasswordChanged(password))
}
override fun signinSubmit(password: String) {
cleanupUi()
softLogoutViewModel.handle(SoftLogoutAction.SignInAgain(password))
}
override fun signinFallbackSubmit() {
loginSharedActionViewModel.post(LoginNavigation.OnSignModeSelected)
}
override fun clearData() {
withState(softLogoutViewModel) { state ->
cleanupUi()
val messageResId = if (state.hasUnsavedKeys) {
R.string.soft_logout_clear_data_dialog_e2e_warning_content
} else {
R.string.soft_logout_clear_data_dialog_content
}
AlertDialog.Builder(requireActivity())
.setTitle(R.string.soft_logout_clear_data_dialog_title)
.setMessage(messageResId)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.soft_logout_clear_data_submit) { _, _ ->
softLogoutViewModel.handle(SoftLogoutAction.ClearData)
}
.show()
.withColoredButton(DialogInterface.BUTTON_POSITIVE)
}
}
private fun cleanupUi() {
recyclerView.hideKeyboard()
}
override fun forgetPasswordClicked() {
loginSharedActionViewModel.post(LoginNavigation.OnForgetPasswordClicked)
}
override fun revealPasswordClicked() {
softLogoutViewModel.handle(SoftLogoutAction.TogglePassword)
}
override fun resetViewModel() {
// No op
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.soft
/**
* Transient events for SoftLogout
*/
sealed class SoftLogoutViewEvents {
data class ErrorNotSameUser(val currentUserId: String, val newUserId: String) : SoftLogoutViewEvents()
data class Error(val throwable: Throwable) : SoftLogoutViewEvents()
object ClearData : SoftLogoutViewEvents()
}

View File

@ -0,0 +1,255 @@
/*
* 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.soft
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.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.DataSource
import im.vector.riotx.core.utils.PublishDataSource
import im.vector.riotx.features.login.LoginMode
import timber.log.Timber
/**
* TODO Test push: disable the pushers?
*/
class SoftLogoutViewModel @AssistedInject constructor(
@Assisted initialState: SoftLogoutViewState,
private val session: Session,
private val activeSessionHolder: ActiveSessionHolder,
private val authenticationService: AuthenticationService
) : 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(),
userId = userId,
deviceId = activity.session.sessionParams.credentials.deviceId ?: "",
userDisplayName = activity.session.getUser(userId)?.displayName ?: userId,
hasUnsavedKeys = activity.session.hasUnsavedKeys()
)
}
@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
private val _viewEvents = PublishDataSource<SoftLogoutViewEvents>()
val viewEvents: DataSource<SoftLogoutViewEvents> = _viewEvents
init {
// Get the supported login flow
getSupportedLoginFlow()
}
private fun getSupportedLoginFlow() {
val homeServerConnectionConfig = session.sessionParams.homeServerConnectionConfig
currentTask?.cancel()
currentTask = null
authenticationService.cancelPendingLoginOrRegistration()
setState {
copy(
asyncHomeServerLoginFlowRequest = Loading()
)
}
currentTask = authenticationService.getLoginFlow(homeServerConnectionConfig, object : MatrixCallback<LoginFlowResult> {
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(failure)
)
}
}
override fun onSuccess(data: LoginFlowResult) {
when (data) {
is LoginFlowResult.Success -> {
val loginMode = when {
// SSO login is taken first
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.SSO } -> LoginMode.Sso
data.loginFlowResponse.flows.any { it.type == LoginFlowTypes.PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
}
if (loginMode == LoginMode.Password && !data.isLoginAndRegistrationSupported) {
notSupported()
} else {
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
}
}
}
is LoginFlowResult.OutdatedHomeserver -> {
notSupported()
}
}
}
private fun notSupported() {
// Should not happen since it's a re-logout
// Notify the UI
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(IllegalStateException("Should not happen"))
)
}
}
})
}
override fun handle(action: SoftLogoutAction) {
when (action) {
is SoftLogoutAction.RetryLoginFlow -> getSupportedLoginFlow()
is SoftLogoutAction.PasswordChanged -> handlePasswordChange(action)
is SoftLogoutAction.TogglePassword -> handleTogglePassword()
is SoftLogoutAction.SignInAgain -> handleSignInAgain(action)
is SoftLogoutAction.WebLoginSuccess -> handleWebLoginSuccess(action)
is SoftLogoutAction.ClearData -> handleClearData()
}
}
private fun handleClearData() {
// Notify the Activity
_viewEvents.post(SoftLogoutViewEvents.ClearData)
}
private fun handlePasswordChange(action: SoftLogoutAction.PasswordChanged) {
setState {
copy(
asyncLoginAction = Uninitialized,
submitEnabled = action.password.isNotBlank()
)
}
}
private fun handleTogglePassword() {
withState {
setState {
copy(
passwordShown = !this.passwordShown
)
}
}
}
private fun handleWebLoginSuccess(action: SoftLogoutAction.WebLoginSuccess) {
// User may have been connected with SSO with another userId
// We have to check this
withState { softLogoutViewState ->
if (softLogoutViewState.userId != action.credentials.userId) {
Timber.w("User login again with SSO, but using another account")
_viewEvents.post(SoftLogoutViewEvents.ErrorNotSameUser(
softLogoutViewState.userId,
action.credentials.userId))
} else {
setState {
copy(
asyncLoginAction = Loading()
)
}
currentTask = session.updateCredentials(action.credentials,
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
_viewEvents.post(SoftLogoutViewEvents.Error(failure))
setState {
copy(
asyncLoginAction = Uninitialized
)
}
}
override fun onSuccess(data: Unit) {
onSessionRestored()
}
}
)
}
}
}
private fun handleSignInAgain(action: SoftLogoutAction.SignInAgain) {
setState {
copy(
asyncLoginAction = Loading(),
// Ensure password is hidden
passwordShown = false
)
}
currentTask = session.signInAgain(action.password,
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
}
override fun onSuccess(data: Unit) {
onSessionRestored()
}
}
)
}
private fun onSessionRestored() {
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,39 @@
/*
* 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.soft
import com.airbnb.mvrx.*
import im.vector.riotx.features.login.LoginMode
data class SoftLogoutViewState(
val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized,
val asyncLoginAction: Async<Unit> = Uninitialized,
val homeServerUrl: String,
val userId: String,
val deviceId: String,
val userDisplayName: String,
val hasUnsavedKeys: Boolean,
val passwordShown: Boolean = false,
val submitEnabled: Boolean = false
) : MvRxState {
fun isLoading(): Boolean {
return asyncLoginAction is Loading
// Keep loading when it is success because of the delay to switch to the next Activity
|| asyncLoginAction is Success
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.soft.epoxy
import android.widget.Button
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_login_centered_button)
abstract class LoginCenterButtonItem : VectorEpoxyModel<LoginCenterButtonItem.Holder>() {
@EpoxyAttribute var text: String? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.button.setTextOrHide(text)
holder.button.setOnClickListener {
listener?.invoke()
}
}
class Holder : VectorEpoxyHolder() {
val button by bind<Button>(R.id.itemLoginCenteredButton)
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.soft.epoxy
import android.widget.Button
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_login_error_retry)
abstract class LoginErrorWithRetryItem : VectorEpoxyModel<LoginErrorWithRetryItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
holder.textView.text = text
holder.buttonView.setOnClickListener { listener?.invoke() }
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.itemLoginErrorRetryText)
val buttonView by bind<Button>(R.id.itemLoginErrorRetryButton)
}
}

View File

@ -0,0 +1,27 @@
/*
* 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.soft.epoxy
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_login_header)
abstract class LoginHeaderItem : VectorEpoxyModel<LoginHeaderItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View File

@ -0,0 +1,96 @@
/*
* 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.soft.epoxy
import android.os.Build
import android.text.Editable
import android.widget.Button
import android.widget.ImageView
import androidx.autofill.HintConstants
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.resources.StringProvider
@EpoxyModelClass(layout = R.layout.item_login_password_form)
abstract class LoginPasswordFormItem : VectorEpoxyModel<LoginPasswordFormItem.Holder>() {
@EpoxyAttribute var passwordShown: Boolean = false
@EpoxyAttribute var submitEnabled: Boolean = false
@EpoxyAttribute var errorText: String? = null
@EpoxyAttribute lateinit var stringProvider: StringProvider
@EpoxyAttribute var passwordRevealClickListener: (() -> Unit)? = null
@EpoxyAttribute var forgetPasswordClickListener: (() -> Unit)? = null
@EpoxyAttribute var submitClickListener: ((String) -> Unit)? = null
@EpoxyAttribute var onPasswordEdited: ((String) -> Unit)? = null
private val textChangeListener = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
onPasswordEdited?.invoke(s.toString())
}
}
override fun bind(holder: Holder) {
super.bind(holder)
setupAutoFill(holder)
holder.passwordFieldTil.error = errorText
renderPasswordField(holder)
holder.passwordReveal.setOnClickListener { passwordRevealClickListener?.invoke() }
holder.forgetPassword.setOnClickListener { forgetPasswordClickListener?.invoke() }
holder.submit.isEnabled = submitEnabled
holder.submit.setOnClickListener { submitClickListener?.invoke(holder.passwordField.text.toString()) }
holder.passwordField.addTextChangedListener(textChangeListener)
}
override fun unbind(holder: Holder) {
super.unbind(holder)
holder.passwordField.removeTextChangedListener(textChangeListener)
}
private fun setupAutoFill(holder: Holder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
holder.passwordField.setAutofillHints(HintConstants.AUTOFILL_HINT_PASSWORD)
}
}
private fun renderPasswordField(holder: Holder) {
holder.passwordField.showPassword(passwordShown)
if (passwordShown) {
holder.passwordReveal.setImageResource(R.drawable.ic_eye_closed_black)
holder.passwordReveal.contentDescription = stringProvider.getString(R.string.a11y_hide_password)
} else {
holder.passwordReveal.setImageResource(R.drawable.ic_eye_black)
holder.passwordReveal.contentDescription = stringProvider.getString(R.string.a11y_show_password)
}
}
class Holder : VectorEpoxyHolder() {
val passwordField by bind<TextInputEditText>(R.id.itemLoginPasswordFormPasswordField)
val passwordFieldTil by bind<TextInputLayout>(R.id.itemLoginPasswordFormPasswordFieldTil)
val passwordReveal by bind<ImageView>(R.id.itemLoginPasswordFormPasswordReveal)
val forgetPassword by bind<Button>(R.id.itemLoginPasswordFormForgetPasswordButton)
val submit by bind<Button>(R.id.itemLoginPasswordFormSubmit)
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.soft.epoxy
import android.widget.Button
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_login_red_button)
abstract class LoginRedButtonItem : VectorEpoxyModel<LoginRedButtonItem.Holder>() {
@EpoxyAttribute var text: String? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.button.setTextOrHide(text)
holder.button.setOnClickListener {
listener?.invoke()
}
}
class Holder : VectorEpoxyHolder() {
val button by bind<Button>(R.id.itemLoginRedButton)
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.soft.epoxy
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_login_text)
abstract class LoginTextItem : VectorEpoxyModel<LoginTextItem.Holder>() {
@EpoxyAttribute var text: String? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(text)
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.itemLoginText)
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.soft.epoxy
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_login_title)
abstract class LoginTitleItem : VectorEpoxyModel<LoginTitleItem.Holder>() {
@EpoxyAttribute var text: String? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(text)
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.itemLoginTitleText)
}
}

View File

@ -0,0 +1,41 @@
/*
* 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.soft.epoxy
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_login_title_small)
abstract class LoginTitleSmallItem : VectorEpoxyModel<LoginTitleSmallItem.Holder>() {
@EpoxyAttribute var text: String? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.textView.setTextOrHide(text)
}
class Holder : VectorEpoxyHolder() {
val textView by bind<TextView>(R.id.itemLoginTitleSmallText)
}
}

View File

@ -34,9 +34,9 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute
fun render(newState: SyncState) {
syncStateProgressBar.visibility = when (newState) {
is SyncState.RUNNING -> if (newState.afterPause) View.VISIBLE else View.GONE
is SyncState.Running -> if (newState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncStateNoNetwork.isVisible = newState == SyncState.NO_NETWORK
syncStateNoNetwork.isVisible = newState == SyncState.NoNetwork
}
}

View File

@ -21,20 +21,20 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.hasUnsavedKeys
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.MainActivityArgs
class SignOutUiWorker(private val activity: FragmentActivity) {
lateinit var notificationDrawerManager: NotificationDrawerManager
lateinit var activeSessionHolder: ActiveSessionHolder
fun perform(context: Context) {
notificationDrawerManager = context.vectorComponent().notificationDrawerManager()
activeSessionHolder = context.vectorComponent().activeSessionHolder()
val session = activeSessionHolder.getActiveSession()
if (SignOutViewModel.doYouNeedToBeDisplayed(session)) {
if (session.hasUnsavedKeys()) {
// The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready
val signOutDialog = SignOutBottomSheetDialogFragment.newInstance()
signOutDialog.onSignOut = Runnable {
doSignOut()
@ -54,10 +54,6 @@ class SignOutUiWorker(private val activity: FragmentActivity) {
}
private fun doSignOut() {
// Dismiss all notifications
notificationDrawerManager.clearAllEvents()
notificationDrawerManager.persistInfo()
MainActivity.restartApp(activity, clearCache = true, clearCredentials = true)
MainActivity.restartApp(activity, MainActivityArgs(clearCredentials = true))
}
}

View File

@ -71,14 +71,4 @@ class SignOutViewModel @Inject constructor(private val session: Session) : ViewM
session.getKeysBackupService().checkAndStartKeysBackup()
}
}
companion object {
/**
* The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready
*/
fun doYouNeedToBeDisplayed(session: Session): Boolean {
return session.inboundGroupSessionsCount(false) > 0
&& session.getKeysBackupService().state != KeysBackupState.ReadyToBackUp
}
}
}

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/signedOut"
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/signed_out_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:gravity="start"
android:text="@string/signed_out_notice"
android:textAppearance="@style/TextAppearance.Vector.Login.Text" />
<com.google.android.material.button.MaterialButton
android:id="@+id/signedOutSubmit"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="22dp"
android:text="@string/signed_out_submit" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,7 +2,6 @@
<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/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemLoginCenteredButton"
style="@style/Style.Vector.Login.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
tools:text="@string/login_signin_sso" />
</FrameLayout>

View File

@ -0,0 +1,35 @@
<?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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp">
<TextView
android:id="@+id/itemLoginErrorRetryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:textColor="@color/riotx_notice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Error" />
<com.google.android.material.button.MaterialButton
android:id="@+id/itemLoginErrorRetryButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/global_retry"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemLoginErrorRetryText" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@id/loginLogo"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:importantForAccessibility="no"
android:src="@drawable/riotx_logo" />

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
android:paddingStart="36dp"
android:paddingEnd="36dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/itemLoginPasswordFormPasswordFieldTil"
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/itemLoginPasswordFormPasswordField"
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/itemLoginPasswordFormPasswordReveal"
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/itemLoginPasswordFormForgetPasswordButton"
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/itemLoginPasswordFormSubmit"
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>
</LinearLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemLoginRedButton"
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:layout_marginEnd="36dp"
app:backgroundTint="@color/vector_error_color"
tools:text="@string/soft_logout_clear_data_submit" />
</FrameLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemLoginText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:paddingStart="36dp"
android:paddingEnd="36dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Text"
tools:text="Login Title" />

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemLoginTitleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="32dp"
android:paddingStart="36dp"
android:paddingEnd="36dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Title"
tools:text="Login Title" />

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemLoginTitleSmallText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:paddingStart="36dp"
android:paddingEnd="36dp"
android:textAppearance="@style/TextAppearance.Vector.Login.Title.Small"
tools:text="Login Title" />

Some files were not shown because too many files have changed in this diff Show More