mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-28 16:59:01 +03:00
Merge branch 'develop' into feature/save_media_to_gallery
This commit is contained in:
commit
754f220596
85 changed files with 1656 additions and 326 deletions
CHANGES.md
matrix-sdk-android/src
androidTest/java/im/vector/matrix/android/internal/crypto/ssss
main/java/im/vector/matrix/android
api
internal
crypto
database
session
DefaultSession.ktSessionComponent.kt
account
content
group
homeserver
CapabilitiesAPI.ktDefaultGetHomeServerCapabilitiesTask.ktGetCapabilitiesResult.ktGetUploadCapabilitiesResult.kt
pushers
room
relation
send
sync/job
worker
tools/check
vector/src/main
java/im/vector/riotx
core
di
error
platform
ui/views
features
crypto
keysbackup/setup
recover
home
login
media
navigation
settings
res
|
@ -2,6 +2,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
|
- Change password (#528)
|
||||||
- Cross-Signing | Support SSSS secret sharing (#944)
|
- Cross-Signing | Support SSSS secret sharing (#944)
|
||||||
- Cross-Signing | Verify new session from existing session (#1134)
|
- Cross-Signing | Verify new session from existing session (#1134)
|
||||||
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||||
|
@ -20,6 +21,7 @@ Improvements 🙌:
|
||||||
- Cross-Sign | QR code scan confirmation screens design update (#1187)
|
- Cross-Sign | QR code scan confirmation screens design update (#1187)
|
||||||
- Emoji Verification | It's not the same butterfly! (#1220)
|
- Emoji Verification | It's not the same butterfly! (#1220)
|
||||||
- Cross-Signing | Composer decoration: shields (#1077)
|
- Cross-Signing | Composer decoration: shields (#1077)
|
||||||
|
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix summary notification staying after "mark as read"
|
- Fix summary notification staying after "mark as read"
|
||||||
|
@ -32,6 +34,7 @@ Bugfix 🐛:
|
||||||
- Fix crash when trying to download file without internet connection (#1229)
|
- Fix crash when trying to download file without internet connection (#1229)
|
||||||
- Local echo are not updated in timeline (for failed & encrypted states)
|
- Local echo are not updated in timeline (for failed & encrypted states)
|
||||||
- Render image event even if thumbnail_info does not have mimetype defined (#1209)
|
- Render image event even if thumbnail_info does not have mimetype defined (#1209)
|
||||||
|
- Fix issue with media path (#1227)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
@ -43,6 +46,7 @@ Build 🧱:
|
||||||
- Compile with Android SDK 29 (Android Q)
|
- Compile with Android SDK 29 (Android Q)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
|
- Add a setting to prevent screenshots of the application, disabled by default (#1027)
|
||||||
- Increase File Logger capacities ( + use dev log preferences)
|
- Increase File Logger capacities ( + use dev log preferences)
|
||||||
|
|
||||||
Changes in RiotX 0.18.1 (2020-03-17)
|
Changes in RiotX 0.18.1 (2020-03-17)
|
||||||
|
|
|
@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest {
|
||||||
val TEST_KEY_ID = "my.test.Key"
|
val TEST_KEY_ID = "my.test.Key"
|
||||||
|
|
||||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert Account data is updated
|
// Assert Account data is updated
|
||||||
|
@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest {
|
||||||
val TEST_KEY_ID = "my.test.Key"
|
val TEST_KEY_ID = "my.test.Key"
|
||||||
|
|
||||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
|
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
|
||||||
|
@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest {
|
||||||
val quadS = session.sharedSecretStorageService
|
val quadS = session.sharedSecretStorageService
|
||||||
|
|
||||||
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
|
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||||
quadS.generateKey(keyId, keyId, emptyKeySigner, it)
|
quadS.generateKey(keyId, null, keyId, emptyKeySigner, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||||
|
|
|
@ -31,3 +31,9 @@ fun Throwable.shouldBeRetried(): Boolean {
|
||||||
return this is Failure.NetworkConnection
|
return this is Failure.NetworkConnection
|
||||||
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
|| (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Throwable.isInvalidPassword(): Boolean {
|
||||||
|
return this is Failure.ServerError
|
||||||
|
&& error.code == MatrixError.M_FORBIDDEN
|
||||||
|
&& error.message == "Invalid password"
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||||
import im.vector.matrix.android.api.failure.GlobalError
|
import im.vector.matrix.android.api.failure.GlobalError
|
||||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||||
|
import im.vector.matrix.android.api.session.account.AccountService
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||||
import im.vector.matrix.android.api.session.cache.CacheService
|
import im.vector.matrix.android.api.session.cache.CacheService
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
|
@ -59,7 +60,8 @@ interface Session :
|
||||||
InitialSyncProgressService,
|
InitialSyncProgressService,
|
||||||
HomeServerCapabilitiesService,
|
HomeServerCapabilitiesService,
|
||||||
SecureStorageService,
|
SecureStorageService,
|
||||||
AccountDataService {
|
AccountDataService,
|
||||||
|
AccountService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The params associated to the session
|
* The params associated to the session
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.account
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This interface defines methods to manage the account. It's implemented at the session level.
|
||||||
|
*/
|
||||||
|
interface AccountService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the homeserver to change the password.
|
||||||
|
* @param password Current password.
|
||||||
|
* @param newPassword New password
|
||||||
|
*/
|
||||||
|
fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.account.model
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to pass request parameters to update the password.
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class ChangePasswordParams(
|
||||||
|
@Json(name = "auth")
|
||||||
|
val auth: UserPasswordAuth? = null,
|
||||||
|
|
||||||
|
@Json(name = "new_password")
|
||||||
|
val newPassword: String? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams {
|
||||||
|
return ChangePasswordParams(
|
||||||
|
auth = UserPasswordAuth(user = userId, password = oldPassword),
|
||||||
|
newPassword = newPassword
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,11 @@ interface ContentUrlResolver {
|
||||||
SCALE("scale")
|
SCALE("scale")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL to use to upload content
|
||||||
|
*/
|
||||||
|
val uploadUrl: String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the actual URL for accessing the full-size image of a Matrix media content URI.
|
* Get the actual URL for accessing the full-size image of a Matrix media content URI.
|
||||||
*
|
*
|
||||||
|
|
|
@ -217,4 +217,6 @@ interface KeysBackupService {
|
||||||
// For gossiping
|
// For gossiping
|
||||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||||
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||||
|
|
||||||
|
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
package im.vector.matrix.android.api.session.homeserver
|
package im.vector.matrix.android.api.session.homeserver
|
||||||
|
|
||||||
data class HomeServerCapabilities(
|
data class HomeServerCapabilities(
|
||||||
|
/**
|
||||||
|
* True if it is possible to change the password of the account.
|
||||||
|
*/
|
||||||
|
val canChangePassword: Boolean = true,
|
||||||
/**
|
/**
|
||||||
* Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet
|
* Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -29,7 +29,8 @@ data class MessageLocationContent(
|
||||||
@Json(name = "msgtype") override val msgType: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
|
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind
|
||||||
|
* of content description for accessibility e.g. 'location attachment'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
|
|
@ -35,12 +35,14 @@ interface SharedSecretStorageService {
|
||||||
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...)
|
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...)
|
||||||
*
|
*
|
||||||
* @param keyId the ID of the key
|
* @param keyId the ID of the key
|
||||||
|
* @param key keep null if you want to generate a random key
|
||||||
* @param keyName a human readable name
|
* @param keyName a human readable name
|
||||||
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
|
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
|
||||||
*
|
*
|
||||||
* @param callback Get key creation info
|
* @param callback Get key creation info
|
||||||
*/
|
*/
|
||||||
fun generateKey(keyId: String,
|
fun generateKey(keyId: String,
|
||||||
|
key: SsssKeySpec?,
|
||||||
keyName: String,
|
keyName: String,
|
||||||
keySigner: KeySigner?,
|
keySigner: KeySigner?,
|
||||||
callback: MatrixCallback<SsssKeyCreationInfo>)
|
callback: MatrixCallback<SsssKeyCreationInfo>)
|
||||||
|
|
|
@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) {
|
||||||
|
val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) }
|
||||||
|
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
|
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let {
|
||||||
|
callback.onSuccess(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable backing up of keys.
|
* Enable backing up of keys.
|
||||||
* This method will update the state and will start sending keys in nominal case
|
* This method will update the state and will start sending keys in nominal case
|
||||||
|
|
|
@ -29,7 +29,8 @@ data class CreateKeysBackupVersionBody(
|
||||||
override val algorithm: String? = null,
|
override val algorithm: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
* see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth_data")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict? = null
|
override val authData: JsonDict? = null
|
||||||
|
|
|
@ -29,7 +29,8 @@ data class KeysVersionResult(
|
||||||
override val algorithm: String? = null,
|
override val algorithm: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
* see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth_data")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict? = null,
|
override val authData: JsonDict? = null,
|
||||||
|
|
|
@ -29,7 +29,8 @@ data class UpdateKeysBackupVersionBody(
|
||||||
override val algorithm: String? = null,
|
override val algorithm: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
* see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth_data")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict? = null,
|
override val authData: JsonDict? = null,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides the authentication data to delete a device
|
* This class provides the authentication data by using user and password
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UserPasswordAuth(
|
data class UserPasswordAuth(
|
||||||
|
|
|
@ -65,14 +65,16 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
) : SharedSecretStorageService {
|
) : SharedSecretStorageService {
|
||||||
|
|
||||||
override fun generateKey(keyId: String,
|
override fun generateKey(keyId: String,
|
||||||
|
key: SsssKeySpec?,
|
||||||
keyName: String,
|
keyName: String,
|
||||||
keySigner: KeySigner?,
|
keySigner: KeySigner?,
|
||||||
callback: MatrixCallback<SsssKeyCreationInfo>) {
|
callback: MatrixCallback<SsssKeyCreationInfo>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
val key = try {
|
val bytes = try {
|
||||||
ByteArray(32).also {
|
(key as? RawBytesKeySpec)?.privateKey
|
||||||
SecureRandom().nextBytes(it)
|
?: ByteArray(32).also {
|
||||||
}
|
SecureRandom().nextBytes(it)
|
||||||
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
callback.onFailure(failure)
|
callback.onFailure(failure)
|
||||||
return@launch
|
return@launch
|
||||||
|
@ -102,8 +104,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||||
callback.onSuccess(SsssKeyCreationInfo(
|
callback.onSuccess(SsssKeyCreationInfo(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
content = storageKeyContent,
|
content = storageKeyContent,
|
||||||
recoveryKey = computeRecoveryKey(key),
|
recoveryKey = computeRecoveryKey(bytes),
|
||||||
keySpec = RawBytesKeySpec(key)
|
keySpec = RawBytesKeySpec(bytes)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ import im.vector.matrix.android.internal.di.DeviceId
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -102,7 +103,8 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory,
|
private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory,
|
||||||
private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory,
|
private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory,
|
||||||
private val crossSigningService: CrossSigningService
|
private val crossSigningService: CrossSigningService,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : DefaultVerificationTransaction.Listener, VerificationService {
|
) : DefaultVerificationTransaction.Listener, VerificationService {
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
private val uiHandler = Handler(Looper.getMainLooper())
|
||||||
|
@ -125,7 +127,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
// Event received from the sync
|
// Event received from the sync
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.KEY_VERIFICATION_START -> {
|
EventType.KEY_VERIFICATION_START -> {
|
||||||
onStartRequestReceived(event)
|
onStartRequestReceived(event)
|
||||||
|
|
|
@ -30,6 +30,10 @@ import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: None
|
||||||
|
* Possible next worker : None
|
||||||
|
*/
|
||||||
internal class SendVerificationMessageWorker(context: Context,
|
internal class SendVerificationMessageWorker(context: Context,
|
||||||
params: WorkerParameters)
|
params: WorkerParameters)
|
||||||
: CoroutineWorker(context, params) {
|
: CoroutineWorker(context, params) {
|
||||||
|
@ -48,7 +52,7 @@ internal class SendVerificationMessageWorker(context: Context,
|
||||||
lateinit var cryptoService: CryptoService
|
lateinit var cryptoService: CryptoService
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val errorOutputData = Data.Builder().putBoolean("failed", true).build()
|
val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build()
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success(errorOutputData)
|
?: return Result.success(errorOutputData)
|
||||||
|
|
||||||
|
@ -76,4 +80,12 @@ internal class SendVerificationMessageWorker(context: Context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val OUTPUT_KEY_FAILED = "failed"
|
||||||
|
|
||||||
|
fun hasFailed(outputData: Data): Boolean {
|
||||||
|
return outputData.getBoolean(SendVerificationMessageWorker.OUTPUT_KEY_FAILED, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,9 @@ internal interface VerificationTransport {
|
||||||
onErrorReason: CancelCode,
|
onErrorReason: CancelCode,
|
||||||
onDone: (() -> Unit)?)
|
onDone: (() -> Unit)?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callback will be called with eventId and ValidVerificationInfoRequest in case of success
|
||||||
|
*/
|
||||||
fun sendVerificationRequest(supportedMethods: List<String>,
|
fun sendVerificationRequest(supportedMethods: List<String>,
|
||||||
localId: String,
|
localId: String,
|
||||||
otherUserId: String,
|
otherUserId: String,
|
||||||
|
|
|
@ -115,7 +115,7 @@ internal class VerificationTransportRoomMessage(
|
||||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||||
?.firstOrNull { it.id == enqueueInfo.second }
|
?.firstOrNull { it.id == enqueueInfo.second }
|
||||||
?.let { wInfo ->
|
?.let { wInfo ->
|
||||||
if (wInfo.outputData.getBoolean("failed", false)) {
|
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
|
||||||
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
|
||||||
tx?.cancel(onErrorReason)
|
tx?.cancel(onErrorReason)
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,12 +196,15 @@ internal class VerificationTransportRoomMessage(
|
||||||
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
?.filter { it.state == WorkInfo.State.SUCCEEDED }
|
||||||
?.firstOrNull { it.id == workRequest.id }
|
?.firstOrNull { it.id == workRequest.id }
|
||||||
?.let { wInfo ->
|
?.let { wInfo ->
|
||||||
if (wInfo.outputData.getBoolean("failed", false)) {
|
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
|
||||||
callback(null, null)
|
callback(null, null)
|
||||||
} else if (wInfo.outputData.getString(localId) != null) {
|
|
||||||
callback(wInfo.outputData.getString(localId), validInfo)
|
|
||||||
} else {
|
} else {
|
||||||
callback(null, null)
|
val eventId = wInfo.outputData.getString(localId)
|
||||||
|
if (eventId != null) {
|
||||||
|
callback(eventId, validInfo)
|
||||||
|
} else {
|
||||||
|
callback(null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
workLiveData.removeObserver(this)
|
workLiveData.removeObserver(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
|
|
||||||
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
|
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
|
||||||
// // Nothing to verify
|
// Nothing to verify
|
||||||
cancel(CancelCode.MismatchedKeys)
|
cancel(CancelCode.MismatchedKeys)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,14 @@ internal object HomeServerCapabilitiesMapper {
|
||||||
|
|
||||||
fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities {
|
fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities {
|
||||||
return HomeServerCapabilities(
|
return HomeServerCapabilities(
|
||||||
|
canChangePassword = entity.canChangePassword,
|
||||||
maxUploadFileSize = entity.maxUploadFileSize
|
maxUploadFileSize = entity.maxUploadFileSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity {
|
fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity {
|
||||||
return HomeServerCapabilitiesEntity(
|
return HomeServerCapabilitiesEntity(
|
||||||
|
canChangePassword = domain.canChangePassword,
|
||||||
maxUploadFileSize = domain.maxUploadFileSize
|
maxUploadFileSize = domain.maxUploadFileSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
|
|
||||||
internal open class HomeServerCapabilitiesEntity(
|
internal open class HomeServerCapabilitiesEntity(
|
||||||
|
var canChangePassword: Boolean = true,
|
||||||
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
|
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
|
||||||
var lastUpdatedTimestamp: Long = 0L
|
var lastUpdatedTimestamp: Long = 0L
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.failure.GlobalError
|
||||||
import im.vector.matrix.android.api.pushrules.PushRuleService
|
import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||||
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
import im.vector.matrix.android.api.session.InitialSyncProgressService
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.account.AccountService
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||||
import im.vector.matrix.android.api.session.cache.CacheService
|
import im.vector.matrix.android.api.session.cache.CacheService
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
|
@ -94,6 +95,7 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
|
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
|
||||||
private val accountDataService: Lazy<AccountDataService>,
|
private val accountDataService: Lazy<AccountDataService>,
|
||||||
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
|
||||||
|
private val accountService: Lazy<AccountService>,
|
||||||
private val timelineEventDecryptor: TimelineEventDecryptor,
|
private val timelineEventDecryptor: TimelineEventDecryptor,
|
||||||
private val shieldTrustUpdater: ShieldTrustUpdater)
|
private val shieldTrustUpdater: ShieldTrustUpdater)
|
||||||
: Session,
|
: Session,
|
||||||
|
@ -110,7 +112,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
SecureStorageService by secureStorageService.get(),
|
SecureStorageService by secureStorageService.get(),
|
||||||
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
|
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
|
||||||
ProfileService by profileService.get(),
|
ProfileService by profileService.get(),
|
||||||
AccountDataService by accountDataService.get() {
|
AccountDataService by accountDataService.get(),
|
||||||
|
AccountService by accountService.get() {
|
||||||
|
|
||||||
override val sharedSecretStorageService: SharedSecretStorageService
|
override val sharedSecretStorageService: SharedSecretStorageService
|
||||||
get() = _sharedSecretStorageService.get()
|
get() = _sharedSecretStorageService.get()
|
||||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.internal.crypto.verification.SendVerificationMes
|
||||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||||
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||||
|
import im.vector.matrix.android.internal.session.account.AccountModule
|
||||||
import im.vector.matrix.android.internal.session.cache.CacheModule
|
import im.vector.matrix.android.internal.session.cache.CacheModule
|
||||||
import im.vector.matrix.android.internal.session.content.ContentModule
|
import im.vector.matrix.android.internal.session.content.ContentModule
|
||||||
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||||
|
@ -55,24 +56,25 @@ import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
|
||||||
@Component(dependencies = [MatrixComponent::class],
|
@Component(dependencies = [MatrixComponent::class],
|
||||||
modules = [
|
modules = [
|
||||||
SessionModule::class,
|
SessionModule::class,
|
||||||
RoomModule::class,
|
RoomModule::class,
|
||||||
SyncModule::class,
|
SyncModule::class,
|
||||||
HomeServerCapabilitiesModule::class,
|
HomeServerCapabilitiesModule::class,
|
||||||
SignOutModule::class,
|
SignOutModule::class,
|
||||||
GroupModule::class,
|
GroupModule::class,
|
||||||
UserModule::class,
|
UserModule::class,
|
||||||
FilterModule::class,
|
FilterModule::class,
|
||||||
GroupModule::class,
|
GroupModule::class,
|
||||||
ContentModule::class,
|
ContentModule::class,
|
||||||
CacheModule::class,
|
CacheModule::class,
|
||||||
CryptoModule::class,
|
CryptoModule::class,
|
||||||
PushersModule::class,
|
PushersModule::class,
|
||||||
AccountDataModule::class,
|
AccountDataModule::class,
|
||||||
ProfileModule::class,
|
ProfileModule::class,
|
||||||
SessionAssistedInjectModule::class
|
SessionAssistedInjectModule::class,
|
||||||
]
|
AccountModule::class
|
||||||
|
]
|
||||||
)
|
)
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal interface SessionComponent {
|
internal interface SessionComponent {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.account
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
|
||||||
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
internal interface AccountAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the homeserver to change the password with the provided new password.
|
||||||
|
* @param params parameters to change password.
|
||||||
|
*/
|
||||||
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
|
||||||
|
fun changePassword(@Body params: ChangePasswordParams): Call<Unit>
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.account
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import im.vector.matrix.android.api.session.account.AccountService
|
||||||
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
|
||||||
|
@Module
|
||||||
|
internal abstract class AccountModule {
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
@SessionScope
|
||||||
|
fun providesAccountAPI(retrofit: Retrofit): AccountAPI {
|
||||||
|
return retrofit.create(AccountAPI::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindChangePasswordTask(task: DefaultChangePasswordTask): ChangePasswordTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindAccountService(service: DefaultAccountService): AccountService
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.account
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
|
import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
|
||||||
|
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||||
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> {
|
||||||
|
data class Params(
|
||||||
|
val password: String,
|
||||||
|
val newPassword: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultChangePasswordTask @Inject constructor(
|
||||||
|
private val accountAPI: AccountAPI,
|
||||||
|
private val eventBus: EventBus,
|
||||||
|
@UserId private val userId: String
|
||||||
|
) : ChangePasswordTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: ChangePasswordTask.Params) {
|
||||||
|
val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword)
|
||||||
|
try {
|
||||||
|
executeRequest<Unit>(eventBus) {
|
||||||
|
apiCall = accountAPI.changePassword(changePasswordParams)
|
||||||
|
}
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
if (throwable is Failure.OtherServerError
|
||||||
|
&& throwable.httpCode == 401
|
||||||
|
/* Avoid infinite loop */
|
||||||
|
&& changePasswordParams.auth?.session == null) {
|
||||||
|
try {
|
||||||
|
MoshiProvider.providesMoshi()
|
||||||
|
.adapter(RegistrationFlowResponse::class.java)
|
||||||
|
.fromJson(throwable.errorBody)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}?.let {
|
||||||
|
// Retry with authentication
|
||||||
|
try {
|
||||||
|
executeRequest<Unit>(eventBus) {
|
||||||
|
apiCall = accountAPI.changePassword(
|
||||||
|
changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = it.session))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.account
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.account.AccountService
|
||||||
|
import im.vector.matrix.android.api.util.Cancelable
|
||||||
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class DefaultAccountService @Inject constructor(private val changePasswordTask: ChangePasswordTask,
|
||||||
|
private val taskExecutor: TaskExecutor) : AccountService {
|
||||||
|
|
||||||
|
override fun changePassword(password: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
return changePasswordTask
|
||||||
|
.configureWith(ChangePasswordTask.Params(password, newPassword)) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,46 +18,46 @@ package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
|
private const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
|
||||||
private const val URI_PREFIX_CONTENT_API = "_matrix/media/v1/"
|
|
||||||
|
|
||||||
internal class DefaultContentUrlResolver @Inject constructor(private val homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
|
internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
|
||||||
|
|
||||||
companion object {
|
private val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
|
||||||
fun getUploadUrl(homeServerConnectionConfig: HomeServerConnectionConfig): String {
|
private val sep = if (baseUrl.endsWith("/")) "" else "/"
|
||||||
val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
|
|
||||||
val sep = if (baseUrl.endsWith("/")) "" else "/"
|
|
||||||
|
|
||||||
return baseUrl + sep + URI_PREFIX_CONTENT_API + "upload"
|
override val uploadUrl = baseUrl + sep + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun resolveFullSize(contentUrl: String?): String? {
|
override fun resolveFullSize(contentUrl: String?): String? {
|
||||||
if (contentUrl?.isValidMatrixContentUrl() == true) {
|
return contentUrl
|
||||||
val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
|
// do not allow non-mxc content URLs
|
||||||
val prefix = URI_PREFIX_CONTENT_API + "download/"
|
?.takeIf { it.isValidMatrixContentUrl() }
|
||||||
return resolve(baseUrl, contentUrl, prefix)
|
?.let {
|
||||||
}
|
resolve(
|
||||||
return null
|
contentUrl = it,
|
||||||
|
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? {
|
override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? {
|
||||||
if (contentUrl?.isValidMatrixContentUrl() == true) {
|
return contentUrl
|
||||||
val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
|
// do not allow non-mxc content URLs
|
||||||
val prefix = URI_PREFIX_CONTENT_API + "thumbnail/"
|
?.takeIf { it.isValidMatrixContentUrl() }
|
||||||
val params = "?width=$width&height=$height&method=${method.value}"
|
?.let {
|
||||||
return resolve(baseUrl, contentUrl, prefix, params)
|
resolve(
|
||||||
}
|
contentUrl = it,
|
||||||
// do not allow non-mxc content URLs
|
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/",
|
||||||
return null
|
params = "?width=$width&height=$height&method=${method.value}"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolve(baseUrl: String,
|
private fun resolve(contentUrl: String,
|
||||||
contentUrl: String,
|
|
||||||
prefix: String,
|
prefix: String,
|
||||||
params: String? = null): String? {
|
params: String = ""): String? {
|
||||||
var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME)
|
var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME)
|
||||||
val fragmentOffset = serverAndMediaId.indexOf("#")
|
val fragmentOffset = serverAndMediaId.indexOf("#")
|
||||||
var fragment = ""
|
var fragment = ""
|
||||||
|
@ -66,9 +66,7 @@ internal class DefaultContentUrlResolver @Inject constructor(private val homeSer
|
||||||
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
|
serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sep = if (baseUrl.endsWith("/")) "" else "/"
|
return baseUrl + sep + prefix + serverAndMediaId + params + fragment
|
||||||
|
|
||||||
return baseUrl + sep + prefix + serverAndMediaId + (params ?: "") + fragment
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.isValidMatrixContentUrl(): Boolean {
|
private fun String.isValidMatrixContentUrl(): Boolean {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.session.content
|
package im.vector.matrix.android.internal.session.content
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.internal.di.Authenticated
|
import im.vector.matrix.android.internal.di.Authenticated
|
||||||
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
import im.vector.matrix.android.internal.network.ProgressRequestBody
|
||||||
import im.vector.matrix.android.internal.network.awaitResponse
|
import im.vector.matrix.android.internal.network.awaitResponse
|
||||||
|
@ -37,10 +37,10 @@ import javax.inject.Inject
|
||||||
internal class FileUploader @Inject constructor(@Authenticated
|
internal class FileUploader @Inject constructor(@Authenticated
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val eventBus: EventBus,
|
private val eventBus: EventBus,
|
||||||
sessionParams: SessionParams,
|
contentUrlResolver: ContentUrlResolver,
|
||||||
moshi: Moshi) {
|
moshi: Moshi) {
|
||||||
|
|
||||||
private val uploadUrl = DefaultContentUrlResolver.getUploadUrl(sessionParams.homeServerConnectionConfig)
|
private val uploadUrl = contentUrlResolver.uploadUrl
|
||||||
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
|
||||||
|
|
||||||
suspend fun uploadFile(file: File,
|
suspend fun uploadFile(file: File,
|
||||||
|
|
|
@ -46,6 +46,10 @@ private data class NewImageAttributes(
|
||||||
val newFileSize: Int
|
val newFileSize: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: None
|
||||||
|
* Possible next worker : Always [MultipleEventSendingDispatcherWorker]
|
||||||
|
*/
|
||||||
internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -64,12 +68,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success()
|
?: return Result.success()
|
||||||
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
|
|
||||||
Timber.v("Starting upload media work with params $params")
|
Timber.v("Starting upload media work with params $params")
|
||||||
|
|
||||||
if (params.lastFailureMessage != null) {
|
if (params.lastFailureMessage != null) {
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
Timber.v("Stop upload media work due to input failure")
|
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just defensive code to ensure that we never have an uncaught exception that could break the queue
|
// Just defensive code to ensure that we never have an uncaught exception that could break the queue
|
||||||
|
|
|
@ -23,8 +23,13 @@ import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: None
|
||||||
|
* Possible next worker : None
|
||||||
|
*/
|
||||||
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -39,6 +44,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
|
@ -69,13 +69,13 @@ internal class GroupSummaryUpdater @Inject constructor(
|
||||||
|
|
||||||
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
|
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
|
||||||
|
|
||||||
val sendWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<GetGroupDataWorker>()
|
val getGroupWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<GetGroupDataWorker>()
|
||||||
.setInputData(workData)
|
.setInputData(workData)
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
workManagerProvider.workManager
|
workManagerProvider.workManager
|
||||||
.beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, sendWork)
|
.beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, getGroupWork)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,12 @@ import retrofit2.http.GET
|
||||||
|
|
||||||
internal interface CapabilitiesAPI {
|
internal interface CapabilitiesAPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the homeserver capabilities
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
|
||||||
|
fun getCapabilities(): Call<GetCapabilitiesResult>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the upload capabilities
|
* Request the upload capabilities
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -51,15 +51,23 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
|
||||||
apiCall = capabilitiesAPI.getUploadCapabilities()
|
apiCall = capabilitiesAPI.getUploadCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val capabilities = runCatching {
|
||||||
|
executeRequest<GetCapabilitiesResult>(eventBus) {
|
||||||
|
apiCall = capabilitiesAPI.getCapabilities()
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
// TODO Add other call here (get version, etc.)
|
// TODO Add other call here (get version, etc.)
|
||||||
|
|
||||||
insertInDb(uploadCapabilities)
|
insertInDb(capabilities, uploadCapabilities)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
|
private suspend fun insertInDb(getCapabilitiesResult: GetCapabilitiesResult?, getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
|
||||||
monarchy.awaitTransaction { realm ->
|
monarchy.awaitTransaction { realm ->
|
||||||
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
|
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
|
||||||
|
|
||||||
|
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
|
||||||
|
|
||||||
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
|
||||||
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.homeserver
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import im.vector.matrix.android.api.extensions.orTrue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-capabilities
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class GetCapabilitiesResult(
|
||||||
|
/**
|
||||||
|
* Required. The custom capabilities the server supports, using the Java package naming convention.
|
||||||
|
*/
|
||||||
|
@Json(name = "capabilities")
|
||||||
|
val capabilities: Capabilities? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class Capabilities(
|
||||||
|
/**
|
||||||
|
* Capability to indicate if the user can change their password.
|
||||||
|
*/
|
||||||
|
@Json(name = "m.change_password")
|
||||||
|
val changePassword: ChangePassword? = null
|
||||||
|
|
||||||
|
// No need for m.room_versions for the moment
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
internal data class ChangePassword(
|
||||||
|
/**
|
||||||
|
* Required. True if the user can change their password, false otherwise.
|
||||||
|
*/
|
||||||
|
@Json(name = "enabled")
|
||||||
|
val enabled: Boolean?
|
||||||
|
)
|
||||||
|
|
||||||
|
// The spec says: If not present, the client should assume that password changes are possible via the API
|
||||||
|
internal fun GetCapabilitiesResult?.canChangePassword(): Boolean {
|
||||||
|
return this?.capabilities?.changePassword?.enabled.orTrue()
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class GetUploadCapabilitiesResult(
|
internal data class GetUploadCapabilitiesResult(
|
||||||
/**
|
/**
|
||||||
* The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content.
|
* The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content.
|
||||||
* If not listed or null, the size limit should be treated as unknown.
|
* If not listed or null, the size limit should be treated as unknown.
|
||||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
||||||
|
@ -50,6 +51,7 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// TODO This is not used. Delete?
|
// TODO This is not used. Delete?
|
||||||
|
@ -51,10 +52,12 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
|
|
||||||
if (params.lastFailureMessage != null) {
|
if (params.lastFailureMessage != null) {
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
|
|
|
@ -228,7 +228,7 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
keys.forEach { isRoomEncrypted ->
|
keys.forEach { isRoomEncrypted ->
|
||||||
// Should never be empty
|
// Should never be empty
|
||||||
val localEchoes = get(isRoomEncrypted).orEmpty()
|
val localEchoes = get(isRoomEncrypted).orEmpty()
|
||||||
val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending, startChain = true)
|
val uploadWork = createUploadMediaWork(localEchoes, attachment, isRoomEncrypted, compressBeforeSending)
|
||||||
|
|
||||||
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
val dispatcherWork = createMultipleEventDispatcherWork(isRoomEncrypted)
|
||||||
|
|
||||||
|
@ -293,14 +293,13 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
||||||
attachment: ContentAttachmentData,
|
attachment: ContentAttachmentData,
|
||||||
isRoomEncrypted: Boolean,
|
isRoomEncrypted: Boolean,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean): OneTimeWorkRequest {
|
||||||
startChain: Boolean): OneTimeWorkRequest {
|
|
||||||
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending)
|
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending)
|
||||||
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
|
||||||
|
|
||||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.startChain(startChain)
|
.startChain(true)
|
||||||
.setInputData(uploadWorkData)
|
.setInputData(uploadWorkData)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -35,6 +35,10 @@ import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: None
|
||||||
|
* Possible next worker : Always [SendEventWorker]
|
||||||
|
*/
|
||||||
internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||||
: CoroutineWorker(context, params) {
|
: CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
@ -53,14 +57,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.v("Start Encrypt work")
|
Timber.v("Start Encrypt work")
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success().also {
|
?: return Result.success()
|
||||||
Timber.e("Work cancelled due to input error from parent")
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
}
|
|
||||||
|
|
||||||
Timber.v("Start Encrypt work for event ${params.event.eventId}")
|
Timber.v("Start Encrypt work for event ${params.event.eventId}")
|
||||||
if (params.lastFailureMessage != null) {
|
if (params.lastFailureMessage != null) {
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.squareup.moshi.JsonClass
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||||
|
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||||
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
|
@ -36,6 +37,9 @@ import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This worker creates a new work for each events passed in parameter
|
* This worker creates a new work for each events passed in parameter
|
||||||
|
*
|
||||||
|
* Possible previous worker: Always [UploadContentWorker]
|
||||||
|
* Possible next worker : None, but it will post new work to send events, encrypted or not
|
||||||
*/
|
*/
|
||||||
internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters)
|
internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters)
|
||||||
: CoroutineWorker(context, params) {
|
: CoroutineWorker(context, params) {
|
||||||
|
@ -55,9 +59,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.v("Start dispatch sending multiple event work")
|
Timber.v("Start dispatch sending multiple event work")
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success().also {
|
?: return Result.success()
|
||||||
Timber.e("Work cancelled due to input error from parent")
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
}
|
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
@ -68,6 +71,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
||||||
}
|
}
|
||||||
// Transmit the error if needed?
|
// Transmit the error if needed?
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a work for every event
|
// Create a work for every event
|
||||||
|
|
|
@ -26,8 +26,13 @@ import im.vector.matrix.android.internal.worker.SessionWorkerParams
|
||||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.worker.getSessionComponent
|
import im.vector.matrix.android.internal.worker.getSessionComponent
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: None
|
||||||
|
* Possible next worker : None
|
||||||
|
*/
|
||||||
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -46,10 +51,12 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
|
|
||||||
if (params.lastFailureMessage != null) {
|
if (params.lastFailureMessage != null) {
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||||
}
|
}
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
|
|
|
@ -32,6 +32,10 @@ import org.greenrobot.eventbus.EventBus
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: [EncryptEventWorker] or first worker
|
||||||
|
* Possible next worker : None
|
||||||
|
*/
|
||||||
internal class SendEventWorker(context: Context,
|
internal class SendEventWorker(context: Context,
|
||||||
params: WorkerParameters)
|
params: WorkerParameters)
|
||||||
: CoroutineWorker(context, params) {
|
: CoroutineWorker(context, params) {
|
||||||
|
@ -49,9 +53,8 @@ internal class SendEventWorker(context: Context,
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.success().also {
|
?: return Result.success()
|
||||||
Timber.e("Work cancelled due to input error from parent")
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
}
|
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
|
@ -65,6 +68,7 @@ internal class SendEventWorker(context: Context,
|
||||||
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED)
|
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED)
|
||||||
// Transmit the error
|
// Transmit the error
|
||||||
return Result.success(inputData)
|
return Result.success(inputData)
|
||||||
|
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||||
}
|
}
|
||||||
return try {
|
return try {
|
||||||
sendEvent(event)
|
sendEvent(event)
|
||||||
|
|
|
@ -35,6 +35,10 @@ import javax.inject.Inject
|
||||||
|
|
||||||
private const val DEFAULT_LONG_POOL_TIMEOUT = 0L
|
private const val DEFAULT_LONG_POOL_TIMEOUT = 0L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible previous worker: None
|
||||||
|
* Possible next worker : None
|
||||||
|
*/
|
||||||
internal class SyncWorker(context: Context,
|
internal class SyncWorker(context: Context,
|
||||||
workerParameters: WorkerParameters
|
workerParameters: WorkerParameters
|
||||||
) : CoroutineWorker(context, workerParameters) {
|
) : CoroutineWorker(context, workerParameters) {
|
||||||
|
@ -53,7 +57,10 @@ internal class SyncWorker(context: Context,
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
Timber.i("Sync work starting")
|
Timber.i("Sync work starting")
|
||||||
val params = WorkerParamsFactory.fromData<Params>(inputData) ?: return Result.success()
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
|
?: return Result.success()
|
||||||
|
.also { Timber.e("Unable to parse work parameters") }
|
||||||
|
|
||||||
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
|
||||||
sessionComponent.inject(this)
|
sessionComponent.inject(this)
|
||||||
return runCatching {
|
return runCatching {
|
||||||
|
@ -76,7 +83,6 @@ internal class SyncWorker(context: Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
|
private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
|
||||||
|
|
||||||
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {
|
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {
|
||||||
|
|
|
@ -16,9 +16,16 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.worker
|
package im.vector.matrix.android.internal.worker
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Note about the Worker usage:
|
||||||
|
* The workers we chain, or when using the append strategy, should never return Result.Failure(), else the chain will be broken forever
|
||||||
|
*/
|
||||||
interface SessionWorkerParams {
|
interface SessionWorkerParams {
|
||||||
val sessionId: String
|
val sessionId: String
|
||||||
|
|
||||||
// Null is no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers
|
/**
|
||||||
|
* Null when no error occurs. When chaining Workers, first step is to check that there is no lastFailureMessage from the previous workers
|
||||||
|
* If it is the case, the worker should just transmit the error and shouldn't do anything else
|
||||||
|
*/
|
||||||
val lastFailureMessage: String?
|
val lastFailureMessage: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ private short
|
||||||
final short
|
final short
|
||||||
|
|
||||||
### Line length is limited to 160 chars. Please split long lines
|
### Line length is limited to 160 chars. Please split long lines
|
||||||
.{161}
|
[^─]{161}
|
||||||
|
|
||||||
### "DO NOT COMMIT" has been committed
|
### "DO NOT COMMIT" has been committed
|
||||||
DO NOT COMMIT
|
DO NOT COMMIT
|
||||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
||||||
|
import im.vector.riotx.features.crypto.recover.BootstrapMigrateBackupFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
||||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||||
|
@ -444,4 +445,8 @@ interface FragmentModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
||||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(BootstrapMigrateBackupFragment::class)
|
||||||
|
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.riotx.core.error
|
||||||
|
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
|
import im.vector.matrix.android.api.failure.isInvalidPassword
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
|
@ -54,8 +55,7 @@ class DefaultErrorFormatter @Inject constructor(
|
||||||
// Special case for terms and conditions
|
// Special case for terms and conditions
|
||||||
stringProvider.getString(R.string.error_terms_not_accepted)
|
stringProvider.getString(R.string.error_terms_not_accepted)
|
||||||
}
|
}
|
||||||
throwable.error.code == MatrixError.M_FORBIDDEN
|
throwable.isInvalidPassword() -> {
|
||||||
&& throwable.error.message == "Invalid password" -> {
|
|
||||||
stringProvider.getString(R.string.auth_invalid_login_param)
|
stringProvider.getString(R.string.auth_invalid_login_param)
|
||||||
}
|
}
|
||||||
throwable.error.code == MatrixError.M_USER_IN_USE -> {
|
throwable.error.code == MatrixError.M_USER_IN_USE -> {
|
||||||
|
@ -67,7 +67,7 @@ class DefaultErrorFormatter @Inject constructor(
|
||||||
throwable.error.code == MatrixError.M_NOT_JSON -> {
|
throwable.error.code == MatrixError.M_NOT_JSON -> {
|
||||||
stringProvider.getString(R.string.login_error_not_json)
|
stringProvider.getString(R.string.login_error_not_json)
|
||||||
}
|
}
|
||||||
throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
|
throwable.error.code == MatrixError.M_THREEPID_DENIED -> {
|
||||||
stringProvider.getString(R.string.login_error_threepid_denied)
|
stringProvider.getString(R.string.login_error_threepid_denied)
|
||||||
}
|
}
|
||||||
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
|
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.os.Parcelable
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
|
@ -183,6 +184,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
||||||
handleGlobalError(it)
|
handleGlobalError(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set flag FLAG_SECURE
|
||||||
|
if (vectorPreferences.useFlagSecure()) {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||||
|
}
|
||||||
|
|
||||||
doBeforeSetContentView()
|
doBeforeSetContentView()
|
||||||
|
|
||||||
if (getLayoutRes() != -1) {
|
if (getLayoutRes() != -1) {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.platform
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
interface ViewModelTask<Params, Result> {
|
||||||
|
operator fun invoke(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
params: Params,
|
||||||
|
onResult: (Result) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val backgroundJob = scope.async { execute(params) }
|
||||||
|
scope.launch { onResult(backgroundJob.await()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(params: Params): Result
|
||||||
|
}
|
|
@ -159,7 +159,7 @@ class KeysBackupBanner @JvmOverloads constructor(
|
||||||
render(state, true)
|
render(state, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************************************************************************
|
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
inflate(context, R.layout.view_keys_backup_banner, this)
|
inflate(context, R.layout.view_keys_backup_banner, this)
|
||||||
|
|
|
@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************************************************************************
|
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
inflate(context, R.layout.view_notification_area, this)
|
inflate(context, R.layout.view_notification_area, this)
|
||||||
|
|
|
@ -128,7 +128,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportKeysManually() {
|
private fun exportKeysManually() {
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||||
|
R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
showWaitingView()
|
showWaitingView()
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.crypto.recover
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||||
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.ViewModelTask
|
||||||
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BackupToQuadSMigrationTask @Inject constructor(
|
||||||
|
val session: Session,
|
||||||
|
val stringProvider: StringProvider
|
||||||
|
) : ViewModelTask<BackupToQuadSMigrationTask.Params, BackupToQuadSMigrationTask.Result> {
|
||||||
|
|
||||||
|
sealed class Result {
|
||||||
|
object Success : Result()
|
||||||
|
abstract class Failure(val error: String?) : Result()
|
||||||
|
object InvalidRecoverySecret : Failure(null)
|
||||||
|
object NoKeyBackupVersion : Failure(null)
|
||||||
|
object IllegalParams : Failure(null)
|
||||||
|
class ErrorFailure(throwable: Throwable) : Failure(throwable.localizedMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val passphrase: String?,
|
||||||
|
val recoveryKey: String?,
|
||||||
|
val progressListener: BootstrapProgressListener? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun execute(params: Params): Result {
|
||||||
|
try {
|
||||||
|
// We need to use the current secret for keybackup and use it as the new master key for SSSS
|
||||||
|
// Then we need to put back the backup key in sss
|
||||||
|
val keysBackupService = session.cryptoService().keysBackupService()
|
||||||
|
val quadS = session.sharedSecretStorageService
|
||||||
|
|
||||||
|
val version = keysBackupService.keysBackupVersion ?: return Result.NoKeyBackupVersion
|
||||||
|
|
||||||
|
reportProgress(params, R.string.bootstrap_progress_checking_backup)
|
||||||
|
val curveKey =
|
||||||
|
(if (params.recoveryKey != null) {
|
||||||
|
extractCurveKeyFromRecoveryKey(params.recoveryKey)
|
||||||
|
} else if (!params.passphrase.isNullOrEmpty() && version.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null) {
|
||||||
|
version.getAuthDataAsMegolmBackupAuthData()?.let { authData ->
|
||||||
|
deriveKey(params.passphrase, authData.privateKeySalt!!, authData.privateKeyIterations!!, object : ProgressListener {
|
||||||
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
|
params.progressListener?.onProgress(WaitingViewData(
|
||||||
|
stringProvider.getString(R.string.bootstrap_progress_checking_backup_with_info,
|
||||||
|
"$progress/$total")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else null)
|
||||||
|
?: return Result.IllegalParams
|
||||||
|
|
||||||
|
reportProgress(params, R.string.bootstrap_progress_compute_curve_key)
|
||||||
|
val recoveryKey = computeRecoveryKey(curveKey)
|
||||||
|
|
||||||
|
val isValid = awaitCallback<Boolean> {
|
||||||
|
keysBackupService.isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) return Result.InvalidRecoverySecret
|
||||||
|
|
||||||
|
val info: SsssKeyCreationInfo =
|
||||||
|
when {
|
||||||
|
params.passphrase?.isNotEmpty() == true -> {
|
||||||
|
reportProgress(params, R.string.bootstrap_progress_generating_ssss)
|
||||||
|
awaitCallback {
|
||||||
|
quadS.generateKeyWithPassphrase(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
"ssss_key",
|
||||||
|
params.passphrase,
|
||||||
|
EmptyKeySigner(),
|
||||||
|
object : ProgressListener {
|
||||||
|
override fun onProgress(progress: Int, total: Int) {
|
||||||
|
params.progressListener?.onProgress(
|
||||||
|
WaitingViewData(
|
||||||
|
stringProvider.getString(
|
||||||
|
R.string.bootstrap_progress_generating_ssss_with_info,
|
||||||
|
"$progress/$total")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.recoveryKey != null -> {
|
||||||
|
reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery)
|
||||||
|
awaitCallback {
|
||||||
|
quadS.generateKey(
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) },
|
||||||
|
"ssss_key",
|
||||||
|
EmptyKeySigner(),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
return Result.IllegalParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, so now we have migrated the old keybackup secret as the quadS key
|
||||||
|
// Now we need to store the keybackup key in SSSS in a compatible way
|
||||||
|
reportProgress(params, R.string.bootstrap_progress_storing_in_sss)
|
||||||
|
awaitCallback<Unit> {
|
||||||
|
quadS.storeSecret(
|
||||||
|
KEYBACKUP_SECRET_SSSS_NAME,
|
||||||
|
curveKey.toBase64NoPadding(),
|
||||||
|
listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save for gossiping
|
||||||
|
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
|
||||||
|
|
||||||
|
// while we are there let's restore, but do not block
|
||||||
|
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
|
||||||
|
version,
|
||||||
|
recoveryKey,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
NoOpMatrixCallback()
|
||||||
|
)
|
||||||
|
|
||||||
|
return Result.Success
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "## BackupToQuadSMigrationTask - Failed to migrate backup")
|
||||||
|
return Result.ErrorFailure(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reportProgress(params: Params, stringRes: Int) {
|
||||||
|
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(stringRes)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,4 +40,8 @@ sealed class BootstrapActions : VectorViewModelAction {
|
||||||
object SaveReqQueryStarted : BootstrapActions()
|
object SaveReqQueryStarted : BootstrapActions()
|
||||||
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
|
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
|
||||||
object SaveReqFailed : BootstrapActions()
|
object SaveReqFailed : BootstrapActions()
|
||||||
|
|
||||||
|
object HandleForgotBackupPassphrase : BootstrapActions()
|
||||||
|
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
|
||||||
|
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -26,18 +27,26 @@ import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.commitTransaction
|
import im.vector.riotx.core.extensions.commitTransaction
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
|
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val isNewAccount: Boolean
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
override val showExpanded = true
|
override val showExpanded = true
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -113,40 +122,70 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
|
||||||
when (state.step) {
|
when (state.step) {
|
||||||
is BootstrapStep.SetupPassphrase -> {
|
is BootstrapStep.CheckingMigration -> {
|
||||||
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||||
|
bootstrapTitleText.text = getString(R.string.upgrade_security)
|
||||||
|
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||||
|
}
|
||||||
|
is BootstrapStep.SetupPassphrase -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||||
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||||
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
|
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.ConfirmPassphrase -> {
|
is BootstrapStep.ConfirmPassphrase -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||||
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
|
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||||
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.AccountPassword -> {
|
is BootstrapStep.AccountPassword -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
||||||
bootstrapTitleText.text = getString(R.string.account_password)
|
bootstrapTitleText.text = getString(R.string.account_password)
|
||||||
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
|
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.Initializing -> {
|
is BootstrapStep.Initializing -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||||
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
|
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
|
||||||
showFragment(BootstrapWaitingFragment::class, Bundle())
|
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.SaveRecoveryKey -> {
|
is BootstrapStep.SaveRecoveryKey -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||||
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
|
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
|
||||||
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
|
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
is BootstrapStep.DoneSuccess -> {
|
is BootstrapStep.DoneSuccess -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||||
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
||||||
showFragment(BootstrapConclusionFragment::class, Bundle())
|
showFragment(BootstrapConclusionFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
}
|
is BootstrapStep.GetBackupSecretForMigration -> {
|
||||||
|
val isKey = when (state.step) {
|
||||||
|
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
val drawableRes = if (isKey) R.drawable.ic_message_key else R.drawable.ic_message_password
|
||||||
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(
|
||||||
|
requireContext(),
|
||||||
|
drawableRes)
|
||||||
|
)
|
||||||
|
bootstrapTitleText.text = getString(R.string.upgrade_security)
|
||||||
|
showFragment(BootstrapMigrateBackupFragment::class, Bundle())
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
super.invalidate()
|
super.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager, isAccountCreation: Boolean) {
|
||||||
|
BootstrapBottomSheet().apply {
|
||||||
|
isCancelable = false
|
||||||
|
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(isAccountCreation)) }
|
||||||
|
}.show(fragmentManager, "BootstrapBottomSheet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||||
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||||
childFragmentManager.commitTransaction {
|
childFragmentManager.commitTransaction {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY
|
||||||
import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
||||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
|
@ -33,11 +34,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.util.awaitCallback
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.ViewModelTask
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -67,24 +66,16 @@ interface BootstrapProgressListener {
|
||||||
data class Params(
|
data class Params(
|
||||||
val userPasswordAuth: UserPasswordAuth? = null,
|
val userPasswordAuth: UserPasswordAuth? = null,
|
||||||
val progressListener: BootstrapProgressListener? = null,
|
val progressListener: BootstrapProgressListener? = null,
|
||||||
val passphrase: String?
|
val passphrase: String?,
|
||||||
|
val keySpec: SsssKeySpec? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
class BootstrapCrossSigningTask @Inject constructor(
|
class BootstrapCrossSigningTask @Inject constructor(
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider
|
||||||
) {
|
) : ViewModelTask<Params, BootstrapResult> {
|
||||||
|
|
||||||
operator fun invoke(
|
override suspend fun execute(params: Params): BootstrapResult {
|
||||||
scope: CoroutineScope,
|
|
||||||
params: Params,
|
|
||||||
onResult: (BootstrapResult) -> Unit = {}
|
|
||||||
) {
|
|
||||||
val backgroundJob = scope.async { execute(params) }
|
|
||||||
scope.launch { onResult(backgroundJob.await()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun execute(params: Params): BootstrapResult {
|
|
||||||
params.progressListener?.onProgress(
|
params.progressListener?.onProgress(
|
||||||
WaitingViewData(
|
WaitingViewData(
|
||||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
||||||
|
@ -124,6 +115,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||||
} ?: kotlin.run {
|
} ?: kotlin.run {
|
||||||
ssssService.generateKey(
|
ssssService.generateKey(
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
|
params.keySpec,
|
||||||
"ssss_key",
|
"ssss_key",
|
||||||
EmptyKeySigner(),
|
EmptyKeySigner(),
|
||||||
it
|
it
|
||||||
|
@ -205,14 +197,16 @@ class BootstrapCrossSigningTask @Inject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
|
||||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||||
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||||
|
}
|
||||||
|
val version = awaitCallback<KeysVersion> {
|
||||||
|
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
||||||
|
}
|
||||||
|
// Save it for gossiping
|
||||||
|
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||||
}
|
}
|
||||||
val version = awaitCallback<KeysVersion> {
|
|
||||||
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
|
|
||||||
}
|
|
||||||
// Save it for gossiping
|
|
||||||
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.crypto.recover
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputType.TYPE_CLASS_TEXT
|
||||||
|
import android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
import android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||||
|
import android.view.View
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
|
import com.jakewharton.rxbinding3.view.clicks
|
||||||
|
import com.jakewharton.rxbinding3.widget.editorActionEvents
|
||||||
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.isValidRecoveryKey
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
|
import im.vector.riotx.core.extensions.showPassword
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.utils.colorizeMatchingText
|
||||||
|
import im.vector.riotx.core.utils.startImportTextFromFileIntent
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText
|
||||||
|
import kotlinx.android.synthetic.main.fragment_bootstrap_migrate_backup.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class BootstrapMigrateBackupFragment @Inject constructor(
|
||||||
|
private val colorProvider: ColorProvider
|
||||||
|
) : VectorBaseFragment() {
|
||||||
|
|
||||||
|
override fun getLayoutResId() = R.layout.fragment_bootstrap_migrate_backup
|
||||||
|
|
||||||
|
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
withState(sharedViewModel) {
|
||||||
|
// set initial value (usefull when coming back)
|
||||||
|
bootstrapMigrateEditText.setText(it.passphrase ?: "")
|
||||||
|
}
|
||||||
|
bootstrapMigrateEditText.editorActionEvents()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
bootstrapMigrateEditText.textChanges()
|
||||||
|
.skipInitialValue()
|
||||||
|
.subscribe {
|
||||||
|
bootstrapRecoveryKeyEnterTil.error = null
|
||||||
|
// sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: ""))
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
// sharedViewModel.observeViewEvents {}
|
||||||
|
bootstrapMigrateContinueButton.clicks()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
bootstrapMigrateShowPassword.clicks()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
bootstrapMigrateForgotPassphrase.clicks()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase)
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
bootstrapMigrateUseFile.clicks()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
startImportTextFromFileIntent(this, IMPORT_FILE_REQ)
|
||||||
|
}
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submit() = withState(sharedViewModel) { state ->
|
||||||
|
if (state.step !is BootstrapStep.GetBackupSecretForMigration) {
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
val isEnteringKey =
|
||||||
|
when (state.step) {
|
||||||
|
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
val secret = bootstrapMigrateEditText.text?.toString()
|
||||||
|
if (secret.isNullOrBlank()) {
|
||||||
|
val errRes = if (isEnteringKey) R.string.recovery_key_empty_error_message else R.string.passphrase_empty_error_message
|
||||||
|
bootstrapRecoveryKeyEnterTil.error = getString(errRes)
|
||||||
|
} else if (isEnteringKey && !isValidRecoveryKey(secret)) {
|
||||||
|
bootstrapRecoveryKeyEnterTil.error = getString(R.string.bootstrap_invalid_recovery_key)
|
||||||
|
} else {
|
||||||
|
view?.hideKeyboard()
|
||||||
|
if (isEnteringKey) {
|
||||||
|
sharedViewModel.handle(BootstrapActions.DoMigrateWithRecoveryKey(secret))
|
||||||
|
} else {
|
||||||
|
sharedViewModel.handle(BootstrapActions.DoMigrateWithPassphrase(secret))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||||
|
if (state.step !is BootstrapStep.GetBackupSecretForMigration) {
|
||||||
|
return@withState
|
||||||
|
}
|
||||||
|
|
||||||
|
val isEnteringKey =
|
||||||
|
when (state.step) {
|
||||||
|
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnteringKey) {
|
||||||
|
bootstrapMigrateShowPassword.isVisible = false
|
||||||
|
bootstrapMigrateEditText.inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
|
||||||
|
val recKey = getString(R.string.bootstrap_migration_backup_recovery_key)
|
||||||
|
bootstrapDescriptionText.text = getString(R.string.enter_account_password, recKey)
|
||||||
|
.toSpannable()
|
||||||
|
.colorizeMatchingText(recKey, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||||
|
|
||||||
|
bootstrapMigrateEditText.hint = recKey
|
||||||
|
|
||||||
|
bootstrapMigrateEditText.hint = recKey
|
||||||
|
bootstrapMigrateForgotPassphrase.isVisible = false
|
||||||
|
bootstrapMigrateUseFile.isVisible = true
|
||||||
|
} else {
|
||||||
|
bootstrapMigrateShowPassword.isVisible = true
|
||||||
|
|
||||||
|
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
|
||||||
|
val isPasswordVisible = state.step.isPasswordVisible
|
||||||
|
bootstrapMigrateEditText.showPassword(isPasswordVisible, updateCursor = false)
|
||||||
|
bootstrapMigrateShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapDescriptionText.text = getString(R.string.bootstrap_migration_enter_backup_password)
|
||||||
|
|
||||||
|
bootstrapMigrateEditText.hint = getString(R.string.passphrase_enter_passphrase)
|
||||||
|
|
||||||
|
bootstrapMigrateForgotPassphrase.isVisible = true
|
||||||
|
|
||||||
|
val recKey = getString(R.string.bootstrap_migration_use_recovery_key)
|
||||||
|
bootstrapMigrateForgotPassphrase.text = getString(R.string.bootstrap_migration_with_passphrase_helper_with_link, recKey)
|
||||||
|
.toSpannable()
|
||||||
|
.colorizeMatchingText(recKey, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||||
|
|
||||||
|
bootstrapMigrateUseFile.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) {
|
||||||
|
data?.data?.let { dataURI ->
|
||||||
|
tryThis {
|
||||||
|
activity?.contentResolver?.openInputStream(dataURI)
|
||||||
|
?.bufferedReader()
|
||||||
|
?.use { it.readText() }
|
||||||
|
?.let {
|
||||||
|
bootstrapMigrateEditText.setText(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val IMPORT_FILE_REQ = 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,20 +31,26 @@ import com.nulabinc.zxcvbn.Zxcvbn
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.exhaustive
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.features.login.ReAuthHelper
|
import im.vector.riotx.features.login.ReAuthHelper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
data class BootstrapViewState(
|
data class BootstrapViewState(
|
||||||
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
||||||
val passphrase: String? = null,
|
val passphrase: String? = null,
|
||||||
|
val migrationRecoveryKey: String? = null,
|
||||||
val passphraseRepeat: String? = null,
|
val passphraseRepeat: String? = null,
|
||||||
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
||||||
val passphraseStrength: Async<Strength> = Uninitialized,
|
val passphraseStrength: Async<Strength> = Uninitialized,
|
||||||
|
@ -55,20 +61,13 @@ data class BootstrapViewState(
|
||||||
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
sealed class BootstrapStep {
|
|
||||||
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
|
||||||
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
|
||||||
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
|
||||||
object Initializing : BootstrapStep()
|
|
||||||
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
|
|
||||||
object DoneSuccess : BootstrapStep()
|
|
||||||
}
|
|
||||||
|
|
||||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: BootstrapViewState,
|
@Assisted initialState: BootstrapViewState,
|
||||||
|
@Assisted val args: BootstrapBottomSheet.Args,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val bootstrapTask: BootstrapCrossSigningTask,
|
private val bootstrapTask: BootstrapCrossSigningTask,
|
||||||
|
private val migrationTask: BackupToQuadSMigrationTask,
|
||||||
private val reAuthHelper: ReAuthHelper
|
private val reAuthHelper: ReAuthHelper
|
||||||
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
||||||
|
|
||||||
|
@ -76,7 +75,53 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: BootstrapViewState): BootstrapSharedViewModel
|
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// need to check if user have an existing keybackup
|
||||||
|
if (args.isNewAccount) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.SetupPassphrase(false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.CheckingMigration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check if there is an existing backup
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val version = awaitCallback<KeysVersionResult?> {
|
||||||
|
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||||
|
}
|
||||||
|
if (version == null) {
|
||||||
|
// we just resume plain bootstrap
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.SetupPassphrase(false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we need to get existing backup passphrase/key and convert to SSSS
|
||||||
|
val keyVersion = awaitCallback<KeysVersionResult?> {
|
||||||
|
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
|
||||||
|
}
|
||||||
|
if (keyVersion == null) {
|
||||||
|
// strange case... just finish?
|
||||||
|
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||||
|
} else {
|
||||||
|
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||||
|
if (isBackupCreatedFromPassphrase) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: BootstrapActions) = withState { state ->
|
override fun handle(action: BootstrapActions) = withState { state ->
|
||||||
|
@ -84,23 +129,27 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
is BootstrapActions.GoBack -> queryBack()
|
is BootstrapActions.GoBack -> queryBack()
|
||||||
BootstrapActions.TogglePasswordVisibility -> {
|
BootstrapActions.TogglePasswordVisibility -> {
|
||||||
when (state.step) {
|
when (state.step) {
|
||||||
is BootstrapStep.SetupPassphrase -> {
|
is BootstrapStep.SetupPassphrase -> {
|
||||||
setState {
|
setState {
|
||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapStep.ConfirmPassphrase -> {
|
is BootstrapStep.ConfirmPassphrase -> {
|
||||||
setState {
|
setState {
|
||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is BootstrapStep.AccountPassword -> {
|
||||||
is BootstrapStep.AccountPassword -> {
|
|
||||||
setState {
|
setState {
|
||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
is BootstrapStep.GetBackupSecretPassForMigration -> {
|
||||||
|
setState {
|
||||||
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,12 +246,25 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
copy(step = BootstrapStep.AccountPassword(false))
|
copy(step = BootstrapStep.AccountPassword(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BootstrapActions.HandleForgotBackupPassphrase -> {
|
||||||
|
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true))
|
||||||
|
}
|
||||||
|
} else return@withState
|
||||||
|
}
|
||||||
is BootstrapActions.ReAuth -> {
|
is BootstrapActions.ReAuth -> {
|
||||||
startInitializeFlow(
|
startInitializeFlow(
|
||||||
state.currentReAuth?.copy(password = action.pass)
|
state.currentReAuth?.copy(password = action.pass)
|
||||||
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is BootstrapActions.DoMigrateWithPassphrase -> {
|
||||||
|
startMigrationFlow(state.step, action.passphrase, null)
|
||||||
|
}
|
||||||
|
is BootstrapActions.DoMigrateWithRecoveryKey -> {
|
||||||
|
startMigrationFlow(state.step, null, action.recoveryKey)
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +272,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
// Business Logic
|
// Business Logic
|
||||||
// =======================================
|
// =======================================
|
||||||
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
|
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
os.use {
|
os.use {
|
||||||
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray())
|
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray())
|
||||||
|
@ -231,6 +293,57 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startMigrationFlow(prevState: BootstrapStep, passphrase: String?, recoveryKey: String?) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.Initializing)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val progressListener = object : BootstrapProgressListener {
|
||||||
|
override fun onProgress(data: WaitingViewData) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
initializationWaitingViewData = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
migrationTask.invoke(this, BackupToQuadSMigrationTask.Params(passphrase, recoveryKey, progressListener)) {
|
||||||
|
if (it is BackupToQuadSMigrationTask.Result.Success) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
passphrase = passphrase,
|
||||||
|
passphraseRepeat = passphrase,
|
||||||
|
migrationRecoveryKey = recoveryKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val auth = reAuthHelper.rememberedAuth()
|
||||||
|
if (auth == null) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = BootstrapStep.AccountPassword(false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startInitializeFlow(auth)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(
|
||||||
|
BootstrapViewEvents.ModalError(
|
||||||
|
(it as? BackupToQuadSMigrationTask.Result.Failure)?.error
|
||||||
|
?: stringProvider.getString(R.string.matrix_error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = prevState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
||||||
setState {
|
setState {
|
||||||
copy(step = BootstrapStep.Initializing)
|
copy(step = BootstrapStep.Initializing)
|
||||||
|
@ -247,11 +360,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
withState { state ->
|
withState { state ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
bootstrapTask.invoke(this, Params(
|
bootstrapTask.invoke(this, Params(
|
||||||
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
||||||
progressListener = progressListener,
|
progressListener = progressListener,
|
||||||
passphrase = state.passphrase
|
passphrase = state.passphrase,
|
||||||
|
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
|
||||||
)) {
|
)) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is BootstrapResult.Success -> {
|
is BootstrapResult.Success -> {
|
||||||
|
@ -309,11 +423,30 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
private fun queryBack() = withState { state ->
|
private fun queryBack() = withState { state ->
|
||||||
when (state.step) {
|
when (state.step) {
|
||||||
is BootstrapStep.SetupPassphrase -> {
|
is BootstrapStep.GetBackupSecretPassForMigration -> {
|
||||||
|
if (state.step.useKey) {
|
||||||
|
// go back to passphrase
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = BootstrapStep.GetBackupSecretPassForMigration(
|
||||||
|
isPasswordVisible = state.step.isPasswordVisible,
|
||||||
|
useKey = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is BootstrapStep.GetBackupSecretKeyForMigration -> {
|
||||||
// do we let you cancel from here?
|
// do we let you cancel from here?
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||||
}
|
}
|
||||||
is BootstrapStep.ConfirmPassphrase -> {
|
is BootstrapStep.SetupPassphrase -> {
|
||||||
|
// do we let you cancel from here?
|
||||||
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||||
|
}
|
||||||
|
is BootstrapStep.ConfirmPassphrase -> {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
step = BootstrapStep.SetupPassphrase(
|
step = BootstrapStep.SetupPassphrase(
|
||||||
|
@ -322,15 +455,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapStep.AccountPassword -> {
|
is BootstrapStep.AccountPassword -> {
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
||||||
}
|
}
|
||||||
BootstrapStep.Initializing -> {
|
BootstrapStep.Initializing -> {
|
||||||
// do we let you cancel from here?
|
// do we let you cancel from here?
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
||||||
}
|
}
|
||||||
is BootstrapStep.SaveRecoveryKey,
|
is BootstrapStep.SaveRecoveryKey,
|
||||||
BootstrapStep.DoneSuccess -> {
|
BootstrapStep.DoneSuccess -> {
|
||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,7 +477,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
||||||
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
return fragment.bootstrapViewModelFactory.create(state)
|
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
|
||||||
|
?: BootstrapBottomSheet.Args(true)
|
||||||
|
return fragment.bootstrapViewModelFactory.create(state, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.crypto.recover
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ┌─────────────────────────┐
|
||||||
|
* │ User has signing keys? │──────────── Account
|
||||||
|
* └─────────────────────────┘ Creation ?
|
||||||
|
* │ │
|
||||||
|
* No │
|
||||||
|
* │ │
|
||||||
|
* │ │
|
||||||
|
* ▼ │
|
||||||
|
* ┌───────────────────────────────────┐ │
|
||||||
|
* │ BootstrapStep.CheckingMigration │ │
|
||||||
|
* └───────────────────────────────────┘ │
|
||||||
|
* │ │
|
||||||
|
* │ │
|
||||||
|
* Existing ├─────────No ───────┐ │
|
||||||
|
* ┌────Keybackup───────┘ KeyBackup │ │
|
||||||
|
* │ │ │
|
||||||
|
* │ ▼ ▼
|
||||||
|
* ▼ ┌────────────────────────────────────┐
|
||||||
|
* ┌─────────────────────────────────────────┐ │ BootstrapStep.SetupPassphrase │◀─┐
|
||||||
|
* │BootstrapStep.GetBackupSecretForMigration│ └────────────────────────────────────┘ │
|
||||||
|
* └─────────────────────────────────────────┘ │ │
|
||||||
|
* │ │ ┌Back
|
||||||
|
* │ ▼ │
|
||||||
|
* │ ┌────────────────────────────────────┤
|
||||||
|
* │ │ BootstrapStep.ConfirmPassphrase │──┐
|
||||||
|
* │ └────────────────────────────────────┘ │
|
||||||
|
* │ │ │
|
||||||
|
* │ is password needed? │
|
||||||
|
* │ │ │
|
||||||
|
* │ ▼ │
|
||||||
|
* │ ┌────────────────────────────────────┐ │
|
||||||
|
* │ │ BootstrapStep.AccountPassword │ │
|
||||||
|
* │ └────────────────────────────────────┘ │
|
||||||
|
* │ │ │
|
||||||
|
* │ │ │
|
||||||
|
* │ ┌──────────────────┘ password not needed (in
|
||||||
|
* │ │ memory)
|
||||||
|
* │ │ │
|
||||||
|
* │ ▼ │
|
||||||
|
* │ ┌────────────────────────────────────┐ │
|
||||||
|
* └────────▶│ BootstrapStep.Initializing │◀────────────────────┘
|
||||||
|
* └────────────────────────────────────┘
|
||||||
|
* │
|
||||||
|
* │
|
||||||
|
* │
|
||||||
|
* ▼
|
||||||
|
* ┌────────────────────────────────────┐
|
||||||
|
* │ BootstrapStep.SaveRecoveryKey │
|
||||||
|
* └────────────────────────────────────┘
|
||||||
|
* │
|
||||||
|
* │
|
||||||
|
* │
|
||||||
|
* ▼
|
||||||
|
* ┌────────────────────────────────────────┐
|
||||||
|
* │ BootstrapStep.DoneSuccess │
|
||||||
|
* └────────────────────────────────────────┘
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
sealed class BootstrapStep {
|
||||||
|
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||||
|
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||||
|
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
||||||
|
object CheckingMigration : BootstrapStep()
|
||||||
|
|
||||||
|
abstract class GetBackupSecretForMigration : BootstrapStep()
|
||||||
|
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()
|
||||||
|
object GetBackupSecretKeyForMigration : GetBackupSecretForMigration()
|
||||||
|
|
||||||
|
object Initializing : BootstrapStep()
|
||||||
|
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
|
||||||
|
object DoneSuccess : BootstrapStep()
|
||||||
|
}
|
|
@ -16,8 +16,7 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.crypto.recover
|
package im.vector.riotx.features.crypto.recover
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.core.view.isVisible
|
||||||
import android.view.View
|
|
||||||
import com.airbnb.mvrx.parentFragmentViewModel
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
@ -31,12 +30,22 @@ class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() {
|
||||||
|
|
||||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||||
if (state.step !is BootstrapStep.Initializing) return@withState
|
when (state.step) {
|
||||||
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
|
is BootstrapStep.Initializing -> {
|
||||||
|
bootstrapLoadingStatusText.isVisible = true
|
||||||
|
bootstrapDescriptionText.isVisible = true
|
||||||
|
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
|
||||||
|
}
|
||||||
|
// is BootstrapStep.CheckingMigration -> {
|
||||||
|
// bootstrapLoadingStatusText.isVisible = false
|
||||||
|
// bootstrapDescriptionText.isVisible = false
|
||||||
|
// }
|
||||||
|
else -> {
|
||||||
|
// just show the spinner
|
||||||
|
bootstrapLoadingStatusText.isVisible = false
|
||||||
|
bootstrapDescriptionText.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,13 +91,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
.observe()
|
.observe()
|
||||||
.subscribe { sharedAction ->
|
.subscribe { sharedAction ->
|
||||||
when (sharedAction) {
|
when (sharedAction) {
|
||||||
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
|
||||||
is HomeActivitySharedAction.OpenGroup -> {
|
is HomeActivitySharedAction.OpenGroup -> {
|
||||||
drawerLayout.closeDrawer(GravityCompat.START)
|
drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||||
}
|
}
|
||||||
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
||||||
BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet")
|
BootstrapBottomSheet.show(supportFragmentManager, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
}
|
}
|
||||||
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
||||||
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
||||||
|
sharedActionViewModel.isAccountCreation = true
|
||||||
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,29 +164,48 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||||
.getMyCrossSigningKeys()
|
.getMyCrossSigningKeys()
|
||||||
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
|
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
|
||||||
|
|
||||||
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
|
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
|
||||||
// We need to ask
|
// We need to ask
|
||||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
promptSecurityEvent(
|
||||||
popupAlertManager.postVectorAlert(
|
session,
|
||||||
VerificationVectorAlert(
|
R.string.upgrade_security,
|
||||||
uid = "completeSecurity",
|
R.string.security_prompt_text
|
||||||
title = getString(R.string.complete_security),
|
) {
|
||||||
description = getString(R.string.crosssigning_verify_this_session),
|
it.navigator.upgradeSessionSecurity(it)
|
||||||
iconId = R.drawable.ic_shield_warning
|
}
|
||||||
).apply {
|
} else if (myCrossSigningKeys?.isTrusted() == false) {
|
||||||
matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
|
// We need to ask
|
||||||
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
|
promptSecurityEvent(
|
||||||
contentAction = Runnable {
|
session,
|
||||||
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
R.string.complete_security,
|
||||||
it.navigator.waitSessionVerification(it)
|
R.string.crosssigning_verify_this_session
|
||||||
}
|
) {
|
||||||
}
|
it.navigator.waitSessionVerification(it)
|
||||||
dismissedAction = Runnable {}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
|
||||||
|
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||||
|
popupAlertManager.postVectorAlert(
|
||||||
|
VerificationVectorAlert(
|
||||||
|
uid = "upgradeSecurity",
|
||||||
|
title = getString(titleRes),
|
||||||
|
description = getString(descRes),
|
||||||
|
iconId = R.drawable.ic_shield_warning
|
||||||
|
).apply {
|
||||||
|
matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
|
||||||
|
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
|
||||||
|
contentAction = Runnable {
|
||||||
|
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
||||||
|
action(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismissedAction = Runnable {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
|
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
|
||||||
|
|
|
@ -102,7 +102,7 @@ class HomeDetailFragment @Inject constructor(
|
||||||
VerificationVectorAlert(
|
VerificationVectorAlert(
|
||||||
uid = uid,
|
uid = uid,
|
||||||
title = getString(R.string.new_session),
|
title = getString(R.string.new_session),
|
||||||
description = getString(R.string.new_session_review),
|
description = getString(R.string.new_session_review_with_info, newest.displayName ?: "", newest.deviceId ?: ""),
|
||||||
iconId = R.drawable.ic_shield_warning
|
iconId = R.drawable.ic_shield_warning
|
||||||
).apply {
|
).apply {
|
||||||
matrixItem = user
|
matrixItem = user
|
||||||
|
|
|
@ -21,4 +21,5 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
|
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
|
||||||
var hasDisplayedCompleteSecurityPrompt : Boolean = false
|
var hasDisplayedCompleteSecurityPrompt : Boolean = false
|
||||||
|
var isAccountCreation : Boolean = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
||||||
}
|
}
|
||||||
VerificationState.CANCELED_BY_OTHER -> {
|
VerificationState.CANCELED_BY_OTHER -> {
|
||||||
holder.buttonBar.isVisible = false
|
holder.buttonBar.isVisible = false
|
||||||
holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
|
holder.statusTextView.text = holder.view.context
|
||||||
|
.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
|
||||||
holder.statusTextView.isVisible = true
|
holder.statusTextView.isVisible = true
|
||||||
}
|
}
|
||||||
VerificationState.CANCELED_BY_ME -> {
|
VerificationState.CANCELED_BY_ME -> {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import com.airbnb.mvrx.Success
|
||||||
import com.jakewharton.rxbinding3.widget.textChanges
|
import com.jakewharton.rxbinding3.widget.textChanges
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
import im.vector.matrix.android.api.failure.Failure
|
||||||
import im.vector.matrix.android.api.failure.MatrixError
|
import im.vector.matrix.android.api.failure.MatrixError
|
||||||
|
import im.vector.matrix.android.api.failure.isInvalidPassword
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.hideKeyboard
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.showPassword
|
import im.vector.riotx.core.extensions.showPassword
|
||||||
|
@ -209,10 +210,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
|
||||||
} else {
|
} else {
|
||||||
// Trick to display the error without text.
|
// Trick to display the error without text.
|
||||||
loginFieldTil.error = " "
|
loginFieldTil.error = " "
|
||||||
if (error is Failure.ServerError
|
if (error.isInvalidPassword() && spaceInPassword()) {
|
||||||
&& error.error.code == MatrixError.M_FORBIDDEN
|
|
||||||
&& error.error.message == "Invalid password"
|
|
||||||
&& spaceInPassword()) {
|
|
||||||
passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
|
passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
|
||||||
} else {
|
} else {
|
||||||
passwordFieldTil.error = errorFormatter.toHumanReadable(error)
|
passwordFieldTil.error = errorFormatter.toHumanReadable(error)
|
||||||
|
|
|
@ -54,8 +54,12 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
|
||||||
mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)!!
|
mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)!!
|
||||||
|
|
||||||
configureToolbar(videoMediaViewerToolbar, mediaData)
|
configureToolbar(videoMediaViewerToolbar, mediaData)
|
||||||
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
|
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
|
||||||
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
|
videoContentRenderer.render(mediaData,
|
||||||
|
videoMediaViewerThumbnailView,
|
||||||
|
videoMediaViewerLoading,
|
||||||
|
videoMediaViewerVideoView,
|
||||||
|
videoMediaViewerErrorView)
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.riotx.core.utils.toast
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||||
|
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.riotx.features.debug.DebugMenuActivity
|
import im.vector.riotx.features.debug.DebugMenuActivity
|
||||||
|
@ -107,6 +108,12 @@ class DefaultNavigator @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun upgradeSessionSecurity(context: Context) {
|
||||||
|
if (context is VectorBaseActivity) {
|
||||||
|
BootstrapBottomSheet.show(context.supportFragmentManager, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) {
|
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) {
|
||||||
if (context is VectorBaseActivity) {
|
if (context is VectorBaseActivity) {
|
||||||
context.notImplemented("Open not joined room")
|
context.notImplemented("Open not joined room")
|
||||||
|
|
|
@ -34,6 +34,8 @@ interface Navigator {
|
||||||
|
|
||||||
fun waitSessionVerification(context: Context)
|
fun waitSessionVerification(context: Context)
|
||||||
|
|
||||||
|
fun upgradeSessionSecurity(context: Context)
|
||||||
|
|
||||||
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
|
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
|
||||||
|
|
||||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
|
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
|
||||||
|
|
|
@ -150,6 +150,9 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||||
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
|
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
|
||||||
const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
|
const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
|
||||||
|
|
||||||
|
// Security
|
||||||
|
const val SETTINGS_SECURITY_USE_FLAG_SECURE = "SETTINGS_SECURITY_USE_FLAG_SECURE"
|
||||||
|
|
||||||
// other
|
// other
|
||||||
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
|
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
|
||||||
private const val SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY"
|
private const val SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY"
|
||||||
|
@ -199,7 +202,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||||
SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY,
|
SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY,
|
||||||
SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY,
|
SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY,
|
||||||
|
|
||||||
SETTINGS_USE_RAGE_SHAKE_KEY
|
SETTINGS_USE_RAGE_SHAKE_KEY,
|
||||||
|
SETTINGS_SECURITY_USE_FLAG_SECURE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -746,4 +750,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||||
fun displayAllEvents(): Boolean {
|
fun displayAllEvents(): Boolean {
|
||||||
return defaultPrefs.getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false)
|
return defaultPrefs.getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user does not allow screenshots of the application
|
||||||
|
*/
|
||||||
|
fun useFlagSecure(): Boolean {
|
||||||
|
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_FLAG_SECURE, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.engine.cache.DiskCache
|
import com.bumptech.glide.load.engine.cache.DiskCache
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.failure.isInvalidPassword
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.hideKeyboard
|
import im.vector.riotx.core.extensions.hideKeyboard
|
||||||
import im.vector.riotx.core.extensions.showPassword
|
import im.vector.riotx.core.extensions.showPassword
|
||||||
|
@ -108,10 +110,14 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
// Hide the preference if password can not be updated
|
||||||
notImplemented()
|
if (session.getHomeServerCapabilities().canChangePassword) {
|
||||||
// onPasswordUpdateClick()
|
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
false
|
onPasswordUpdateClick()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mPasswordPreference.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Email
|
// Add Email
|
||||||
|
@ -684,21 +690,20 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
var passwordShown = false
|
var passwordShown = false
|
||||||
|
|
||||||
showPassword.setOnClickListener(object : View.OnClickListener {
|
showPassword.setOnClickListener {
|
||||||
override fun onClick(v: View?) {
|
passwordShown = !passwordShown
|
||||||
passwordShown = !passwordShown
|
|
||||||
|
|
||||||
oldPasswordText.showPassword(passwordShown)
|
oldPasswordText.showPassword(passwordShown)
|
||||||
newPasswordText.showPassword(passwordShown)
|
newPasswordText.showPassword(passwordShown)
|
||||||
confirmNewPasswordText.showPassword(passwordShown)
|
confirmNewPasswordText.showPassword(passwordShown)
|
||||||
|
|
||||||
showPassword.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
showPassword.setImageResource(if (passwordShown) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(activity)
|
val dialog = AlertDialog.Builder(activity)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
.setPositiveButton(R.string.settings_change_password_submit, null)
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(R.string.settings_change_password, null)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setOnDismissListener {
|
.setOnDismissListener {
|
||||||
view.hideKeyboard()
|
view.hideKeyboard()
|
||||||
|
@ -707,12 +712,13 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
dialog.setOnShowListener {
|
dialog.setOnShowListener {
|
||||||
val updateButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
val updateButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
val cancelButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
|
||||||
updateButton.isEnabled = false
|
updateButton.isEnabled = false
|
||||||
|
|
||||||
fun updateUi() {
|
fun updateUi() {
|
||||||
val oldPwd = oldPasswordText.text.toString().trim()
|
val oldPwd = oldPasswordText.text.toString()
|
||||||
val newPwd = newPasswordText.text.toString().trim()
|
val newPwd = newPasswordText.text.toString()
|
||||||
val newConfirmPwd = confirmNewPasswordText.text.toString().trim()
|
val newConfirmPwd = confirmNewPasswordText.text.toString()
|
||||||
|
|
||||||
updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd
|
updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd
|
||||||
|
|
||||||
|
@ -750,6 +756,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
confirmNewPasswordText.isEnabled = false
|
confirmNewPasswordText.isEnabled = false
|
||||||
changePasswordLoader.isVisible = true
|
changePasswordLoader.isVisible = true
|
||||||
updateButton.isEnabled = false
|
updateButton.isEnabled = false
|
||||||
|
cancelButton.isEnabled = false
|
||||||
} else {
|
} else {
|
||||||
showPassword.isEnabled = true
|
showPassword.isEnabled = true
|
||||||
oldPasswordText.isEnabled = true
|
oldPasswordText.isEnabled = true
|
||||||
|
@ -757,6 +764,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
confirmNewPasswordText.isEnabled = true
|
confirmNewPasswordText.isEnabled = true
|
||||||
changePasswordLoader.isVisible = false
|
changePasswordLoader.isVisible = false
|
||||||
updateButton.isEnabled = true
|
updateButton.isEnabled = true
|
||||||
|
cancelButton.isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,47 +776,32 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
|
||||||
|
|
||||||
view.hideKeyboard()
|
view.hideKeyboard()
|
||||||
|
|
||||||
val oldPwd = oldPasswordText.text.toString().trim()
|
val oldPwd = oldPasswordText.text.toString()
|
||||||
val newPwd = newPasswordText.text.toString().trim()
|
val newPwd = newPasswordText.text.toString()
|
||||||
|
|
||||||
notImplemented()
|
|
||||||
/* TODO
|
|
||||||
showPasswordLoadingView(true)
|
showPasswordLoadingView(true)
|
||||||
|
session.changePassword(oldPwd, newPwd, object : MatrixCallback<Unit> {
|
||||||
session.updatePassword(oldPwd, newPwd, object : MatrixCallback<Unit> {
|
override fun onSuccess(data: Unit) {
|
||||||
private fun onDone(@StringRes textResId: Int) {
|
if (!isAdded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
showPasswordLoadingView(false)
|
showPasswordLoadingView(false)
|
||||||
|
dialog.dismiss()
|
||||||
|
activity.toast(R.string.settings_password_updated)
|
||||||
|
}
|
||||||
|
|
||||||
if (textResId == R.string.settings_fail_to_update_password_invalid_current_password) {
|
override fun onFailure(failure: Throwable) {
|
||||||
oldPasswordTil.error = getString(textResId)
|
if (!isAdded) {
|
||||||
} else {
|
return
|
||||||
dialog.dismiss()
|
|
||||||
activity.toast(textResId, Toast.LENGTH_LONG)
|
|
||||||
}
|
}
|
||||||
}
|
showPasswordLoadingView(false)
|
||||||
|
if (failure.isInvalidPassword()) {
|
||||||
override fun onSuccess(info: Void?) {
|
oldPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
|
||||||
onDone(R.string.settings_password_updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNetworkError(e: Exception) {
|
|
||||||
onDone(R.string.settings_fail_to_update_password)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMatrixError(e: MatrixError) {
|
|
||||||
if (e.error == "Invalid password") {
|
|
||||||
onDone(R.string.settings_fail_to_update_password_invalid_current_password)
|
|
||||||
} else {
|
} else {
|
||||||
dialog.dismiss()
|
oldPasswordTil.error = getString(R.string.settings_fail_to_update_password)
|
||||||
onDone(R.string.settings_fail_to_update_password)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnexpectedError(e: Exception) {
|
|
||||||
onDone(R.string.settings_fail_to_update_password)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
|
@ -58,23 +58,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
override var titleRes = R.string.settings_security_and_privacy
|
override var titleRes = R.string.settings_security_and_privacy
|
||||||
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
|
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
|
||||||
|
|
||||||
// devices: device IDs and device names
|
|
||||||
private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf()
|
|
||||||
|
|
||||||
private var mMyDeviceInfo: DeviceInfo? = null
|
|
||||||
|
|
||||||
// cryptography
|
// cryptography
|
||||||
private val mCryptographyCategory by lazy {
|
private val mCryptographyCategory by lazy {
|
||||||
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
|
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
|
||||||
}
|
}
|
||||||
// cryptography manage
|
|
||||||
private val mCryptographyManageCategory by lazy {
|
|
||||||
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!!
|
|
||||||
}
|
|
||||||
// displayed pushers
|
|
||||||
private val mPushersSettingsCategory by lazy {
|
|
||||||
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mCrossSigningStatePreference by lazy {
|
private val mCrossSigningStatePreference by lazy {
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!!
|
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!!
|
||||||
|
@ -106,7 +93,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
// My device name may have been updated
|
// My device name may have been updated
|
||||||
refreshMyDevice()
|
refreshMyDevice()
|
||||||
refreshXSigningStatus()
|
refreshXSigningStatus()
|
||||||
mCryptographyCategory.isVisible = vectorPreferences.developerMode()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bindPref() {
|
override fun bindPref() {
|
||||||
|
@ -133,7 +119,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshXSigningStatus() {
|
private fun refreshXSigningStatus() {
|
||||||
if (vectorPreferences.developerMode()) {
|
|
||||||
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
|
||||||
val xSigningIsEnableInAccount = crossSigningKeys != null
|
val xSigningIsEnableInAccount = crossSigningKeys != null
|
||||||
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
|
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
|
||||||
|
@ -154,9 +139,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
mCrossSigningStatePreference.isVisible = true
|
mCrossSigningStatePreference.isVisible = true
|
||||||
} else {
|
|
||||||
mCrossSigningStatePreference.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||||
|
@ -203,7 +185,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private fun exportKeys() {
|
private fun exportKeys() {
|
||||||
// We need WRITE_EXTERNAL permission
|
// We need WRITE_EXTERNAL permission
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||||
|
R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
|
@ -346,15 +331,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
// Cryptography
|
// Cryptography
|
||||||
// ==============================================================================================================
|
// ==============================================================================================================
|
||||||
|
|
||||||
private fun removeCryptographyPreference() {
|
|
||||||
preferenceScreen.let {
|
|
||||||
it.removePreference(mCryptographyCategory)
|
|
||||||
|
|
||||||
// Also remove keys management section
|
|
||||||
it.removePreference(mCryptographyManageCategory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the cryptography preference section.
|
* Build the cryptography preference section.
|
||||||
*
|
*
|
||||||
|
|
|
@ -24,13 +24,15 @@ import im.vector.riotx.core.ui.list.genericItem
|
||||||
import im.vector.riotx.core.ui.list.genericItemWithValue
|
import im.vector.riotx.core.ui.list.genericItemWithValue
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||||
|
import im.vector.riotx.features.settings.VectorPreferences
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CrossSigningEpoxyController @Inject constructor(
|
class CrossSigningEpoxyController @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val colorProvider: ColorProvider,
|
private val colorProvider: ColorProvider,
|
||||||
private val dimensionConverter: DimensionConverter
|
private val dimensionConverter: DimensionConverter,
|
||||||
|
private val vectorPreferences: VectorPreferences
|
||||||
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
|
||||||
|
|
||||||
interface InteractionListener {
|
interface InteractionListener {
|
||||||
|
@ -49,7 +51,7 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||||
titleIconResourceId(R.drawable.ic_shield_trusted)
|
titleIconResourceId(R.drawable.ic_shield_trusted)
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
|
||||||
}
|
}
|
||||||
if (!data.isUploadingKeys) {
|
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
|
||||||
bottomSheetVerificationActionItem {
|
bottomSheetVerificationActionItem {
|
||||||
id("resetkeys")
|
id("resetkeys")
|
||||||
title("Reset keys")
|
title("Reset keys")
|
||||||
|
@ -68,14 +70,16 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
|
||||||
}
|
}
|
||||||
if (!data.isUploadingKeys) {
|
if (!data.isUploadingKeys) {
|
||||||
bottomSheetVerificationActionItem {
|
if (vectorPreferences.developerMode()) {
|
||||||
id("resetkeys")
|
bottomSheetVerificationActionItem {
|
||||||
title("Reset keys")
|
id("resetkeys")
|
||||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
title("Reset keys")
|
||||||
iconRes(R.drawable.ic_arrow_right)
|
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
listener {
|
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
interactionListener?.onResetCrossSigningKeys()
|
listener {
|
||||||
|
interactionListener?.onResetCrossSigningKeys()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,14 +110,16 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||||
interactionListener?.verifySession()
|
interactionListener?.verifySession()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bottomSheetVerificationActionItem {
|
if (vectorPreferences.developerMode()) {
|
||||||
id("resetkeys")
|
bottomSheetVerificationActionItem {
|
||||||
title("Reset keys")
|
id("resetkeys")
|
||||||
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
title("Reset keys")
|
||||||
iconRes(R.drawable.ic_arrow_right)
|
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
iconRes(R.drawable.ic_arrow_right)
|
||||||
listener {
|
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
|
||||||
interactionListener?.onResetCrossSigningKeys()
|
listener {
|
||||||
|
interactionListener?.onResetCrossSigningKeys()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -121,7 +127,7 @@ class CrossSigningEpoxyController @Inject constructor(
|
||||||
id("not")
|
id("not")
|
||||||
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
|
||||||
}
|
}
|
||||||
if (!data.isUploadingKeys) {
|
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
|
||||||
bottomSheetVerificationActionItem {
|
bottomSheetVerificationActionItem {
|
||||||
id("initKeys")
|
id("initKeys")
|
||||||
title("Initialize keys")
|
title("Initialize keys")
|
||||||
|
|
21
vector/src/main/res/drawable/ic_file.xml
Normal file
21
vector/src/main/res/drawable/ic_file.xml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M13,2H6C4.8954,2 4,2.8954 4,4V20C4,21.1046 4.8954,22 6,22H18C19.1046,22 20,21.1046 20,20V9L13,2Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:strokeColor="#2E2F32"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M13,2V9H20"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#2E2F32"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -7,13 +7,10 @@
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M21,11.4445C21.0038,12.911 20.6612,14.3577 20,15.6667C18.401,18.8659 15.1321,20.8875 11.5555,20.8889C10.089,20.8927 8.6423,20.5501 7.3333,19.8889L1,22L3.1111,15.6667C2.4499,14.3577 2.1073,12.911 2.1111,11.4445C2.1125,7.8679 4.1341,4.599 7.3333,3C8.6423,2.3388 10.089,1.9962 11.5555,2H12.1111C16.9064,2.2646 20.7354,6.0936 21,10.8889V11.4445V11.4445Z"
|
android:pathData="M19,14C19,16.0333 17.7458,17.9018 16.043,19.4808C14.3615,21.0401 12.4,22.1689 11.3349,22.7219C11.1216,22.8327 10.8784,22.8327 10.6651,22.7219C9.6,22.1689 7.6385,21.0401 5.957,19.4808C4.2542,17.9018 3,16.0333 3,14V3.6043C3,3.1356 3.3255,2.7298 3.7831,2.6282L10.7831,1.0726C10.9259,1.0409 11.0741,1.0409 11.2169,1.0726L18.2169,2.6282C18.6745,2.7298 19,3.1356 19,3.6043V14Z"
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
android:strokeColor="#2E2F32"/>
|
||||||
android:strokeColor="#2E2F32"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</group>
|
</group>
|
||||||
<path
|
<path
|
||||||
android:pathData="M2,8C0.8954,8 0,8.8954 0,10V13C0,14.1046 0.8954,15 2,15H20C21.1046,15 22,14.1046 22,13V10C22,8.8954 21.1046,8 20,8H2ZM4.25,9.5C3.8358,9.5 3.5,9.8358 3.5,10.25C3.5,10.6642 3.8358,11 4.25,11H6.75C7.1642,11 7.5,10.6642 7.5,10.25C7.5,9.8358 7.1642,9.5 6.75,9.5H4.25ZM8.5,10.25C8.5,9.8358 8.8358,9.5 9.25,9.5H9.75C10.1642,9.5 10.5,9.8358 10.5,10.25C10.5,10.6642 10.1642,11 9.75,11H9.25C8.8358,11 8.5,10.6642 8.5,10.25ZM12.25,9.5C11.8358,9.5 11.5,9.8358 11.5,10.25C11.5,10.6642 11.8358,11 12.25,11H14.75C15.1642,11 15.5,10.6642 15.5,10.25C15.5,9.8358 15.1642,9.5 14.75,9.5H12.25ZM16.5,10.25C16.5,9.8358 16.8358,9.5 17.25,9.5H17.75C18.1642,9.5 18.5,9.8358 18.5,10.25C18.5,10.6642 18.1642,11 17.75,11H17.25C16.8358,11 16.5,10.6642 16.5,10.25ZM4.25,12C3.8358,12 3.5,12.3358 3.5,12.75C3.5,13.1642 3.8358,13.5 4.25,13.5H4.75C5.1642,13.5 5.5,13.1642 5.5,12.75C5.5,12.3358 5.1642,12 4.75,12H4.25ZM6.5,12.75C6.5,12.3358 6.8358,12 7.25,12H9.75C10.1642,12 10.5,12.3358 10.5,12.75C10.5,13.1642 10.1642,13.5 9.75,13.5H7.25C6.8358,13.5 6.5,13.1642 6.5,12.75ZM12.25,12C11.8358,12 11.5,12.3358 11.5,12.75C11.5,13.1642 11.8358,13.5 12.25,13.5H12.75C13.1642,13.5 13.5,13.1642 13.5,12.75C13.5,12.3358 13.1642,12 12.75,12H12.25Z"
|
android:pathData="M2,8C0.8954,8 0,8.8954 0,10V13C0,14.1046 0.8954,15 2,15H20C21.1046,15 22,14.1046 22,13V10C22,8.8954 21.1046,8 20,8H2ZM4.25,9.5C3.8358,9.5 3.5,9.8358 3.5,10.25C3.5,10.6642 3.8358,11 4.25,11H6.75C7.1642,11 7.5,10.6642 7.5,10.25C7.5,9.8358 7.1642,9.5 6.75,9.5H4.25ZM8.5,10.25C8.5,9.8358 8.8358,9.5 9.25,9.5H9.75C10.1642,9.5 10.5,9.8358 10.5,10.25C10.5,10.6642 10.1642,11 9.75,11H9.25C8.8358,11 8.5,10.6642 8.5,10.25ZM12.25,9.5C11.8358,9.5 11.5,9.8358 11.5,10.25C11.5,10.6642 11.8358,11 12.25,11H14.75C15.1642,11 15.5,10.6642 15.5,10.25C15.5,9.8358 15.1642,9.5 14.75,9.5H12.25ZM16.5,10.25C16.5,9.8358 16.8358,9.5 17.25,9.5H17.75C18.1642,9.5 18.5,9.8358 18.5,10.25C18.5,10.6642 18.1642,11 17.75,11H17.25C16.8358,11 16.5,10.6642 16.5,10.25ZM4.25,12C3.8358,12 3.5,12.3358 3.5,12.75C3.5,13.1642 3.8358,13.5 4.25,13.5H4.75C5.1642,13.5 5.5,13.1642 5.5,12.75C5.5,12.3358 5.1642,12 4.75,12H4.25ZM6.5,12.75C6.5,12.3358 6.8358,12 7.25,12H9.75C10.1642,12 10.5,12.3358 10.5,12.75C10.5,13.1642 10.1642,13.5 9.75,13.5H7.25C6.8358,13.5 6.5,13.1642 6.5,12.75ZM12.25,12C11.8358,12 11.5,12.3358 11.5,12.75C11.5,13.1642 11.8358,13.5 12.25,13.5H12.75C13.1642,13.5 13.5,13.1642 13.5,12.75C13.5,12.3358 13.1642,12 12.75,12H12.25Z"
|
||||||
|
|
|
@ -7,13 +7,10 @@
|
||||||
<clip-path
|
<clip-path
|
||||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M21,11.4445C21.0038,12.911 20.6612,14.3577 20,15.6667C18.401,18.8659 15.1321,20.8875 11.5555,20.8889C10.089,20.8927 8.6423,20.5501 7.3333,19.8889L1,22L3.1111,15.6667C2.4499,14.3577 2.1073,12.911 2.1111,11.4445C2.1125,7.8679 4.1341,4.599 7.3333,3C8.6423,2.3388 10.089,1.9962 11.5555,2H12.1111C16.9064,2.2646 20.7354,6.0936 21,10.8889V11.4445V11.4445Z"
|
android:pathData="M11.3349,22.7219C11.1216,22.8327 10.8784,22.8327 10.6651,22.7219C9.6,22.1689 7.6385,21.0401 5.957,19.4808C4.2542,17.9018 3,16.0333 3,14V3.6043C3,3.1356 3.3255,2.7298 3.7831,2.6282L10.7831,1.0726C10.9259,1.0409 11.0741,1.0409 11.2169,1.0726L18.2169,2.6282C18.6745,2.7298 19,3.1356 19,3.6043V14C19,16.0333 17.7458,17.9018 16.043,19.4808C14.3615,21.0401 12.4,22.1689 11.3349,22.7219Z"
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
android:strokeWidth="2"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#00000000"
|
||||||
android:fillType="evenOdd"
|
android:strokeColor="#2E2F32"/>
|
||||||
android:strokeColor="#2E2F32"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</group>
|
</group>
|
||||||
<path
|
<path
|
||||||
android:pathData="M0,10C0,8.8954 0.8954,8 2,8H20C21.1046,8 22,8.8954 22,10V13C22,14.1046 21.1046,15 20,15H2C0.8954,15 0,14.1046 0,13V10ZM5,11.5C5,12.3284 4.3284,13 3.5,13C2.6716,13 2,12.3284 2,11.5C2,10.6716 2.6716,10 3.5,10C4.3284,10 5,10.6716 5,11.5ZM8.5,13C9.3284,13 10,12.3284 10,11.5C10,10.6716 9.3284,10 8.5,10C7.6716,10 7,10.6716 7,11.5C7,12.3284 7.6716,13 8.5,13ZM15,11.5C15,12.3284 14.3284,13 13.5,13C12.6716,13 12,12.3284 12,11.5C12,10.6716 12.6716,10 13.5,10C14.3284,10 15,10.6716 15,11.5ZM18.5,13C19.3284,13 20,12.3284 20,11.5C20,10.6716 19.3284,10 18.5,10C17.6716,10 17,10.6716 17,11.5C17,12.3284 17.6716,13 18.5,13Z"
|
android:pathData="M0,10C0,8.8954 0.8954,8 2,8H20C21.1046,8 22,8.8954 22,10V13C22,14.1046 21.1046,15 20,15H2C0.8954,15 0,14.1046 0,13V10ZM5,11.5C5,12.3284 4.3284,13 3.5,13C2.6716,13 2,12.3284 2,11.5C2,10.6716 2.6716,10 3.5,10C4.3284,10 5,10.6716 5,11.5ZM8.5,13C9.3284,13 10,12.3284 10,11.5C10,10.6716 9.3284,10 8.5,10C7.6716,10 7,10.6716 7,11.5C7,12.3284 7.6716,13 8.5,13ZM15,11.5C15,12.3284 14.3284,13 13.5,13C12.6716,13 12,12.3284 12,11.5C12,10.6716 12.6716,10 13.5,10C14.3284,10 15,10.6716 15,11.5ZM18.5,13C19.3284,13 20,12.3284 20,11.5C20,10.6716 19.3284,10 18.5,10C17.6716,10 17,10.6716 17,11.5C17,12.3284 17.6716,13 18.5,13Z"
|
||||||
|
|
|
@ -39,7 +39,8 @@
|
||||||
android:id="@+id/change_password_old_pwd_til"
|
android:id="@+id/change_password_old_pwd_til"
|
||||||
style="@style/VectorTextInputLayout"
|
style="@style/VectorTextInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
app:errorEnabled="true">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/change_password_old_pwd_text"
|
android:id="@+id/change_password_old_pwd_text"
|
||||||
|
@ -53,7 +54,9 @@
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
style="@style/VectorTextInputLayout"
|
style="@style/VectorTextInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:errorEnabled="true">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/change_password_new_pwd_text"
|
android:id="@+id/change_password_new_pwd_text"
|
||||||
|
@ -69,6 +72,7 @@
|
||||||
style="@style/VectorTextInputLayout"
|
style="@style/VectorTextInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
app:errorEnabled="true">
|
app:errorEnabled="true">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bootstrapDescriptionText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bootstrapRecoveryKeyEnterTil"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="@string/bootstrap_enter_recovery" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/bootstrapRecoveryKeyEnterTil"
|
||||||
|
style="@style/VectorTextInputLayout"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/bootstrapMigrateShowPassword"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/bootstrapMigrateEditText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:singleLine="false"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
tools:hint="@string/keys_backup_restore_key_enter_hint"
|
||||||
|
tools:inputType="textPassword" />
|
||||||
|
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/bootstrapMigrateUseFile"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/use_file"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
app:icon="@drawable/ic_file"
|
||||||
|
app:iconTint="@color/button_positive_text_color_selector"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bootstrapMigrateForgotPassphrase"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:text="@string/keys_backup_restore_with_passphrase_helper_with_link"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/bootstrapMigrateShowPassword"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_eye_black"
|
||||||
|
android:tint="?colorAccent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/bootstrapRecoveryKeyEnterTil"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/bootstrapRecoveryKeyEnterTil" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/bootstrapMigrateContinueButton"
|
||||||
|
style="@style/VectorButtonStyleText"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||||
|
android:text="@string/_continue"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bootstrapRecoveryKeyEnterTil" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -2202,7 +2202,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
|
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
|
|
||||||
<string name="new_session">New Session</string>
|
<string name="new_session">Unverified login. Was this you?</string>
|
||||||
<string name="new_session_review">Tap to review & verify</string>
|
<string name="new_session_review">Tap to review & verify</string>
|
||||||
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
|
<string name="verify_new_session_notice">Use this session to verify your new one, granting it access to encrypted messages.</string>
|
||||||
<string name="verify_new_session_was_not_me">This wasn’t me</string>
|
<string name="verify_new_session_was_not_me">This wasn’t me</string>
|
||||||
|
@ -2219,7 +2219,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
<string name="verify_cancelled_notice">Verify your devices from Settings.</string>
|
<string name="verify_cancelled_notice">Verify your devices from Settings.</string>
|
||||||
<string name="verification_cancelled">Verification Cancelled</string>
|
<string name="verification_cancelled">Verification Cancelled</string>
|
||||||
|
|
||||||
<string name="recovery_passphrase">Message Password</string>
|
<string name="recovery_passphrase">Recovery Passphrase</string>
|
||||||
<string name="message_key">Message Key</string>
|
<string name="message_key">Message Key</string>
|
||||||
<string name="account_password">Account Password</string>
|
<string name="account_password">Account Password</string>
|
||||||
|
|
||||||
|
@ -2243,7 +2243,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
|
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
|
||||||
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
||||||
<string name="your_recovery_key">Your recovery key</string>
|
<string name="your_recovery_key">Your recovery key</string>
|
||||||
<string name="bootstrap_finish_title">You‘re done!</string>
|
<string name="bootstrap_finish_title">"You're done!"</string>
|
||||||
<string name="keep_it_safe">Keep it safe</string>
|
<string name="keep_it_safe">Keep it safe</string>
|
||||||
<string name="finish">Finish</string>
|
<string name="finish">Finish</string>
|
||||||
|
|
||||||
|
@ -2269,8 +2269,8 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
|
||||||
|
|
||||||
<string name="auth_flow_not_supported">You cannot do that from mobile</string>
|
<string name="auth_flow_not_supported">You cannot do that from mobile</string>
|
||||||
|
|
||||||
<string name="bootstrap_skip_text">Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
<string name="bootstrap_skip_text">Setting a Recovery Passphrase lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
||||||
<string name="bootstrap_skip_text_no_gen_key">Setting a Message Password lets you secure & unlock encrypted messages and trust.</string>
|
<string name="bootstrap_skip_text_no_gen_key">Setting a Recovery Passphrase lets you secure & unlock encrypted messages and trust.</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="encryption_enabled">Encryption enabled</string>
|
<string name="encryption_enabled">Encryption enabled</string>
|
||||||
|
|
|
@ -7,6 +7,37 @@
|
||||||
|
|
||||||
<!-- BEGIN Strings added by Valere -->
|
<!-- BEGIN Strings added by Valere -->
|
||||||
<string name="room_message_placeholder">Message…</string>
|
<string name="room_message_placeholder">Message…</string>
|
||||||
|
|
||||||
|
<string name="upgrade_security">Encryption upgrade available</string>
|
||||||
|
<string name="security_prompt_text">Verify yourself & others to keep your chats safe</string>
|
||||||
|
|
||||||
|
<!-- %s will be replaced by recovery_key -->
|
||||||
|
<string name="bootstrap_enter_recovery">Enter your %s to continue</string>
|
||||||
|
<string name="use_file">Use File</string>
|
||||||
|
|
||||||
|
<!-- %s will be replaced by recovery_passphrase -->
|
||||||
|
<!-- <string name="upgrade_account_desc">Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.</string>-->
|
||||||
|
<string name="enter_backup_passphrase">Enter %s</string>
|
||||||
|
<string name="backup_recovery_passphrase">Recovery Passphrase</string>
|
||||||
|
<string name="bootstrap_invalid_recovery_key">"It's not a valid recovery key"</string>
|
||||||
|
<string name="recovery_key_empty_error_message">Please enter a recovery key</string>
|
||||||
|
|
||||||
|
<string name="bootstrap_progress_checking_backup">Checking backup Key</string>
|
||||||
|
<string name="bootstrap_progress_checking_backup_with_info">Checking backup Key (%s)</string>
|
||||||
|
<string name="bootstrap_progress_compute_curve_key">Getting curve key</string>
|
||||||
|
<string name="bootstrap_progress_generating_ssss">Generating SSSS key from passphrase</string>
|
||||||
|
<string name="bootstrap_progress_generating_ssss_with_info">Generating SSSS key from passphrase (%s)</string>
|
||||||
|
<string name="bootstrap_progress_generating_ssss_recovery">Generating SSSS key from recovery key</string>
|
||||||
|
<string name="bootstrap_progress_storing_in_sss">Storing keybackup secret in SSSS</string>
|
||||||
|
<!-- To produce things like 'RiotX Android (IQDHUVJTTV)' -->
|
||||||
|
<string name="new_session_review_with_info">%1$s (%2$s)</string>
|
||||||
|
|
||||||
|
<string name="bootstrap_migration_enter_backup_password">Enter your Key Backup Passphrase to continue.</string>
|
||||||
|
<string name="bootstrap_migration_use_recovery_key">use your Key Backup recovery key</string>
|
||||||
|
<!-- %s will be replaced by the value of bootstrap_migration_use_recovery_key -->
|
||||||
|
<string name="bootstrap_migration_with_passphrase_helper_with_link">Don’t know your Key Backup Passphrase, you can %s.</string>
|
||||||
|
<string name="bootstrap_migration_backup_recovery_key">Key Backup recovery key</string>
|
||||||
|
|
||||||
<!-- END Strings added by Valere -->
|
<!-- END Strings added by Valere -->
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +45,12 @@
|
||||||
|
|
||||||
<!-- END Strings added by Benoit -->
|
<!-- END Strings added by Benoit -->
|
||||||
|
|
||||||
|
<!-- BEGIN Strings added by Benoit2 -->
|
||||||
|
<string name="settings_security_prevent_screenshots_title">Prevent screenshots of the application</string>
|
||||||
|
<string name="settings_security_prevent_screenshots_summary">Enabling this setting adds the FLAG_SECURE to all Activities. Restart the application for the change to take effect.</string>
|
||||||
|
|
||||||
|
<!-- END Strings added by Benoit2 -->
|
||||||
|
|
||||||
|
|
||||||
<!-- BEGIN Strings added by Ganfra -->
|
<!-- BEGIN Strings added by Ganfra -->
|
||||||
|
|
||||||
|
@ -30,4 +67,7 @@
|
||||||
|
|
||||||
<!-- END Strings added by Others -->
|
<!-- END Strings added by Others -->
|
||||||
|
|
||||||
|
<!-- BEGIN Strings added by Benoit -->
|
||||||
|
<string name="change_password_summary">Set a new account password…</string>
|
||||||
|
<!---->
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreference
|
<im.vector.riotx.core.preference.VectorPreference
|
||||||
android:key="SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY"
|
android:key="SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY"
|
||||||
android:summary="@string/password_hint"
|
android:summary="@string/change_password_summary"
|
||||||
android:title="@string/settings_password" />
|
android:title="@string/settings_password" />
|
||||||
|
|
||||||
<!-- Email will be added here -->
|
<!-- Email will be added here -->
|
||||||
|
|
|
@ -6,36 +6,33 @@
|
||||||
<!-- ************ Cryptography section ************ -->
|
<!-- ************ Cryptography section ************ -->
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||||
android:key="SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"
|
android:key="SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"
|
||||||
tools:isPreferenceVisible="true"
|
|
||||||
app:isPreferenceVisible="false"
|
|
||||||
android:title="@string/settings_cryptography">
|
android:title="@string/settings_cryptography">
|
||||||
<im.vector.riotx.core.preference.VectorPreference
|
<im.vector.riotx.core.preference.VectorPreference
|
||||||
android:key="SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
|
android:key="SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
|
||||||
tools:icon="@drawable/ic_shield_trusted"
|
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:title="@string/encryption_information_cross_signing_state"
|
android:title="@string/encryption_information_cross_signing_state"
|
||||||
tools:summary="@string/encryption_information_dg_xsigning_complete"
|
|
||||||
app:fragment="im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment"
|
app:fragment="im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment"
|
||||||
/>
|
tools:icon="@drawable/ic_shield_trusted"
|
||||||
|
tools:summary="@string/encryption_information_dg_xsigning_complete" />
|
||||||
|
|
||||||
|
|
||||||
<!-- <im.vector.riotx.core.preference.VectorPreference-->
|
<!-- <im.vector.riotx.core.preference.VectorPreference-->
|
||||||
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"-->
|
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"-->
|
||||||
<!-- android:title="@string/encryption_information_device_name" />-->
|
<!-- android:title="@string/encryption_information_device_name" />-->
|
||||||
|
|
||||||
<!-- <im.vector.riotx.core.preference.VectorPreference-->
|
<!-- <im.vector.riotx.core.preference.VectorPreference-->
|
||||||
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"-->
|
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"-->
|
||||||
<!-- android:title="@string/encryption_information_device_id" />-->
|
<!-- android:title="@string/encryption_information_device_id" />-->
|
||||||
|
|
||||||
<!-- <im.vector.riotx.core.preference.VectorPreference-->
|
<!-- <im.vector.riotx.core.preference.VectorPreference-->
|
||||||
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"-->
|
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"-->
|
||||||
<!-- android:title="@string/encryption_information_device_key" />-->
|
<!-- android:title="@string/encryption_information_device_key" />-->
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorSwitchPreference
|
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||||
|
android:enabled="false"
|
||||||
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
|
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
|
||||||
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
|
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
|
||||||
android:title="@string/encryption_never_send_to_unverified_devices_title"
|
android:title="@string/encryption_never_send_to_unverified_devices_title" />
|
||||||
android:enabled="false" />
|
|
||||||
|
|
||||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
|
@ -85,4 +82,15 @@
|
||||||
|
|
||||||
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
|
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||||
|
android:title="@string/settings_other">
|
||||||
|
|
||||||
|
<im.vector.riotx.core.preference.VectorSwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="SETTINGS_SECURITY_USE_FLAG_SECURE"
|
||||||
|
android:summary="@string/settings_security_prevent_screenshots_summary"
|
||||||
|
android:title="@string/settings_security_prevent_screenshots_title" />
|
||||||
|
|
||||||
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
</androidx.preference.PreferenceScreen>
|
</androidx.preference.PreferenceScreen>
|
Loading…
Add table
Reference in a new issue