mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-15 10:48:54 +03:00
Merge pull request #558 from vector-im/feature/login_sso
Quick implementation of SSO login - Also handling of magic link
This commit is contained in:
commit
77168bfd6a
66 changed files with 2232 additions and 378 deletions
|
@ -2,7 +2,8 @@ Changes in RiotX 0.5.0 (2019-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features:
|
||||
-
|
||||
- Handle M_CONSENT_NOT_GIVEN error (#64)
|
||||
- Auto configure homeserver and identity server URLs of LoginActivity with a magic link
|
||||
|
||||
Improvements:
|
||||
- Reduce default release build log level, and lab option to enable more logs.
|
||||
|
@ -14,6 +15,7 @@ Bugfix:
|
|||
- Fix crash due to missing informationData (#535)
|
||||
- Progress in initial sync dialog is decreasing for a step and should not (#532)
|
||||
- Fix rendering issue of accepted third party invitation event
|
||||
- All current notifications were dismissed by mistake when the app is launched from the launcher
|
||||
|
||||
Translations:
|
||||
-
|
||||
|
|
|
@ -139,6 +139,9 @@ dependencies {
|
|||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
|
||||
|
||||
// Bus
|
||||
implementation 'org.greenrobot:eventbus:3.1.1'
|
||||
|
||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.4.0'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.4.0'
|
||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.4.0'
|
||||
|
|
|
@ -17,16 +17,23 @@
|
|||
package im.vector.matrix.android.api.auth
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
|
||||
/**
|
||||
* This interface defines methods to authenticate to a matrix server.
|
||||
*/
|
||||
interface Authenticator {
|
||||
|
||||
/**
|
||||
* Request the supported login flows for this homeserver
|
||||
*/
|
||||
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* @param homeServerConnectionConfig this param is used to configure the Homeserver
|
||||
* @param login the login field
|
||||
|
@ -56,4 +63,9 @@ interface Authenticator {
|
|||
* @return the associated session if any, or null
|
||||
*/
|
||||
fun getSession(sessionParams: SessionParams): Session?
|
||||
|
||||
/**
|
||||
* Create a session after a SSO successful login
|
||||
*/
|
||||
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.api.failure
|
||||
|
||||
// This data class will be sent to the bus
|
||||
data class ConsentNotGivenError(
|
||||
val consentUri: String
|
||||
)
|
|
@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.group.GroupService
|
|||
import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
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
|
||||
|
@ -50,7 +51,8 @@ interface Session :
|
|||
FileService,
|
||||
PushRuleService,
|
||||
PushersService,
|
||||
InitialSyncProgressService {
|
||||
InitialSyncProgressService,
|
||||
SecureStorageService {
|
||||
|
||||
/**
|
||||
* The params associated to the session
|
||||
|
@ -87,7 +89,7 @@ interface Session :
|
|||
/**
|
||||
* This method start the sync thread.
|
||||
*/
|
||||
fun startSync(fromForeground : Boolean)
|
||||
fun startSync(fromForeground: Boolean)
|
||||
|
||||
/**
|
||||
* This method stop the sync thread.
|
||||
|
|
|
@ -109,8 +109,6 @@ interface CryptoService {
|
|||
|
||||
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)
|
||||
|
||||
fun clearCryptoCache(callback: MatrixCallback<Unit>)
|
||||
|
||||
fun addNewSessionListener(newSessionListener: NewSessionListener)
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.securestorage
|
||||
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
interface SecureStorageService {
|
||||
|
||||
fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream)
|
||||
|
||||
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T?
|
||||
|
||||
}
|
|
@ -17,10 +17,12 @@
|
|||
package im.vector.matrix.android.internal.auth
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
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.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
|
@ -29,6 +31,13 @@ import retrofit2.http.POST
|
|||
*/
|
||||
internal interface AuthAPI {
|
||||
|
||||
/**
|
||||
* Get the supported login flow
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||
fun getLoginFlows(): Call<LoginFlowResponse>
|
||||
|
||||
/**
|
||||
* Pass params to the server for the current login phase.
|
||||
* Set all the timeouts to 1 minute
|
||||
|
|
|
@ -23,7 +23,7 @@ import dagger.Provides
|
|||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
|
||||
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
|
||||
import im.vector.matrix.android.internal.database.configureEncryption
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||
import io.realm.RealmConfiguration
|
||||
import java.io.File
|
||||
|
@ -33,16 +33,21 @@ internal abstract class AuthModule {
|
|||
|
||||
@Module
|
||||
companion object {
|
||||
private const val DB_ALIAS = "matrix-sdk-auth"
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@AuthDatabase
|
||||
fun providesRealmConfiguration(context: Context): RealmConfiguration {
|
||||
fun providesRealmConfiguration(context: Context, realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
val old = File(context.filesDir, "matrix-sdk-auth")
|
||||
if (old.exists()) {
|
||||
old.renameTo(File(context.filesDir, "matrix-sdk-auth.realm"))
|
||||
}
|
||||
|
||||
return RealmConfiguration.Builder()
|
||||
.configureEncryption("matrix-sdk-auth", context)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, DB_ALIAS)
|
||||
}
|
||||
.name("matrix-sdk-auth.realm")
|
||||
.modules(AuthRealmModule())
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
|
|||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
|
@ -62,11 +63,20 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
|||
return sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
|
||||
val job = GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val result = runCatching {
|
||||
getLoginFlowInternal(homeServerConnectionConfig)
|
||||
}
|
||||
result.foldToCallback(callback)
|
||||
}
|
||||
return CancelableCoroutine(job)
|
||||
}
|
||||
|
||||
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
login: String,
|
||||
password: String,
|
||||
callback: MatrixCallback<Session>): Cancelable {
|
||||
|
||||
val job = GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val sessionOrFailure = runCatching {
|
||||
authenticate(homeServerConnectionConfig, login, password)
|
||||
|
@ -74,7 +84,14 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
|||
sessionOrFailure.foldToCallback(callback)
|
||||
}
|
||||
return CancelableCoroutine(job)
|
||||
}
|
||||
|
||||
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
|
||||
val authAPI = buildAuthAPI(homeServerConnectionConfig)
|
||||
|
||||
executeRequest<LoginFlowResponse> {
|
||||
apiCall = authAPI.getLoginFlows()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
|
@ -95,6 +112,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
|||
sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
|
||||
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
return sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
||||
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
|
||||
return retrofit.create(AuthAPI::class.java)
|
||||
|
|
|
@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow(
|
|||
|
||||
@Json(name = "stages")
|
||||
val stages: List<String>? = null
|
||||
)
|
||||
) {
|
||||
|
||||
companion object {
|
||||
// Possible values for type
|
||||
const val TYPE_LOGIN_SSO = "m.login.sso"
|
||||
const val TYPE_LOGIN_TOKEN = "m.login.token"
|
||||
const val TYPE_LOGIN_PASSWORD = "m.login.password"
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class LoginFlowResponse(
|
||||
data class LoginFlowResponse(
|
||||
@Json(name = "flows")
|
||||
val flows: List<InteractiveAuthenticationFlow>
|
||||
)
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
@ -30,12 +29,13 @@ import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
|
|||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import im.vector.matrix.android.internal.crypto.tasks.*
|
||||
import im.vector.matrix.android.internal.database.configureEncryption
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.UserCacheDirectory
|
||||
import im.vector.matrix.android.internal.di.UserMd5
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
||||
import im.vector.matrix.android.internal.util.md5
|
||||
import io.realm.RealmConfiguration
|
||||
import retrofit2.Retrofit
|
||||
import java.io.File
|
||||
|
@ -45,17 +45,20 @@ internal abstract class CryptoModule {
|
|||
|
||||
@Module
|
||||
companion object {
|
||||
internal const val DB_ALIAS_PREFIX = "crypto_module_"
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@CryptoDatabase
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(context: Context, credentials: Credentials): RealmConfiguration {
|
||||
val userIDHash = credentials.userId.md5()
|
||||
|
||||
fun providesRealmConfiguration(@UserCacheDirectory directory: File,
|
||||
@UserMd5 userMd5: String,
|
||||
realmKeysUtils: RealmKeysUtils): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(File(context.filesDir, userIDHash))
|
||||
.configureEncryption("crypto_module_$userIDHash", context)
|
||||
.directory(directory)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
|
||||
}
|
||||
.name("crypto_store.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION)
|
||||
|
|
|
@ -62,11 +62,9 @@ import im.vector.matrix.android.internal.crypto.tasks.*
|
|||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
|
@ -135,7 +133,6 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
private val setDeviceNameTask: SetDeviceNameTask,
|
||||
private val uploadKeysTask: UploadKeysTask,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||
private val monarchy: Monarchy,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val taskExecutor: TaskExecutor
|
||||
|
@ -1047,14 +1044,6 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
|
||||
clearCryptoDataTask
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
|
||||
roomDecryptorProvider.addNewSessionListener(newSessionListener)
|
||||
}
|
||||
|
|
|
@ -17,10 +17,12 @@ package im.vector.matrix.android.internal.database
|
|||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import im.vector.matrix.android.api.util.SecretStoringUtils
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils
|
||||
import io.realm.RealmConfiguration
|
||||
import timber.log.Timber
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* On creation a random key is generated, this key is then encrypted using the system KeyStore.
|
||||
|
@ -34,12 +36,13 @@ import java.security.SecureRandom
|
|||
* then we generate a random secret key. The database key is encrypted with the secret key; The secret
|
||||
* key is encrypted with the public RSA key and stored with the encrypted key in the shared pref
|
||||
*/
|
||||
private object RealmKeysUtils {
|
||||
|
||||
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
|
||||
internal class RealmKeysUtils @Inject constructor(context: Context,
|
||||
private val secretStoringUtils: SecretStoringUtils) {
|
||||
|
||||
private val rng = SecureRandom()
|
||||
|
||||
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
|
||||
|
||||
private fun generateKeyForRealm(): ByteArray {
|
||||
val keyForRealm = ByteArray(RealmConfiguration.KEY_LENGTH)
|
||||
rng.nextBytes(keyForRealm)
|
||||
|
@ -49,8 +52,7 @@ private object RealmKeysUtils {
|
|||
/**
|
||||
* Check if there is already a key for this alias
|
||||
*/
|
||||
fun hasKeyForDatabase(alias: String, context: Context): Boolean {
|
||||
val sharedPreferences = getSharedPreferences(context)
|
||||
private fun hasKeyForDatabase(alias: String): Boolean {
|
||||
return sharedPreferences.contains("${ENCRYPTED_KEY_PREFIX}_$alias")
|
||||
}
|
||||
|
||||
|
@ -59,13 +61,12 @@ private object RealmKeysUtils {
|
|||
* The random key is then encrypted by the keystore, and the encrypted key is stored
|
||||
* in shared preferences.
|
||||
*
|
||||
* @return the generate key (can be passed to Realm Configuration)
|
||||
* @return the generated key (can be passed to Realm Configuration)
|
||||
*/
|
||||
fun createAndSaveKeyForDatabase(alias: String, context: Context): ByteArray {
|
||||
private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
|
||||
val key = generateKeyForRealm()
|
||||
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
|
||||
val toStore = SecretStoringUtils.securelyStoreString(encodedKey, alias, context)
|
||||
val sharedPreferences = getSharedPreferences(context)
|
||||
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
|
||||
|
@ -77,30 +78,43 @@ private object RealmKeysUtils {
|
|||
* Retrieves the key for this database
|
||||
* throws if something goes wrong
|
||||
*/
|
||||
fun extractKeyForDatabase(alias: String, context: Context): ByteArray {
|
||||
val sharedPreferences = getSharedPreferences(context)
|
||||
private fun extractKeyForDatabase(alias: String): ByteArray {
|
||||
val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
|
||||
val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
|
||||
val b64 = SecretStoringUtils.loadSecureSecret(encryptedKey, alias, context)
|
||||
val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
|
||||
return Base64.decode(b64!!, Base64.NO_PADDING)
|
||||
}
|
||||
|
||||
private fun getSharedPreferences(context: Context) =
|
||||
context.getSharedPreferences("im.vector.matrix.android.keys", Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
|
||||
fun RealmConfiguration.Builder.configureEncryption(alias: String, context: Context): RealmConfiguration.Builder {
|
||||
if (RealmKeysUtils.hasKeyForDatabase(alias, context)) {
|
||||
Timber.i("Found key for alias:$alias")
|
||||
RealmKeysUtils.extractKeyForDatabase(alias, context).also {
|
||||
this.encryptionKey(it)
|
||||
fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {
|
||||
val key = if (hasKeyForDatabase(alias)) {
|
||||
Timber.i("Found key for alias:$alias")
|
||||
extractKeyForDatabase(alias)
|
||||
} else {
|
||||
Timber.i("Create key for DB alias:$alias")
|
||||
createAndSaveKeyForDatabase(alias)
|
||||
}
|
||||
} else {
|
||||
Timber.i("Create key for DB alias:$alias")
|
||||
RealmKeysUtils.createAndSaveKeyForDatabase(alias, context).also {
|
||||
this.encryptionKey(it)
|
||||
|
||||
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||
val log = key.joinToString("") { "%02x".format(it) }
|
||||
Timber.w("Database key for alias `$alias`: $log")
|
||||
}
|
||||
|
||||
realmConfigurationBuilder.encryptionKey(key)
|
||||
}
|
||||
|
||||
// Delete elements related to the alias
|
||||
fun clear(alias: String) {
|
||||
if (hasKeyForDatabase(alias)) {
|
||||
secretStoringUtils.safeDeleteKey(alias)
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.remove("${ENCRYPTED_KEY_PREFIX}_$alias")
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
return this
|
||||
|
||||
companion object {
|
||||
private const val ENCRYPTED_KEY_PREFIX = "REALM_ENCRYPTED_KEY"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class UserCacheDirectory
|
|
@ -18,6 +18,9 @@ package im.vector.matrix.android.internal.di
|
|||
|
||||
import javax.inject.Scope
|
||||
|
||||
/**
|
||||
* Use the annotation @MatrixScope to annotate classes we want the SDK to instantiate only once
|
||||
*/
|
||||
@Scope
|
||||
@MustBeDocumented
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
|
|
|
@ -35,7 +35,7 @@ internal object NetworkModule {
|
|||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
fun providesHttpLogingInterceptor(): HttpLoggingInterceptor {
|
||||
fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
val logger = FormattedJsonHttpLogger()
|
||||
val interceptor = HttpLoggingInterceptor(logger)
|
||||
interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.di
|
||||
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class UserMd5
|
|
@ -18,10 +18,12 @@ package im.vector.matrix.android.internal.network
|
|||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import okhttp3.ResponseBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
@ -65,6 +67,11 @@ internal class Request<DATA> {
|
|||
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
return Failure.ServerError(matrixError, httpCode)
|
||||
}
|
||||
} catch (ex: JsonDataException) {
|
||||
|
|
|
@ -35,16 +35,15 @@ import im.vector.matrix.android.api.session.group.GroupService
|
|||
import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
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.api.util.MatrixCallbackDelegate
|
||||
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 im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
@ -65,6 +64,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
|||
private val pushersService: Lazy<PushersService>,
|
||||
private val cryptoService: Lazy<DefaultCryptoService>,
|
||||
private val fileService: Lazy<FileService>,
|
||||
private val secureStorageService: Lazy<SecureStorageService>,
|
||||
private val syncThreadProvider: Provider<SyncThread>,
|
||||
private val contentUrlResolver: ContentUrlResolver,
|
||||
private val contentUploadProgressTracker: ContentUploadStateTracker,
|
||||
|
@ -75,13 +75,13 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
|||
GroupService by groupService.get(),
|
||||
UserService by userService.get(),
|
||||
CryptoService by cryptoService.get(),
|
||||
CacheService by cacheService.get(),
|
||||
SignOutService by signOutService.get(),
|
||||
FilterService by filterService.get(),
|
||||
PushRuleService by pushRuleService.get(),
|
||||
PushersService by pushersService.get(),
|
||||
FileService by fileService.get(),
|
||||
InitialSyncProgressService by initialSyncProgressService.get() {
|
||||
InitialSyncProgressService by initialSyncProgressService.get(),
|
||||
SecureStorageService by secureStorageService.get() {
|
||||
|
||||
private var isOpen = false
|
||||
|
||||
|
@ -144,43 +144,6 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
|
|||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
override fun signOut(callback: MatrixCallback<Unit>) {
|
||||
Timber.w("SIGN_OUT: start")
|
||||
|
||||
assert(isOpen)
|
||||
|
||||
Timber.w("SIGN_OUT: call webservice")
|
||||
return signOutService.get().signOut(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.w("SIGN_OUT: call webservice -> SUCCESS: clear cache")
|
||||
stopSync()
|
||||
stopAnyBackgroundSync()
|
||||
// Clear the cache
|
||||
cacheService.get().clearCache(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache")
|
||||
cryptoService.get().clearCryptoCache(MatrixCallbackDelegate(callback))
|
||||
WorkManagerUtil.cancelAllWorks(context)
|
||||
callback.onSuccess(Unit)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// ignore error
|
||||
Timber.e("SIGN_OUT: clear cache -> ERROR: ignoring")
|
||||
onSuccess(Unit)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// Ignore failure
|
||||
Timber.e("SIGN_OUT: call webservice -> ERROR: ignoring")
|
||||
onSuccess(Unit)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun clearCache(callback: MatrixCallback<Unit>) {
|
||||
stopSync()
|
||||
stopAnyBackgroundSync()
|
||||
|
|
|
@ -27,21 +27,20 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
|||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import im.vector.matrix.android.internal.database.LiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.configureEncryption
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
||||
import im.vector.matrix.android.internal.di.Authenticated
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
import im.vector.matrix.android.internal.di.*
|
||||
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.DefaultRoomFactory
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.session.room.RoomFactory
|
||||
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
|
||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
|
||||
import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService
|
||||
import im.vector.matrix.android.internal.util.md5
|
||||
import io.realm.RealmConfiguration
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -53,6 +52,7 @@ internal abstract class SessionModule {
|
|||
|
||||
@Module
|
||||
companion object {
|
||||
internal const val DB_ALIAS_PREFIX = "session_db_"
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
|
@ -67,18 +67,33 @@ internal abstract class SessionModule {
|
|||
return sessionParams.credentials
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@UserMd5
|
||||
@Provides
|
||||
fun providesUserMd5(sessionParams: SessionParams): String {
|
||||
return sessionParams.credentials.userId.md5()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@UserCacheDirectory
|
||||
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File {
|
||||
return File(context.filesDir, userMd5)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@SessionDatabase
|
||||
@SessionScope
|
||||
fun providesRealmConfiguration(sessionParams: SessionParams, context: Context): RealmConfiguration {
|
||||
val childPath = sessionParams.credentials.userId.md5()
|
||||
val directory = File(context.filesDir, childPath)
|
||||
|
||||
fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils,
|
||||
@UserCacheDirectory directory: File,
|
||||
@UserMd5 userMd5: String): RealmConfiguration {
|
||||
return RealmConfiguration.Builder()
|
||||
.directory(directory)
|
||||
.name("disk_store.realm")
|
||||
.configureEncryption("session_db_$childPath", context)
|
||||
.apply {
|
||||
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
|
||||
}
|
||||
.modules(SessionRealmModule())
|
||||
.deleteRealmIfMigrationNeeded()
|
||||
.build()
|
||||
|
@ -101,7 +116,18 @@ internal abstract class SessionModule {
|
|||
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
|
||||
accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient {
|
||||
return okHttpClient.newBuilder()
|
||||
.addInterceptor(accessTokenInterceptor)
|
||||
.apply {
|
||||
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
|
||||
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
|
||||
interceptors().removeAll(existingCurlInterceptors)
|
||||
|
||||
addInterceptor(accessTokenInterceptor)
|
||||
|
||||
// Re add eventually the curl logging interceptors
|
||||
existingCurlInterceptors.forEach {
|
||||
addInterceptor(it)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -142,4 +168,7 @@ internal abstract class SessionModule {
|
|||
@Binds
|
||||
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
|
||||
|
||||
@Binds
|
||||
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.securestorage
|
||||
|
||||
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultSecureStorageService @Inject constructor(private val secretStoringUtils: SecretStoringUtils) : SecureStorageService {
|
||||
|
||||
override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) {
|
||||
secretStoringUtils.securelyStoreObject(any, keyAlias, outputStream)
|
||||
}
|
||||
|
||||
override fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
|
||||
return secretStoringUtils.loadSecureSecret(inputStream, keyAlias)
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.util
|
||||
package im.vector.matrix.android.internal.session.securestorage
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
@ -22,17 +22,20 @@ import android.security.KeyPairGeneratorSpec
|
|||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import androidx.annotation.RequiresApi
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.SecureRandom
|
||||
import java.util.Calendar
|
||||
import java.util.*
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.inject.Inject
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
|
||||
|
@ -65,22 +68,24 @@ import javax.security.auth.x500.X500Principal
|
|||
* val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
|
||||
* </code>
|
||||
*
|
||||
* You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
|
||||
* You can also just use this utility to store a secret key, and use any encryption algorithm that you want.
|
||||
*
|
||||
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
|
||||
* add a pin or change the schema); So you might and with a useless pile of bytes.
|
||||
*/
|
||||
object SecretStoringUtils {
|
||||
internal class SecretStoringUtils @Inject constructor(private val context: Context) {
|
||||
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val AES_MODE = "AES/GCM/NoPadding";
|
||||
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
|
||||
companion object {
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val AES_MODE = "AES/GCM/NoPadding"
|
||||
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
|
||||
|
||||
const val FORMAT_API_M: Byte = 0
|
||||
const val FORMAT_1: Byte = 1
|
||||
const val FORMAT_2: Byte = 2
|
||||
private const val FORMAT_API_M: Byte = 0
|
||||
private const val FORMAT_1: Byte = 1
|
||||
private const val FORMAT_2: Byte = 2
|
||||
}
|
||||
|
||||
val keyStore: KeyStore by lazy {
|
||||
private val keyStore: KeyStore by lazy {
|
||||
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
|
||||
load(null)
|
||||
}
|
||||
|
@ -88,24 +93,30 @@ object SecretStoringUtils {
|
|||
|
||||
private val secureRandom = SecureRandom()
|
||||
|
||||
fun safeDeleteKey(keyAlias: String) {
|
||||
try {
|
||||
keyStore.deleteEntry(keyAlias)
|
||||
} catch (e: KeyStoreException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given secret using the android Keystore.
|
||||
* On android >= M, will directly use the keystore to generate a symetric key
|
||||
* On KitKat >= KitKat and <M, as symetric key gen is not available, will use an asymetric key generated
|
||||
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
|
||||
* On android >= M, will directly use the keystore to generate a symmetric key
|
||||
* On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated
|
||||
* in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned
|
||||
* in the bytearray (in can be stored anywhere, it is encrypted)
|
||||
* On older version a key in generated from alias with random salt.
|
||||
*
|
||||
* The secret is encrypted using the following method: AES/GCM/NoPadding
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return encryptStringM(secret, keyAlias)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return encryptStringJ(secret, keyAlias, context)
|
||||
} else {
|
||||
return encryptForOldDevicesNotGood(secret, keyAlias)
|
||||
fun securelyStoreString(secret: String, keyAlias: String): ByteArray? {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias)
|
||||
else -> encryptForOldDevicesNotGood(secret, keyAlias)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,39 +124,33 @@ object SecretStoringUtils {
|
|||
* Decrypt a secret that was encrypted by #securelyStoreString()
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return decryptStringM(encrypted, keyAlias)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return decryptStringJ(encrypted, keyAlias, context)
|
||||
} else {
|
||||
return decryptForOldDevicesNotGood(encrypted, keyAlias)
|
||||
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias)
|
||||
else -> decryptForOldDevicesNotGood(encrypted, keyAlias)
|
||||
}
|
||||
}
|
||||
|
||||
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
saveSecureObjectM(keyAlias, output, any)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return saveSecureObjectK(keyAlias, output, any, context)
|
||||
} else {
|
||||
return saveSecureObjectOldNotGood(keyAlias, output, any)
|
||||
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any)
|
||||
else -> saveSecureObjectOldNotGood(keyAlias, output, any)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return loadSecureObjectM(keyAlias, inputStream)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return loadSecureObjectK(keyAlias, inputStream, context)
|
||||
} else {
|
||||
return loadSecureObjectOldNotGood(keyAlias, inputStream)
|
||||
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream)
|
||||
else -> loadSecureObjectOldNotGood(keyAlias, inputStream)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
|
||||
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
|
||||
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
|
||||
?.secretKey
|
||||
if (secretKeyEntry == null) {
|
||||
|
@ -163,7 +168,6 @@ object SecretStoringUtils {
|
|||
return secretKeyEntry
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Symetric Key Generation is only available in M, so before M the idea is to:
|
||||
- Generate a pair of RSA keys;
|
||||
|
@ -172,8 +176,8 @@ object SecretStoringUtils {
|
|||
- Store the encrypted AES
|
||||
Generate a key pair for encryption
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
|
||||
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
|
||||
|
||||
if (privateKeyEntry != null) return privateKeyEntry
|
||||
|
@ -201,7 +205,7 @@ object SecretStoringUtils {
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
|
@ -212,10 +216,10 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
|
||||
private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
|
||||
val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))
|
||||
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
|
@ -224,15 +228,15 @@ object SecretStoringUtils {
|
|||
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun encryptStringK(text: String, keyAlias: String): ByteArray? {
|
||||
//we generate a random symetric key
|
||||
val key = ByteArray(16)
|
||||
secureRandom.nextBytes(key)
|
||||
val sKey = SecretKeySpec(key, "AES")
|
||||
|
||||
//we encrypt this key thanks to the key store
|
||||
val encryptedKey = rsaEncrypt(keyAlias, key, context)
|
||||
val encryptedKey = rsaEncrypt(keyAlias, key)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||
|
@ -242,7 +246,7 @@ object SecretStoringUtils {
|
|||
return format1Make(encryptedKey, iv, encryptedBytes)
|
||||
}
|
||||
|
||||
fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
|
||||
private fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
|
||||
val salt = ByteArray(8)
|
||||
secureRandom.nextBytes(salt)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
|
@ -258,11 +262,11 @@ object SecretStoringUtils {
|
|||
return format2Make(salt, iv, encryptedBytes)
|
||||
}
|
||||
|
||||
fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
|
||||
private fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
|
||||
|
||||
val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
|
||||
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10_000, 128)
|
||||
val tmp = factory.generateSecret(spec)
|
||||
val sKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
|
||||
|
@ -277,25 +281,23 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {
|
||||
private fun decryptStringK(data: ByteArray, keyAlias: String): String? {
|
||||
|
||||
val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
|
||||
|
||||
//we need to decrypt the key
|
||||
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
|
||||
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
|
||||
|
||||
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Throws(IOException::class)
|
||||
fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
|
||||
|
@ -314,14 +316,14 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
|
||||
private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
//we generate a random symetric key
|
||||
val key = ByteArray(16)
|
||||
secureRandom.nextBytes(key)
|
||||
val sKey = SecretKeySpec(key, "AES")
|
||||
|
||||
//we encrypt this key thanks to the key store
|
||||
val encryptedKey = rsaEncrypt(keyAlias, key, context)
|
||||
val encryptedKey = rsaEncrypt(keyAlias, key)
|
||||
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, sKey)
|
||||
|
@ -342,7 +344,7 @@ object SecretStoringUtils {
|
|||
output.write(bos1.toByteArray())
|
||||
}
|
||||
|
||||
fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
private fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
|
||||
val salt = ByteArray(8)
|
||||
secureRandom.nextBytes(salt)
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
|
@ -387,8 +389,8 @@ object SecretStoringUtils {
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Throws(IOException::class)
|
||||
fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
|
||||
private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
|
||||
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
|
||||
|
||||
val format = inputStream.read()
|
||||
assert(format.toByte() == FORMAT_API_M)
|
||||
|
@ -411,12 +413,12 @@ object SecretStoringUtils {
|
|||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
@Throws(IOException::class)
|
||||
fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {
|
||||
private fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? {
|
||||
|
||||
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
|
||||
|
||||
//we need to decrypt the key
|
||||
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
|
||||
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
|
||||
val cipher = Cipher.getInstance(AES_MODE)
|
||||
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
|
||||
|
@ -432,8 +434,7 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
|
||||
|
||||
private fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
|
||||
val (salt, iv, encrypted) = format2Extract(inputStream)
|
||||
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||
|
@ -456,10 +457,10 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
@Throws(Exception::class)
|
||||
private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
|
||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
|
||||
private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
|
||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
|
||||
// Encrypt the text
|
||||
val inputCipher = Cipher.getInstance(RSA_MODE)
|
||||
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
|
||||
|
@ -472,10 +473,10 @@ object SecretStoringUtils {
|
|||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
@Throws(Exception::class)
|
||||
private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray {
|
||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
|
||||
private fun rsaDecrypt(alias: String, encrypted: InputStream): ByteArray {
|
||||
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
|
||||
val output = Cipher.getInstance(RSA_MODE)
|
||||
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
|
||||
|
||||
|
@ -504,7 +505,6 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
|
||||
|
||||
val format = bis.read()
|
||||
assert(format.toByte() == FORMAT_1)
|
||||
|
||||
|
@ -548,7 +548,6 @@ object SecretStoringUtils {
|
|||
}
|
||||
|
||||
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
|
||||
|
||||
val format = bis.read()
|
||||
assert(format.toByte() == FORMAT_2)
|
||||
|
|
@ -16,25 +16,64 @@
|
|||
|
||||
package im.vector.matrix.android.internal.session.signout
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||
import im.vector.matrix.android.internal.di.CryptoDatabase
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.di.UserCacheDirectory
|
||||
import im.vector.matrix.android.internal.di.UserMd5
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.SessionModule
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SignOutTask : Task<Unit, Unit>
|
||||
|
||||
internal class DefaultSignOutTask @Inject constructor(private val credentials: Credentials,
|
||||
internal class DefaultSignOutTask @Inject constructor(private val context: Context,
|
||||
private val credentials: Credentials,
|
||||
private val signOutAPI: SignOutAPI,
|
||||
private val sessionManager: SessionManager,
|
||||
private val sessionParamsStore: SessionParamsStore) : SignOutTask {
|
||||
private val sessionParamsStore: SessionParamsStore,
|
||||
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
|
||||
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||
@UserCacheDirectory private val userFile: File,
|
||||
private val realmKeysUtils: RealmKeysUtils,
|
||||
@UserMd5 private val userMd5: String) : SignOutTask {
|
||||
|
||||
override suspend fun execute(params: Unit) {
|
||||
Timber.d("SignOut: send request...")
|
||||
executeRequest<Unit> {
|
||||
apiCall = signOutAPI.signOut()
|
||||
}
|
||||
sessionParamsStore.delete(credentials.userId)
|
||||
|
||||
Timber.d("SignOut: release session...")
|
||||
sessionManager.releaseSession(credentials.userId)
|
||||
|
||||
Timber.d("SignOut: cancel pending works...")
|
||||
WorkManagerUtil.cancelAllWorks(context)
|
||||
|
||||
Timber.d("SignOut: delete session params...")
|
||||
sessionParamsStore.delete(credentials.userId)
|
||||
|
||||
Timber.d("SignOut: clear session data...")
|
||||
clearSessionDataTask.execute(Unit)
|
||||
|
||||
Timber.d("SignOut: clear crypto data...")
|
||||
clearCryptoDataTask.execute(Unit)
|
||||
|
||||
Timber.d("SignOut: clear file system")
|
||||
userFile.deleteRecursively()
|
||||
|
||||
Timber.d("SignOut: clear the database keys")
|
||||
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
|
||||
realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.worker
|
|||
import android.content.Context
|
||||
import androidx.work.*
|
||||
|
||||
// TODO Multiaccount
|
||||
internal object WorkManagerUtil {
|
||||
private const val MATRIX_SDK_TAG = "MatrixSDK"
|
||||
|
||||
|
|
3
tools/tests/test_configuration_link.sh
Executable file
3
tools/tests/test_configuration_link.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW -d "https://riot.im/config/config?hs_url=https%3A%2F%2Fmozilla-test.modular.im"
|
|
@ -280,6 +280,9 @@ dependencies {
|
|||
implementation "ru.noties.markwon:html:$markwon_version"
|
||||
implementation 'me.saket:better-link-movement-method:2.2.0'
|
||||
|
||||
// Bus
|
||||
implementation 'org.greenrobot:eventbus:3.1.1'
|
||||
|
||||
// Passphrase strength helper
|
||||
implementation 'com.nulab-inc:zxcvbn:1.2.5'
|
||||
|
||||
|
|
|
@ -65,6 +65,19 @@
|
|||
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
|
||||
<activity android:name=".features.debug.DebugMenuActivity" />
|
||||
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
|
||||
<activity android:name=".features.webview.VectorWebViewActivity" />
|
||||
<activity android:name=".features.link.LinkHandlerActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="riot.im" />
|
||||
<data android:pathPrefix="/config/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
|
|
@ -349,6 +349,11 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch
|
||||
</li>
|
||||
<li>
|
||||
<b>EventBus</b>
|
||||
<br/>
|
||||
Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org)
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
|
|
@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyFragment
|
||||
|
@ -42,15 +41,14 @@ import im.vector.riotx.features.home.createdirect.CreateDirectRoomKnownUsersFrag
|
|||
import im.vector.riotx.features.home.group.GroupListFragment
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailFragment
|
||||
import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.MessageMenuFragment
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.QuickReactionFragment
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
|
||||
import im.vector.riotx.features.home.room.detail.timeline.action.*
|
||||
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||
import im.vector.riotx.features.invite.VectorInviteView
|
||||
import im.vector.riotx.features.link.LinkHandlerActivity
|
||||
import im.vector.riotx.features.login.LoginActivity
|
||||
import im.vector.riotx.features.login.LoginFragment
|
||||
import im.vector.riotx.features.login.LoginSsoFallbackFragment
|
||||
import im.vector.riotx.features.media.ImageMediaViewerActivity
|
||||
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
||||
import im.vector.riotx.features.navigation.Navigator
|
||||
|
@ -65,20 +63,14 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
|||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
||||
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
||||
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
|
||||
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
|
||||
import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment
|
||||
import im.vector.riotx.features.settings.VectorSettingsNotificationsTroubleshootFragment
|
||||
import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
|
||||
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
|
||||
import im.vector.riotx.features.settings.*
|
||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||
|
||||
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
|
||||
@ScreenScope
|
||||
interface ScreenComponent {
|
||||
|
||||
fun session(): Session
|
||||
fun activeSessionHolder(): ActiveSessionHolder
|
||||
|
||||
fun viewModelFactory(): ViewModelProvider.Factory
|
||||
|
||||
|
@ -134,6 +126,10 @@ interface ScreenComponent {
|
|||
|
||||
fun inject(publicRoomsFragment: PublicRoomsFragment)
|
||||
|
||||
fun inject(loginFragment: LoginFragment)
|
||||
|
||||
fun inject(loginSsoFallbackFragment: LoginSsoFallbackFragment)
|
||||
|
||||
fun inject(sasVerificationIncomingFragment: SASVerificationIncomingFragment)
|
||||
|
||||
fun inject(quickReactionFragment: QuickReactionFragment)
|
||||
|
@ -142,6 +138,8 @@ interface ScreenComponent {
|
|||
|
||||
fun inject(loginActivity: LoginActivity)
|
||||
|
||||
fun inject(linkHandlerActivity: LinkHandlerActivity)
|
||||
|
||||
fun inject(mainActivity: MainActivity)
|
||||
|
||||
fun inject(roomDirectoryActivity: RoomDirectoryActivity)
|
||||
|
|
|
@ -26,9 +26,9 @@ private const val KEY_DIALOG_IS_DISPLAYED = "DialogLocker.KEY_DIALOG_IS_DISPLAYE
|
|||
/**
|
||||
* Class to avoid displaying twice the same dialog
|
||||
*/
|
||||
class DialogLocker() : Restorable {
|
||||
class DialogLocker(savedInstanceState: Bundle?) : Restorable {
|
||||
|
||||
private var isDialogDisplayed: Boolean = false
|
||||
private var isDialogDisplayed = savedInstanceState?.getBoolean(KEY_DIALOG_IS_DISPLAYED, false) == true
|
||||
|
||||
private fun unlock() {
|
||||
isDialogDisplayed = false
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.riotx.core.error
|
||||
|
||||
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.resources.StringProvider
|
||||
import javax.inject.Inject
|
||||
|
@ -34,8 +35,13 @@ class ErrorFormatter @Inject constructor(val stringProvider: StringProvider) {
|
|||
null -> null
|
||||
is Failure.NetworkConnection -> stringProvider.getString(R.string.error_no_network)
|
||||
is Failure.ServerError -> {
|
||||
throwable.error.message.takeIf { it.isNotEmpty() }
|
||||
?: throwable.error.code.takeIf { it.isNotEmpty() }
|
||||
if (throwable.error.code == MatrixError.M_CONSENT_NOT_GIVEN) {
|
||||
// Special case for terms and conditions
|
||||
stringProvider.getString(R.string.error_terms_not_accepted)
|
||||
} else {
|
||||
throwable.error.message.takeIf { it.isNotEmpty() }
|
||||
?: throwable.error.code.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
else -> throwable.localizedMessage
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {
|
|||
|
||||
@CallSuper
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
session = injector.session()
|
||||
session = injector.activeSessionHolder().getActiveSession()
|
||||
}
|
||||
|
||||
override fun initUiAndData() {
|
||||
|
|
|
@ -36,11 +36,14 @@ import butterknife.Unbinder
|
|||
import com.airbnb.mvrx.BaseMvRxActivity
|
||||
import com.bumptech.glide.util.Util
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import im.vector.matrix.android.api.failure.ConsentNotGivenError
|
||||
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.utils.toast
|
||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||
import im.vector.riotx.features.consent.ConsentNotGivenHelper
|
||||
import im.vector.riotx.features.navigation.Navigator
|
||||
import im.vector.riotx.features.rageshake.BugReportActivity
|
||||
import im.vector.riotx.features.rageshake.BugReporter
|
||||
|
@ -50,6 +53,9 @@ import im.vector.riotx.features.themes.ThemeUtils
|
|||
import im.vector.riotx.receivers.DebugReceiver
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import timber.log.Timber
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
|
@ -73,6 +79,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
protected lateinit var bugReporter: BugReporter
|
||||
private lateinit var rageShake: RageShake
|
||||
protected lateinit var navigator: Navigator
|
||||
private lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
|
||||
private var unBinder: Unbinder? = null
|
||||
|
||||
|
@ -127,6 +134,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
bugReporter = screenComponent.bugReporter()
|
||||
rageShake = screenComponent.rageShake()
|
||||
navigator = screenComponent.navigator()
|
||||
activeSessionHolder = screenComponent.activeSessionHolder()
|
||||
configurationViewModel.activityRestarter.observe(this, Observer {
|
||||
if (!it.hasBeenHandled) {
|
||||
// Recreate the Activity because configuration has changed
|
||||
|
@ -175,7 +183,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
configurationViewModel.onActivityResumed()
|
||||
|
||||
if (this !is BugReportActivity) {
|
||||
rageShake?.start()
|
||||
rageShake.start()
|
||||
}
|
||||
|
||||
DebugReceiver
|
||||
|
@ -190,7 +198,7 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
rageShake?.stop()
|
||||
rageShake.stop()
|
||||
|
||||
debugReceiver?.let {
|
||||
unregisterReceiver(debugReceiver)
|
||||
|
@ -265,18 +273,21 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
protected fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
|
||||
// if (fm.backStackEntryCount == 0)
|
||||
// return false
|
||||
override fun onBackPressed() {
|
||||
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
||||
if (!handled) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
|
||||
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
|
||||
val reverseOrder = fm.fragments.filter { it is VectorBaseFragment }.reversed()
|
||||
for (f in reverseOrder) {
|
||||
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
|
||||
if (handledByChildFragments) {
|
||||
return true
|
||||
}
|
||||
val backPressable = f as OnBackPressed
|
||||
if (backPressable.onBackPressed()) {
|
||||
if (f is OnBackPressed && f.onBackPressed()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -388,6 +399,31 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* User Consent
|
||||
* ========================================================================================== */
|
||||
|
||||
private val consentNotGivenHelper by lazy {
|
||||
ConsentNotGivenHelper(this, DialogLocker(savedInstanceState))
|
||||
.apply { restorables.add(this) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
|
||||
consentNotGivenHelper.displayDialog(consentNotGivenError.consentUri,
|
||||
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Temporary method
|
||||
* ========================================================================================== */
|
||||
|
@ -399,5 +435,4 @@ abstract class VectorBaseActivity : BaseMvRxActivity(), HasScreenInjector {
|
|||
toast(getString(R.string.not_implemented))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -19,11 +19,7 @@ package im.vector.riotx.core.platform
|
|||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.MainThread
|
||||
|
@ -42,7 +38,7 @@ import io.reactivex.disposables.CompositeDisposable
|
|||
import io.reactivex.disposables.Disposable
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreenInjector {
|
||||
abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
||||
|
||||
// Butterknife unbinder
|
||||
private var mUnBinder: Unbinder? = null
|
||||
|
@ -132,10 +128,6 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), OnBackPressed, HasScreen
|
|||
super.onViewStateRestored(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun invalidate() {
|
||||
//no-ops by default
|
||||
Timber.w("invalidate() method has not been implemented")
|
||||
|
|
|
@ -17,11 +17,8 @@
|
|||
package im.vector.riotx.core.platform
|
||||
|
||||
import com.airbnb.mvrx.*
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.riotx.BuildConfig
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class VectorViewModel<S : MvRxState>(initialState: S)
|
||||
: BaseMvRxViewModel<S>(initialState, false) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2018 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.core.utils
|
||||
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
fun <T> weak(value: T) = WeakReferenceDelegate(value)
|
||||
|
||||
class WeakReferenceDelegate<T>(value: T) {
|
||||
|
||||
private var weakReference: WeakReference<T> = WeakReference(value)
|
||||
|
||||
operator fun getValue(thisRef: Any, property: KProperty<*>): T? = weakReference.get()
|
||||
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
|
||||
weakReference = WeakReference(value)
|
||||
}
|
||||
}
|
|
@ -19,12 +19,15 @@ package im.vector.riotx.features
|
|||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.bumptech.glide.Glide
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.utils.deleteAllFiles
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
|
@ -57,6 +60,7 @@ class MainActivity : VectorBaseActivity() {
|
|||
@Inject lateinit var matrix: Matrix
|
||||
@Inject lateinit var authenticator: Authenticator
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
|
@ -69,42 +73,68 @@ class MainActivity : VectorBaseActivity() {
|
|||
|
||||
// Handle some wanted cleanup
|
||||
if (clearCache || clearCredentials) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
// On UI Thread
|
||||
Glide.get(this@MainActivity).clearMemory()
|
||||
withContext(Dispatchers.IO) {
|
||||
// On BG thread
|
||||
Glide.get(this@MainActivity).clearDiskCache()
|
||||
|
||||
// Also clear cache (Logs, etc...)
|
||||
deleteAllFiles(this@MainActivity.cacheDir)
|
||||
}
|
||||
}
|
||||
doCleanUp(clearCache, clearCredentials)
|
||||
} else {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) {
|
||||
when {
|
||||
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.w("SIGN_OUT: success, start app")
|
||||
sessionHolder.clearActiveSession()
|
||||
start()
|
||||
doLocalCleanupAndStart()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
displayError(failure, clearCache, clearCredentials)
|
||||
}
|
||||
})
|
||||
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
start()
|
||||
doLocalCleanupAndStart()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
displayError(failure, clearCache, clearCredentials)
|
||||
}
|
||||
})
|
||||
else -> start()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLocalCleanupAndStart() {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
// On UI Thread
|
||||
Glide.get(this@MainActivity).clearMemory()
|
||||
withContext(Dispatchers.IO) {
|
||||
// On BG thread
|
||||
Glide.get(this@MainActivity).clearDiskCache()
|
||||
|
||||
// Also clear cache (Logs, etc...)
|
||||
deleteAllFiles(this@MainActivity.cacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
start()
|
||||
}
|
||||
|
||||
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) {
|
||||
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() }
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
val intent = if (sessionHolder.hasActiveSession()) {
|
||||
HomeActivity.newIntent(this)
|
||||
} else {
|
||||
LoginActivity.newIntent(this)
|
||||
LoginActivity.newIntent(this, null)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2018 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.consent
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.dialogs.DialogLocker
|
||||
import im.vector.riotx.core.platform.Restorable
|
||||
import im.vector.riotx.features.webview.VectorWebViewActivity
|
||||
import im.vector.riotx.features.webview.WebViewMode
|
||||
|
||||
class ConsentNotGivenHelper(private val activity: Activity,
|
||||
private val dialogLocker: DialogLocker) :
|
||||
Restorable by dialogLocker {
|
||||
|
||||
/* ==========================================================================================
|
||||
* Public methods
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Display the consent dialog, if not already displayed
|
||||
*/
|
||||
fun displayDialog(consentUri: String, homeServerHost: String) {
|
||||
dialogLocker.displayDialog {
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.settings_app_term_conditions)
|
||||
.setMessage(activity.getString(R.string.dialog_user_consent_content, homeServerHost))
|
||||
.setPositiveButton(R.string.dialog_user_consent_submit) { _, _ ->
|
||||
openWebViewActivity(consentUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Private
|
||||
* ========================================================================================== */
|
||||
|
||||
private fun openWebViewActivity(consentUri: String) {
|
||||
val intent = VectorWebViewActivity.getIntent(activity, consentUri, activity.getString(R.string.settings_app_term_conditions), WebViewMode.CONSENT)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
|
@ -111,7 +111,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
}
|
||||
}
|
||||
|
||||
if (intent.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)) {
|
||||
if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
}
|
||||
|
@ -202,10 +202,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
|
||||
if (!handled) {
|
||||
super.onBackPressed()
|
||||
}
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -250,7 +250,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
return true
|
||||
}
|
||||
|
||||
return super.onBackPressed()
|
||||
return false
|
||||
}
|
||||
|
||||
// RoomSummaryController.Callback **************************************************************
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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.link
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.features.login.LoginActivity
|
||||
import im.vector.riotx.features.login.LoginConfig
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* Dummy activity used to dispatch the vector URL links.
|
||||
*/
|
||||
class LinkHandlerActivity : VectorBaseActivity() {
|
||||
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_progress
|
||||
|
||||
override fun initUiAndData() {
|
||||
val uri = intent.data
|
||||
|
||||
if (uri == null) {
|
||||
// Should not happen
|
||||
Timber.w("Uri is null")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (uri.path == PATH_CONFIG) {
|
||||
if (sessionHolder.hasActiveSession()) {
|
||||
displayAlreadyLoginPopup(uri)
|
||||
} else {
|
||||
// user is not yet logged in, this is the nominal case
|
||||
startLoginActivity(uri)
|
||||
}
|
||||
} else {
|
||||
// Other link are not yet handled, but should not comes here (manifest configuration error?)
|
||||
Timber.w("Unable to handle this uir: $uri")
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the login screen with identity server and home server pre-filled
|
||||
*/
|
||||
private fun startLoginActivity(uri: Uri) {
|
||||
val intent = LoginActivity.newIntent(this, LoginConfig.parse(uri))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Propose to disconnect from a previous HS, when clicking on an auto config link
|
||||
*/
|
||||
private fun displayAlreadyLoginPopup(uri: Uri) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.dialog_title_warning)
|
||||
.setMessage(R.string.error_user_already_logged_in)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.logout) { _, _ ->
|
||||
sessionHolder.getSafeActiveSession()?.signOut(object : MatrixCallback<Unit> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
displayError(failure)
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.d("## displayAlreadyLoginPopup(): logout succeeded")
|
||||
sessionHolder.clearActiveSession()
|
||||
startLoginActivity(uri)
|
||||
}
|
||||
}) ?: finish()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun displayError(failure: Throwable) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(errorFormatter.toHumanReadable(failure))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PATH_CONFIG = "/config/config"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.login
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
|
||||
sealed class LoginActions {
|
||||
|
||||
data class UpdateHomeServer(val homeServerUrl: String) : LoginActions()
|
||||
data class Login(val login: String, val password: String) : LoginActions()
|
||||
data class SsoLoginSuccess(val credentials: Credentials) : LoginActions()
|
||||
data class NavigateTo(val target: LoginActivity.Navigation) : LoginActions()
|
||||
data class InitWith(val loginConfig: LoginConfig) : LoginActions()
|
||||
|
||||
}
|
|
@ -18,150 +18,80 @@ package im.vector.riotx.features.login
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import arrow.core.Try
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
import im.vector.riotx.core.extensions.setTextWithColoredPart
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.extensions.addFragment
|
||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||
import im.vector.riotx.core.extensions.observeEvent
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.utils.openUrlInExternalBrowser
|
||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
import im.vector.riotx.features.homeserver.ServerUrlsRepository
|
||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import kotlinx.android.synthetic.main.activity_login.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class LoginActivity : VectorBaseActivity() {
|
||||
|
||||
@Inject lateinit var authenticator: Authenticator
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
|
||||
// Supported navigation actions for this Activity
|
||||
sealed class Navigation {
|
||||
object OpenSsoLoginFallback : Navigation()
|
||||
object GoBack : Navigation()
|
||||
}
|
||||
|
||||
private val loginViewModel: LoginViewModel by viewModel()
|
||||
|
||||
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
|
||||
|
||||
private var passwordShown = false
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
setupNotice()
|
||||
setupAuthButton()
|
||||
setupPasswordReveal()
|
||||
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this))
|
||||
}
|
||||
override fun getLayoutRes() = R.layout.activity_simple
|
||||
|
||||
private fun setupNotice() {
|
||||
riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part)
|
||||
override fun initUiAndData() {
|
||||
if (isFirstCreation()) {
|
||||
addFragment(LoginFragment(), R.id.simpleFragmentContainer)
|
||||
}
|
||||
|
||||
riotx_no_registration_notice.setOnClickListener {
|
||||
openUrlInExternalBrowser(this@LoginActivity, "https://about.riot.im/downloads")
|
||||
// Get config extra
|
||||
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
|
||||
if (loginConfig != null && isFirstCreation()) {
|
||||
loginViewModel.handle(LoginActions.InitWith(loginConfig))
|
||||
}
|
||||
|
||||
loginViewModel.navigationLiveData.observeEvent(this) {
|
||||
when (it) {
|
||||
is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer)
|
||||
is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) {
|
||||
if (it is Success) {
|
||||
val intent = HomeActivity.newIntent(this)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
showDisclaimerDialog(this)
|
||||
}
|
||||
|
||||
private fun authenticate() {
|
||||
passwordShown = false
|
||||
renderPasswordField()
|
||||
|
||||
val login = loginField.text?.trim().toString()
|
||||
val password = passwordField.text?.trim().toString()
|
||||
buildHomeServerConnectionConfig().fold(
|
||||
{ Toast.makeText(this@LoginActivity, "Authenticate failure: $it", Toast.LENGTH_LONG).show() },
|
||||
{ authenticateWith(it, login, password) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) {
|
||||
progressBar.isVisible = true
|
||||
touchArea.isVisible = true
|
||||
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
|
||||
override fun onSuccess(data: Session) {
|
||||
activeSessionHolder.setActiveSession(data)
|
||||
data.configureAndStart(pushRuleTriggerListener)
|
||||
goToHome()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
progressBar.isVisible = false
|
||||
touchArea.isVisible = false
|
||||
Toast.makeText(this@LoginActivity, "Authenticate failure: $failure", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun buildHomeServerConnectionConfig(): Try<HomeServerConnectionConfig> {
|
||||
return Try {
|
||||
val homeServerUri = homeServerField.text?.trim().toString()
|
||||
HomeServerConnectionConfig.Builder()
|
||||
.withHomeServerUri(homeServerUri)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAuthButton() {
|
||||
Observable
|
||||
.combineLatest(
|
||||
loginField.textChanges().map { it.trim().isNotEmpty() },
|
||||
passwordField.textChanges().map { it.trim().isNotEmpty() },
|
||||
homeServerField.textChanges().map { it.trim().isNotEmpty() },
|
||||
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
|
||||
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
|
||||
}
|
||||
)
|
||||
.subscribeBy { authenticateButton.isEnabled = it }
|
||||
.disposeOnDestroy()
|
||||
authenticateButton.setOnClickListener { authenticate() }
|
||||
}
|
||||
|
||||
private fun setupPasswordReveal() {
|
||||
passwordShown = false
|
||||
|
||||
passwordReveal.setOnClickListener {
|
||||
passwordShown = !passwordShown
|
||||
|
||||
renderPasswordField()
|
||||
}
|
||||
|
||||
renderPasswordField()
|
||||
}
|
||||
|
||||
private fun renderPasswordField() {
|
||||
passwordField.showPassword(passwordShown)
|
||||
|
||||
passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
|
||||
private fun goToHome() {
|
||||
val intent = HomeActivity.newIntent(this)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newIntent(context: Context): Intent {
|
||||
return Intent(context, LoginActivity::class.java)
|
||||
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
|
||||
|
||||
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
|
||||
return Intent(context, LoginActivity::class.java).apply {
|
||||
putExtra(EXTRA_CONFIG, loginConfig)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.login
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
/**
|
||||
* Parameters extracted from a configuration url
|
||||
* Ex: https://riot.im/config/config?hs_url=https%3A%2F%2Fexample.modular.im&is_url=https%3A%2F%2Fcustom.identity.org
|
||||
*
|
||||
* Note: On RiotX, identityServerUrl will never be used, so is declared private. Keep it for compatibility reason.
|
||||
*/
|
||||
@Parcelize
|
||||
data class LoginConfig(
|
||||
val homeServerUrl: String?,
|
||||
private val identityServerUrl: String?
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
fun parse(from: Uri): LoginConfig {
|
||||
return LoginConfig(
|
||||
homeServerUrl = from.getQueryParameter("hs_url"),
|
||||
identityServerUrl = from.getQueryParameter("is_url")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* 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.login
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.TransitionManager
|
||||
import com.airbnb.mvrx.*
|
||||
import com.jakewharton.rxbinding3.view.focusChanges
|
||||
import com.jakewharton.rxbinding3.widget.textChanges
|
||||
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.setTextWithColoredPart
|
||||
import im.vector.riotx.core.extensions.showPassword
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.openUrlInExternalBrowser
|
||||
import im.vector.riotx.features.homeserver.ServerUrlsRepository
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import kotlinx.android.synthetic.main.fragment_login.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* What can be improved:
|
||||
* - 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 LoginFragment : VectorBaseFragment() {
|
||||
|
||||
private val viewModel: LoginViewModel by activityViewModel()
|
||||
|
||||
private var passwordShown = false
|
||||
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_login
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupNotice()
|
||||
setupAuthButton()
|
||||
setupPasswordReveal()
|
||||
|
||||
homeServerField.focusChanges()
|
||||
.subscribe {
|
||||
if (!it) {
|
||||
// TODO Also when clicking on button?
|
||||
viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString()))
|
||||
}
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
|
||||
val initHsUrl = viewModel.getInitialHomeServerUrl()
|
||||
if (initHsUrl != null) {
|
||||
homeServerField.setText(initHsUrl)
|
||||
} else {
|
||||
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext()))
|
||||
}
|
||||
viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString()))
|
||||
}
|
||||
|
||||
private fun setupNotice() {
|
||||
riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part)
|
||||
|
||||
riotx_no_registration_notice.setOnClickListener {
|
||||
openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads")
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticate() {
|
||||
val login = loginField.text?.trim().toString()
|
||||
val password = passwordField.text?.trim().toString()
|
||||
|
||||
viewModel.handle(LoginActions.Login(login, password))
|
||||
}
|
||||
|
||||
private fun setupAuthButton() {
|
||||
Observable
|
||||
.combineLatest(
|
||||
loginField.textChanges().map { it.trim().isNotEmpty() },
|
||||
passwordField.textChanges().map { it.trim().isNotEmpty() },
|
||||
homeServerField.textChanges().map { it.trim().isNotEmpty() },
|
||||
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
|
||||
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
|
||||
}
|
||||
)
|
||||
.subscribeBy { authenticateButton.isEnabled = it }
|
||||
.disposeOnDestroy()
|
||||
authenticateButton.setOnClickListener { authenticate() }
|
||||
|
||||
authenticateButtonSso.setOnClickListener { openSso() }
|
||||
}
|
||||
|
||||
private fun openSso() {
|
||||
viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback))
|
||||
}
|
||||
|
||||
private fun setupPasswordReveal() {
|
||||
passwordShown = false
|
||||
|
||||
passwordReveal.setOnClickListener {
|
||||
passwordShown = !passwordShown
|
||||
|
||||
renderPasswordField()
|
||||
}
|
||||
|
||||
renderPasswordField()
|
||||
}
|
||||
|
||||
private fun renderPasswordField() {
|
||||
passwordField.showPassword(passwordShown)
|
||||
|
||||
passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
TransitionManager.beginDelayedTransition(login_fragment)
|
||||
|
||||
when (state.asyncHomeServerLoginFlowRequest) {
|
||||
is Incomplete -> {
|
||||
progressBar.isVisible = true
|
||||
touchArea.isVisible = true
|
||||
loginField.isVisible = false
|
||||
passwordContainer.isVisible = false
|
||||
authenticateButton.isVisible = false
|
||||
authenticateButtonSso.isVisible = false
|
||||
passwordShown = false
|
||||
renderPasswordField()
|
||||
}
|
||||
is Fail -> {
|
||||
progressBar.isVisible = false
|
||||
touchArea.isVisible = false
|
||||
loginField.isVisible = false
|
||||
passwordContainer.isVisible = false
|
||||
authenticateButton.isVisible = false
|
||||
authenticateButtonSso.isVisible = false
|
||||
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
is Success -> {
|
||||
progressBar.isVisible = false
|
||||
touchArea.isVisible = false
|
||||
|
||||
when (state.asyncHomeServerLoginFlowRequest()) {
|
||||
LoginMode.Password -> {
|
||||
loginField.isVisible = true
|
||||
passwordContainer.isVisible = true
|
||||
authenticateButton.isVisible = true
|
||||
authenticateButtonSso.isVisible = false
|
||||
}
|
||||
LoginMode.Sso -> {
|
||||
loginField.isVisible = false
|
||||
passwordContainer.isVisible = false
|
||||
authenticateButton.isVisible = false
|
||||
authenticateButtonSso.isVisible = true
|
||||
}
|
||||
LoginMode.Unsupported -> {
|
||||
loginField.isVisible = false
|
||||
passwordContainer.isVisible = false
|
||||
authenticateButton.isVisible = false
|
||||
authenticateButtonSso.isVisible = false
|
||||
Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (state.asyncLoginAction) {
|
||||
is Loading -> {
|
||||
progressBar.isVisible = true
|
||||
touchArea.isVisible = true
|
||||
|
||||
passwordShown = false
|
||||
renderPasswordField()
|
||||
}
|
||||
is Fail -> {
|
||||
progressBar.isVisible = false
|
||||
touchArea.isVisible = false
|
||||
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// Success is handled by the LoginActivity
|
||||
is Success -> Unit
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* 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.login
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Bitmap
|
||||
import android.net.http.SslError
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
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.di.ScreenComponent
|
||||
import im.vector.riotx.core.platform.OnBackPressed
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_login_sso_fallback.*
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
|
||||
|
||||
/**
|
||||
* Only login is supported for the moment
|
||||
*/
|
||||
class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed {
|
||||
|
||||
private val viewModel: LoginViewModel by activityViewModel()
|
||||
|
||||
var homeServerUrl: String = ""
|
||||
|
||||
enum class Mode {
|
||||
MODE_LOGIN,
|
||||
// Not supported in RiotX for the moment
|
||||
MODE_REGISTER
|
||||
}
|
||||
|
||||
// Mode (MODE_LOGIN or MODE_REGISTER)
|
||||
private var mMode = Mode.MODE_LOGIN
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_login_sso_fallback
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupToolbar(login_sso_fallback_toolbar)
|
||||
login_sso_fallback_toolbar.title = getString(R.string.login)
|
||||
|
||||
setupWebview()
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private fun setupWebview() {
|
||||
login_sso_fallback_webview.settings.javaScriptEnabled = true
|
||||
|
||||
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
|
||||
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
|
||||
login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google"
|
||||
|
||||
homeServerUrl = viewModel.getHomeServerUrl()
|
||||
|
||||
if (!homeServerUrl.endsWith("/")) {
|
||||
homeServerUrl += "/"
|
||||
}
|
||||
|
||||
// AppRTC requires third party cookies to work
|
||||
val cookieManager = android.webkit.CookieManager.getInstance()
|
||||
|
||||
// clear the cookies must be cleared
|
||||
if (cookieManager == null) {
|
||||
launchWebView()
|
||||
} else {
|
||||
if (!cookieManager.hasCookies()) {
|
||||
launchWebView()
|
||||
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
cookieManager.removeAllCookie()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, " cookieManager.removeAllCookie() fails")
|
||||
}
|
||||
|
||||
launchWebView()
|
||||
} else {
|
||||
try {
|
||||
cookieManager.removeAllCookies { launchWebView() }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, " cookieManager.removeAllCookie() fails")
|
||||
launchWebView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWebView() {
|
||||
if (mMode == Mode.MODE_LOGIN) {
|
||||
login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/")
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/")
|
||||
}
|
||||
|
||||
login_sso_fallback_webview.webViewClient = object : WebViewClient() {
|
||||
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
|
||||
error: SslError) {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setMessage(R.string.ssl_could_not_verify)
|
||||
.setPositiveButton(R.string.ssl_trust) { dialog, which -> handler.proceed() }
|
||||
.setNegativeButton(R.string.ssl_do_not_trust) { dialog, which -> handler.cancel() }
|
||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
handler.cancel()
|
||||
dialog.dismiss()
|
||||
return@OnKeyListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
|
||||
// on error case, close this fragment
|
||||
viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.GoBack))
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
login_sso_fallback_toolbar.subtitle = url
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
// avoid infinite onPageFinished call
|
||||
if (url.startsWith("http")) {
|
||||
// Generic method to make a bridge between JS and the UIWebView
|
||||
val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" +
|
||||
" var iframe = document.createElement('iframe');" +
|
||||
" iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" +
|
||||
" document.documentElement.appendChild(iframe);" +
|
||||
" iframe.parentNode.removeChild(iframe); iframe = null;" +
|
||||
" };"
|
||||
|
||||
view.loadUrl(mxcJavascriptSendObjectMessage)
|
||||
|
||||
if (mMode == Mode.MODE_LOGIN) {
|
||||
// The function the fallback page calls when the login is complete
|
||||
val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" +
|
||||
" sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" +
|
||||
" };"
|
||||
|
||||
view.loadUrl(mxcJavascriptOnRegistered)
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
// The function the fallback page calls when the registration is complete
|
||||
val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" +
|
||||
" = function(homeserverUrl, userId, accessToken) {" +
|
||||
" sendObjectMessage({ 'action': 'onRegistered'," +
|
||||
" 'homeServer': homeserverUrl," +
|
||||
" 'userId': userId," +
|
||||
" 'accessToken': accessToken });" +
|
||||
" };"
|
||||
|
||||
view.loadUrl(mxcJavascriptOnRegistered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of (formatted) url for MODE_LOGIN:
|
||||
*
|
||||
* <pre>
|
||||
* js:{
|
||||
* "action":"onLogin",
|
||||
* "credentials":{
|
||||
* "user_id":"@user:matrix.org",
|
||||
* "access_token":"[ACCESS_TOKEN]",
|
||||
* "home_server":"matrix.org",
|
||||
* "device_id":"[DEVICE_ID]",
|
||||
* "well_known":{
|
||||
* "m.homeserver":{
|
||||
* "base_url":"https://matrix.org/"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* @param view
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
|
||||
if (null != url && url.startsWith("js:")) {
|
||||
var json = url.substring(3)
|
||||
var parameters: Map<String, Any>? = null
|
||||
|
||||
try {
|
||||
// URL decode
|
||||
json = URLDecoder.decode(json, "UTF-8")
|
||||
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
||||
|
||||
parameters = adapter.fromJson(json) as Map<String, Any>?
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
|
||||
}
|
||||
|
||||
// succeeds to parse parameters
|
||||
if (parameters != null) {
|
||||
val action = parameters["action"] as String
|
||||
|
||||
if (mMode == Mode.MODE_LOGIN) {
|
||||
try {
|
||||
if (action == "onLogin") {
|
||||
val credentials = parameters["credentials"] as Map<String, String>
|
||||
|
||||
val userId = credentials["user_id"]
|
||||
val accessToken = credentials["access_token"]
|
||||
val homeServer = credentials["home_server"]
|
||||
val deviceId = credentials["device_id"]
|
||||
|
||||
// check if the parameters are defined
|
||||
if (null != homeServer && null != userId && null != accessToken) {
|
||||
val credentials = Credentials(
|
||||
userId = userId,
|
||||
accessToken = accessToken,
|
||||
homeServer = homeServer,
|
||||
deviceId = deviceId,
|
||||
refreshToken = null
|
||||
)
|
||||
|
||||
viewModel.handle(LoginActions.SsoLoginSuccess(credentials))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## shouldOverrideUrlLoading() : failed")
|
||||
}
|
||||
|
||||
} else {
|
||||
// MODE_REGISTER
|
||||
// check the required parameters
|
||||
if (action == "onRegistered") {
|
||||
// TODO The keys are very strange, this code comes from Riot-Android...
|
||||
if (parameters.containsKey("homeServer")
|
||||
&& parameters.containsKey("userId")
|
||||
&& parameters.containsKey("accessToken")) {
|
||||
|
||||
// We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756
|
||||
// Build on object manually
|
||||
val credentials = Credentials(
|
||||
userId = parameters["userId"] as String,
|
||||
accessToken = parameters["accessToken"] as String,
|
||||
homeServer = parameters["homeServer"] as String,
|
||||
// TODO We need deviceId on RiotX...
|
||||
deviceId = "TODO",
|
||||
refreshToken = null
|
||||
)
|
||||
|
||||
viewModel.handle(LoginActions.SsoLoginSuccess(credentials))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return super.shouldOverrideUrlLoading(view, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return if (login_sso_fallback_webview.canGoBack()) {
|
||||
login_sso_fallback_webview.goBack()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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.login
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import arrow.core.Try
|
||||
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.Authenticator
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||
import timber.log.Timber
|
||||
|
||||
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
|
||||
private val authenticator: Authenticator,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val pushRuleTriggerListener: PushRuleTriggerListener)
|
||||
: VectorViewModel<LoginViewState>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: LoginViewState): LoginViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<LoginViewModel, LoginViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? {
|
||||
val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return activity.loginViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
private var loginConfig: LoginConfig? = null
|
||||
|
||||
private val _navigationLiveData = MutableLiveData<LiveEvent<LoginActivity.Navigation>>()
|
||||
val navigationLiveData: LiveData<LiveEvent<LoginActivity.Navigation>>
|
||||
get() = _navigationLiveData
|
||||
|
||||
private var homeServerConnectionConfig: HomeServerConnectionConfig? = null
|
||||
private var currentTask: Cancelable? = null
|
||||
|
||||
|
||||
fun handle(action: LoginActions) {
|
||||
when (action) {
|
||||
is LoginActions.InitWith -> handleInitWith(action)
|
||||
is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action)
|
||||
is LoginActions.Login -> handleLogin(action)
|
||||
is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action)
|
||||
is LoginActions.NavigateTo -> handleNavigation(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInitWith(action: LoginActions.InitWith) {
|
||||
loginConfig = action.loginConfig
|
||||
}
|
||||
|
||||
private fun handleLogin(action: LoginActions.Login) {
|
||||
val homeServerConnectionConfigFinal = homeServerConnectionConfig
|
||||
|
||||
if (homeServerConnectionConfigFinal == null) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(Throwable("Bad configuration"))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Loading()
|
||||
)
|
||||
}
|
||||
|
||||
authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback<Session> {
|
||||
override fun onSuccess(data: Session) {
|
||||
onSessionCreated(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Fail(failure)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSessionCreated(session: Session) {
|
||||
activeSessionHolder.setActiveSession(session)
|
||||
session.configureAndStart(pushRuleTriggerListener)
|
||||
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Success(Unit)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) {
|
||||
val homeServerConnectionConfigFinal = homeServerConnectionConfig
|
||||
|
||||
if (homeServerConnectionConfigFinal == null) {
|
||||
// Should not happen
|
||||
Timber.w("homeServerConnectionConfig is null")
|
||||
} else {
|
||||
val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal)
|
||||
|
||||
onSessionCreated(session)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) {
|
||||
currentTask?.cancel()
|
||||
|
||||
Try {
|
||||
val homeServerUri = action.homeServerUrl
|
||||
homeServerConnectionConfig = HomeServerConnectionConfig.Builder()
|
||||
.withHomeServerUri(homeServerUri)
|
||||
.build()
|
||||
}
|
||||
|
||||
val homeServerConnectionConfigFinal = homeServerConnectionConfig
|
||||
|
||||
if (homeServerConnectionConfigFinal == null) {
|
||||
// This is invalid
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Fail(Throwable("Bad format"))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Loading()
|
||||
)
|
||||
}
|
||||
|
||||
currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback<LoginFlowResponse> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Fail(failure)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSuccess(data: LoginFlowResponse) {
|
||||
val loginMode = when {
|
||||
// SSO login is taken first
|
||||
data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso
|
||||
data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password
|
||||
else -> LoginMode.Unsupported
|
||||
}
|
||||
|
||||
setState {
|
||||
copy(
|
||||
asyncHomeServerLoginFlowRequest = Success(loginMode)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavigation(action: LoginActions.NavigateTo) {
|
||||
_navigationLiveData.postValue(LiveEvent(action.target))
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
currentTask?.cancel()
|
||||
}
|
||||
|
||||
fun getInitialHomeServerUrl(): String? {
|
||||
return loginConfig?.homeServerUrl
|
||||
}
|
||||
|
||||
fun getHomeServerUrl(): String {
|
||||
return homeServerConnectionConfig?.homeServerUri?.toString() ?: ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.login
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
|
||||
data class LoginViewState(
|
||||
val asyncLoginAction: Async<Unit> = Uninitialized,
|
||||
val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
|
||||
enum class LoginMode {
|
||||
Password,
|
||||
Sso,
|
||||
Unsupported
|
||||
}
|
|
@ -24,7 +24,6 @@ import androidx.annotation.WorkerThread
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.Person
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.util.SecretStoringUtils
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
|
@ -448,7 +447,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (!file.exists()) file.createNewFile()
|
||||
FileOutputStream(file).use {
|
||||
SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context)
|
||||
activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "## Failed to save cached notification info")
|
||||
|
@ -461,7 +460,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (file.exists()) {
|
||||
FileInputStream(file).use {
|
||||
val events: ArrayList<NotifiableEvent>? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context)
|
||||
val events: ArrayList<NotifiableEvent>? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
||||
if (events != null) {
|
||||
return ArrayList(events.mapNotNull { it as? NotifiableEvent })
|
||||
}
|
||||
|
@ -485,6 +484,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
|
|||
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
|
||||
private const val ROOM_EVENT_NOTIFICATION_ID = 2
|
||||
|
||||
// TODO Mutliaccount
|
||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
||||
|
||||
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ abstract class VectorSettingsBaseFragment : PreferenceFragmentCompat(), HasScree
|
|||
override fun onAttach(context: Context) {
|
||||
screenComponent = DaggerScreenComponent.factory().create(vectorActivity.getVectorComponent(), vectorActivity)
|
||||
super.onAttach(context)
|
||||
session = screenComponent.session()
|
||||
session = screenComponent.activeSessionHolder().getActiveSession()
|
||||
injectWith(injector())
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel<Pus
|
|||
companion object : MvRxViewModelFactory<PushRulesViewModel, PushRulesViewState> {
|
||||
|
||||
override fun initialState(viewModelContext: ViewModelContext): PushRulesViewState? {
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().session()
|
||||
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
|
||||
val rules = session.getPushRules()
|
||||
return PushRulesViewState(rules)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.utils.weak
|
||||
import timber.log.Timber
|
||||
|
||||
private const val SUCCESS_URL_SUFFIX = "/_matrix/consent"
|
||||
private const val RIOT_BOT_ID = "@riot-bot:matrix.org"
|
||||
|
||||
/**
|
||||
* This class is the Consent implementation of WebViewEventListener.
|
||||
* It is used to manage the consent agreement flow.
|
||||
*/
|
||||
class ConsentWebViewEventListener(activity: VectorBaseActivity,
|
||||
private val session: Session,
|
||||
private val delegate: WebViewEventListener)
|
||||
: WebViewEventListener by delegate {
|
||||
|
||||
private val safeActivity: VectorBaseActivity? by weak(activity)
|
||||
|
||||
override fun onPageFinished(url: String) {
|
||||
delegate.onPageFinished(url)
|
||||
if (url.endsWith(SUCCESS_URL_SUFFIX)) {
|
||||
createRiotBotRoomIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods try to create the RiotBot room when the user gives his agreement
|
||||
*/
|
||||
private fun createRiotBotRoomIfNeeded() {
|
||||
safeActivity?.let {
|
||||
/* We do not create a Room with RiotBot in RiotX for the moment
|
||||
val joinedRooms = session.dataHandler.store.rooms.filter {
|
||||
it.isJoined
|
||||
}
|
||||
if (joinedRooms.isEmpty()) {
|
||||
it.showWaitingView()
|
||||
// Ensure we can create a Room with riot-bot. Error can be a MatrixError: "Federation denied with matrix.org.", or any other error.
|
||||
session.profileApiClient
|
||||
.displayname(RIOT_BOT_ID, object : MatrixCallback<String>(createRiotBotRoomCallback) {
|
||||
override fun onSuccess(info: String?) {
|
||||
// Ok, the Home Server knows riot-Bot, so create a Room with him
|
||||
session.createDirectMessageRoom(RIOT_BOT_ID, createRiotBotRoomCallback)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
*/
|
||||
it.finish()
|
||||
/*
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APICallback instance
|
||||
*/
|
||||
private val createRiotBotRoomCallback = object : MatrixCallback<String> {
|
||||
override fun onSuccess(data: String) {
|
||||
Timber.d("## On success : succeed to invite riot-bot")
|
||||
safeActivity?.finish()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## On error : failed to invite riot-bot $failure")
|
||||
safeActivity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* This class is the default implementation of WebViewEventListener.
|
||||
* It can be used with delegation pattern
|
||||
*/
|
||||
|
||||
class DefaultWebViewEventListener : WebViewEventListener {
|
||||
|
||||
override fun pageWillStart(url: String) {
|
||||
Timber.v("On page will start: $url")
|
||||
}
|
||||
|
||||
override fun onPageStarted(url: String) {
|
||||
Timber.d("On page started: $url")
|
||||
}
|
||||
|
||||
override fun onPageFinished(url: String) {
|
||||
Timber.d("On page finished: $url")
|
||||
}
|
||||
|
||||
override fun onPageError(url: String, errorCode: Int, description: String) {
|
||||
Timber.e("On received error: $url - errorCode: $errorCode - message: $description")
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(url: String): Boolean {
|
||||
Timber.v("Should override url: $url")
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.CallSuper
|
||||
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.platform.VectorBaseActivity
|
||||
import kotlinx.android.synthetic.main.activity_vector_web_view.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class is responsible for managing a WebView
|
||||
* It does also have a loading view and a toolbar
|
||||
* It relies on the VectorWebViewClient
|
||||
* This class shouldn't be extended. To add new behaviors, you might create a new WebViewMode and a new WebViewEventListener
|
||||
*/
|
||||
class VectorWebViewActivity : VectorBaseActivity() {
|
||||
|
||||
override fun getLayoutRes() = R.layout.activity_vector_web_view
|
||||
|
||||
@Inject lateinit var session: Session
|
||||
|
||||
@CallSuper
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
session = injector.activeSessionHolder().getActiveSession()
|
||||
}
|
||||
|
||||
override fun initUiAndData() {
|
||||
configureToolbar(webview_toolbar)
|
||||
waitingView = findViewById(R.id.simple_webview_loader)
|
||||
|
||||
simple_webview.settings.apply {
|
||||
// Enable Javascript
|
||||
javaScriptEnabled = true
|
||||
|
||||
// Use WideViewport and Zoom out if there is no viewport defined
|
||||
useWideViewPort = true
|
||||
loadWithOverviewMode = true
|
||||
|
||||
// Enable pinch to zoom without the zoom buttons
|
||||
builtInZoomControls = true
|
||||
|
||||
// Allow use of Local Storage
|
||||
domStorageEnabled = true
|
||||
|
||||
allowFileAccessFromFileURLs = true
|
||||
allowUniversalAccessFromFileURLs = true
|
||||
|
||||
displayZoomControls = false
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val cookieManager = android.webkit.CookieManager.getInstance()
|
||||
cookieManager.setAcceptThirdPartyCookies(simple_webview, true)
|
||||
}
|
||||
|
||||
val url = intent.extras.getString(EXTRA_URL)
|
||||
val title = intent.extras.getString(EXTRA_TITLE, USE_TITLE_FROM_WEB_PAGE)
|
||||
if (title != USE_TITLE_FROM_WEB_PAGE) {
|
||||
setTitle(title)
|
||||
}
|
||||
|
||||
val webViewMode = intent.extras.getSerializable(EXTRA_MODE) as WebViewMode
|
||||
val eventListener = webViewMode.eventListener(this, session)
|
||||
simple_webview.webViewClient = VectorWebViewClient(eventListener)
|
||||
simple_webview.webChromeClient = object : WebChromeClient() {
|
||||
override fun onReceivedTitle(view: WebView, title: String) {
|
||||
if (title == USE_TITLE_FROM_WEB_PAGE) {
|
||||
setTitle(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
simple_webview.loadUrl(url)
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* UI event
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (simple_webview.canGoBack()) {
|
||||
simple_webview.goBack()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Companion
|
||||
* ========================================================================================== */
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_URL = "EXTRA_URL"
|
||||
private const val EXTRA_TITLE = "EXTRA_TITLE"
|
||||
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||
|
||||
private const val USE_TITLE_FROM_WEB_PAGE = ""
|
||||
|
||||
fun getIntent(context: Context,
|
||||
url: String,
|
||||
title: String = USE_TITLE_FROM_WEB_PAGE,
|
||||
mode: WebViewMode = WebViewMode.DEFAULT): Intent {
|
||||
return Intent(context, VectorWebViewActivity::class.java)
|
||||
.apply {
|
||||
putExtra(EXTRA_URL, url)
|
||||
putExtra(EXTRA_TITLE, title)
|
||||
putExtra(EXTRA_MODE, mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
|
||||
/**
|
||||
* This class inherits from WebViewClient. It has to be used with a WebView.
|
||||
* It's responsible for dispatching events to the WebViewEventListener
|
||||
*/
|
||||
class VectorWebViewClient(private val eventListener: WebViewEventListener) : WebViewClient() {
|
||||
|
||||
private var mInError: Boolean = false
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
return shouldOverrideUrl(request.url.toString())
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
|
||||
return shouldOverrideUrl(url)
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
mInError = false
|
||||
eventListener.onPageStarted(url)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
if (!mInError) {
|
||||
eventListener.onPageFinished(url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||
if (!mInError) {
|
||||
mInError = true
|
||||
eventListener.onPageError(failingUrl, errorCode, description)
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
||||
super.onReceivedError(view, request, error)
|
||||
if (!mInError) {
|
||||
mInError = true
|
||||
eventListener.onPageError(request.url.toString(), error.errorCode, error.description.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldOverrideUrl(url: String): Boolean {
|
||||
mInError = false
|
||||
val shouldOverrideUrlLoading = eventListener.shouldOverrideUrlLoading(url)
|
||||
if (!shouldOverrideUrlLoading) {
|
||||
eventListener.pageWillStart(url)
|
||||
}
|
||||
return shouldOverrideUrlLoading
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
interface WebViewEventListener {
|
||||
|
||||
/**
|
||||
* Triggered when a webview page is about to be started.
|
||||
*
|
||||
* @param url The url about to be rendered.
|
||||
*/
|
||||
fun pageWillStart(url: String)
|
||||
|
||||
/**
|
||||
* Triggered when a loading webview page has started.
|
||||
*
|
||||
* @param url The rendering url.
|
||||
*/
|
||||
fun onPageStarted(url: String)
|
||||
|
||||
/**
|
||||
* Triggered when a loading webview page has finished loading but has not been rendered yet.
|
||||
*
|
||||
* @param url The finished url.
|
||||
*/
|
||||
fun onPageFinished(url: String)
|
||||
|
||||
/**
|
||||
* Triggered when an error occurred while loading a page.
|
||||
*
|
||||
* @param url The url that failed.
|
||||
* @param errorCode The error code.
|
||||
* @param description The error description.
|
||||
*/
|
||||
fun onPageError(url: String, errorCode: Int, description: String)
|
||||
|
||||
/**
|
||||
* Triggered when a webview load an url
|
||||
*
|
||||
* @param url The url about to be rendered.
|
||||
* @return true if the method needs to manage some custom handling
|
||||
*/
|
||||
fun shouldOverrideUrlLoading(url: String): Boolean
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
|
||||
interface WebViewEventListenerFactory {
|
||||
|
||||
/**
|
||||
* @return an instance of WebViewEventListener
|
||||
*/
|
||||
fun eventListener(activity: VectorBaseActivity, session: Session): WebViewEventListener
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2018 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.webview
|
||||
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
|
||||
/**
|
||||
* This enum indicates the WebView mode. It's responsible for creating a WebViewEventListener
|
||||
*/
|
||||
enum class WebViewMode : WebViewEventListenerFactory {
|
||||
|
||||
DEFAULT {
|
||||
override fun eventListener(activity: VectorBaseActivity, session: Session): WebViewEventListener {
|
||||
return DefaultWebViewEventListener()
|
||||
}
|
||||
},
|
||||
CONSENT {
|
||||
override fun eventListener(activity: VectorBaseActivity, session: Session): WebViewEventListener {
|
||||
return ConsentWebViewEventListener(activity, session, DefaultWebViewEventListener())
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -106,7 +106,7 @@ class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
|||
val vectorBaseActivity = activity as VectorBaseActivity
|
||||
val screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
|
||||
viewModelFactory = screenComponent.viewModelFactory()
|
||||
session = screenComponent.session()
|
||||
session = screenComponent.activeSessionHolder().getActiveSession()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
|
|
12
vector/src/main/res/layout/activity_progress.xml
Normal file
12
vector/src/main/res/layout/activity_progress.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/vctr_waiting_background_color">
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</FrameLayout>
|
36
vector/src/main/res/layout/activity_vector_web_view.xml
Normal file
36
vector/src/main/res/layout/activity_vector_web_view.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?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="match_parent"
|
||||
tools:context="im.vector.riotx.features.webview.VectorWebViewActivity">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/webview_toolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:title="Title of the web page" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/simple_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/webview_toolbar" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/simple_webview_loader"
|
||||
style="@style/Widget.AppCompat.ProgressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,6 +1,7 @@
|
|||
<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/login_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -54,6 +55,7 @@
|
|||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/passwordContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
@ -114,6 +116,16 @@
|
|||
android:layout_marginTop="22dp"
|
||||
android:text="@string/auth_login" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/authenticateButtonSso"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="22dp"
|
||||
android:text="@string/auth_login_sso"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
23
vector/src/main/res/layout/fragment_login_sso_fallback.xml
Normal file
23
vector/src/main/res/layout/fragment_login_sso_fallback.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/login_sso_fallback_toolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp"
|
||||
tools:subtitle="https://www.example.org"
|
||||
tools:title="@string/auth_login" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/login_sso_fallback_webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -5,4 +5,11 @@
|
|||
<string name="labs_allow_extended_logging">Enable verbose logs.</string>
|
||||
<string name="labs_allow_extended_logging_summary">Verbose logs will help developers by providing more logs when you send a RageShake. Even when enabled, the application does not log message contents or any other private data.</string>
|
||||
|
||||
|
||||
<string name="error_terms_not_accepted">Please retry once you have accepted the terms and conditions of your homeserver.</string>
|
||||
|
||||
<!-- This one is already defined in Riot, but not yet synced-->
|
||||
<string name="error_user_already_logged_in">It looks like you’re trying to connect to another homeserver. Do you want to sign out?</string>
|
||||
|
||||
|
||||
</resources>
|
Loading…
Add table
Reference in a new issue