Merge branch 'develop' into feature/save_media_to_gallery

This commit is contained in:
Onuray Sahin 2020-04-20 11:21:14 +03:00 committed by GitHub
commit 754f220596
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 1656 additions and 326 deletions

View file

@ -2,6 +2,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
===================================================
Features ✨:
- Change password (#528)
- Cross-Signing | Support SSSS secret sharing (#944)
- Cross-Signing | Verify new session from existing session (#1134)
- 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)
- Emoji Verification | It's not the same butterfly! (#1220)
- Cross-Signing | Composer decoration: shields (#1077)
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
Bugfix 🐛:
- Fix summary notification staying after "mark as read"
@ -32,6 +34,7 @@ Bugfix 🐛:
- Fix crash when trying to download file without internet connection (#1229)
- Local echo are not updated in timeline (for failed & encrypted states)
- Render image event even if thumbnail_info does not have mimetype defined (#1209)
- Fix issue with media path (#1227)
Translations 🗣:
-
@ -43,6 +46,7 @@ Build 🧱:
- Compile with Android SDK 29 (Android Q)
Other changes:
- Add a setting to prevent screenshots of the application, disabled by default (#1027)
- Increase File Logger capacities ( + use dev log preferences)
Changes in RiotX 0.18.1 (2020-03-17)

View file

@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key"
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
@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key"
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
@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest {
val quadS = session.sharedSecretStorageService
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")

View file

@ -31,3 +31,9 @@ fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection
|| (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"
}

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.GlobalError
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.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -59,7 +60,8 @@ interface Session :
InitialSyncProgressService,
HomeServerCapabilitiesService,
SecureStorageService,
AccountDataService {
AccountDataService,
AccountService {
/**
* The params associated to the session

View file

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

View file

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

View file

@ -26,6 +26,11 @@ interface ContentUrlResolver {
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.
*

View file

@ -217,4 +217,6 @@ interface KeysBackupService {
// For gossiping
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
}

View file

@ -17,6 +17,10 @@
package im.vector.matrix.android.api.session.homeserver
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
*/

View file

@ -29,7 +29,8 @@ data class MessageLocationContent(
@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,

View file

@ -35,12 +35,14 @@ interface SharedSecretStorageService {
* 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 key keep null if you want to generate a random key
* @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 callback Get key creation info
*/
fun generateKey(keyId: String,
key: SsssKeySpec?,
keyName: String,
keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>)

View file

@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor(
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.
* This method will update the state and will start sending keys in nominal case

View file

@ -29,7 +29,8 @@ data class CreateKeysBackupVersionBody(
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")
override val authData: JsonDict? = null

View file

@ -29,7 +29,8 @@ data class KeysVersionResult(
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")
override val authData: JsonDict? = null,

View file

@ -29,7 +29,8 @@ data class UpdateKeysBackupVersionBody(
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")
override val authData: JsonDict? = null,

View file

@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
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)
data class UserPasswordAuth(

View file

@ -65,14 +65,16 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
) : SharedSecretStorageService {
override fun generateKey(keyId: String,
key: SsssKeySpec?,
keyName: String,
keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val key = try {
ByteArray(32).also {
SecureRandom().nextBytes(it)
}
val bytes = try {
(key as? RawBytesKeySpec)?.privateKey
?: ByteArray(32).also {
SecureRandom().nextBytes(it)
}
} catch (failure: Throwable) {
callback.onFailure(failure)
return@launch
@ -102,8 +104,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
callback.onSuccess(SsssKeyCreationInfo(
keyId = keyId,
content = storageKeyContent,
recoveryKey = computeRecoveryKey(key),
keySpec = RawBytesKeySpec(key)
recoveryKey = computeRecoveryKey(bytes),
keySpec = RawBytesKeySpec(bytes)
))
}
}

View file

@ -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.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
@ -102,7 +103,8 @@ internal class DefaultVerificationService @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val verificationTransportRoomMessageFactory: VerificationTransportRoomMessageFactory,
private val verificationTransportToDeviceFactory: VerificationTransportToDeviceFactory,
private val crossSigningService: CrossSigningService
private val crossSigningService: CrossSigningService,
private val cryptoCoroutineScope: CoroutineScope
) : DefaultVerificationTransaction.Listener, VerificationService {
private val uiHandler = Handler(Looper.getMainLooper())
@ -125,7 +127,7 @@ internal class DefaultVerificationService @Inject constructor(
// Event received from the sync
fun onToDeviceEvent(event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
onStartRequestReceived(event)

View file

@ -30,6 +30,10 @@ import im.vector.matrix.android.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class SendVerificationMessageWorker(context: Context,
params: WorkerParameters)
: CoroutineWorker(context, params) {
@ -48,7 +52,7 @@ internal class SendVerificationMessageWorker(context: Context,
lateinit var cryptoService: CryptoService
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)
?: 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)
}
}
}

View file

@ -34,6 +34,9 @@ internal interface VerificationTransport {
onErrorReason: CancelCode,
onDone: (() -> Unit)?)
/**
* @param callback will be called with eventId and ValidVerificationInfoRequest in case of success
*/
fun sendVerificationRequest(supportedMethods: List<String>,
localId: String,
otherUserId: String,

View file

@ -115,7 +115,7 @@ internal class VerificationTransportRoomMessage(
?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == enqueueInfo.second }
?.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}")
tx?.cancel(onErrorReason)
} else {
@ -196,12 +196,15 @@ internal class VerificationTransportRoomMessage(
?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == workRequest.id }
?.let { wInfo ->
if (wInfo.outputData.getBoolean("failed", false)) {
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) {
callback(null, null)
} else if (wInfo.outputData.getString(localId) != null) {
callback(wInfo.outputData.getString(localId), validInfo)
} else {
callback(null, null)
val eventId = wInfo.outputData.getString(localId)
if (eventId != null) {
callback(eventId, validInfo)
} else {
callback(null, null)
}
}
workLiveData.removeObserver(this)
}

View file

@ -177,7 +177,7 @@ internal class DefaultQrCodeVerificationTransaction(
}.exhaustive
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
// // Nothing to verify
// Nothing to verify
cancel(CancelCode.MismatchedKeys)
return
}

View file

@ -26,12 +26,14 @@ internal object HomeServerCapabilitiesMapper {
fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities {
return HomeServerCapabilities(
canChangePassword = entity.canChangePassword,
maxUploadFileSize = entity.maxUploadFileSize
)
}
fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity {
return HomeServerCapabilitiesEntity(
canChangePassword = domain.canChangePassword,
maxUploadFileSize = domain.maxUploadFileSize
)
}

View file

@ -20,6 +20,7 @@ import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import io.realm.RealmObject
internal open class HomeServerCapabilitiesEntity(
var canChangePassword: Boolean = true,
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {

View file

@ -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.session.InitialSyncProgressService
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.cache.CacheService
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 accountDataService: Lazy<AccountDataService>,
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val accountService: Lazy<AccountService>,
private val timelineEventDecryptor: TimelineEventDecryptor,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session,
@ -110,7 +112,8 @@ internal class DefaultSession @Inject constructor(
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get(),
AccountDataService by accountDataService.get() {
AccountDataService by accountDataService.get(),
AccountService by accountService.get() {
override val sharedSecretStorageService: SharedSecretStorageService
get() = _sharedSecretStorageService.get()

View file

@ -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.SessionAssistedInjectModule
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.content.ContentModule
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
@Component(dependencies = [MatrixComponent::class],
modules = [
SessionModule::class,
RoomModule::class,
SyncModule::class,
HomeServerCapabilitiesModule::class,
SignOutModule::class,
GroupModule::class,
UserModule::class,
FilterModule::class,
GroupModule::class,
ContentModule::class,
CacheModule::class,
CryptoModule::class,
PushersModule::class,
AccountDataModule::class,
ProfileModule::class,
SessionAssistedInjectModule::class
]
modules = [
SessionModule::class,
RoomModule::class,
SyncModule::class,
HomeServerCapabilitiesModule::class,
SignOutModule::class,
GroupModule::class,
UserModule::class,
FilterModule::class,
GroupModule::class,
ContentModule::class,
CacheModule::class,
CryptoModule::class,
PushersModule::class,
AccountDataModule::class,
ProfileModule::class,
SessionAssistedInjectModule::class,
AccountModule::class
]
)
@SessionScope
internal interface SessionComponent {

View file

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

View file

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

View file

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

View file

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

View file

@ -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.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.network.NetworkConstants
import javax.inject.Inject
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 {
fun getUploadUrl(homeServerConnectionConfig: HomeServerConnectionConfig): String {
val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
val sep = if (baseUrl.endsWith("/")) "" else "/"
private val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
private 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? {
if (contentUrl?.isValidMatrixContentUrl() == true) {
val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
val prefix = URI_PREFIX_CONTENT_API + "download/"
return resolve(baseUrl, contentUrl, prefix)
}
return null
return contentUrl
// do not allow non-mxc content URLs
?.takeIf { it.isValidMatrixContentUrl() }
?.let {
resolve(
contentUrl = it,
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/"
)
}
}
override fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ContentUrlResolver.ThumbnailMethod): String? {
if (contentUrl?.isValidMatrixContentUrl() == true) {
val baseUrl = homeServerConnectionConfig.homeServerUri.toString()
val prefix = URI_PREFIX_CONTENT_API + "thumbnail/"
val params = "?width=$width&height=$height&method=${method.value}"
return resolve(baseUrl, contentUrl, prefix, params)
}
// do not allow non-mxc content URLs
return null
return contentUrl
// do not allow non-mxc content URLs
?.takeIf { it.isValidMatrixContentUrl() }
?.let {
resolve(
contentUrl = it,
prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/",
params = "?width=$width&height=$height&method=${method.value}"
)
}
}
private fun resolve(baseUrl: String,
contentUrl: String,
private fun resolve(contentUrl: String,
prefix: String,
params: String? = null): String? {
params: String = ""): String? {
var serverAndMediaId = contentUrl.removePrefix(MATRIX_CONTENT_URI_SCHEME)
val fragmentOffset = serverAndMediaId.indexOf("#")
var fragment = ""
@ -66,9 +66,7 @@ internal class DefaultContentUrlResolver @Inject constructor(private val homeSer
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 {

View file

@ -17,7 +17,7 @@
package im.vector.matrix.android.internal.session.content
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.network.ProgressRequestBody
import im.vector.matrix.android.internal.network.awaitResponse
@ -37,10 +37,10 @@ import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated
private val okHttpClient: OkHttpClient,
private val eventBus: EventBus,
sessionParams: SessionParams,
contentUrlResolver: ContentUrlResolver,
moshi: Moshi) {
private val uploadUrl = DefaultContentUrlResolver.getUploadUrl(sessionParams.homeServerConnectionConfig)
private val uploadUrl = contentUrlResolver.uploadUrl
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
suspend fun uploadFile(file: File,

View file

@ -46,6 +46,10 @@ private data class NewImageAttributes(
val newFileSize: Int
)
/**
* Possible previous worker: None
* Possible next worker : Always [MultipleEventSendingDispatcherWorker]
*/
internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
@ -64,12 +68,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("Starting upload media work with params $params")
if (params.lastFailureMessage != null) {
// Transmit the error
Timber.v("Stop upload media work due to input failure")
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

View file

@ -23,8 +23,13 @@ import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.worker.SessionWorkerParams
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
@ -39,6 +44,7 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)

View file

@ -69,13 +69,13 @@ internal class GroupSummaryUpdater @Inject constructor(
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
val sendWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<GetGroupDataWorker>()
val getGroupWork = workManagerProvider.matrixOneTimeWorkRequestBuilder<GetGroupDataWorker>()
.setInputData(workData)
.setConstraints(WorkManagerProvider.workConstraints)
.build()
workManagerProvider.workManager
.beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, sendWork)
.beginUniqueWork(GET_GROUP_DATA_WORKER, ExistingWorkPolicy.APPEND, getGroupWork)
.enqueue()
}

View file

@ -22,6 +22,12 @@ import retrofit2.http.GET
internal interface CapabilitiesAPI {
/**
* Request the homeserver capabilities
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "capabilities")
fun getCapabilities(): Call<GetCapabilitiesResult>
/**
* Request the upload capabilities
*/

View file

@ -51,15 +51,23 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
apiCall = capabilitiesAPI.getUploadCapabilities()
}
val capabilities = runCatching {
executeRequest<GetCapabilitiesResult>(eventBus) {
apiCall = capabilitiesAPI.getCapabilities()
}
}.getOrNull()
// 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 ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
homeServerCapabilitiesEntity.canChangePassword = getCapabilitiesResult.canChangePassword()
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN

View file

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

View file

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@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.
* If not listed or null, the size limit should be treated as unknown.

View file

@ -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.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
@ -50,6 +51,7 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)

View file

@ -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.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
// TODO This is not used. Delete?
@ -51,10 +52,12 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
.also { Timber.e("Unable to parse work parameters") }
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()

View file

@ -228,7 +228,7 @@ internal class DefaultSendService @AssistedInject constructor(
keys.forEach { isRoomEncrypted ->
// Should never be empty
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)
@ -293,14 +293,13 @@ internal class DefaultSendService @AssistedInject constructor(
private fun createUploadMediaWork(allLocalEchos: List<Event>,
attachment: ContentAttachmentData,
isRoomEncrypted: Boolean,
compressBeforeSending: Boolean,
startChain: Boolean): OneTimeWorkRequest {
compressBeforeSending: Boolean): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.startChain(startChain)
.startChain(true)
.setInputData(uploadWorkData)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()

View file

@ -35,6 +35,10 @@ import im.vector.matrix.android.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : Always [SendEventWorker]
*/
internal class EncryptEventWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
@ -53,14 +57,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
override suspend fun doWork(): Result {
Timber.v("Start Encrypt work")
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success().also {
Timber.e("Work cancelled due to input error from parent")
}
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("Start Encrypt work for event ${params.event.eventId}")
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()

View file

@ -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.room.send.SendState
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.worker.SessionWorkerParams
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
*
* 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)
: CoroutineWorker(context, params) {
@ -55,9 +59,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
override suspend fun doWork(): Result {
Timber.v("Start dispatch sending multiple event work")
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success().also {
Timber.e("Work cancelled due to input error from parent")
}
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
@ -68,6 +71,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
}
// Transmit the error if needed?
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
// Create a work for every event

View file

@ -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.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
@JsonClass(generateAdapter = true)
@ -46,10 +51,12 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure()
.also { Timber.e("Unable to parse work parameters") }
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()

View file

@ -32,6 +32,10 @@ import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
/**
* Possible previous worker: [EncryptEventWorker] or first worker
* Possible next worker : None
*/
internal class SendEventWorker(context: Context,
params: WorkerParameters)
: CoroutineWorker(context, params) {
@ -49,9 +53,8 @@ internal class SendEventWorker(context: Context,
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success().also {
Timber.e("Work cancelled due to input error from parent")
}
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
@ -65,6 +68,7 @@ internal class SendEventWorker(context: Context,
localEchoUpdater.updateSendState(event.eventId, SendState.UNDELIVERED)
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
return try {
sendEvent(event)

View file

@ -35,6 +35,10 @@ import javax.inject.Inject
private const val DEFAULT_LONG_POOL_TIMEOUT = 0L
/**
* Possible previous worker: None
* Possible next worker : None
*/
internal class SyncWorker(context: Context,
workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) {
@ -53,7 +57,10 @@ internal class SyncWorker(context: Context,
override suspend fun doWork(): Result {
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()
sessionComponent.inject(this)
return runCatching {
@ -76,7 +83,6 @@ internal class SyncWorker(context: Context,
}
companion object {
private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {

View file

@ -16,9 +16,16 @@
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 {
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?
}

View file

@ -60,7 +60,7 @@ private short
final short
### Line length is limited to 160 chars. Please split long lines
.{161}
[^─]{161}
### "DO NOT COMMIT" has been committed
DO NOT COMMIT

View file

@ -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.BootstrapConfirmPassphraseFragment
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.BootstrapWaitingFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
@ -444,4 +445,8 @@ interface FragmentModule {
@IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BootstrapMigrateBackupFragment::class)
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
}

View file

@ -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.MatrixError
import im.vector.matrix.android.api.failure.isInvalidPassword
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import java.net.HttpURLConnection
@ -54,8 +55,7 @@ class DefaultErrorFormatter @Inject constructor(
// Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted)
}
throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message == "Invalid password" -> {
throwable.isInvalidPassword() -> {
stringProvider.getString(R.string.auth_invalid_login_param)
}
throwable.error.code == MatrixError.M_USER_IN_USE -> {
@ -67,7 +67,7 @@ class DefaultErrorFormatter @Inject constructor(
throwable.error.code == MatrixError.M_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)
}
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {

View file

@ -23,6 +23,7 @@ import android.os.Parcelable
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.annotation.AttrRes
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
@ -183,6 +184,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
handleGlobalError(it)
}
// Set flag FLAG_SECURE
if (vectorPreferences.useFlagSecure()) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
doBeforeSetContentView()
if (getLayoutRes() != -1) {

View file

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

View file

@ -159,7 +159,7 @@ class KeysBackupBanner @JvmOverloads constructor(
render(state, true)
}
// PRIVATE METHODS *****************************************************************************************************************************************
// PRIVATE METHODS ****************************************************************************************************************************************
private fun setupView() {
inflate(context, R.layout.view_keys_backup_banner, this)

View file

@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor(
}
}
// PRIVATE METHODS *****************************************************************************************************************************************
// PRIVATE METHODS ****************************************************************************************************************************************
private fun setupView() {
inflate(context, R.layout.view_notification_area, this)

View file

@ -128,7 +128,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
}
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 {
override fun onPassphrase(passphrase: String) {
showWaitingView()

View file

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

View file

@ -40,4 +40,8 @@ sealed class BootstrapActions : VectorViewModelAction {
object SaveReqQueryStarted : BootstrapActions()
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
object SaveReqFailed : BootstrapActions()
object HandleForgotBackupPassphrase : BootstrapActions()
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
}

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover
import android.app.Dialog
import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
@ -26,18 +27,26 @@ import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
import javax.inject.Inject
import kotlin.reflect.KClass
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class Args(
val isNewAccount: Boolean
) : Parcelable
override val showExpanded = true
@Inject
@ -113,40 +122,70 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun invalidate() = withState(viewModel) { state ->
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))
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
}
is BootstrapStep.ConfirmPassphrase -> {
is BootstrapStep.ConfirmPassphrase -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
}
is BootstrapStep.AccountPassword -> {
is BootstrapStep.AccountPassword -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
bootstrapTitleText.text = getString(R.string.account_password)
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
}
is BootstrapStep.Initializing -> {
is BootstrapStep.Initializing -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
showFragment(BootstrapWaitingFragment::class, Bundle())
}
is BootstrapStep.SaveRecoveryKey -> {
is BootstrapStep.SaveRecoveryKey -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
}
is BootstrapStep.DoneSuccess -> {
is BootstrapStep.DoneSuccess -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
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()
}
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) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
childFragmentManager.commitTransaction {

View file

@ -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.SharedSecretStorageService
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.registration.RegistrationFlowResponse
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.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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -67,24 +66,16 @@ interface BootstrapProgressListener {
data class Params(
val userPasswordAuth: UserPasswordAuth? = null,
val progressListener: BootstrapProgressListener? = null,
val passphrase: String?
val passphrase: String?,
val keySpec: SsssKeySpec? = null
)
class BootstrapCrossSigningTask @Inject constructor(
private val session: Session,
private val stringProvider: StringProvider
) {
) : ViewModelTask<Params, BootstrapResult> {
operator fun invoke(
scope: CoroutineScope,
params: Params,
onResult: (BootstrapResult) -> Unit = {}
) {
val backgroundJob = scope.async { execute(params) }
scope.launch { onResult(backgroundJob.await()) }
}
suspend fun execute(params: Params): BootstrapResult {
override suspend fun execute(params: Params): BootstrapResult {
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
@ -124,6 +115,7 @@ class BootstrapCrossSigningTask @Inject constructor(
} ?: kotlin.run {
ssssService.generateKey(
UUID.randomUUID().toString(),
params.keySpec,
"ssss_key",
EmptyKeySigner(),
it
@ -205,14 +197,16 @@ class BootstrapCrossSigningTask @Inject constructor(
)
)
try {
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
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) {
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
}

View file

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

View file

@ -31,20 +31,26 @@ import com.nulabinc.zxcvbn.Zxcvbn
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
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.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.util.awaitCallback
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.login.ReAuthHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.OutputStream
data class BootstrapViewState(
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
val passphrase: String? = null,
val migrationRecoveryKey: String? = null,
val passphraseRepeat: String? = null,
val crossSigningInitialization: Async<Unit> = Uninitialized,
val passphraseStrength: Async<Strength> = Uninitialized,
@ -55,20 +61,13 @@ data class BootstrapViewState(
val recoverySaveFileProcess: Async<Unit> = Uninitialized
) : 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(
@Assisted initialState: BootstrapViewState,
@Assisted val args: BootstrapBottomSheet.Args,
private val stringProvider: StringProvider,
private val session: Session,
private val bootstrapTask: BootstrapCrossSigningTask,
private val migrationTask: BackupToQuadSMigrationTask,
private val reAuthHelper: ReAuthHelper
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
@ -76,7 +75,53 @@ class BootstrapSharedViewModel @AssistedInject constructor(
@AssistedInject.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 ->
@ -84,23 +129,27 @@ class BootstrapSharedViewModel @AssistedInject constructor(
is BootstrapActions.GoBack -> queryBack()
BootstrapActions.TogglePasswordVisibility -> {
when (state.step) {
is BootstrapStep.SetupPassphrase -> {
is BootstrapStep.SetupPassphrase -> {
setState {
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
}
is BootstrapStep.ConfirmPassphrase -> {
is BootstrapStep.ConfirmPassphrase -> {
setState {
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
}
is BootstrapStep.AccountPassword -> {
is BootstrapStep.AccountPassword -> {
setState {
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))
}
}
BootstrapActions.HandleForgotBackupPassphrase -> {
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true))
}
} else return@withState
}
is BootstrapActions.ReAuth -> {
startInitializeFlow(
state.currentReAuth?.copy(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
}
@ -210,7 +272,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// Business Logic
// =======================================
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
kotlin.runCatching {
os.use {
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?) {
setState {
copy(step = BootstrapStep.Initializing)
@ -247,11 +360,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
withState { state ->
viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
bootstrapTask.invoke(this, Params(
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
progressListener = progressListener,
passphrase = state.passphrase
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
)) {
when (it) {
is BootstrapResult.Success -> {
@ -309,11 +423,30 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private fun queryBack() = withState { state ->
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?
_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 {
copy(
step = BootstrapStep.SetupPassphrase(
@ -322,15 +455,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
}
is BootstrapStep.AccountPassword -> {
is BootstrapStep.AccountPassword -> {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
}
BootstrapStep.Initializing -> {
BootstrapStep.Initializing -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
}
is BootstrapStep.SaveRecoveryKey,
BootstrapStep.DoneSuccess -> {
BootstrapStep.DoneSuccess -> {
// nop
}
}
@ -344,7 +477,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
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)
}
}
}

View file

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

View file

@ -16,8 +16,7 @@
package im.vector.riotx.features.crypto.recover
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
@ -31,12 +30,22 @@ class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() {
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.Initializing) return@withState
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
when (state.step) {
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
}
}
}
}

View file

@ -91,13 +91,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
}
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)) {
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
sharedActionViewModel.isAccountCreation = true
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
}
@ -163,29 +164,48 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.getMyCrossSigningKeys()
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
// We need to ask
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
popupAlertManager.postVectorAlert(
VerificationVectorAlert(
uid = "completeSecurity",
title = getString(R.string.complete_security),
description = getString(R.string.crosssigning_verify_this_session),
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 {
it.navigator.waitSessionVerification(it)
}
}
dismissedAction = Runnable {}
}
)
promptSecurityEvent(
session,
R.string.upgrade_security,
R.string.security_prompt_text
) {
it.navigator.upgradeSessionSecurity(it)
}
} else if (myCrossSigningKeys?.isTrusted() == false) {
// We need to ask
promptSecurityEvent(
session,
R.string.complete_security,
R.string.crosssigning_verify_this_session
) {
it.navigator.waitSessionVerification(it)
}
}
}
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?) {
super.onNewIntent(intent)
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {

View file

@ -102,7 +102,7 @@ class HomeDetailFragment @Inject constructor(
VerificationVectorAlert(
uid = uid,
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
).apply {
matrixItem = user

View file

@ -21,4 +21,5 @@ import javax.inject.Inject
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
var hasDisplayedCompleteSecurityPrompt : Boolean = false
var isAccountCreation : Boolean = false
}

View file

@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
}
VerificationState.CANCELED_BY_OTHER -> {
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
}
VerificationState.CANCELED_BY_ME -> {

View file

@ -28,6 +28,7 @@ import com.airbnb.mvrx.Success
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.isInvalidPassword
import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.showPassword
@ -209,10 +210,7 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
} else {
// Trick to display the error without text.
loginFieldTil.error = " "
if (error is Failure.ServerError
&& error.error.code == MatrixError.M_FORBIDDEN
&& error.error.message == "Invalid password"
&& spaceInPassword()) {
if (error.isInvalidPassword() && spaceInPassword()) {
passwordFieldTil.error = getString(R.string.auth_invalid_login_param_space_in_password)
} else {
passwordFieldTil.error = errorFormatter.toHumanReadable(error)

View file

@ -54,8 +54,12 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)!!
configureToolbar(videoMediaViewerToolbar, mediaData)
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
videoContentRenderer.render(mediaData,
videoMediaViewerThumbnailView,
videoMediaViewerLoading,
videoMediaViewerVideoView,
videoMediaViewerErrorView)
} else {
finish()
}

View file

@ -34,6 +34,7 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
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.VerificationBottomSheet
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) {
if (context is VectorBaseActivity) {
context.notImplemented("Open not joined room")

View file

@ -34,6 +34,8 @@ interface Navigator {
fun waitSessionVerification(context: Context)
fun upgradeSessionSecurity(context: Context)
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)

View file

@ -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_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
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"
@ -199,7 +202,8 @@ class VectorPreferences @Inject constructor(private val context: Context) {
SETTINGS_SET_SYNC_TIMEOUT_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 {
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)
}
}

View file

@ -35,6 +35,8 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.cache.DiskCache
import com.google.android.material.textfield.TextInputEditText
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.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.showPassword
@ -108,10 +110,14 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
}
// Password
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notImplemented()
// onPasswordUpdateClick()
false
// Hide the preference if password can not be updated
if (session.getHomeServerCapabilities().canChangePassword) {
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
onPasswordUpdateClick()
false
}
} else {
mPasswordPreference.isVisible = false
}
// Add Email
@ -684,21 +690,20 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
var passwordShown = false
showPassword.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
passwordShown = !passwordShown
showPassword.setOnClickListener {
passwordShown = !passwordShown
oldPasswordText.showPassword(passwordShown)
newPasswordText.showPassword(passwordShown)
confirmNewPasswordText.showPassword(passwordShown)
oldPasswordText.showPassword(passwordShown)
newPasswordText.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)
.setView(view)
.setPositiveButton(R.string.settings_change_password_submit, null)
.setCancelable(false)
.setPositiveButton(R.string.settings_change_password, null)
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
view.hideKeyboard()
@ -707,12 +712,13 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
dialog.setOnShowListener {
val updateButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
val cancelButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
updateButton.isEnabled = false
fun updateUi() {
val oldPwd = oldPasswordText.text.toString().trim()
val newPwd = newPasswordText.text.toString().trim()
val newConfirmPwd = confirmNewPasswordText.text.toString().trim()
val oldPwd = oldPasswordText.text.toString()
val newPwd = newPasswordText.text.toString()
val newConfirmPwd = confirmNewPasswordText.text.toString()
updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd
@ -750,6 +756,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
confirmNewPasswordText.isEnabled = false
changePasswordLoader.isVisible = true
updateButton.isEnabled = false
cancelButton.isEnabled = false
} else {
showPassword.isEnabled = true
oldPasswordText.isEnabled = true
@ -757,6 +764,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
confirmNewPasswordText.isEnabled = true
changePasswordLoader.isVisible = false
updateButton.isEnabled = true
cancelButton.isEnabled = true
}
}
@ -768,47 +776,32 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
view.hideKeyboard()
val oldPwd = oldPasswordText.text.toString().trim()
val newPwd = newPasswordText.text.toString().trim()
val oldPwd = oldPasswordText.text.toString()
val newPwd = newPasswordText.text.toString()
notImplemented()
/* TODO
showPasswordLoadingView(true)
session.updatePassword(oldPwd, newPwd, object : MatrixCallback<Unit> {
private fun onDone(@StringRes textResId: Int) {
session.changePassword(oldPwd, newPwd, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
if (!isAdded) {
return
}
showPasswordLoadingView(false)
dialog.dismiss()
activity.toast(R.string.settings_password_updated)
}
if (textResId == R.string.settings_fail_to_update_password_invalid_current_password) {
oldPasswordTil.error = getString(textResId)
} else {
dialog.dismiss()
activity.toast(textResId, Toast.LENGTH_LONG)
override fun onFailure(failure: Throwable) {
if (!isAdded) {
return
}
}
override fun onSuccess(info: Void?) {
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)
showPasswordLoadingView(false)
if (failure.isInvalidPassword()) {
oldPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
} else {
dialog.dismiss()
onDone(R.string.settings_fail_to_update_password)
oldPasswordTil.error = getString(R.string.settings_fail_to_update_password)
}
}
override fun onUnexpectedError(e: Exception) {
onDone(R.string.settings_fail_to_update_password)
}
})
*/
}
}
dialog.show()

View file

@ -58,23 +58,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_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
private val mCryptographyCategory by lazy {
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 {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!!
@ -106,7 +93,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// My device name may have been updated
refreshMyDevice()
refreshXSigningStatus()
mCryptographyCategory.isVisible = vectorPreferences.developerMode()
}
override fun bindPref() {
@ -133,7 +119,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
private fun refreshXSigningStatus() {
if (vectorPreferences.developerMode()) {
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
@ -154,9 +139,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
mCrossSigningStatePreference.isVisible = true
} else {
mCrossSigningStatePreference.isVisible = false
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -203,7 +185,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
*/
private fun exportKeys() {
// 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 ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) {
@ -346,15 +331,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// Cryptography
// ==============================================================================================================
private fun removeCryptographyPreference() {
preferenceScreen.let {
it.removePreference(mCryptographyCategory)
// Also remove keys management section
it.removePreference(mCryptographyManageCategory)
}
}
/**
* Build the cryptography preference section.
*

View file

@ -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.utils.DimensionConverter
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span
import javax.inject.Inject
class CrossSigningEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
interface InteractionListener {
@ -49,7 +51,7 @@ class CrossSigningEpoxyController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_trusted)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
}
if (!data.isUploadingKeys) {
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
@ -68,14 +70,16 @@ class CrossSigningEpoxyController @Inject constructor(
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
}
if (!data.isUploadingKeys) {
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
interactionListener?.onResetCrossSigningKeys()
if (vectorPreferences.developerMode()) {
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
interactionListener?.onResetCrossSigningKeys()
}
}
}
@ -106,14 +110,16 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.verifySession()
}
}
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
interactionListener?.onResetCrossSigningKeys()
if (vectorPreferences.developerMode()) {
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
interactionListener?.onResetCrossSigningKeys()
}
}
}
} else {
@ -121,7 +127,7 @@ class CrossSigningEpoxyController @Inject constructor(
id("not")
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
}
if (!data.isUploadingKeys) {
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
bottomSheetVerificationActionItem {
id("initKeys")
title("Initialize keys")

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

View file

@ -7,13 +7,10 @@
<clip-path
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
<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:strokeLineJoin="round"
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:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
android:strokeColor="#2E2F32"/>
</group>
<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"

View file

@ -7,13 +7,10 @@
<clip-path
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
<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:strokeLineJoin="round"
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:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
android:strokeColor="#2E2F32"/>
</group>
<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"

View file

@ -39,7 +39,8 @@
android:id="@+id/change_password_old_pwd_til"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/change_password_old_pwd_text"
@ -53,7 +54,9 @@
<com.google.android.material.textfield.TextInputLayout
style="@style/VectorTextInputLayout"
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
android:id="@+id/change_password_new_pwd_text"
@ -69,6 +72,7 @@
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText

View file

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

View file

@ -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="new_session">New Session</string>
<string name="new_session">Unverified login. Was this you?</string>
<string name="new_session_review">Tap to review &amp; 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_was_not_me">This wasnt 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="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="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_title">Setting up recovery.</string>
<string name="your_recovery_key">Your recovery key</string>
<string name="bootstrap_finish_title">Youre done!</string>
<string name="bootstrap_finish_title">"You're done!"</string>
<string name="keep_it_safe">Keep it safe</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="bootstrap_skip_text">Setting a Message Password lets you secure &amp; unlock encrypted messages and trust.\n\nIf you dont 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 &amp; unlock encrypted messages and trust.</string>
<string name="bootstrap_skip_text">Setting a Recovery Passphrase lets you secure &amp; unlock encrypted messages and trust.\n\nIf you dont want to set a Message Password, generate a Message Key instead.</string>
<string name="bootstrap_skip_text_no_gen_key">Setting a Recovery Passphrase lets you secure &amp; unlock encrypted messages and trust.</string>
<string name="encryption_enabled">Encryption enabled</string>

View file

@ -7,6 +7,37 @@
<!-- BEGIN Strings added by Valere -->
<string name="room_message_placeholder">Message…</string>
<string name="upgrade_security">Encryption upgrade available</string>
<string name="security_prompt_text">Verify yourself &amp; 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">Dont 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 -->
@ -14,6 +45,12 @@
<!-- 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 -->
@ -30,4 +67,7 @@
<!-- END Strings added by Others -->
<!-- BEGIN Strings added by Benoit -->
<string name="change_password_summary">Set a new account password…</string>
<!---->
</resources>

View file

@ -19,7 +19,7 @@
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY"
android:summary="@string/password_hint"
android:summary="@string/change_password_summary"
android:title="@string/settings_password" />
<!-- Email will be added here -->

View file

@ -6,36 +6,33 @@
<!-- ************ Cryptography section ************ -->
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"
tools:isPreferenceVisible="true"
app:isPreferenceVisible="false"
android:title="@string/settings_cryptography">
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
tools:icon="@drawable/ic_shield_trusted"
android:persistent="false"
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"
/>
tools:icon="@drawable/ic_shield_trusted"
tools:summary="@string/encryption_information_dg_xsigning_complete" />
<!-- <im.vector.riotx.core.preference.VectorPreference-->
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"-->
<!-- android:title="@string/encryption_information_device_name" />-->
<!-- <im.vector.riotx.core.preference.VectorPreference-->
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"-->
<!-- android:title="@string/encryption_information_device_name" />-->
<!-- <im.vector.riotx.core.preference.VectorPreference-->
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"-->
<!-- android:title="@string/encryption_information_device_id" />-->
<!-- <im.vector.riotx.core.preference.VectorPreference-->
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"-->
<!-- android:title="@string/encryption_information_device_id" />-->
<!-- <im.vector.riotx.core.preference.VectorPreference-->
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"-->
<!-- android:title="@string/encryption_information_device_key" />-->
<!-- <im.vector.riotx.core.preference.VectorPreference-->
<!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"-->
<!-- android:title="@string/encryption_information_device_key" />-->
<im.vector.riotx.core.preference.VectorSwitchPreference
android:enabled="false"
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
android:title="@string/encryption_never_send_to_unverified_devices_title"
android:enabled="false" />
android:title="@string/encryption_never_send_to_unverified_devices_title" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
@ -85,4 +82,15 @@
</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>