Merge branch 'release/0.5.0'

This commit is contained in:
Benoit Marty 2019-09-17 14:50:55 +02:00
commit 07fca0922b
118 changed files with 2707 additions and 870 deletions

View File

@ -1,4 +1,25 @@
Changes in RiotX 0.4.0 (2019-XX-XX)
Changes in RiotX 0.5.0 (2019-09-17)
===================================================
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.
- Display a no network indicator when there is no network (#559)
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
Build:
- Fix issue with version name (#533)
- Fix issue with bad versionCode generated by Buildkite (#553)
Changes in RiotX 0.4.0 (2019-08-30)
===================================================
Features:

View File

@ -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'

View File

@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
import io.realm.RealmConfiguration
import java.util.*
import kotlin.random.Random
internal class CryptoStoreHelper {
@ -35,7 +35,7 @@ internal class CryptoStoreHelper {
}
fun createCredential() = Credentials(
userId = "userId_" + Random().nextInt(),
userId = "userId_" + Random.nextInt(),
homeServer = "http://matrix.org",
accessToken = "access_token",
refreshToken = null,

View File

@ -17,16 +17,23 @@
package im.vector.matrix.android.api.auth
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
/**
* This interface defines methods to authenticate to a matrix server.
*/
interface Authenticator {
/**
* Request the supported login flows for this homeserver
*/
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
/**
* @param homeServerConnectionConfig this param is used to configure the Homeserver
* @param login the login field
@ -56,4 +63,9 @@ interface Authenticator {
* @return the associated session if any, or null
*/
fun getSession(sessionParams: SessionParams): Session?
/**
* Create a session after a SSO successful login
*/
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
}

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.api.comparators
import im.vector.matrix.android.api.interfaces.DatedObject
import java.util.*
object DatedObjectComparators {

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.extensions
import im.vector.matrix.android.api.comparators.DatedObjectComparators
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import java.util.*
import java.util.Collections
/* ==========================================================================================
* MXDeviceInfo

View File

@ -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
)

View File

@ -18,18 +18,17 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.RoomService
import timber.log.Timber
import java.util.regex.Pattern
private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$")
class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {
class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) {
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(this)
}
override fun technicalDescription(): String {
return "Room member count is $`is`"
return "Room member count is $iz"
}
fun isSatisfied(event: Event, session: RoomService?): Boolean {
@ -56,12 +55,9 @@ class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_co
*/
private fun parseIsField(): Pair<String?, Int>? {
try {
val match = regex.matcher(`is`)
if (match.find()) {
val prefix = match.group(1)
val count = match.group(2).toInt()
return prefix to count
}
val match = regex.find(iz) ?: return null
val (prefix, count) = match.destructured
return prefix to count.toInt()
} catch (t: Throwable) {
Timber.d(t)
}

View File

@ -20,10 +20,10 @@ import androidx.lifecycle.LiveData
interface InitialSyncProgressService {
fun getLiveStatus() : LiveData<Status?>
fun getInitialSyncProgressStatus() : LiveData<Status?>
data class Status(
@StringRes val statusText: Int?,
@StringRes val statusText: Int,
val percentProgress: Int = 0
)
}

View File

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

View File

@ -109,8 +109,6 @@ interface CryptoService {
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)
fun clearCryptoCache(callback: MatrixCallback<Unit>)
fun addNewSessionListener(newSessionListener: NewSessionListener)
fun removeSessionListener(listener: NewSessionListener)

View File

@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.pushers
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import java.util.*
import java.util.UUID
interface PushersService {

View File

@ -29,7 +29,6 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import java.util.*
/**
* Parameter to create a room, with facilities functions to configure it
@ -133,7 +132,7 @@ class CreateRoomParams {
)
if (null == initialStates) {
initialStates = Arrays.asList<Event>(algoEvent)
initialStates = mutableListOf(algoEvent)
} else {
initialStates!!.add(algoEvent)
}
@ -166,7 +165,7 @@ class CreateRoomParams {
content = contentMap.toContent())
if (null == initialStates) {
initialStates = Arrays.asList<Event>(historyVisibilityEvent)
initialStates = mutableListOf(historyVisibilityEvent)
} else {
initialStates!!.add(historyVisibilityEvent)
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.securestorage
import java.io.InputStream
import java.io.OutputStream
interface SecureStorageService {
fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream)
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T?
}

View File

@ -22,4 +22,5 @@ sealed class SyncState {
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()
object NO_NETWORK : SyncState()
}

View File

@ -17,10 +17,12 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
@ -29,6 +31,13 @@ import retrofit2.http.POST
*/
internal interface AuthAPI {
/**
* Get the supported login flow
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun getLoginFlows(): Call<LoginFlowResponse>
/**
* Pass params to the server for the current login phase.
* Set all the timeouts to 1 minute

View File

@ -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()

View File

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.di.Unauthenticated
@ -62,11 +63,20 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
authenticate(homeServerConnectionConfig, login, password)
@ -74,7 +84,14 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
}
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
@ -95,6 +112,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
sessionManager.getOrCreateSession(sessionParams)
}
override fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)

View File

@ -30,4 +30,12 @@ data class InteractiveAuthenticationFlow(
@Json(name = "stages")
val stages: List<String>? = null
)
) {
companion object {
// Possible values for type
const val TYPE_LOGIN_SSO = "m.login.sso"
const val TYPE_LOGIN_TOKEN = "m.login.token"
const val TYPE_LOGIN_PASSWORD = "m.login.password"
}
}

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class LoginFlowResponse(
data class LoginFlowResponse(
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>
)

View File

@ -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)
@ -105,7 +108,7 @@ internal abstract class CryptoModule {
}
@Binds
abstract fun bindCryptoService(cryptoManager: CryptoManager): CryptoService
abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService
@Binds
abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask

View File

@ -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
@ -93,7 +91,7 @@ import kotlin.math.max
* Specially, it tracks all room membership changes events in order to do keys updates.
*/
@SessionScope
internal class CryptoManager @Inject constructor(
internal class DefaultCryptoService @Inject constructor(
// Olm Manager
private val olmManager: OlmManager,
// The credentials,
@ -135,7 +133,6 @@ internal class CryptoManager @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 CryptoManager @Inject constructor(
}
}
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
clearCryptoDataTask
.configureWith {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun addNewSessionListener(newSessionListener: NewSessionListener) {
roomDecryptorProvider.addNewSessionListener(newSessionListener)
}
@ -1067,6 +1056,6 @@ internal class CryptoManager @Inject constructor(
* ========================================================================================== */
override fun toString(): String {
return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
}
}

View File

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.auth.data.Credentials
import java.util.*
import javax.inject.Inject
internal class ObjectSigner @Inject constructor(private val credentials: Credentials,

View File

@ -31,7 +31,6 @@ import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.convertFromUTF8
import timber.log.Timber
import java.util.*
internal class MXOlmDecryption(
// The olm device interface
@ -158,33 +157,14 @@ internal class MXOlmDecryption(
* @return payload, if decrypted successfully.
*/
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
val sessionIdsSet = olmDevice.getSessionIds(theirDeviceIdentityKey)
val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey) ?: emptySet()
val sessionIds: List<String>
if (null == sessionIdsSet) {
sessionIds = ArrayList()
} else {
sessionIds = ArrayList(sessionIdsSet)
}
val messageBody = message["body"] as? String
var messageType: Int? = null
val typeAsVoid = message["type"]
if (null != typeAsVoid) {
if (typeAsVoid is Double) {
messageType = typeAsVoid.toInt()
} else if (typeAsVoid is Int) {
messageType = typeAsVoid
} else if (typeAsVoid is Long) {
messageType = typeAsVoid.toInt()
}
}
if (null == messageBody || null == messageType) {
return null
val messageBody = message["body"] as? String ?: return null
val messageType = when (val typeAsVoid = message["type"]) {
is Double -> typeAsVoid.toInt()
is Int -> typeAsVoid
is Long -> typeAsVoid.toInt()
else -> return null
}
// Try each session in turn

View File

@ -26,7 +26,6 @@ import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -59,8 +58,7 @@ object MXEncryptedAttachments {
// Half of the IV is random, the lower order bits are zeroed
// such that the counter never wraps.
// See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
val initVectorBytes = ByteArray(16)
Arrays.fill(initVectorBytes, 0.toByte())
val initVectorBytes = ByteArray(16) { 0.toByte() }
val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart)
@ -115,7 +113,7 @@ object MXEncryptedAttachments {
encryptedByteArray = outStream.toByteArray()
)
Timber.v("Encrypt in " + (System.currentTimeMillis() - t0) + " ms")
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
return Try.just(result)
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## encryptAttachment failed")
@ -206,13 +204,13 @@ object MXEncryptedAttachments {
val decryptedStream = ByteArrayInputStream(outStream.toByteArray())
outStream.close()
Timber.v("Decrypt in " + (System.currentTimeMillis() - t0) + " ms")
Timber.v("Decrypt in ${System.currentTimeMillis() - t0} ms")
return decryptedStream
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## decryptAttachment() : failed " + oom.message)
Timber.e(oom, "## decryptAttachment() : failed ${oom.message}")
} catch (e: Exception) {
Timber.e(e, "## decryptAttachment() : failed " + e.message)
Timber.e(e, "## decryptAttachment() : failed ${e.message}")
}
try {
@ -228,34 +226,20 @@ object MXEncryptedAttachments {
* Base64 URL conversion methods
*/
private fun base64UrlToBase64(base64Url: String?): String? {
var result = base64Url
if (null != result) {
result = result.replace("-".toRegex(), "+")
result = result.replace("_".toRegex(), "/")
}
return result
private fun base64UrlToBase64(base64Url: String): String {
return base64Url.replace('-', '+')
.replace('_', '/')
}
private fun base64ToBase64Url(base64: String?): String? {
var result = base64
if (null != result) {
result = result.replace("\n".toRegex(), "")
result = result.replace("\\+".toRegex(), "-")
result = result.replace("/".toRegex(), "_")
result = result.replace("=".toRegex(), "")
}
return result
private fun base64ToBase64Url(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("\\+".toRegex(), "-")
.replace('/', '_')
.replace("=", "")
}
private fun base64ToUnpaddedBase64(base64: String?): String? {
var result = base64
if (null != result) {
result = result.replace("\n".toRegex(), "")
result = result.replace("=".toRegex(), "")
}
return result
private fun base64ToUnpaddedBase64(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("=", "")
}
}

View File

@ -66,9 +66,8 @@ import org.matrix.olm.OlmPkEncryption
import org.matrix.olm.OlmPkMessage
import timber.log.Timber
import java.security.InvalidParameterException
import java.util.*
import javax.inject.Inject
import kotlin.collections.HashMap
import kotlin.random.Random
/**
* A KeysBackup class instance manage incremental backup of e2e keys (megolm keys)
@ -114,8 +113,6 @@ internal class KeysBackup @Inject constructor(
// The backup key being used.
private var backupOlmPkEncryption: OlmPkEncryption? = null
private val random = Random()
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
private var keysBackupStateListener: KeysBackupStateListener? = null
@ -848,7 +845,7 @@ internal class KeysBackup @Inject constructor(
// Wait between 0 and 10 seconds, to avoid backup requests from
// different clients hitting the server all at the same time when a
// new key is sent
val delayInMs = random.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong()
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS)
uiHandler.postDelayed({ backupKeys() }, delayInMs)
}
@ -1307,7 +1304,7 @@ internal class KeysBackup @Inject constructor(
// Make the request
storeSessionDataTask
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)){
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) {
this.callback = sendingRequestCallback
}
.executeBy(taskExecutor)
@ -1405,7 +1402,7 @@ internal class KeysBackup @Inject constructor(
companion object {
// Maximum delay in ms in {@link maybeBackupKeys}
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
// Maximum number of keys to send at a time to the homeserver.
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100

View File

@ -22,7 +22,7 @@ package im.vector.matrix.android.internal.crypto.keysbackup
import androidx.annotation.WorkerThread
import im.vector.matrix.android.api.listeners.ProgressListener
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.xor

View File

@ -20,7 +20,6 @@ import android.os.Handler
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import timber.log.Timber
import java.util.*
internal class KeysBackupStateManager(private val uiHandler: Handler) {

View File

@ -23,7 +23,6 @@ import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys
import java.io.Serializable
import java.util.*
@JsonClass(generateAdapter = true)
data class MXDeviceInfo(

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.crypto.model
import im.vector.matrix.android.api.util.JsonDict
import timber.log.Timber
import java.util.*
data class MXKey(
/**
@ -46,11 +45,7 @@ data class MXKey(
* @return the signed data map
*/
fun signalableJSONDictionary(): Map<String, Any> {
val map = HashMap<String, Any>()
map["key"] = value
return map
return mapOf("key" to value)
}
/**

View File

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.crypto.model
import java.util.*
class MXUsersDevicesMap<E> {
@ -27,7 +26,7 @@ class MXUsersDevicesMap<E> {
* @return the user Ids
*/
val userIds: List<String>
get() = ArrayList(map.keys)
get() = map.keys.toList()
val isEmpty: Boolean
get() = map.isEmpty()
@ -40,7 +39,7 @@ class MXUsersDevicesMap<E> {
* @return the device ids list
*/
fun getUserDeviceIds(userId: String?): List<String>? {
return if (userId?.isNotBlank() == true && map.containsKey(userId)) {
return if (!userId.isNullOrBlank() && map.containsKey(userId)) {
map[userId]!!.keys.toList()
} else null
}
@ -53,7 +52,7 @@ class MXUsersDevicesMap<E> {
* @return the object
*/
fun getObject(userId: String?, deviceId: String?): E? {
return if (userId?.isNotBlank() == true && deviceId?.isNotBlank() == true && map.containsKey(userId)) {
return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) {
map[userId]?.get(deviceId)
} else null
}
@ -67,11 +66,8 @@ class MXUsersDevicesMap<E> {
*/
fun setObject(userId: String?, deviceId: String?, o: E?) {
if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) {
if (map[userId] == null) {
map[userId] = HashMap()
}
map[userId]?.put(deviceId, o)
val devices = map.getOrPut(userId) { HashMap() }
devices[deviceId] = o
}
}
@ -82,7 +78,7 @@ class MXUsersDevicesMap<E> {
* @param userId the user id
*/
fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
if (userId?.isNotBlank() == true) {
if (!userId.isNullOrBlank()) {
if (null == objectsPerDevices) {
map.remove(userId)
} else {
@ -97,7 +93,7 @@ class MXUsersDevicesMap<E> {
* @param userId the user id.
*/
fun removeUserObjects(userId: String?) {
if (userId?.isNotBlank() == true) {
if (!userId.isNullOrBlank()) {
map.remove(userId)
}
}

View File

@ -21,8 +21,8 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import java.util.*
import javax.inject.Inject
import kotlin.random.Random
internal interface SendToDeviceTask : Task<SendToDeviceTask.Params, Unit> {
data class Params(
@ -45,7 +45,7 @@ internal class DefaultSendToDeviceTask @Inject constructor(private val cryptoApi
return executeRequest {
apiCall = cryptoApi.sendToDevice(
params.eventType,
params.transactionId ?: Random().nextInt(Integer.MAX_VALUE).toString(),
params.transactionId ?: Random.nextInt(Integer.MAX_VALUE).toString(),
sendToDeviceBody
)
}

View File

@ -44,7 +44,7 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.*
import java.util.UUID
import javax.inject.Inject
import kotlin.collections.HashMap
@ -161,7 +161,7 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
cancelTransaction(
startReq.transactionID!!,
otherUserId!!,
startReq?.fromDevice ?: event.getSenderKey()!!,
startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod
)
}
@ -388,14 +388,13 @@ internal class DefaultSasVerificationService @Inject constructor(private val cre
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid
*/
private fun createUniqueIDForTransaction(userId: String, deviceID: String): String {
val buff = StringBuffer()
buff
.append(credentials.userId).append("|")
.append(credentials.deviceId).append("|")
.append(userId).append("|")
.append(deviceID).append("|")
.append(UUID.randomUUID().toString())
return buff.toString()
return buildString {
append(credentials.userId).append("|")
append(credentials.deviceId).append("|")
append(userId).append("|")
append(deviceID).append("|")
append(UUID.randomUUID().toString())
}
}

View File

@ -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"
}
}

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import java.util.*
import java.util.UUID
import javax.inject.Inject
internal class RoomSummaryMapper @Inject constructor(

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -18,12 +18,10 @@ package im.vector.matrix.android.internal.network
import android.content.Context
import com.novoda.merlin.Merlin
import com.novoda.merlin.MerlinsBeard
import im.vector.matrix.android.internal.di.MatrixScope
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@ -35,29 +33,38 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
.withDisconnectableCallbacks()
.build(context)
private val merlinsBeard = MerlinsBeard.Builder().build(context)
private val listeners = Collections.synchronizedList(ArrayList<Listener>())
private val listeners = Collections.synchronizedSet(LinkedHashSet<Listener>())
// True when internet is available
var hasInternetAccess = false
private set
init {
merlin.bind()
merlin.registerDisconnectable {
Timber.v("On Disconnect")
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
if (hasInternetAccess) {
Timber.v("On Disconnect")
hasInternetAccess = false
val localListeners = listeners.toList()
localListeners.forEach {
it.onDisconnect()
}
}
}
merlin.registerConnectable {
Timber.v("On Connect")
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
if (!hasInternetAccess) {
Timber.v("On Connect")
hasInternetAccess = true
val localListeners = listeners.toList()
localListeners.forEach {
it.onConnect()
}
}
}
}
suspend fun waitUntilConnected() {
if (isConnected()) {
if (hasInternetAccess) {
return
} else {
suspendCoroutine<Unit> { continuation ->
@ -79,10 +86,6 @@ internal class NetworkConnectivityChecker @Inject constructor(context: Context)
listeners.remove(listener)
}
fun isConnected(): Boolean {
return merlinsBeard.isConnected
}
interface Listener {
fun onConnect() {

View File

@ -18,10 +18,12 @@ package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import timber.log.Timber
import java.io.IOException
@ -65,6 +67,11 @@ internal class Request<DATA> {
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri))
}
return Failure.ServerError(matrixError, httpCode)
}
} catch (ex: JsonDataException) {

View File

@ -24,7 +24,6 @@ import java.security.KeyStore
import java.security.MessageDigest
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.*
import javax.net.ssl.*
import kotlin.experimental.and

View File

@ -25,7 +25,6 @@ import java.net.UnknownHostException
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
@ -101,25 +100,16 @@ constructor(trustPinned: Array<TrustManager>, acceptedTlsVersions: List<TlsVersi
}
private fun enableTLSOnSocket(socket: Socket?): Socket? {
if (socket != null && socket is SSLSocket) {
val sslSocket = socket as SSLSocket?
if (socket is SSLSocket) {
val supportedProtocols = socket.supportedProtocols.toSet()
val filteredEnabledProtocols = enabledProtocols.filter { it in supportedProtocols }
val supportedProtocols = Arrays.asList(*sslSocket!!.supportedProtocols)
val filteredEnabledProtocols = ArrayList<String>()
for (protocol in enabledProtocols) {
if (supportedProtocols.contains(protocol)) {
filteredEnabledProtocols.add(protocol)
}
}
if (!filteredEnabledProtocols.isEmpty()) {
if (filteredEnabledProtocols.isNotEmpty()) {
try {
sslSocket.enabledProtocols = filteredEnabledProtocols.toTypedArray()
socket.enabledProtocols = filteredEnabledProtocols.toTypedArray()
} catch (e: Exception) {
Timber.e(e)
}
}
}
return socket

View File

@ -15,6 +15,7 @@
*/
package im.vector.matrix.android.internal.session
import androidx.annotation.StringRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.session.InitialSyncProgressService
@ -25,31 +26,33 @@ import javax.inject.Inject
@SessionScope
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {
var status = MutableLiveData<InitialSyncProgressService.Status>()
private var status = MutableLiveData<InitialSyncProgressService.Status>()
var rootTask: TaskInfo? = null
private var rootTask: TaskInfo? = null
override fun getLiveStatus(): LiveData<InitialSyncProgressService.Status?> {
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status?> {
return status
}
fun startTask(nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) {
fun startTask(@StringRes nameRes: Int, totalProgress: Int, parentWeight: Float = 1f) {
// Create a rootTask, or add a child to the leaf
if (rootTask == null) {
rootTask = TaskInfo(nameRes, totalProgress)
} else {
val currentLeaf = rootTask!!.leaf()
val newTask = TaskInfo(nameRes, totalProgress)
newTask.parent = currentLeaf
newTask.offset = currentLeaf.currentProgress
val newTask = TaskInfo(nameRes,
totalProgress,
currentLeaf,
parentWeight)
currentLeaf.child = newTask
newTask.parentWeight = parentWeight
}
reportProgress(0)
}
fun reportProgress(progress: Int) {
rootTask?.leaf()?.incrementProgress(progress)
rootTask?.leaf()?.setProgress(progress)
}
fun endTask(nameRes: Int) {
@ -58,7 +61,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
//close it
val parent = endedTask.parent
parent?.child = null
parent?.incrementProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
}
if (endedTask?.parent == null) {
status.postValue(null)
@ -71,14 +74,17 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
}
inner class TaskInfo(var nameRes: Int,
var totalProgress: Int) {
var parent: TaskInfo? = null
private inner class TaskInfo(@StringRes var nameRes: Int,
var totalProgress: Int,
var parent: TaskInfo? = null,
var parentWeight: Float = 1f,
var offset: Int = parent?.currentProgress ?: 0) {
var child: TaskInfo? = null
var parentWeight: Float = 1f
var currentProgress: Int = 0
var offset: Int = 0
/**
* Get the further child
*/
fun leaf(): TaskInfo {
var last = this
while (last.child != null) {
@ -87,26 +93,27 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
return last
}
fun incrementProgress(progress: Int) {
/**
* Set progress of the parent if any (which will post value), or post the value
*/
fun setProgress(progress: Int) {
currentProgress = progress
// val newProgress = Math.min(currentProgress + progress, totalProgress)
parent?.let {
val parentProgress = (currentProgress * parentWeight).toInt()
it.incrementProgress(offset + parentProgress)
}
if (parent == null) {
Timber.e("--- ${leaf().nameRes}: ${currentProgress}")
it.setProgress(offset + parentProgress)
} ?: run {
Timber.e("--- ${leaf().nameRes}: $currentProgress")
status.postValue(
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
)
}
}
}
}
inline fun <T> reportSubtask(reporter: DefaultInitialSyncProgressService?,
nameRes: Int,
@StringRes nameRes: Int,
totalProgress: Int,
parentWeight: Float = 1f,
block: () -> T): T {
@ -121,11 +128,11 @@ inline fun <K, V, R> Map<out K, V>.mapWithProgress(reporter: DefaultInitialSyncP
taskId: Int,
weight: Float,
transform: (Map.Entry<K, V>) -> R): List<R> {
val total = count()
val total = count().toFloat()
var current = 0
reporter?.startTask(taskId, 100, weight)
return this.map {
reporter?.reportProgress((current / total.toFloat() * 100).toInt())
return map {
reporter?.reportProgress((current / total * 100).toInt())
current++
transform.invoke(it)
}.also {

View File

@ -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.CryptoManager
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
@ -63,8 +62,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val signOutService: Lazy<SignOutService>,
private val pushRuleService: Lazy<PushRuleService>,
private val pushersService: Lazy<PushersService>,
private val cryptoService: Lazy<CryptoManager>,
private val cryptoService: Lazy<DefaultCryptoService>,
private val fileService: Lazy<FileService>,
private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val contentUploadProgressTracker: ContentUploadStateTracker,
@ -75,13 +75,13 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
GroupService by groupService.get(),
UserService by userService.get(),
CryptoService by cryptoService.get(),
CacheService by cacheService.get(),
SignOutService by signOutService.get(),
FilterService by filterService.get(),
PushRuleService by pushRuleService.get(),
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get() {
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get() {
private var isOpen = false
@ -144,43 +144,6 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
}
}
@MainThread
override fun signOut(callback: MatrixCallback<Unit>) {
Timber.w("SIGN_OUT: start")
assert(isOpen)
Timber.w("SIGN_OUT: call webservice")
return signOutService.get().signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: call webservice -> SUCCESS: clear cache")
stopSync()
stopAnyBackgroundSync()
// Clear the cache
cacheService.get().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: clear cache -> SUCCESS: clear crypto cache")
cryptoService.get().clearCryptoCache(MatrixCallbackDelegate(callback))
WorkManagerUtil.cancelAllWorks(context)
callback.onSuccess(Unit)
}
override fun onFailure(failure: Throwable) {
// ignore error
Timber.e("SIGN_OUT: clear cache -> ERROR: ignoring")
onSuccess(Unit)
}
})
}
override fun onFailure(failure: Throwable) {
// Ignore failure
Timber.e("SIGN_OUT: call webservice -> ERROR: ignoring")
onSuccess(Unit)
}
})
}
override fun clearCache(callback: MatrixCallback<Unit>) {
stopSync()
stopAnyBackgroundSync()

View File

@ -27,21 +27,20 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.configureEncryption
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.di.*
import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomFactory
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.RoomFactory
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
import okhttp3.OkHttpClient
@ -53,6 +52,7 @@ internal abstract class SessionModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "session_db_"
@JvmStatic
@Provides
@ -67,18 +67,33 @@ internal abstract class SessionModule {
return sessionParams.credentials
}
@JvmStatic
@UserMd5
@Provides
fun providesUserMd5(sessionParams: SessionParams): String {
return sessionParams.credentials.userId.md5()
}
@JvmStatic
@Provides
@UserCacheDirectory
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File {
return File(context.filesDir, userMd5)
}
@JvmStatic
@Provides
@SessionDatabase
@SessionScope
fun providesRealmConfiguration(sessionParams: SessionParams, context: Context): RealmConfiguration {
val childPath = sessionParams.credentials.userId.md5()
val directory = File(context.filesDir, childPath)
fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory directory: File,
@UserMd5 userMd5: String): RealmConfiguration {
return RealmConfiguration.Builder()
.directory(directory)
.name("disk_store.realm")
.configureEncryption("session_db_$childPath", context)
.apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
}
.modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded()
.build()
@ -101,7 +116,18 @@ internal abstract class SessionModule {
fun providesOkHttpClient(@Unauthenticated okHttpClient: OkHttpClient,
accessTokenInterceptor: AccessTokenInterceptor): OkHttpClient {
return okHttpClient.newBuilder()
.addInterceptor(accessTokenInterceptor)
.apply {
// Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
interceptors().removeAll(existingCurlInterceptors)
addInterceptor(accessTokenInterceptor)
// Re add eventually the curl logging interceptors
existingCurlInterceptors.forEach {
addInterceptor(it)
}
}
.build()
}
@ -142,4 +168,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
@Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
}

View File

@ -30,9 +30,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()
override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
val listeners = listeners[key] ?: ArrayList()
val listeners = listeners.getOrPut(key) { ArrayList() }
listeners.add(updateListener)
this.listeners[key] = listeners
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
mainHandler.post { updateListener.onUpdate(currentState) }
}

View File

@ -32,7 +32,7 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import java.util.*
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject

View File

@ -38,7 +38,7 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import java.util.*
import java.util.UUID
import javax.inject.Inject
/**
@ -304,17 +304,22 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
}
private fun buildReplyFallback(body: TextContent, originalSenderId: String?, newBodyText: String): String {
val lines = body.text.split("\n")
val replyFallback = StringBuffer("> <$originalSenderId>")
lines.forEachIndexed { index, s ->
if (index == 0) {
replyFallback.append(" $s")
} else {
replyFallback.append("\n> $s")
return buildString {
append("> <")
append(originalSenderId)
append(">")
val lines = body.text.split("\n")
lines.forEachIndexed { index, s ->
if (index == 0) {
append(" $s")
} else {
append("\n> $s")
}
}
append("\n\n")
append(newBodyText)
}
replyFallback.append("\n\n").append(newBodyText)
return replyFallback.toString()
}
/**

View File

@ -0,0 +1,33 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.securestorage
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
internal class DefaultSecureStorageService @Inject constructor(private val secretStoringUtils: SecretStoringUtils) : SecureStorageService {
override fun securelyStoreObject(any: Any, keyAlias: String, outputStream: OutputStream) {
secretStoringUtils.securelyStoreObject(any, keyAlias, outputStream)
}
override fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
return secretStoringUtils.loadSecureSecret(inputStream, keyAlias)
}
}

View File

@ -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,10 +22,12 @@ 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.*
import javax.crypto.*
@ -33,6 +35,7 @@ import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
import javax.security.auth.x500.X500Principal
@ -65,22 +68,24 @@ import javax.security.auth.x500.X500Principal
* val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
* </code>
*
* You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
* You can also just use this utility to store a secret key, and use any encryption algorithm that you want.
*
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
* add a pin or change the schema); So you might and with a useless pile of bytes.
*/
object SecretStoringUtils {
internal class SecretStoringUtils @Inject constructor(private val context: Context) {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding";
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding"
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"
const val FORMAT_API_M: Byte = 0
const val FORMAT_1: Byte = 1
const val FORMAT_2: Byte = 2
private const val FORMAT_API_M: Byte = 0
private const val FORMAT_1: Byte = 1
private const val FORMAT_2: Byte = 2
}
val keyStore: KeyStore by lazy {
private val keyStore: KeyStore by lazy {
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null)
}
@ -88,24 +93,30 @@ object SecretStoringUtils {
private val secureRandom = SecureRandom()
fun safeDeleteKey(keyAlias: String) {
try {
keyStore.deleteEntry(keyAlias)
} catch (e: KeyStoreException) {
Timber.e(e)
}
}
/**
* Encrypt the given secret using the android Keystore.
* On android >= M, will directly use the keystore to generate a symetric key
* On KitKat >= KitKat and <M, as symetric key gen is not available, will use an asymetric key generated
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
* On android >= M, will directly use the keystore to generate a symmetric key
* On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated
* in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned
* in the bytearray (in can be stored anywhere, it is encrypted)
* On older version a key in generated from alias with random salt.
*
* The secret is encrypted using the following method: AES/GCM/NoPadding
*/
@Throws(Exception::class)
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return encryptStringM(secret, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return encryptStringJ(secret, keyAlias, context)
} else {
return encryptForOldDevicesNotGood(secret, keyAlias)
fun securelyStoreString(secret: String, keyAlias: String): ByteArray? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> encryptStringK(secret, keyAlias)
else -> encryptForOldDevicesNotGood(secret, keyAlias)
}
}
@ -113,39 +124,33 @@ object SecretStoringUtils {
* Decrypt a secret that was encrypted by #securelyStoreString()
*/
@Throws(Exception::class)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return decryptStringM(encrypted, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return decryptStringJ(encrypted, keyAlias, context)
} else {
return decryptForOldDevicesNotGood(encrypted, keyAlias)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> decryptStringK(encrypted, keyAlias)
else -> decryptForOldDevicesNotGood(encrypted, keyAlias)
}
}
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
saveSecureObjectM(keyAlias, output, any)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return saveSecureObjectK(keyAlias, output, any, context)
} else {
return saveSecureObjectOldNotGood(keyAlias, output, any)
fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> saveSecureObjectK(keyAlias, output, any)
else -> saveSecureObjectOldNotGood(keyAlias, output, any)
}
}
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return loadSecureObjectM(keyAlias, inputStream)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return loadSecureObjectK(keyAlias, inputStream, context)
} else {
return loadSecureObjectOldNotGood(keyAlias, inputStream)
fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT -> loadSecureObjectK(keyAlias, inputStream)
else -> loadSecureObjectOldNotGood(keyAlias, inputStream)
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
?.secretKey
if (secretKeyEntry == null) {
@ -163,7 +168,6 @@ object SecretStoringUtils {
return secretKeyEntry
}
/*
Symetric Key Generation is only available in M, so before M the idea is to:
- Generate a pair of RSA keys;
@ -172,8 +176,8 @@ object SecretStoringUtils {
- Store the encrypted AES
Generate a key pair for encryption
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
if (privateKeyEntry != null) return privateKeyEntry
@ -201,7 +205,7 @@ object SecretStoringUtils {
@RequiresApi(Build.VERSION_CODES.M)
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
@ -212,10 +216,10 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.M)
fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
@ -224,15 +228,15 @@ object SecretStoringUtils {
return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
@RequiresApi(Build.VERSION_CODES.KITKAT)
private fun encryptStringK(text: String, keyAlias: String): ByteArray? {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")
//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)
val encryptedKey = rsaEncrypt(keyAlias, key)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
@ -242,7 +246,7 @@ object SecretStoringUtils {
return format1Make(encryptedKey, iv, encryptedBytes)
}
fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
private fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
@ -258,11 +262,11 @@ object SecretStoringUtils {
return format2Make(salt, iv, encryptedBytes)
}
fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
private fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {
val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10_000, 128)
val tmp = factory.generateSecret(spec)
val sKey = SecretKeySpec(tmp.encoded, "AES")
@ -277,25 +281,23 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {
private fun decryptStringK(data: ByteArray, keyAlias: String): String? {
val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
return String(cipher.doFinal(encrypted), Charsets.UTF_8)
}
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
@ -314,14 +316,14 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.KITKAT)
fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
private fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any) {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")
//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)
val encryptedKey = rsaEncrypt(keyAlias, key)
val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
@ -342,7 +344,7 @@ object SecretStoringUtils {
output.write(bos1.toByteArray())
}
fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
private fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
@ -387,8 +389,8 @@ object SecretStoringUtils {
@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)
private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
val format = inputStream.read()
assert(format.toByte() == FORMAT_API_M)
@ -411,12 +413,12 @@ object SecretStoringUtils {
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(IOException::class)
fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {
private fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream): T? {
val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
@ -432,8 +434,7 @@ object SecretStoringUtils {
}
@Throws(Exception::class)
fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
private fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {
val (salt, iv, encrypted) = format2Extract(inputStream)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
@ -456,10 +457,10 @@ object SecretStoringUtils {
}
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(Exception::class)
private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
// Encrypt the text
val inputCipher = Cipher.getInstance(RSA_MODE)
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
@ -472,19 +473,14 @@ 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)
val bos = ByteArrayOutputStream()
CipherInputStream(encrypted, output).use {
it.copyTo(bos)
}
return bos.toByteArray()
return CipherInputStream(encrypted, output).use { it.readBytes() }
}
private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
@ -495,14 +491,7 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize)
bis.read(iv, 0, ivSize)
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
val encrypted = bis.readBytes()
return Pair(iv, encrypted)
}
@ -516,7 +505,6 @@ object SecretStoringUtils {
}
private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_1)
@ -530,14 +518,7 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize)
bis.read(iv)
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
val encrypted = bis.readBytes()
return Triple(encryptedKey, iv, encrypted)
}
@ -567,7 +548,6 @@ object SecretStoringUtils {
}
private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
val format = bis.read()
assert(format.toByte() == FORMAT_2)
@ -579,14 +559,7 @@ object SecretStoringUtils {
val iv = ByteArray(ivSize)
bis.read(iv)
val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
val encrypted = bis.readBytes()
return Triple(salt, iv, encrypted)
}
}

View File

@ -16,25 +16,64 @@
package im.vector.matrix.android.internal.session.signout
import android.content.Context
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal interface SignOutTask : Task<Unit, Unit>
internal class DefaultSignOutTask @Inject constructor(private val credentials: Credentials,
internal class DefaultSignOutTask @Inject constructor(private val context: Context,
private val credentials: Credentials,
private val signOutAPI: SignOutAPI,
private val sessionManager: SessionManager,
private val sessionParamsStore: SessionParamsStore) : SignOutTask {
private val sessionParamsStore: SessionParamsStore,
@SessionDatabase private val clearSessionDataTask: ClearCacheTask,
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@UserCacheDirectory private val userFile: File,
private val realmKeysUtils: RealmKeysUtils,
@UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) {
Timber.d("SignOut: send request...")
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}
sessionParamsStore.delete(credentials.userId)
Timber.d("SignOut: release session...")
sessionManager.releaseSession(credentials.userId)
Timber.d("SignOut: cancel pending works...")
WorkManagerUtil.cancelAllWorks(context)
Timber.d("SignOut: delete session params...")
sessionParamsStore.delete(credentials.userId)
Timber.d("SignOut: clear session data...")
clearSessionDataTask.execute(Unit)
Timber.d("SignOut: clear crypto data...")
clearCryptoDataTask.execute(Unit)
Timber.d("SignOut: clear file system")
userFile.deleteRecursively()
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
}
}

View File

@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
@ -33,7 +33,7 @@ import timber.log.Timber
import javax.inject.Inject
internal class CryptoSyncHandler @Inject constructor(private val cryptoManager: CryptoManager,
internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
private val sasVerificationService: DefaultSasVerificationService) {
fun handleToDevice(toDevice: ToDeviceSyncResponse, initialSyncProgressService: DefaultInitialSyncProgressService? = null) {
@ -47,13 +47,13 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoManager:
Timber.e("## handleToDeviceEvent() : Warning: Unable to decrypt to-device event : " + event.content)
} else {
sasVerificationService.onToDeviceEvent(event)
cryptoManager.onToDeviceEvent(event)
cryptoService.onToDeviceEvent(event)
}
}
}
fun onSyncCompleted(syncResponse: SyncResponse) {
cryptoManager.onSyncCompleted(syncResponse)
cryptoService.onSyncCompleted(syncResponse)
}
@ -68,7 +68,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoManager:
if (event.getClearType() == EventType.ENCRYPTED) {
var result: MXEventDecryptionResult? = null
try {
result = cryptoManager.decryptEvent(event, timelineId ?: "")
result = cryptoService.decryptEvent(event, timelineId ?: "")
} catch (exception: MXCryptoError) {
event.mCryptoError = (exception as? MXCryptoError.Base)?.errorType //setCryptoError(exception.cryptoError)
}

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
@ -50,7 +50,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private val readReceiptHandler: ReadReceiptHandler,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler,
private val cryptoManager: CryptoManager,
private val cryptoService: DefaultCryptoService,
private val tokenStore: SyncTokenStore,
private val pushRuleService: DefaultPushRuleService,
private val processForPushTask: ProcessEventForPushTask,
@ -97,12 +97,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
handleJoinedRoom(realm, it.key, it.value, isInitialSync)
}
is HandlingStrategy.INVITED ->
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.4f) {
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) {
handleInvitedRoom(realm, it.key, it.value)
}
is HandlingStrategy.LEFT -> {
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.2f) {
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_left_rooms, 0.3f) {
handleLeftRoom(realm, it.key, it.value)
}
}
@ -125,8 +125,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
}
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
if (roomEntity.membership == Membership.INVITE) {
roomEntity.chunks.deleteAllFromRealm()
@ -135,13 +134,12 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
// State event
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
?: Int.MIN_VALUE
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt() ?: Int.MIN_VALUE
val untimelinedStateIndex = minStateIndex + 1
roomSync.state.events.forEach { event ->
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
// Give info to crypto module
cryptoManager.onStateEvent(roomId, event)
cryptoService.onStateEvent(roomId, event)
UserEntityFactory.createOrNull(event)?.also {
realm.insertOrUpdate(it)
}
@ -167,8 +165,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
roomSync:
InvitedRoomSync): RoomEntity {
Timber.v("Handle invited sync for room $roomId")
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
roomEntity.membership = Membership.INVITE
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
@ -181,8 +178,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
private fun handleLeftRoom(realm: Realm,
roomId: String,
roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
roomEntity.membership = Membership.LEAVE
roomEntity.chunks.deleteAllFromRealm()
@ -214,7 +210,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
event.eventId?.also { eventIds.add(it) }
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
// Give info to crypto module
cryptoManager.onLiveEvent(roomEntity.roomId, event)
cryptoService.onLiveEvent(roomEntity.roomId, event)
// Try to remove local echo
event.unsignedData?.transactionId?.also {
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)

View File

@ -21,7 +21,6 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomTagEntity
import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm
import java.util.*
import javax.inject.Inject
internal class RoomTagHandler @Inject constructor() {
@ -30,16 +29,8 @@ internal class RoomTagHandler @Inject constructor() {
if (content == null) {
return
}
val tags = ArrayList<RoomTagEntity>()
for (tagName in content.tags.keys) {
val params = content.tags[tagName]
val order = params?.get("order")
val tag = if (order is Double) {
RoomTagEntity(tagName, order)
} else {
RoomTagEntity(tagName, null)
}
tags.add(tag)
val tags = content.tags.entries.map { (tagName, params) ->
RoomTagEntity(tagName, params["order"] as? Double)
}
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId)

View File

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.sync
import arrow.core.Try
import im.vector.matrix.android.R
import im.vector.matrix.android.internal.crypto.CryptoManager
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.reportSubtask
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
@ -30,7 +30,7 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val groupSyncHandler: GroupSyncHandler,
private val cryptoSyncHandler: CryptoSyncHandler,
private val cryptoManager: CryptoManager,
private val cryptoService: DefaultCryptoService,
private val initialSyncProgressService: DefaultInitialSyncProgressService) {
fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try<SyncResponse> {
@ -40,12 +40,12 @@ internal class SyncResponseHandler @Inject constructor(private val roomSyncHandl
val reporter = initialSyncProgressService.takeIf { isInitialSync }
measureTimeMillis {
if (!cryptoManager.isStarted()) {
Timber.v("Should start cryptoManager")
cryptoManager.start(isInitialSync)
if (!cryptoService.isStarted()) {
Timber.v("Should start cryptoService")
cryptoService.start(isInitialSync)
}
}.also {
Timber.v("Finish handling start cryptoManager in $it ms")
Timber.v("Finish handling start cryptoService in $it ms")
}
val measure = measureTimeMillis {
// Handle the to device events before the room ones

View File

@ -93,8 +93,8 @@ open class SyncService : Service() {
}
fun doSync(once: Boolean = false) {
if (!networkConnectivityChecker.isConnected()) {
Timber.v("Sync is Paused. Waiting...")
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No internet access. Waiting...")
//TODO Retry in ?
timer.schedule(object : TimerTask() {
override fun run() {

View File

@ -50,6 +50,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val lock = Object()
private var cancelableTask: Cancelable? = null
private var isStarted = false
init {
updateStateTo(SyncState.IDLE)
}
@ -60,19 +62,18 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
fun restart() = synchronized(lock) {
if (state is SyncState.PAUSED) {
if (!isStarted) {
Timber.v("Resume sync...")
updateStateTo(SyncState.RUNNING(afterPause = true))
isStarted = true
lock.notify()
}
}
fun pause() = synchronized(lock) {
if (state is SyncState.RUNNING) {
if (isStarted) {
Timber.v("Pause sync...")
updateStateTo(SyncState.PAUSED)
isStarted = false
cancelableTask?.cancel()
lock.notify()
}
}
@ -87,19 +88,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
return liveState
}
override fun onConnect() {
Timber.v("Network is back")
synchronized(lock) {
lock.notify()
}
}
override fun run() {
Timber.v("Start syncing...")
isStarted = true
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.isConnected() || state == SyncState.PAUSED) {
Timber.v("No network or sync is Paused. Waiting...")
synchronized(lock) {
lock.wait()
}
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else {
if (state !is SyncState.RUNNING) {
@ -167,16 +180,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
private fun updateStateTo(newState: SyncState) {
Timber.v("Update state to $newState")
Timber.v("Update state from $state to $newState")
state = newState
liveState.postValue(newState)
}
override fun onConnect() {
synchronized(lock) {
lock.notify()
}
}
override fun onMoveToForeground() {
restart()

View File

@ -33,7 +33,7 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
private set
private
val listeners = ArrayList<Listener>()
val listeners = LinkedHashSet<Listener>()
fun register(listener: Listener) {
listeners.add(listener)

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.util
import android.content.Context
import androidx.work.WorkManager
import im.vector.matrix.android.api.util.Cancelable
import java.util.*
import java.util.UUID
internal class CancelableWork(private val context: Context,
private val workId: UUID) : Cancelable {

View File

@ -34,7 +34,7 @@ import java.security.*
import java.security.cert.CertificateException
import java.security.spec.AlgorithmParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.*
import java.util.Calendar
import java.util.zip.GZIPOutputStream
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec

View File

@ -22,7 +22,7 @@ import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import java.util.*
import java.util.TreeSet
/**
* Build canonical Json
@ -60,43 +60,33 @@ object JsonCanonicalizer {
when (any) {
is JSONArray -> {
// Canonicalize each element of the array
val result = StringBuilder("[")
for (i in 0 until any.length()) {
result.append(canonicalizeRecursive(any.get(i)))
if (i < any.length() - 1) {
result.append(",")
}
return (0 until any.length()).joinToString(separator = ",", prefix = "[", postfix = "]") {
canonicalizeRecursive(any.get(it))
}
result.append("]")
return result.toString()
}
is JSONObject -> {
// Sort the attributes by name, and the canonicalize each element of the JSONObject
val result = StringBuilder("{")
val attributes = TreeSet<String>()
for (entry in any.keys()) {
attributes.add(entry)
}
for (attribute in attributes.withIndex()) {
result.append("\"")
.append(attribute.value)
.append("\"")
.append(":")
.append(canonicalizeRecursive(any[attribute.value]))
return buildString {
append("{")
for ((index, value) in attributes.withIndex()) {
append("\"")
append(value)
append("\"")
append(":")
append(canonicalizeRecursive(any[value]))
if (attribute.index < attributes.size - 1) {
result.append(",")
if (index < attributes.size - 1) {
append(",")
}
}
append("}")
}
result.append("}")
return result.toString()
}
is String -> return JSONObject.quote(any)
else -> return any.toString()

View File

@ -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"

View File

@ -3,4 +3,7 @@
<string name="no_network_indicator">There is no network connection right now</string>
</resources>

View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
adb shell am start -a android.intent.action.VIEW -d "https://riot.im/config/config?hs_url=https%3A%2F%2Fmozilla-test.modular.im"

View File

@ -15,7 +15,7 @@ androidExtensions {
}
ext.versionMajor = 0
ext.versionMinor = 4
ext.versionMinor = 5
ext.versionPatch = 0
static def getGitTimestamp() {
@ -51,8 +51,15 @@ static def gitRevisionDate() {
}
static def gitBranchName() {
def cmd = "git name-rev --name-only HEAD"
return cmd.execute().text.trim()
def fromEnv = System.env.BUILDKITE_BRANCH as String ?: ""
if (!fromEnv.isEmpty()) {
return fromEnv
} else {
// Note: this command return "HEAD" on Buildkite, so use the system env 'BUILDKITE_BRANCH' content first
def cmd = "git rev-parse --abbrev-ref HEAD"
return cmd.execute().text.trim()
}
}
static def getVersionSuffix() {
@ -75,7 +82,7 @@ project.android.buildTypes.all { buildType ->
// 64 bits have greater value than 32 bits
ext.abiVersionCodes = ["armeabi-v7a": 1, "arm64-v8a": 2, "x86": 3, "x86_64": 4].withDefault { 0 }
def buildNumber = System.getenv("BUILDKITE_BUILD_NUMBER") as Integer ?: 0
def buildNumber = System.env.BUILDKITE_BUILD_NUMBER as Integer ?: 0
android {
compileSdkVersion 28
@ -273,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'

View File

@ -65,6 +65,19 @@
<activity android:name=".features.home.room.detail.RoomDetailActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" />
<activity android:name=".features.link.LinkHandlerActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="riot.im" />
<data android:pathPrefix="/config/" />
</intent-filter>
</activity>
<!-- Services -->

View File

@ -349,6 +349,11 @@ SOFTWARE.
<br/>
Copyright 2018 The diff-match-patch Authors. https://github.com/google/diff-match-patch
</li>
<li>
<b>EventBus</b>
<br/>
Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org)
</li>
</ul>

View File

@ -86,9 +86,12 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
vectorComponent = DaggerVectorComponent.factory().create(this)
vectorComponent.inject(this)
vectorUncaughtExceptionHandler.activate(this)
// Log
VectorFileLogger.init(this)
Timber.plant(Timber.DebugTree(), VectorFileLogger)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
Timber.plant(vectorComponent.vectorFileLogger())
if (BuildConfig.DEBUG) {
Stetho.initializeWithDefaults(this)
}

View File

@ -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)

View File

@ -41,6 +41,7 @@ import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Singleton
@ -101,6 +102,8 @@ interface VectorComponent {
fun vectorPreferences(): VectorPreferences
fun vectorFileLogger(): VectorFileLogger
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): VectorComponent

View File

@ -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

View File

@ -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
}

View File

@ -49,7 +49,7 @@ abstract class SimpleFragmentActivity : VectorBaseActivity() {
@CallSuper
override fun injectWith(injector: ScreenComponent) {
session = injector.session()
session = injector.activeSessionHolder().getActiveSession()
}
override fun initUiAndData() {

View File

@ -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))
}
}
}

View File

@ -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")

View File

@ -17,11 +17,8 @@
package im.vector.riotx.core.platform
import com.airbnb.mvrx.*
import im.vector.matrix.android.api.util.CancelableBag
import im.vector.riotx.BuildConfig
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.disposables.Disposable
abstract class VectorViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, false) {

View File

@ -0,0 +1,32 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.utils
import java.lang.ref.WeakReference
import kotlin.reflect.KProperty
fun <T> weak(value: T) = WeakReferenceDelegate(value)
class WeakReferenceDelegate<T>(value: T) {
private var weakReference: WeakReference<T> = WeakReference(value)
operator fun getValue(thisRef: Any, property: KProperty<*>): T? = weakReference.get()
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
weakReference = WeakReference(value)
}
}

View File

@ -19,12 +19,15 @@ package im.vector.riotx.features
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity
@ -57,6 +60,7 @@ class MainActivity : VectorBaseActivity() {
@Inject lateinit var matrix: Matrix
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
@ -69,42 +73,68 @@ class MainActivity : VectorBaseActivity() {
// Handle some wanted cleanup
if (clearCache || clearCredentials) {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(this@MainActivity).clearDiskCache()
// Also clear cache (Logs, etc...)
deleteAllFiles(this@MainActivity.cacheDir)
}
}
doCleanUp(clearCache, clearCredentials)
} else {
start()
}
}
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) {
when {
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
start()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
start()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
else -> start()
}
}
private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
Glide.get(this@MainActivity).clearMemory()
withContext(Dispatchers.IO) {
// On BG thread
Glide.get(this@MainActivity).clearDiskCache()
// Also clear cache (Logs, etc...)
deleteAllFiles(this@MainActivity.cacheDir)
}
}
start()
}
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) }
.setNegativeButton(R.string.cancel) { _, _ -> start() }
.setCancelable(false)
.show()
}
private fun start() {
val intent = if (sessionHolder.hasActiveSession()) {
HomeActivity.newIntent(this)
} else {
LoginActivity.newIntent(this)
LoginActivity.newIntent(this, null)
}
startActivity(intent)
finish()

View File

@ -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)
}
}

View File

@ -26,7 +26,6 @@ import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.airbnb.mvrx.viewModel
@ -36,12 +35,10 @@ import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.workers.signout.SignOutViewModel
@ -114,27 +111,27 @@ 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)
}
activeSessionHolder.getSafeActiveSession()?.getLiveStatus()?.observe(this, Observer { sprogress ->
Timber.e("${sprogress?.statusText?.let { getString(it) }} ${sprogress?.percentProgress}")
if (sprogress == null) {
activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
if (status == null) {
waiting_view.isVisible = false
} else {
Timber.e("${getString(status.statusText)} ${status.percentProgress}")
waiting_view.setOnClickListener {
//block interactions
}
waiting_view_status_horizontal_progress.apply {
isIndeterminate = false
max = 100
progress = sprogress.percentProgress
progress = status.percentProgress
isVisible = true
}
waiting_view_status_text.apply {
text = sprogress.statusText?.let { getString(it) }
text = getString(status.statusText)
isVisible = true
}
waiting_view.isVisible = true
@ -205,16 +202,11 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
if (!handled) {
super.onBackPressed()
}
super.onBackPressed()
}
}
companion object {
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.home
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.forEachIndexed
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
@ -30,7 +29,6 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.ToolbarConfigurable
@ -208,11 +206,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
unreadCounterBadgeViews[INDEX_CATCHUP].render(UnreadCounterBadgeView.State(it.notificationCountCatchup, it.notificationHighlightCatchup))
unreadCounterBadgeViews[INDEX_PEOPLE].render(UnreadCounterBadgeView.State(it.notificationCountPeople, it.notificationHighlightPeople))
unreadCounterBadgeViews[INDEX_ROOMS].render(UnreadCounterBadgeView.State(it.notificationCountRooms, it.notificationHighlightRooms))
syncProgressBar.visibility = when (it.syncState) {
is SyncState.RUNNING -> if (it.syncState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncProgressBarWrap.visibility = syncProgressBar.visibility
syncStateView.render(it.syncState)
}
companion object {

View File

@ -65,7 +65,6 @@ import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@ -77,8 +76,8 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
@ -251,11 +250,7 @@ class RoomDetailFragment :
}
roomDetailViewModel.selectSubscribe(RoomDetailViewState::syncState) { syncState ->
syncProgressBar.visibility = when (syncState) {
is SyncState.RUNNING -> if (syncState.afterPause) View.VISIBLE else View.GONE
else -> View.GONE
}
syncProgressBarWrap.visibility = syncProgressBar.visibility
syncStateView.render(syncState)
}
}

View File

@ -28,15 +28,7 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
@ -47,26 +39,12 @@ import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderAvatar
import im.vector.riotx.features.home.room.detail.timeline.item.BlankItem_
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem
import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.media.ImageContentRenderer
@ -103,32 +81,28 @@ class MessageItemFactory @Inject constructor(
val messageContent: MessageContent =
event.getLastMessageContent()
?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
?: //Malformed content, we should echo something on screen
return buildNotHandledMessageItem(stringProvider.getString(R.string.malformed_message),
informationData, highlight, callback)
if (messageContent.relatesTo?.type == RelationType.REPLACE
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) {
// This is an edit event, we should it when debugging as a notice event
// This is an edit event, we should display it when debugging as a notice event
return noticeItemFactory.create(event, highlight, callback)
}
// val all = event.root.toContent()
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
highlight,
callback)
is MessageTextContent -> buildTextMessageItem(messageContent,
informationData,
highlight,
callback)
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback)
is MessageTextContent -> buildTextMessageItem(messageContent, informationData, highlight, callback)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback)
else -> buildNotHandledMessageItem(messageContent, highlight)
else -> buildNotHandledMessageItem("${messageContent.type} message events are not yet handled",
informationData, highlight, callback)
}
}
@ -157,7 +131,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}
@ -182,7 +156,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
.clickListener(
DebouncedClickListener(View.OnClickListener { _ ->
@ -190,11 +164,17 @@ class MessageItemFactory @Inject constructor(
}))
}
private fun buildNotHandledMessageItem(messageContent: MessageContent, highlight: Boolean): DefaultItem? {
val text = "${messageContent.type} message events are not yet handled"
private fun buildNotHandledMessageItem(text: String,
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?): DefaultItem? {
return DefaultItem_()
.text(text)
.avatarRenderer(avatarRenderer)
.highlighted(highlight)
.informationData(informationData)
.baseCallback(callback)
.readReceiptsCallback(callback)
}
private fun buildImageMessageItem(messageContent: MessageImageContent,
@ -237,7 +217,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}
@ -250,7 +230,7 @@ class MessageItemFactory @Inject constructor(
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
@ -286,7 +266,7 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}
@ -326,7 +306,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}
@ -356,9 +336,9 @@ class MessageItemFactory @Inject constructor(
//nop
}
},
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable
}
@ -396,7 +376,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}
@ -433,7 +413,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}
@ -453,7 +433,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, null, view)
?: false
?: false
}
}

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.home.room.detail.timeline.format
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
@ -24,12 +23,14 @@ import im.vector.matrix.android.api.session.room.model.*
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import timber.log.Timber
import javax.inject.Inject
class NoticeEventFormatter @Inject constructor(private val stringProvider: StringProvider) {
class NoticeEventFormatter @Inject constructor(private val sessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider) {
fun format(timelineEvent: TimelineEvent): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
@ -74,10 +75,10 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
return if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name)
} else {
return if (content.name.isNullOrBlank()) {
stringProvider.getString(R.string.notice_room_name_removed, senderName)
} else {
stringProvider.getString(R.string.notice_room_name_changed, senderName, content.name)
}
}
@ -95,8 +96,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
}
private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?): CharSequence? {
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility
?: return null
val historyVisibility = event.getClearContent().toModel<RoomHistoryVisibilityContent>()?.historyVisibility ?: return null
val formattedVisibility = when (historyVisibility) {
RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared)
@ -138,7 +138,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
private fun buildProfileNotice(event: Event, senderName: String?, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
if (eventContent?.displayName != prevEventContent?.displayName) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_display_name_set, event.senderId, eventContent?.displayName)
@ -146,12 +146,12 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
stringProvider.getString(R.string.notice_display_name_removed, event.senderId, prevEventContent?.displayName)
else ->
stringProvider.getString(R.string.notice_display_name_changed_from,
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
event.senderId, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
if (eventContent?.avatarUrl != prevEventContent?.avatarUrl) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
@ -168,17 +168,18 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: ""
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId = ""
val selfUserId = sessionHolder.getSafeActiveSession()?.myUserId
when {
eventContent.thirdPartyInvite != null ->
eventContent.thirdPartyInvite != null -> {
val userWhoHasAccepted = eventContent.thirdPartyInvite?.signed?.mxid ?: event.stateKey
stringProvider.getString(R.string.notice_room_third_party_registered_invite,
targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.stateKey, selfUserId) ->
userWhoHasAccepted, eventContent.thirdPartyInvite?.displayName)
}
event.stateKey == selfUserId ->
stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.stateKey.isNullOrEmpty() ->
event.stateKey.isNullOrEmpty() ->
stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else ->
else ->
stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
@ -186,7 +187,7 @@ class NoticeEventFormatter @Inject constructor(private val stringProvider: Strin
stringProvider.getString(R.string.notice_room_join, senderDisplayName)
Membership.LEAVE == eventContent?.membership ->
// 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked
return if (TextUtils.equals(event.senderId, event.stateKey)) {
return if (event.senderId == event.stateKey) {
if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_reject, senderDisplayName)
} else {

View File

@ -250,7 +250,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
return true
}
return super.onBackPressed()
return false
}
// RoomSummaryController.Callback **************************************************************

View File

@ -0,0 +1,120 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.link
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AlertDialog
import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.login.LoginConfig
import timber.log.Timber
import javax.inject.Inject
/**
* Dummy activity used to dispatch the vector URL links.
*/
class LinkHandlerActivity : VectorBaseActivity() {
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun getLayoutRes() = R.layout.activity_progress
override fun initUiAndData() {
val uri = intent.data
if (uri == null) {
// Should not happen
Timber.w("Uri is null")
finish()
return
}
if (uri.path == PATH_CONFIG) {
if (sessionHolder.hasActiveSession()) {
displayAlreadyLoginPopup(uri)
} else {
// user is not yet logged in, this is the nominal case
startLoginActivity(uri)
}
} else {
// Other link are not yet handled, but should not comes here (manifest configuration error?)
Timber.w("Unable to handle this uir: $uri")
finish()
}
}
/**
* Start the login screen with identity server and home server pre-filled
*/
private fun startLoginActivity(uri: Uri) {
val intent = LoginActivity.newIntent(this, LoginConfig.parse(uri))
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
finish()
}
/**
* Propose to disconnect from a previous HS, when clicking on an auto config link
*/
private fun displayAlreadyLoginPopup(uri: Uri) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.error_user_already_logged_in)
.setCancelable(false)
.setPositiveButton(R.string.logout) { _, _ ->
sessionHolder.getSafeActiveSession()?.signOut(object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
displayError(failure)
}
override fun onSuccess(data: Unit) {
Timber.d("## displayAlreadyLoginPopup(): logout succeeded")
sessionHolder.clearActiveSession()
startLoginActivity(uri)
}
}) ?: finish()
}
.setNegativeButton(R.string.cancel) { _, _ -> finish() }
.show()
}
private fun displayError(failure: Throwable) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
}
companion object {
private const val PATH_CONFIG = "/config/config"
}
}

View File

@ -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()
}

View File

@ -18,150 +18,80 @@ package im.vector.riotx.features.login
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.core.view.isVisible
import arrow.core.Try
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.session.Session
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.extensions.setTextWithColoredPart
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.addFragmentToBackstack
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.activity_login.*
import javax.inject.Inject
class LoginActivity : VectorBaseActivity() {
@Inject lateinit var authenticator: Authenticator
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var pushRuleTriggerListener: PushRuleTriggerListener
// Supported navigation actions for this Activity
sealed class Navigation {
object OpenSsoLoginFallback : Navigation()
object GoBack : Navigation()
}
private val loginViewModel: LoginViewModel by viewModel()
@Inject lateinit var loginViewModelFactory: LoginViewModel.Factory
private var passwordShown = false
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
setupNotice()
setupAuthButton()
setupPasswordReveal()
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(this))
}
override fun getLayoutRes() = R.layout.activity_simple
private fun setupNotice() {
riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part)
override fun initUiAndData() {
if (isFirstCreation()) {
addFragment(LoginFragment(), R.id.simpleFragmentContainer)
}
riotx_no_registration_notice.setOnClickListener {
openUrlInExternalBrowser(this@LoginActivity, "https://about.riot.im/downloads")
// Get config extra
val loginConfig = intent.getParcelableExtra<LoginConfig?>(EXTRA_CONFIG)
if (loginConfig != null && isFirstCreation()) {
loginViewModel.handle(LoginActions.InitWith(loginConfig))
}
loginViewModel.navigationLiveData.observeEvent(this) {
when (it) {
is Navigation.OpenSsoLoginFallback -> addFragmentToBackstack(LoginSsoFallbackFragment(), R.id.simpleFragmentContainer)
is Navigation.GoBack -> supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}
loginViewModel.selectSubscribe(this, LoginViewState::asyncLoginAction) {
if (it is Success) {
val intent = HomeActivity.newIntent(this)
startActivity(intent)
finish()
}
}
}
override fun onResume() {
super.onResume()
showDisclaimerDialog(this)
}
private fun authenticate() {
passwordShown = false
renderPasswordField()
val login = loginField.text?.trim().toString()
val password = passwordField.text?.trim().toString()
buildHomeServerConnectionConfig().fold(
{ Toast.makeText(this@LoginActivity, "Authenticate failure: $it", Toast.LENGTH_LONG).show() },
{ authenticateWith(it, login, password) }
)
}
private fun authenticateWith(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String) {
progressBar.isVisible = true
touchArea.isVisible = true
authenticator.authenticate(homeServerConnectionConfig, login, password, object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
activeSessionHolder.setActiveSession(data)
data.configureAndStart(pushRuleTriggerListener)
goToHome()
}
override fun onFailure(failure: Throwable) {
progressBar.isVisible = false
touchArea.isVisible = false
Toast.makeText(this@LoginActivity, "Authenticate failure: $failure", Toast.LENGTH_LONG).show()
}
})
}
private fun buildHomeServerConnectionConfig(): Try<HomeServerConnectionConfig> {
return Try {
val homeServerUri = homeServerField.text?.trim().toString()
HomeServerConnectionConfig.Builder()
.withHomeServerUri(homeServerUri)
.build()
}
}
private fun setupAuthButton() {
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
}
)
.subscribeBy { authenticateButton.isEnabled = it }
.disposeOnDestroy()
authenticateButton.setOnClickListener { authenticate() }
}
private fun setupPasswordReveal() {
passwordShown = false
passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
passwordField.showPassword(passwordShown)
passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
private fun goToHome() {
val intent = HomeActivity.newIntent(this)
startActivity(intent)
finish()
}
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, LoginActivity::class.java)
private const val EXTRA_CONFIG = "EXTRA_CONFIG"
fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
return Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_CONFIG, loginConfig)
}
}
}

View File

@ -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")
)
}
}
}

View File

@ -0,0 +1,208 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.airbnb.mvrx.*
import com.jakewharton.rxbinding3.view.focusChanges
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.setTextWithColoredPart
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.features.homeserver.ServerUrlsRepository
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import kotlinx.android.synthetic.main.fragment_login.*
import javax.inject.Inject
/**
* What can be improved:
* - When filtering more (when entering new chars), we could filter on result we already have, during the new server request, to avoid empty screen effect
*/
class LoginFragment : VectorBaseFragment() {
private val viewModel: LoginViewModel by activityViewModel()
private var passwordShown = false
@Inject lateinit var errorFormatter: ErrorFormatter
override fun getLayoutResId() = R.layout.fragment_login
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupNotice()
setupAuthButton()
setupPasswordReveal()
homeServerField.focusChanges()
.subscribe {
if (!it) {
// TODO Also when clicking on button?
viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString()))
}
}
.disposeOnDestroy()
val initHsUrl = viewModel.getInitialHomeServerUrl()
if (initHsUrl != null) {
homeServerField.setText(initHsUrl)
} else {
homeServerField.setText(ServerUrlsRepository.getDefaultHomeServerUrl(requireContext()))
}
viewModel.handle(LoginActions.UpdateHomeServer(homeServerField.text.toString()))
}
private fun setupNotice() {
riotx_no_registration_notice.setTextWithColoredPart(R.string.riotx_no_registration_notice, R.string.riotx_no_registration_notice_colored_part)
riotx_no_registration_notice.setOnClickListener {
openUrlInExternalBrowser(requireActivity(), "https://about.riot.im/downloads")
}
}
private fun authenticate() {
val login = loginField.text?.trim().toString()
val password = passwordField.text?.trim().toString()
viewModel.handle(LoginActions.Login(login, password))
}
private fun setupAuthButton() {
Observable
.combineLatest(
loginField.textChanges().map { it.trim().isNotEmpty() },
passwordField.textChanges().map { it.trim().isNotEmpty() },
homeServerField.textChanges().map { it.trim().isNotEmpty() },
Function3<Boolean, Boolean, Boolean, Boolean> { isLoginNotEmpty, isPasswordNotEmpty, isHomeServerNotEmpty ->
isLoginNotEmpty && isPasswordNotEmpty && isHomeServerNotEmpty
}
)
.subscribeBy { authenticateButton.isEnabled = it }
.disposeOnDestroy()
authenticateButton.setOnClickListener { authenticate() }
authenticateButtonSso.setOnClickListener { openSso() }
}
private fun openSso() {
viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.OpenSsoLoginFallback))
}
private fun setupPasswordReveal() {
passwordShown = false
passwordReveal.setOnClickListener {
passwordShown = !passwordShown
renderPasswordField()
}
renderPasswordField()
}
private fun renderPasswordField() {
passwordField.showPassword(passwordShown)
passwordReveal.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}
override fun invalidate() = withState(viewModel) { state ->
TransitionManager.beginDelayedTransition(login_fragment)
when (state.asyncHomeServerLoginFlowRequest) {
is Incomplete -> {
progressBar.isVisible = true
touchArea.isVisible = true
loginField.isVisible = false
passwordContainer.isVisible = false
authenticateButton.isVisible = false
authenticateButtonSso.isVisible = false
passwordShown = false
renderPasswordField()
}
is Fail -> {
progressBar.isVisible = false
touchArea.isVisible = false
loginField.isVisible = false
passwordContainer.isVisible = false
authenticateButton.isVisible = false
authenticateButtonSso.isVisible = false
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncHomeServerLoginFlowRequest.error}", Toast.LENGTH_LONG).show()
}
is Success -> {
progressBar.isVisible = false
touchArea.isVisible = false
when (state.asyncHomeServerLoginFlowRequest()) {
LoginMode.Password -> {
loginField.isVisible = true
passwordContainer.isVisible = true
authenticateButton.isVisible = true
authenticateButtonSso.isVisible = false
}
LoginMode.Sso -> {
loginField.isVisible = false
passwordContainer.isVisible = false
authenticateButton.isVisible = false
authenticateButtonSso.isVisible = true
}
LoginMode.Unsupported -> {
loginField.isVisible = false
passwordContainer.isVisible = false
authenticateButton.isVisible = false
authenticateButtonSso.isVisible = false
Toast.makeText(requireActivity(), "None of the homeserver login mode is supported by RiotX", Toast.LENGTH_LONG).show()
}
}
}
}
when (state.asyncLoginAction) {
is Loading -> {
progressBar.isVisible = true
touchArea.isVisible = true
passwordShown = false
renderPasswordField()
}
is Fail -> {
progressBar.isVisible = false
touchArea.isVisible = false
Toast.makeText(requireActivity(), "Authenticate failure: ${state.asyncLoginAction.error}", Toast.LENGTH_LONG).show()
}
// Success is handled by the LoginActivity
is Success -> Unit
}
}
}

View File

@ -0,0 +1,304 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.graphics.Bitmap
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.webkit.SslErrorHandler
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.platform.OnBackPressed
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_login_sso_fallback.*
import timber.log.Timber
import java.net.URLDecoder
/**
* Only login is supported for the moment
*/
class LoginSsoFallbackFragment : VectorBaseFragment(), OnBackPressed {
private val viewModel: LoginViewModel by activityViewModel()
var homeServerUrl: String = ""
enum class Mode {
MODE_LOGIN,
// Not supported in RiotX for the moment
MODE_REGISTER
}
// Mode (MODE_LOGIN or MODE_REGISTER)
private var mMode = Mode.MODE_LOGIN
override fun getLayoutResId() = R.layout.fragment_login_sso_fallback
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(login_sso_fallback_toolbar)
login_sso_fallback_toolbar.title = getString(R.string.login)
setupWebview()
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebview() {
login_sso_fallback_webview.settings.javaScriptEnabled = true
// Due to https://developers.googleblog.com/2016/08/modernizing-oauth-interactions-in-native-apps.html, we hack
// the user agent to bypass the limitation of Google, as a quick fix (a proper solution will be to use the SSO SDK)
login_sso_fallback_webview.settings.userAgentString = "Mozilla/5.0 Google"
homeServerUrl = viewModel.getHomeServerUrl()
if (!homeServerUrl.endsWith("/")) {
homeServerUrl += "/"
}
// AppRTC requires third party cookies to work
val cookieManager = android.webkit.CookieManager.getInstance()
// clear the cookies must be cleared
if (cookieManager == null) {
launchWebView()
} else {
if (!cookieManager.hasCookies()) {
launchWebView()
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
try {
cookieManager.removeAllCookie()
} catch (e: Exception) {
Timber.e(e, " cookieManager.removeAllCookie() fails")
}
launchWebView()
} else {
try {
cookieManager.removeAllCookies { launchWebView() }
} catch (e: Exception) {
Timber.e(e, " cookieManager.removeAllCookie() fails")
launchWebView()
}
}
}
}
private fun launchWebView() {
if (mMode == Mode.MODE_LOGIN) {
login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/login/")
} else {
// MODE_REGISTER
login_sso_fallback_webview.loadUrl(homeServerUrl + "_matrix/static/client/register/")
}
login_sso_fallback_webview.webViewClient = object : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler,
error: SslError) {
AlertDialog.Builder(requireActivity())
.setMessage(R.string.ssl_could_not_verify)
.setPositiveButton(R.string.ssl_trust) { dialog, which -> handler.proceed() }
.setNegativeButton(R.string.ssl_do_not_trust) { dialog, which -> handler.cancel() }
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
handler.cancel()
dialog.dismiss()
return@OnKeyListener true
}
false
})
.show()
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
super.onReceivedError(view, errorCode, description, failingUrl)
// on error case, close this fragment
viewModel.handle(LoginActions.NavigateTo(LoginActivity.Navigation.GoBack))
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
login_sso_fallback_toolbar.subtitle = url
}
override fun onPageFinished(view: WebView, url: String) {
// avoid infinite onPageFinished call
if (url.startsWith("http")) {
// Generic method to make a bridge between JS and the UIWebView
val mxcJavascriptSendObjectMessage = "javascript:window.sendObjectMessage = function(parameters) {" +
" var iframe = document.createElement('iframe');" +
" iframe.setAttribute('src', 'js:' + JSON.stringify(parameters));" +
" document.documentElement.appendChild(iframe);" +
" iframe.parentNode.removeChild(iframe); iframe = null;" +
" };"
view.loadUrl(mxcJavascriptSendObjectMessage)
if (mMode == Mode.MODE_LOGIN) {
// The function the fallback page calls when the login is complete
val mxcJavascriptOnRegistered = "javascript:window.matrixLogin.onLogin = function(response) {" +
" sendObjectMessage({ 'action': 'onLogin', 'credentials': response });" +
" };"
view.loadUrl(mxcJavascriptOnRegistered)
} else {
// MODE_REGISTER
// The function the fallback page calls when the registration is complete
val mxcJavascriptOnRegistered = "javascript:window.matrixRegistration.onRegistered" +
" = function(homeserverUrl, userId, accessToken) {" +
" sendObjectMessage({ 'action': 'onRegistered'," +
" 'homeServer': homeserverUrl," +
" 'userId': userId," +
" 'accessToken': accessToken });" +
" };"
view.loadUrl(mxcJavascriptOnRegistered)
}
}
}
/**
* Example of (formatted) url for MODE_LOGIN:
*
* <pre>
* js:{
* "action":"onLogin",
* "credentials":{
* "user_id":"@user:matrix.org",
* "access_token":"[ACCESS_TOKEN]",
* "home_server":"matrix.org",
* "device_id":"[DEVICE_ID]",
* "well_known":{
* "m.homeserver":{
* "base_url":"https://matrix.org/"
* }
* }
* }
* }
* </pre>
* @param view
* @param url
* @return
*/
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
if (null != url && url.startsWith("js:")) {
var json = url.substring(3)
var parameters: Map<String, Any>? = null
try {
// URL decode
json = URLDecoder.decode(json, "UTF-8")
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
parameters = adapter.fromJson(json) as Map<String, Any>?
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : fromJson failed")
}
// succeeds to parse parameters
if (parameters != null) {
val action = parameters["action"] as String
if (mMode == Mode.MODE_LOGIN) {
try {
if (action == "onLogin") {
val credentials = parameters["credentials"] as Map<String, String>
val userId = credentials["user_id"]
val accessToken = credentials["access_token"]
val homeServer = credentials["home_server"]
val deviceId = credentials["device_id"]
// check if the parameters are defined
if (null != homeServer && null != userId && null != accessToken) {
val credentials = Credentials(
userId = userId,
accessToken = accessToken,
homeServer = homeServer,
deviceId = deviceId,
refreshToken = null
)
viewModel.handle(LoginActions.SsoLoginSuccess(credentials))
}
}
} catch (e: Exception) {
Timber.e(e, "## shouldOverrideUrlLoading() : failed")
}
} else {
// MODE_REGISTER
// check the required parameters
if (action == "onRegistered") {
// TODO The keys are very strange, this code comes from Riot-Android...
if (parameters.containsKey("homeServer")
&& parameters.containsKey("userId")
&& parameters.containsKey("accessToken")) {
// We cannot parse Credentials here because of https://github.com/matrix-org/synapse/issues/4756
// Build on object manually
val credentials = Credentials(
userId = parameters["userId"] as String,
accessToken = parameters["accessToken"] as String,
homeServer = parameters["homeServer"] as String,
// TODO We need deviceId on RiotX...
deviceId = "TODO",
refreshToken = null
)
viewModel.handle(LoginActions.SsoLoginSuccess(credentials))
}
}
}
}
return true
}
return super.shouldOverrideUrlLoading(view, url)
}
}
}
override fun onBackPressed(): Boolean {
return if (login_sso_fallback_webview.canGoBack()) {
login_sso_fallback_webview.goBack()
true
} else {
false
}
}
}

View File

@ -0,0 +1,211 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Try
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import timber.log.Timber
class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginViewState,
private val authenticator: Authenticator,
private val activeSessionHolder: ActiveSessionHolder,
private val pushRuleTriggerListener: PushRuleTriggerListener)
: VectorViewModel<LoginViewState>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: LoginViewState): LoginViewModel
}
companion object : MvRxViewModelFactory<LoginViewModel, LoginViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: LoginViewState): LoginViewModel? {
val activity: LoginActivity = (viewModelContext as ActivityViewModelContext).activity()
return activity.loginViewModelFactory.create(state)
}
}
private var loginConfig: LoginConfig? = null
private val _navigationLiveData = MutableLiveData<LiveEvent<LoginActivity.Navigation>>()
val navigationLiveData: LiveData<LiveEvent<LoginActivity.Navigation>>
get() = _navigationLiveData
private var homeServerConnectionConfig: HomeServerConnectionConfig? = null
private var currentTask: Cancelable? = null
fun handle(action: LoginActions) {
when (action) {
is LoginActions.InitWith -> handleInitWith(action)
is LoginActions.UpdateHomeServer -> handleUpdateHomeserver(action)
is LoginActions.Login -> handleLogin(action)
is LoginActions.SsoLoginSuccess -> handleSsoLoginSuccess(action)
is LoginActions.NavigateTo -> handleNavigation(action)
}
}
private fun handleInitWith(action: LoginActions.InitWith) {
loginConfig = action.loginConfig
}
private fun handleLogin(action: LoginActions.Login) {
val homeServerConnectionConfigFinal = homeServerConnectionConfig
if (homeServerConnectionConfigFinal == null) {
setState {
copy(
asyncLoginAction = Fail(Throwable("Bad configuration"))
)
}
} else {
setState {
copy(
asyncLoginAction = Loading()
)
}
authenticator.authenticate(homeServerConnectionConfigFinal, action.login, action.password, object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
onSessionCreated(data)
}
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncLoginAction = Fail(failure)
)
}
}
})
}
}
private fun onSessionCreated(session: Session) {
activeSessionHolder.setActiveSession(session)
session.configureAndStart(pushRuleTriggerListener)
setState {
copy(
asyncLoginAction = Success(Unit)
)
}
}
private fun handleSsoLoginSuccess(action: LoginActions.SsoLoginSuccess) {
val homeServerConnectionConfigFinal = homeServerConnectionConfig
if (homeServerConnectionConfigFinal == null) {
// Should not happen
Timber.w("homeServerConnectionConfig is null")
} else {
val session = authenticator.createSessionFromSso(action.credentials, homeServerConnectionConfigFinal)
onSessionCreated(session)
}
}
private fun handleUpdateHomeserver(action: LoginActions.UpdateHomeServer) {
currentTask?.cancel()
Try {
val homeServerUri = action.homeServerUrl
homeServerConnectionConfig = HomeServerConnectionConfig.Builder()
.withHomeServerUri(homeServerUri)
.build()
}
val homeServerConnectionConfigFinal = homeServerConnectionConfig
if (homeServerConnectionConfigFinal == null) {
// This is invalid
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(Throwable("Bad format"))
)
}
} else {
setState {
copy(
asyncHomeServerLoginFlowRequest = Loading()
)
}
currentTask = authenticator.getLoginFlow(homeServerConnectionConfigFinal, object : MatrixCallback<LoginFlowResponse> {
override fun onFailure(failure: Throwable) {
setState {
copy(
asyncHomeServerLoginFlowRequest = Fail(failure)
)
}
}
override fun onSuccess(data: LoginFlowResponse) {
val loginMode = when {
// SSO login is taken first
data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_SSO } -> LoginMode.Sso
data.flows.any { it.type == InteractiveAuthenticationFlow.TYPE_LOGIN_PASSWORD } -> LoginMode.Password
else -> LoginMode.Unsupported
}
setState {
copy(
asyncHomeServerLoginFlowRequest = Success(loginMode)
)
}
}
})
}
}
private fun handleNavigation(action: LoginActions.NavigateTo) {
_navigationLiveData.postValue(LiveEvent(action.target))
}
override fun onCleared() {
super.onCleared()
currentTask?.cancel()
}
fun getInitialHomeServerUrl(): String? {
return loginConfig?.homeServerUrl
}
fun getHomeServerUrl(): String {
return homeServerConnectionConfig?.homeServerUri?.toString() ?: ""
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.login
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
data class LoginViewState(
val asyncLoginAction: Async<Unit> = Uninitialized,
val asyncHomeServerLoginFlowRequest: Async<LoginMode> = Uninitialized
) : MvRxState
enum class LoginMode {
Password,
Sso,
Unsupported
}

View File

@ -24,7 +24,6 @@ import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.util.SecretStoringUtils
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
@ -448,7 +447,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (!file.exists()) file.createNewFile()
FileOutputStream(file).use {
SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context)
activeSessionHolder.getSafeActiveSession()?.securelyStoreObject(eventList, KEY_ALIAS_SECRET_STORAGE, it)
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to save cached notification info")
@ -461,7 +460,7 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) {
FileInputStream(file).use {
val events: ArrayList<NotifiableEvent>? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context)
val events: ArrayList<NotifiableEvent>? = activeSessionHolder.getSafeActiveSession()?.loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
if (events != null) {
return ArrayList(events.mapNotNull { it as? NotifiableEvent })
}
@ -485,6 +484,9 @@ class NotificationDrawerManager @Inject constructor(private val context: Context
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
private const val ROOM_EVENT_NOTIFICATION_ID = 2
// TODO Mutliaccount
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
}
}

View File

@ -52,7 +52,8 @@ import javax.inject.Singleton
*/
@Singleton
class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val versionProvider: VersionProvider) {
private val versionProvider: VersionProvider,
private val vectorFileLogger : VectorFileLogger) {
var inMultiWindowMode = false
companion object {
@ -162,7 +163,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
val gzippedFiles = ArrayList<File>()
if (withDevicesLogs) {
val files = VectorFileLogger.getLogFiles()
val files = vectorFileLogger.getLogFiles()
for (f in files) {
if (!mIsCancelled) {
@ -348,20 +349,20 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
} else if (null == response || null == response.body()) {
serverError = "Failed with error $responseCode"
} else {
var `is`: InputStream? = null
var inputStream: InputStream? = null
try {
`is` = response.body()!!.byteStream()
inputStream = response.body()!!.byteStream()
if (null != `is`) {
var ch = `is`.read()
if (null != inputStream) {
var ch = inputStream.read()
val b = StringBuilder()
while (ch != -1) {
b.append(ch.toChar())
ch = `is`.read()
ch = inputStream.read()
}
serverError = b.toString()
`is`.close()
inputStream.close()
// check if the error message
try {
@ -380,7 +381,7 @@ class BugReporter @Inject constructor(private val activeSessionHolder: ActiveSes
Timber.e(e, "## sendBugReport() : failed to parse error " + e.message)
} finally {
try {
`is`?.close()
inputStream?.close()
} catch (e: Exception) {
Timber.e(e, "## sendBugReport() : failed to close the error stream " + e.message)
}

View File

@ -17,43 +17,74 @@
package im.vector.riotx.features.rageshake
import android.content.Context
import android.text.TextUtils
import android.util.Log
import im.vector.riotx.features.settings.VectorPreferences
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.*
import java.util.logging.Formatter
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.collections.ArrayList
object VectorFileLogger : Timber.DebugTree() {
private const val LOG_SIZE_BYTES = 20 * 1024 * 1024 // 20MB
private const val LOG_SIZE_BYTES = 50 * 1024 * 1024 // 50MB
private const val LOG_ROTATION_COUNT = 3
@Singleton
class VectorFileLogger @Inject constructor(val context: Context, private val vectorPreferences: VectorPreferences) : Timber.DebugTree() {
// relatively large rotation count because closing > opening the app rotates the log (!)
private const val LOG_ROTATION_COUNT = 15
private val sLogger = Logger.getLogger("im.vector.riotx")
private lateinit var sFileHandler: FileHandler
private lateinit var sCacheDirectory: File
private var sFileName = "riotx"
private var sFileHandler: FileHandler? = null
private var sCacheDirectory: File? = null
private var sFileName = "riotxlogs"
fun init(context: Context) {
private val prioPrefixes = mapOf(
Log.VERBOSE to "V/ ",
Log.DEBUG to "D/ ",
Log.INFO to "I/ ",
Log.WARN to "W/ ",
Log.ERROR to "E/ ",
Log.ASSERT to "WTF/ "
)
init {
val logsDirectoryFile = context.cacheDir.absolutePath + "/logs"
setLogDirectory(File(logsDirectoryFile))
init("RiotXLog")
try {
if (sCacheDirectory != null) {
sFileHandler = FileHandler(sCacheDirectory!!.absolutePath + "/" + sFileName + ".%g.txt", LOG_SIZE_BYTES, LOG_ROTATION_COUNT)
sFileHandler?.formatter = LogFormatter()
sLogger.useParentHandlers = false
sLogger.level = Level.ALL
sLogger.addHandler(sFileHandler)
}
} catch (e: Throwable) {
Timber.e(e, "Failed to initialize FileLogger")
}
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
if (sFileHandler == null) return
if (skipLog(priority)) return
if (t != null) {
logToFile(t)
}
logToFile(prioPrefixes[priority] ?: "$priority ", tag ?: "Tag", message)
}
logToFile("$priority ", tag ?: "Tag", message)
private fun skipLog(priority: Int): Boolean {
return if (vectorPreferences.labAllowedExtendedLogging()) {
false
} else {
priority < Log.ERROR
}
}
/**
@ -68,24 +99,6 @@ object VectorFileLogger : Timber.DebugTree() {
sCacheDirectory = cacheDir
}
/**
* Initialises the logger. Should be called AFTER [Log.setLogDirectory].
*
* @param fileName the base file name
*/
private fun init(fileName: String) {
try {
if (!TextUtils.isEmpty(fileName)) {
sFileName = fileName
}
sFileHandler = FileHandler(sCacheDirectory.absolutePath + "/" + sFileName + ".%g.txt", LOG_SIZE_BYTES, LOG_ROTATION_COUNT)
sFileHandler.formatter = LogFormatter()
sLogger.useParentHandlers = false
sLogger.level = Level.ALL
sLogger.addHandler(sFileHandler)
} catch (e: IOException) {
}
}
/**
* Adds our own log files to the provided list of files.
@ -99,8 +112,8 @@ object VectorFileLogger : Timber.DebugTree() {
try {
// reported by GA
if (null != sFileHandler) {
sFileHandler.flush()
val absPath = sCacheDirectory.absolutePath
sFileHandler!!.flush()
val absPath = sCacheDirectory?.absolutePath ?: return emptyList()
for (i in 0..LOG_ROTATION_COUNT) {
val filepath = "$absPath/$sFileName.$i.txt"
@ -111,7 +124,7 @@ object VectorFileLogger : Timber.DebugTree() {
}
}
} catch (e: Exception) {
Timber.e(e, "## addLogFiles() failed : " + e.message)
Timber.e(e, "## addLogFiles() failed : %s", e.message)
}
return files

View File

@ -149,6 +149,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY"
private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY"
const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
@ -257,6 +259,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true)
}
fun labAllowedExtendedLogging(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
}
/**
* Tells if we have already asked the user to disable battery optimisations on android >= M devices.
*

View File

@ -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())
}

View File

@ -31,7 +31,7 @@ class PushRulesViewModel(initialState: PushRulesViewState) : VectorViewModel<Pus
companion object : MvRxViewModelFactory<PushRulesViewModel, PushRulesViewState> {
override fun initialState(viewModelContext: ViewModelContext): PushRulesViewState? {
val session = (viewModelContext.activity as HasScreenInjector).injector().session()
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
val rules = session.getPushRules()
return PushRulesViewState(rules)
}

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