diff --git a/CHANGES.md b/CHANGES.md index 52f728d2c5..086b8f49e3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) @@ -31,6 +32,7 @@ Bugfix 🐛: - Cross-Signing | web <-> riotX After QR code scan, gossiping fails (#1210) - 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) - RiotX now uses as many threads as it needs to do work and send messages (#1221) Translations 🗣: @@ -43,6 +45,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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index 5dfb0eab9b..3a068af076 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -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" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index c2fa7d2d32..1afeed922f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt new file mode 100644 index 0000000000..68643ff723 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/AccountService.kt @@ -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): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt new file mode 100644 index 0000000000..83ce8eb0aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/account/model/ChangePasswordParams.kt @@ -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 + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt index 9b01cdef3b..c8526985e1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/homeserver/HomeServerCapabilities.kt @@ -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 */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt index eef6b283c6..877746705f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/AudioInfo.kt @@ -24,7 +24,7 @@ data class AudioInfo( /** * The mimetype of the audio e.g. "audio/aac". */ - @Json(name = "mimetype") val mimeType: String, + @Json(name = "mimetype") val mimeType: String?, /** * The size of the audio clip in bytes. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt index 0ef1b8f658..0ee6b88e01 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/ThumbnailInfo.kt @@ -39,5 +39,5 @@ data class ThumbnailInfo( /** * The mimetype of the image, e.g. "image/jpeg". */ - @Json(name = "mimetype") val mimeType: String + @Json(name = "mimetype") val mimeType: String? ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt index 45ad43a0ef..5e672d4f59 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt @@ -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( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt index 59ee23cc62..53fecc4865 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt @@ -177,7 +177,7 @@ internal class DefaultQrCodeVerificationTransaction( }.exhaustive if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { - // // Nothing to verify + // Nothing to verify cancel(CancelCode.MismatchedKeys) return } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 66b0f1b379..a0d3662e03 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -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 ) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt index 0d067286e1..5743597a61 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -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() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 25dc939196..b30c29a719 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -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, private val accountDataService: Lazy, private val _sharedSecretStorageService: Lazy, + private val accountService: Lazy, 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() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt index 22ebb4273a..b20235448a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionComponent.kt @@ -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 { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt new file mode 100644 index 0000000000..8acfb3abb8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt @@ -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.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.Headers +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 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt new file mode 100644 index 0000000000..87e003b0d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountModule.kt @@ -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 +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt new file mode 100644 index 0000000000..ecd4b309d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/ChangePasswordTask.kt @@ -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 { + 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(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(eventBus) { + apiCall = accountAPI.changePassword( + changePasswordParams.copy(auth = changePasswordParams.auth?.copy(session = it.session)) + ) + } + return + } catch (failure: Throwable) { + throw failure + } + } + } + throw throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt new file mode 100644 index 0000000000..fce01994d3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/DefaultAccountService.kt @@ -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): Cancelable { + return changePasswordTask + .configureWith(ChangePasswordTask.Params(password, newPassword)) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt index c83a61cfb0..f37bbfe798 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/CapabilitiesAPI.kt @@ -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 + /** * Request the upload capabilities */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt index 2dbd627ce5..9f068381f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/DefaultGetHomeServerCapabilitiesTask.kt @@ -51,15 +51,23 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( apiCall = capabilitiesAPI.getUploadCapabilities() } + val capabilities = runCatching { + executeRequest(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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt new file mode 100644 index 0000000000..ffaa998789 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetCapabilitiesResult.kt @@ -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() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt index 8e410cc834..c98119db6c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/GetUploadCapabilitiesResult.kt @@ -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. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index a4a6eb6972..5f0515e669 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -314,7 +314,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", audioInfo = AudioInfo( - mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg", + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, size = attachment.size ), url = attachment.queryUri.toString() @@ -327,8 +327,7 @@ internal class LocalEchoEventFactory @Inject constructor( msgType = MessageType.MSGTYPE_FILE, body = attachment.name ?: "file", info = FileInfo( - mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } - ?: "application/octet-stream", + mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, size = attachment.size ), url = attachment.queryUri.toString() diff --git a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt index 88b6ef5463..557bc93bb1 100644 --- a/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/core/error/ErrorFormatter.kt @@ -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 -> { diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index ea954ecf27..08cf8e57e1 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -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) { diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt index 6fb746e095..57f914118b 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginFragment.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index faa48d5686..f0a5a8ace8 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -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) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 0a670e2c5a..f754064fbc 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -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 { - private fun onDone(@StringRes textResId: Int) { + session.changePassword(oldPwd, newPwd, object : MatrixCallback { + 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() diff --git a/vector/src/main/res/layout/dialog_change_password.xml b/vector/src/main/res/layout/dialog_change_password.xml index 925859bcd1..12806a56b7 100644 --- a/vector/src/main/res/layout/dialog_change_password.xml +++ b/vector/src/main/res/layout/dialog_change_password.xml @@ -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"> + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + app:errorEnabled="true"> + + Prevent screenshots of the application + Enabling this setting adds the FLAG_SECURE to all Activities. Restart the application for the change to take effect. + + + @@ -29,4 +35,7 @@ + + Set a new account password… + diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index c49eca825a..ac8fb8445c 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -19,7 +19,7 @@ diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 19bc340500..1002c16782 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -6,36 +6,35 @@ + tools:isPreferenceVisible="true"> + tools:icon="@drawable/ic_shield_trusted" + tools:summary="@string/encryption_information_dg_xsigning_complete" /> - - - + + + - - - + + + - - - + + + + android:title="@string/encryption_never_send_to_unverified_devices_title" /> @@ -85,4 +84,15 @@ + + + + + + \ No newline at end of file