diff --git a/CHANGES.md b/CHANGES.md index 8769b11ea7..477746fc7e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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: - diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index fbe0969159..8002625e12 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -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' diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt index 932ffbead9..2dc2d0ef5f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/Authenticator.kt @@ -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): 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 } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt new file mode 100644 index 0000000000..c780720a18 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt @@ -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 +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index 2dde175bed..53dc8e77a0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 0397b51439..43c783a13d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -109,8 +109,6 @@ interface CryptoService { fun downloadKeys(userIds: List, forceDownload: Boolean, callback: MatrixCallback>) - fun clearCryptoCache(callback: MatrixCallback) - fun addNewSessionListener(newSessionListener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt new file mode 100644 index 0000000000..d56b6150ee --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SecureStorageService.kt @@ -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 loadSecureSecret(inputStream: InputStream, keyAlias: String): T? + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index d795a3c413..d42962c53e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -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 + /** * Pass params to the server for the current login phase. * Set all the timeouts to 1 minute diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt index 2459c3546a..399605469d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt @@ -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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt index adea7c894b..949aa6611e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticator.kt @@ -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): 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): 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 { + 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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt index ae75b2737d..e1f963ff3d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/InteractiveAuthenticationFlow.kt @@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow( @Json(name = "stages") val stages: List? = null -) \ No newline at end of file +) { + + 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" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt index 834b0aee16..78fd372beb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/LoginFlowResponse.kt @@ -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 ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 4272dbd340..742e3ff21a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index a3786d481d..1a94bebde4 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -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) { - clearCryptoDataTask - .configureWith { - this.callback = callback - } - .executeBy(taskExecutor) - } - override fun addNewSessionListener(newSessionListener: NewSessionListener) { roomDecryptorProvider.addNewSessionListener(newSessionListener) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt index ee8ee41821..d2ab764087 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmKeysUtils.kt @@ -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" + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt new file mode 100644 index 0000000000..ad4991d5ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/FileQualifiers.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt index 9c9327df55..fadcdacf21 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixScope.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index 4f3130c5ce..2061d03bed 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt new file mode 100644 index 0000000000..8ca81a1dab --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt index 3d1e433b1b..4be2d4a27f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/Request.kt @@ -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 { 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) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index a18c339b47..02addaceab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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, private val cryptoService: Lazy, private val fileService: Lazy, + private val secureStorageService: Lazy, private val syncThreadProvider: Provider, 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) { - Timber.w("SIGN_OUT: start") - - assert(isOpen) - - Timber.w("SIGN_OUT: call webservice") - return signOutService.get().signOut(object : MatrixCallback { - override fun onSuccess(data: Unit) { - Timber.w("SIGN_OUT: call webservice -> SUCCESS: clear cache") - stopSync() - stopAnyBackgroundSync() - // Clear the cache - cacheService.get().clearCache(object : MatrixCallback { - 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) { stopSync() stopAnyBackgroundSync() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index ab44a4aa93..180cdb6ea2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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() + 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 + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt new file mode 100644 index 0000000000..2a3c88e84e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/DefaultSecureStorageService.kt @@ -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 loadSecureSecret(inputStream: InputStream, keyAlias: String): T? { + return secretStoringUtils.loadSecureSecret(inputStream, keyAlias) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt similarity index 79% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt index f225dc8b4f..6192aaf148 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/securestorage/SecretStoringUtils.kt @@ -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) * * - * 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, will directly use the keystore to generate a symmetric key + * On android >= KitKat and = 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 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 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 loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? { - val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias) + private fun 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 loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? { + private fun 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 loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? { - + private fun 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 { - val format = bis.read() assert(format.toByte() == FORMAT_1) @@ -548,7 +548,6 @@ object SecretStoringUtils { } private fun format2Extract(bis: InputStream): Triple { - val format = bis.read() assert(format.toByte() == FORMAT_2) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 6f4441b189..18c2de71d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -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 -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 { 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) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt index ea0ff28293..56516dbda0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/worker/WorkManagerUtil.kt @@ -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" diff --git a/tools/tests/test_configuration_link.sh b/tools/tests/test_configuration_link.sh new file mode 100755 index 0000000000..33b1699e70 --- /dev/null +++ b/tools/tests/test_configuration_link.sh @@ -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" diff --git a/vector/build.gradle b/vector/build.gradle index de47937676..866a95b740 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -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' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e4cdaee2e4..01d8db467e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -65,6 +65,19 @@ + + + + + + + + + + + + + diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 9e87d466af..1195d38b90 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -349,6 +349,11 @@ SOFTWARE.
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch +
  • + EventBus +
    + Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org) +
  • diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 6bfbddbabb..01db4b4a01 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt b/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt index 5418e2270a..60397fb6b6 100644 --- a/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt +++ b/vector/src/main/java/im/vector/riotx/core/dialogs/DialogLocker.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index d42bce64c5..bb7892e109 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt index ff30138990..85244a26db 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/SimpleFragmentActivity.kt @@ -49,7 +49,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() { @CallSuper override fun injectWith(injector: ScreenComponent) { - session = injector.session() + session = injector.activeSessionHolder().getActiveSession() } override fun initUiAndData() { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index 1214bfa045..cb65280907 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -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)) } } - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt index aac19d8097..52cd85f249 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt @@ -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") diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt index 1c2f1d53f0..b0c2dc546a 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorViewModel.kt @@ -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(initialState: S) : BaseMvRxViewModel(initialState, false) { diff --git a/vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt b/vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt new file mode 100644 index 0000000000..d892d64327 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/utils/WeakReferenceDelegate.kt @@ -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 weak(value: T) = WeakReferenceDelegate(value) + +class WeakReferenceDelegate(value: T) { + + private var weakReference: WeakReference = WeakReference(value) + + operator fun getValue(thisRef: Any, property: KProperty<*>): T? = weakReference.get() + operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + weakReference = WeakReference(value) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index 8ef3f0adcb..868a094c0a 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -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 { 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 { 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() diff --git a/vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt b/vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt new file mode 100644 index 0000000000..0108e0cd88 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/consent/ConsentNotGivenHelper.kt @@ -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) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 247c0c1981..2a43ca705a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -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() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index c0df7a8f43..afe3579d76 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -250,7 +250,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O return true } - return super.onBackPressed() + return false } // RoomSummaryController.Callback ************************************************************** diff --git a/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt new file mode 100644 index 0000000000..b114e51607 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/link/LinkHandlerActivity.kt @@ -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 { + 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" + } + +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt new file mode 100644 index 0000000000..0691d41fcd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActions.kt @@ -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() + +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 41eed536e3..2debebfd32 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -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(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 { - 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 { - 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 { 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) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt new file mode 100644 index 0000000000..1613d1b041 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginConfig.kt @@ -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") + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt new file mode 100644 index 0000000000..6e559bcbe0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -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 { 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 + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt new file mode 100644 index 0000000000..1ce282ad77 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginSsoFallbackFragment.kt @@ -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: + * + *
    +             * 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/"
    +             *                 }
    +             *             }
    +             *         }
    +             *    }
    +             * 
    + * @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? = null + + try { + // URL decode + json = URLDecoder.decode(json, "UTF-8") + + val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java) + + parameters = adapter.fromJson(json) as Map? + } 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 + + 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 + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt new file mode 100644 index 0000000000..7231089379 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -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(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: LoginViewState): LoginViewModel + } + + companion object : MvRxViewModelFactory { + + @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>() + val navigationLiveData: LiveData> + 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 { + 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 { + 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() ?: "" + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt new file mode 100644 index 0000000000..837c2c7056 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewState.kt @@ -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 = Uninitialized, + val asyncHomeServerLoginFlowRequest: Async = Uninitialized +) : MvRxState + + +enum class LoginMode { + Password, + Sso, + Unsupported +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt index 6695c10955..551bbb808b 100644 --- a/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/riotx/features/notifications/NotificationDrawerManager.kt @@ -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? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context) + val events: ArrayList? = 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" } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt index b78921b5b9..52c332cfb3 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsBaseFragment.kt @@ -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()) } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt index 9de839b364..a8ea087727 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/push/PushRulesViewModel.kt @@ -31,7 +31,7 @@ class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel { 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) } diff --git a/vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt new file mode 100644 index 0000000000..c09639c29f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/ConsentWebViewEventListener.kt @@ -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(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 { + 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() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt new file mode 100644 index 0000000000..f72d719872 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/DefaultWebViewEventListener.kt @@ -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 + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt new file mode 100644 index 0000000000..99c9d94336 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewActivity.kt @@ -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) + } + } + } +} + diff --git a/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt new file mode 100644 index 0000000000..080bbacae8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/VectorWebViewClient.kt @@ -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 + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt new file mode 100644 index 0000000000..571eeff4fe --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListener.kt @@ -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 + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt new file mode 100644 index 0000000000..d84b72a49d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewEventListenerFactory.kt @@ -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 + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt b/vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt new file mode 100644 index 0000000000..86e9a2f18b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/webview/WebViewMode.kt @@ -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()) + } + }; + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt index c06a40e849..641a1ec8d0 100644 --- a/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -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?) { diff --git a/vector/src/main/res/layout/activity_progress.xml b/vector/src/main/res/layout/activity_progress.xml new file mode 100644 index 0000000000..5942160525 --- /dev/null +++ b/vector/src/main/res/layout/activity_progress.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/vector/src/main/res/layout/activity_vector_web_view.xml b/vector/src/main/res/layout/activity_vector_web_view.xml new file mode 100644 index 0000000000..8a5633acbc --- /dev/null +++ b/vector/src/main/res/layout/activity_vector_web_view.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/vector/src/main/res/layout/activity_login.xml b/vector/src/main/res/layout/fragment_login.xml similarity index 91% rename from vector/src/main/res/layout/activity_login.xml rename to vector/src/main/res/layout/fragment_login.xml index 0b76b461df..ed3eeffb9c 100644 --- a/vector/src/main/res/layout/activity_login.xml +++ b/vector/src/main/res/layout/fragment_login.xml @@ -1,6 +1,7 @@ @@ -54,6 +55,7 @@ @@ -114,6 +116,16 @@ android:layout_marginTop="22dp" android:text="@string/auth_login" /> + + diff --git a/vector/src/main/res/layout/fragment_login_sso_fallback.xml b/vector/src/main/res/layout/fragment_login_sso_fallback.xml new file mode 100644 index 0000000000..e83680d2cd --- /dev/null +++ b/vector/src/main/res/layout/fragment_login_sso_fallback.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 3dd8c6b36a..e02de69806 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,4 +5,11 @@ Enable verbose logs. 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. + + Please retry once you have accepted the terms and conditions of your homeserver. + + + It looks like you’re trying to connect to another homeserver. Do you want to sign out? + + \ No newline at end of file