mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-17 11:48:51 +03:00
Merge pull request #768 from vector-im/feature/soft_logout
Handle invalid tokens gracefully
This commit is contained in:
commit
3feb2d8980
102 changed files with 2254 additions and 397 deletions
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
|
@ -18,6 +18,7 @@
|
|||
<w>pbkdf</w>
|
||||
<w>pkcs</w>
|
||||
<w>signin</w>
|
||||
<w>signout</w>
|
||||
<w>signup</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
|
|
@ -2,7 +2,7 @@ Changes in RiotX 0.11.0 (2019-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Implement soft logout (#281)
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ internal abstract class AuthModule {
|
|||
.name("matrix-sdk-auth.realm")
|
||||
.modules(AuthRealmModule())
|
||||
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
|
||||
.migration(AuthRealmMigration())
|
||||
.migration(AuthRealmMigration)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -16,12 +16,27 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
internal interface SignOutAPI {
|
||||
|
||||
/**
|
||||
* Attempt to login again to the same account.
|
||||
* Set all the timeouts to 1 minute
|
||||
* It is similar to [AuthAPI.login]
|
||||
*
|
||||
* @param loginParams the login parameters
|
||||
*/
|
||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||
fun loginAgain(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
||||
|
||||
/**
|
||||
* Invalidate the access token, so that it can no longer be used for authorization.
|
||||
*/
|
||||
|
|
|
@ -37,8 +37,11 @@ internal abstract class SignOutModule {
|
|||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask
|
||||
abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService
|
||||
abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService
|
||||
}
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 == '/' }
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) :
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 == '/' }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
51
vector/src/main/res/layout/activity_signed_out.xml
Normal file
51
vector/src/main/res/layout/activity_signed_out.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
16
vector/src/main/res/layout/item_login_centered_button.xml
Normal file
16
vector/src/main/res/layout/item_login_centered_button.xml
Normal 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>
|
35
vector/src/main/res/layout/item_login_error_retry.xml
Normal file
35
vector/src/main/res/layout/item_login_error_retry.xml
Normal 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>
|
9
vector/src/main/res/layout/item_login_header.xml
Normal file
9
vector/src/main/res/layout/item_login_header.xml
Normal 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" />
|
78
vector/src/main/res/layout/item_login_password_form.xml
Normal file
78
vector/src/main/res/layout/item_login_password_form.xml
Normal 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>
|
19
vector/src/main/res/layout/item_login_red_button.xml
Normal file
19
vector/src/main/res/layout/item_login_red_button.xml
Normal 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>
|
12
vector/src/main/res/layout/item_login_text.xml
Normal file
12
vector/src/main/res/layout/item_login_text.xml
Normal 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" />
|
12
vector/src/main/res/layout/item_login_title.xml
Normal file
12
vector/src/main/res/layout/item_login_title.xml
Normal 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" />
|
12
vector/src/main/res/layout/item_login_title_small.xml
Normal file
12
vector/src/main/res/layout/item_login_title_small.xml
Normal 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
Loading…
Add table
Reference in a new issue