Merge branch 'develop' into fix_697

This commit is contained in:
Benoit Marty 2020-01-08 18:17:23 +01:00 committed by GitHub
commit f4492e570d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2044 additions and 1002 deletions

View file

@ -8,15 +8,22 @@ Improvements 🙌:
- The initial sync is now handled by a foreground service - The initial sync is now handled by a foreground service
- Render aliases and canonical alias change in the timeline - Render aliases and canonical alias change in the timeline
- Fix autocompletion issues and add support for rooms and groups - Fix autocompletion issues and add support for rooms and groups
- Introduce developer mode in the settings (#796)
- Improve devices list screen
- Add settings for rageshake sensibility
- Fix autocompletion issues and add support for rooms, groups, and emoji (#780)
- Show skip to bottom FAB while scrolling down (#752)
Other changes: Other changes:
- - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800)
Bugfix 🐛: Bugfix 🐛:
- Fix crash when opening room creation screen from the room filtering screen - Fix crash when opening room creation screen from the room filtering screen
- Fix avatar image disappearing (#777) - Fix avatar image disappearing (#777)
- Fix read marker banner when permalink - Fix read marker banner when permalink
- Fix joining upgraded rooms (#697) - Fix joining upgraded rooms (#697)
- Fix matrix.org room directory not being browsable (#807)
- Hide non working settings (#751)
Translations 🗣: Translations 🗣:
- -

View file

@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4) ?.chunked(4)
?.joinToString(separator = " ") ?.joinToString(separator = " ")
fun MutableList<DeviceInfo>.sortByLastSeen() { /* ==========================================================================================
sortWith(DatedObjectComparators.descComparator) * DeviceInfo
* ========================================================================================== */
fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> {
val list = toMutableList()
list.sortWith(DatedObjectComparators.descComparator)
return list
} }

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -89,6 +90,8 @@ interface CryptoService {
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
fun isRoomEncrypted(roomId: String): Boolean fun isRoomEncrypted(roomId: String): Boolean

View file

@ -0,0 +1,23 @@
/*
* Copyright 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.auth
import im.vector.matrix.android.internal.util.md5
internal fun createSessionId(userId: String, deviceId: String?): String {
return (if (deviceId.isNullOrBlank()) userId else "$userId|$deviceId").md5()
}

View file

@ -16,6 +16,9 @@
package im.vector.matrix.android.internal.auth.db package im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.createSessionId
import im.vector.matrix.android.internal.di.MoshiProvider
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
@ -23,35 +26,60 @@ import timber.log.Timber
internal object AuthRealmMigration : RealmMigration { internal object AuthRealmMigration : RealmMigration {
// Current schema version // Current schema version
const val SCHEMA_VERSION = 2L const val SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) { if (oldVersion <= 0) migrateTo1(realm)
Timber.d("Step 0 -> 1") if (oldVersion <= 1) migrateTo2(realm)
Timber.d("Create PendingSessionEntity") if (oldVersion <= 2) migrateTo3(realm)
}
realm.schema.create("PendingSessionEntity") private fun migrateTo1(realm: DynamicRealm) {
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java) Timber.d("Step 0 -> 1")
.setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true) Timber.d("Create PendingSessionEntity")
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
if (oldVersion <= 1) { realm.schema.create("PendingSessionEntity")
Timber.d("Step 1 -> 2") .addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true") .setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
realm.schema.get("SessionParamsEntity") private fun migrateTo2(realm: DynamicRealm) {
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) Timber.d("Step 1 -> 2")
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
}
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3")
Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
realm.schema.get("SessionParamsEntity")
?.removePrimaryKey()
?.addField(SessionParamsEntityFields.SESSION_ID, String::class.java)
?.setRequired(SessionParamsEntityFields.SESSION_ID, true)
?.transform {
val userId = it.getString(SessionParamsEntityFields.USER_ID)
val credentialsJson = it.getString(SessionParamsEntityFields.CREDENTIALS_JSON)
val credentials = MoshiProvider.providesMoshi()
.adapter(Credentials::class.java)
.fromJson(credentialsJson)
it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId))
}
?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID)
} }
} }

View file

@ -20,7 +20,8 @@ import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity( internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "", @PrimaryKey var sessionId: String = "",
var userId: String = "",
var credentialsJson: String = "", var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "", var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out // Set to false when the token is invalid and the user has been soft logged out

View file

@ -20,6 +20,7 @@ import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.createSessionId
import javax.inject.Inject import javax.inject.Inject
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) { internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
return null return null
} }
return SessionParamsEntity( return SessionParamsEntity(
createSessionId(sessionParams.credentials.userId, sessionParams.credentials.deviceId),
sessionParams.credentials.userId, sessionParams.credentials.userId,
credentialsJson, credentialsJson,
homeServerConnectionConfigJson, homeServerConnectionConfigJson,

View file

@ -47,7 +47,7 @@ internal abstract class CryptoModule {
@Module @Module
companion object { companion object {
internal const val DB_ALIAS_PREFIX = "crypto_module_" internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
@JvmStatic @JvmStatic
@Provides @Provides
@ -59,7 +59,7 @@ internal abstract class CryptoModule {
return RealmConfiguration.Builder() return RealmConfiguration.Builder()
.directory(directory) .directory(directory)
.apply { .apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5") realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
} }
.name("crypto_store.realm") .name("crypto_store.realm")
.modules(RealmCryptoStoreModule()) .modules(RealmCryptoStoreModule())
@ -123,6 +123,9 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
@Binds @Binds
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask

View file

@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask, private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
// Tasks // Tasks
private val getDevicesTask: GetDevicesTask, private val getDevicesTask: GetDevicesTask,
private val getDeviceInfoTask: GetDeviceInfoTask,
private val setDeviceNameTask: SetDeviceNameTask, private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask, private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
getDeviceInfoTask
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
} }

View file

@ -25,11 +25,18 @@ internal interface CryptoApi {
/** /**
* Get the devices list * Get the devices list
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices * Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
fun getDevices(): Call<DevicesListResponse> fun getDevices(): Call<DevicesListResponse>
/**
* Get the device info by id
* Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}")
fun getDeviceInfo(@Path("deviceId") deviceId: String): Call<DeviceInfo>
/** /**
* Upload device and/or one-time keys. * Upload device and/or one-time keys.
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload

View file

@ -0,0 +1,37 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.internal.crypto.api.CryptoApi
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface GetDeviceInfoTask : Task<GetDeviceInfoTask.Params, DeviceInfo> {
data class Params(val deviceId: String)
}
internal class DefaultGetDeviceInfoTask @Inject constructor(private val cryptoApi: CryptoApi)
: GetDeviceInfoTask {
override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo {
return executeRequest {
apiCall = cryptoApi.getDeviceInfo(params.deviceId)
}
}
}

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.database
import android.content.Context import android.content.Context
import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.UserCacheDirectory import im.vector.matrix.android.internal.di.UserCacheDirectory
import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.session.SessionModule import im.vector.matrix.android.internal.session.SessionModule
@ -37,13 +38,14 @@ private const val REALM_NAME = "disk_store.realm"
*/ */
internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils, internal class SessionRealmConfigurationFactory @Inject constructor(private val realmKeysUtils: RealmKeysUtils,
@UserCacheDirectory val directory: File, @UserCacheDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String, @UserMd5 val userMd5: String,
context: Context) { context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
fun create(): RealmConfiguration { fun create(): RealmConfiguration {
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
if (shouldClearRealm) { if (shouldClearRealm) {
Timber.v("************************************************************") Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.") Timber.v("The realm file session was corrupted and couldn't be loaded.")
@ -53,14 +55,14 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
} }
sharedPreferences sharedPreferences
.edit() .edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true) .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.apply() .apply()
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.directory(directory) .directory(directory)
.name(REALM_NAME) .name(REALM_NAME)
.apply { .apply {
realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
} }
.modules(SessionRealmModule()) .modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded() .deleteRealmIfMigrationNeeded()
@ -71,7 +73,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
Timber.v("Successfully create realm instance") Timber.v("Successfully create realm instance")
sharedPreferences sharedPreferences
.edit() .edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false) .putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.apply() .apply()
} }
return realmConfiguration return realmConfiguration

View file

@ -31,3 +31,10 @@ internal annotation class UserId
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class UserMd5 internal annotation class UserMd5
/**
* Used to inject the sessionId, which is defined as md5(userId|deviceId)
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class SessionId

View file

@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.UserMd5 import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
@ -42,7 +42,7 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor(private val context: Context, internal class DefaultFileService @Inject constructor(private val context: Context,
@UserMd5 private val userMd5: String, @SessionId private val sessionId: String,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService { private val coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
return when (downloadMode) { return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> { FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File): // Create dir tree (MF stands for Matrix File):
// <cache>/MF/<md5(userId)>/<md5(id)>/ // <cache>/MF/<sessionId>/<md5(id)>/
val tmpFolderRoot = File(context.cacheDir, "MF") val tmpFolderRoot = File(context.cacheDir, "MF")
val tmpFolderUser = File(tmpFolderRoot, userMd5) val tmpFolderUser = File(tmpFolderRoot, sessionId)
File(tmpFolderUser, id.md5()) File(tmpFolderUser, id.md5())
} }
FileService.DownloadMode.TO_EXPORT -> { FileService.DownloadMode.TO_EXPORT -> {

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.auth.createSessionId
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.di.*
@ -54,8 +55,7 @@ internal abstract class SessionModule {
@Module @Module
companion object { companion object {
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
internal const val DB_ALIAS_PREFIX = "session_db_"
@JvmStatic @JvmStatic
@Provides @Provides
@ -83,11 +83,26 @@ internal abstract class SessionModule {
return userId.md5() return userId.md5()
} }
@JvmStatic
@SessionId
@Provides
fun providesSessionId(credentials: Credentials): String {
return createSessionId(credentials.userId, credentials.deviceId)
}
@JvmStatic @JvmStatic
@Provides @Provides
@UserCacheDirectory @UserCacheDirectory
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File { fun providesFilesDir(@UserMd5 userMd5: String,
return File(context.filesDir, userMd5) @SessionId sessionId: String,
context: Context): File {
// Temporary code for migration
val old = File(context.filesDir, userMd5)
if (old.exists()) {
old.renameTo(File(context.filesDir, sessionId))
}
return File(context.filesDir, sessionId)
} }
@JvmStatic @JvmStatic

View file

@ -97,8 +97,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
userFile.deleteRecursively() userFile.deleteRecursively()
Timber.d("SignOut: clear the database keys") Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5) realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5) realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
// Sanity check // Sanity check
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {

View file

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:name=".VectorApplication" android:name=".VectorApplication"

View file

@ -45,6 +45,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@ -228,6 +229,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsIgnoredUsersFragment::class) @FragmentKey(VectorSettingsIgnoredUsersFragment::class)
fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsDevicesFragment::class)
fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(SASVerificationIncomingFragment::class) @FragmentKey(SASVerificationIncomingFragment::class)

View file

@ -0,0 +1,32 @@
/*
* Copyright 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.hardware
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
fun vibrate(context: Context, durationMillis: Long = 100) {
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(durationMillis)
}
}

View file

@ -54,6 +54,7 @@ import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.themes.ActivityOtherThemes import im.vector.riotx.features.themes.ActivityOtherThemes
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import im.vector.riotx.receivers.DebugReceiver import im.vector.riotx.receivers.DebugReceiver
@ -88,9 +89,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
private lateinit var configurationViewModel: ConfigurationViewModel private lateinit var configurationViewModel: ConfigurationViewModel
private lateinit var sessionListener: SessionListener private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter protected lateinit var bugReporter: BugReporter
private lateinit var rageShake: RageShake lateinit var rageShake: RageShake
private set
protected lateinit var navigator: Navigator protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var activeSessionHolder: ActiveSessionHolder
private lateinit var vectorPreferences: VectorPreferences
// Filter for multiple invalid token error // Filter for multiple invalid token error
private var mainActivityStarted = false private var mainActivityStarted = false
@ -135,7 +138,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
screenComponent = DaggerScreenComponent.factory().create(getVectorComponent(), this) val vectorComponent = getVectorComponent()
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
val timeForInjection = measureTimeMillis { val timeForInjection = measureTimeMillis {
injectWith(screenComponent) injectWith(screenComponent)
} }
@ -150,6 +154,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
rageShake = screenComponent.rageShake() rageShake = screenComponent.rageShake()
navigator = screenComponent.navigator() navigator = screenComponent.navigator()
activeSessionHolder = screenComponent.activeSessionHolder() activeSessionHolder = screenComponent.activeSessionHolder()
vectorPreferences = vectorComponent.vectorPreferences()
configurationViewModel.activityRestarter.observe(this, Observer { configurationViewModel.activityRestarter.observe(this, Observer {
if (!it.hasBeenHandled) { if (!it.hasBeenHandled) {
// Recreate the Activity because configuration has changed // Recreate the Activity because configuration has changed
@ -226,7 +231,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
configurationViewModel.onActivityResumed() configurationViewModel.onActivityResumed()
if (this !is BugReportActivity) { if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
rageShake.start() rageShake.start()
} }

View file

@ -1,36 +0,0 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.preference
import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import im.vector.riotx.R
/**
* Divider for Preference screen
*/
class VectorPreferenceDivider @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
init {
layoutResource = R.layout.vector_preference_divider
}
}

View file

@ -19,16 +19,14 @@ package im.vector.riotx.core.utils
import android.os.Handler import android.os.Handler
internal class Debouncer(private val handler: Handler) { class Debouncer(private val handler: Handler) {
private val runnables = HashMap<String, Runnable>() private val runnables = HashMap<String, Runnable>()
fun debounce(identifier: String, millis: Long, r: Runnable): Boolean { fun debounce(identifier: String, millis: Long, r: Runnable): Boolean {
if (runnables.containsKey(identifier)) { // debounce
// debounce cancel(identifier)
val old = runnables[identifier]
handler.removeCallbacks(old)
}
insertRunnable(identifier, r, millis) insertRunnable(identifier, r, millis)
return true return true
} }
@ -37,6 +35,14 @@ internal class Debouncer(private val handler: Handler) {
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
} }
fun cancel(identifier: String) {
if (runnables.containsKey(identifier)) {
val old = runnables[identifier]
handler.removeCallbacks(old)
runnables.remove(identifier)
}
}
private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
val chained = Runnable { val chained = Runnable {
handler.post(r) handler.post(r)

View file

@ -0,0 +1,82 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.autocomplete.emoji
import android.graphics.Typeface
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.reactions.ReactionClickListener
import im.vector.riotx.features.reactions.data.EmojiItem
import javax.inject.Inject
class AutocompleteEmojiController @Inject constructor(
private val fontProvider: EmojiCompatFontProvider
) : TypedEpoxyController<List<EmojiItem>>() {
var emojiTypeface: Typeface? = fontProvider.typeface
private val fontProviderListener = object : EmojiCompatFontProvider.FontProviderListener {
override fun compatibilityFontUpdate(typeface: Typeface?) {
emojiTypeface = typeface
}
}
init {
fontProvider.addListener(fontProviderListener)
}
var listener: AutocompleteClickListener<String>? = null
override fun buildModels(data: List<EmojiItem>?) {
if (data.isNullOrEmpty()) {
return
}
data
.take(MAX)
.forEach { emojiItem ->
autocompleteEmojiItem {
id(emojiItem.name)
emojiItem(emojiItem)
emojiTypeFace(emojiTypeface)
onClickListener(
object : ReactionClickListener {
override fun onReactionSelected(reaction: String) {
listener?.onItemClick(reaction)
}
}
)
}
}
if (data.size > MAX) {
autocompleteMoreResultItem {
id("more_result")
}
}
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
fontProvider.removeListener(fontProviderListener)
}
companion object {
const val MAX = 50
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.autocomplete.emoji
import android.graphics.Typeface
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.reactions.ReactionClickListener
import im.vector.riotx.features.reactions.data.EmojiItem
@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji)
abstract class AutocompleteEmojiItem : VectorEpoxyModel<AutocompleteEmojiItem.Holder>() {
@EpoxyAttribute
lateinit var emojiItem: EmojiItem
@EpoxyAttribute
var emojiTypeFace: Typeface? = null
@EpoxyAttribute
var onClickListener: ReactionClickListener? = null
override fun bind(holder: Holder) {
holder.emojiText.text = emojiItem.emoji
holder.emojiText.typeface = emojiTypeFace ?: Typeface.DEFAULT
holder.emojiNameText.text = emojiItem.name
holder.emojiKeywordText.setTextOrHide(emojiItem.keywords.joinToString())
holder.view.setOnClickListener {
onClickListener?.onReactionSelected(emojiItem.emoji)
}
}
class Holder : VectorEpoxyHolder() {
val emojiText by bind<TextView>(R.id.itemAutocompleteEmoji)
val emojiNameText by bind<TextView>(R.id.itemAutocompleteEmojiName)
val emojiKeywordText by bind<TextView>(R.id.itemAutocompleteEmojiSubname)
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.autocomplete.emoji
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.reactions.data.EmojiDataSource
import javax.inject.Inject
class AutocompleteEmojiPresenter @Inject constructor(context: Context,
private val emojiDataSource: EmojiDataSource,
private val controller: AutocompleteEmojiController) :
RecyclerViewPresenter<String>(context), AutocompleteClickListener<String> {
init {
controller.listener = this
}
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
// Also remove animation
recyclerView?.itemAnimator = null
return controller.adapter
}
override fun onItemClick(t: String) {
dispatchClick(t)
}
override fun onQuery(query: CharSequence?) {
val data = if (query.isNullOrBlank()) {
// Return common emojis
emojiDataSource.getQuickReactions()
} else {
emojiDataSource.filterWith(query.toString())
}
controller.setData(data)
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.autocomplete.emoji
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@EpoxyModelClass(layout = R.layout.item_autocomplete_more_result)
abstract class AutocompleteMoreResultItem : VectorEpoxyModel<AutocompleteMoreResultItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -0,0 +1,237 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.Spannable
import android.widget.EditText
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.emoji.AutocompleteEmojiPresenter
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.themes.ThemeUtils
import javax.inject.Inject
class AutoCompleter @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val commandAutocompletePolicy: CommandAutocompletePolicy,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteUserPresenter: AutocompleteUserPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter
) {
private lateinit var editText: EditText
fun enterSpecialMode() {
commandAutocompletePolicy.enabled = false
}
fun exitSpecialMode() {
commandAutocompletePolicy.enabled = true
}
private val glideRequests by lazy {
GlideApp.with(editText)
}
fun setup(editText: EditText, listener: AutoCompleterListener) {
this.editText = editText
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background))
setupCommands(backgroundDrawable, editText)
setupUsers(backgroundDrawable, editText, listener)
setupRooms(backgroundDrawable, editText, listener)
setupGroups(backgroundDrawable, editText, listener)
setupEmojis(backgroundDrawable, editText)
}
fun render(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
}
private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on<Command>(editText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
editable.clear()
editable
.append(item.command)
.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) {
autocompleteUserPresenter.callback = listener
Autocomplete.on<User>(editText)
.with(CharPolicy('@', true))
.with(autocompleteUserPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<User> {
override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
insertMatrixItem(editText, editable, "@", item.toMatrixItem())
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) {
autocompleteRoomPresenter.callback = listener
Autocomplete.on<RoomSummary>(editText)
.with(CharPolicy('#', true))
.with(autocompleteRoomPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomSummary> {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem())
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) {
autocompleteGroupPresenter.callback = listener
Autocomplete.on<GroupSummary>(editText)
.with(CharPolicy('+', true))
.with(autocompleteGroupPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<GroupSummary> {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
insertMatrixItem(editText, editable, "+", item.toMatrixItem())
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) {
Autocomplete.on<String>(editText)
.with(CharPolicy(':', false))
.with(autocompleteEmojiPresenter)
.with(ELEVATION)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<String> {
override fun onPopupItemClicked(editable: Editable, item: String): Boolean {
// Detect last ":" and remove it
var startIndex = editable.lastIndexOf(":")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
editable.replace(startIndex, endIndex, item)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
}
private fun insertMatrixItem(editText: EditText, editable: Editable, firstChar: String, matrixItem: MatrixItem) {
// Detect last firstChar and remove it
var startIndex = editable.lastIndexOf(firstChar)
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
editText.context,
matrixItem
)
span.bind(editText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
interface AutoCompleterListener :
AutocompleteUserPresenter.Callback,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback
companion object {
private const val ELEVATION = 6f
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 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.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import im.vector.riotx.core.utils.Debouncer
import timber.log.Timber
/**
* Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event
* - When user scrolls up (i.e. going to the past): hide
* - When user scrolls down: show if not displaying last event
* - When user stops scrolling: show if not displaying last event
*/
class JumpToBottomViewVisibilityManager(
private val jumpToBottomView: FloatingActionButton,
private val debouncer: Debouncer,
recyclerView: RecyclerView,
private val layoutManager: LinearLayoutManager) {
init {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
debouncer.cancel("jump_to_bottom_visibility")
val scrollingToPast = dy < 0
if (scrollingToPast) {
jumpToBottomView.hide()
} else {
maybeShowJumpToBottomViewVisibility()
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
maybeShowJumpToBottomViewVisibilityWithDelay()
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> Unit
}
}
})
}
fun maybeShowJumpToBottomViewVisibilityWithDelay() {
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
maybeShowJumpToBottomViewVisibility()
})
}
private fun maybeShowJumpToBottomViewVisibility() {
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
if (layoutManager.findFirstVisibleItemPosition() != 0) {
jumpToBottomView.show()
} else {
jumpToBottomView.hide()
}
}
}

View file

@ -20,12 +20,10 @@ import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Editable
import android.text.Spannable import android.text.Spannable
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
@ -84,11 +75,6 @@ import im.vector.riotx.core.utils.*
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
import im.vector.riotx.features.attachments.AttachmentsHelper import im.vector.riotx.features.attachments.AttachmentsHelper
import im.vector.riotx.features.attachments.ContactAttachment import im.vector.riotx.features.attachments.ContactAttachment
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.group.AutocompleteGroupPresenter
import im.vector.riotx.features.autocomplete.room.AutocompleteRoomPresenter
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.getColorFromUserId
@ -117,7 +103,6 @@ import im.vector.riotx.features.permalink.PermalinkHandler
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor(
private val session: Session, private val session: Session,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val timelineEventController: TimelineEventController, private val timelineEventController: TimelineEventController,
private val commandAutocompletePolicy: CommandAutocompletePolicy, private val autoCompleter: AutoCompleter,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteUserPresenter: AutocompleteUserPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val permalinkHandler: PermalinkHandler, private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor(
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
AutocompleteUserPresenter.Callback, AutoCompleter.AutoCompleterListener,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
JumpToReadMarkerView.Callback, JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback, AttachmentTypeSelectorView.Callback,
@ -202,6 +181,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private lateinit var layoutManager: LinearLayoutManager private lateinit var layoutManager: LinearLayoutManager
private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
private var modelBuildListener: OnModelBuildFinishedListener? = null private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var attachmentsHelper: AttachmentsHelper private lateinit var attachmentsHelper: AttachmentsHelper
@ -333,6 +313,13 @@ class RoomDetailFragment @Inject constructor(
} }
} }
} }
jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager(
jumpToBottomView,
debouncer,
recyclerView,
layoutManager
)
} }
private fun setupJumpToReadMarkerView() { private fun setupJumpToReadMarkerView() {
@ -397,7 +384,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun renderRegularMode(text: String) { private fun renderRegularMode(text: String) {
commandAutocompletePolicy.enabled = true autoCompleter.exitSpecialMode()
composerLayout.collapse() composerLayout.collapse()
updateComposerText(text) updateComposerText(text)
@ -408,7 +395,7 @@ class RoomDetailFragment @Inject constructor(
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
@StringRes descriptionRes: Int, @StringRes descriptionRes: Int,
defaultContent: String) { defaultContent: String) {
commandAutocompletePolicy.enabled = false autoCompleter.enterSpecialMode()
// switch to expanded bar // switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName() text = event.getDisambiguatedDisplayName()
@ -495,25 +482,11 @@ class RoomDetailFragment @Inject constructor(
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback) it.dispatchTo(scrollOnHighlightedEventCallback)
updateJumpToReadMarkerViewVisibility() updateJumpToReadMarkerViewVisibility()
updateJumpToBottomViewVisibility() jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
} }
timelineEventController.addModelBuildListener(modelBuildListener) timelineEventController.addModelBuildListener(modelBuildListener)
recyclerView.adapter = timelineEventController.adapter recyclerView.adapter = timelineEventController.adapter
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
updateJumpToBottomViewVisibility()
}
RecyclerView.SCROLL_STATE_DRAGGING,
RecyclerView.SCROLL_STATE_SETTLING -> {
jumpToBottomView.hide()
}
}
}
})
timelineEventController.callback = this timelineEventController.callback = this
if (vectorPreferences.swipeToReplyIsEnabled()) { if (vectorPreferences.swipeToReplyIsEnabled()) {
@ -568,176 +541,8 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun updateJumpToBottomViewVisibility() {
debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
if (layoutManager.findFirstVisibleItemPosition() != 0) {
jumpToBottomView.show()
} else {
jumpToBottomView.hide()
}
})
}
private fun setupComposer() { private fun setupComposer() {
val elevation = 6f autoCompleter.setup(composerLayout.composerEditText, this)
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
Autocomplete.on<Command>(composerLayout.composerEditText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<Command> {
override fun onPopupItemClicked(editable: Editable, item: Command): Boolean {
editable.clear()
editable
.append(item.command)
.append(" ")
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteRoomPresenter.callback = this
Autocomplete.on<RoomSummary>(composerLayout.composerEditText)
.with(CharPolicy('#', true))
.with(autocompleteRoomPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<RoomSummary> {
override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean {
// Detect last '#' and remove it
var startIndex = editable.lastIndexOf("#")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toRoomAliasMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteGroupPresenter.callback = this
Autocomplete.on<GroupSummary>(composerLayout.composerEditText)
.with(CharPolicy('+', true))
.with(autocompleteGroupPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<GroupSummary> {
override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean {
// Detect last '+' and remove it
var startIndex = editable.lastIndexOf("+")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
autocompleteUserPresenter.callback = this
Autocomplete.on<User>(composerLayout.composerEditText)
.with(CharPolicy('@', true))
.with(autocompleteUserPresenter)
.with(elevation)
.with(backgroundDrawable)
.with(object : AutocompleteCallback<User> {
override fun onPopupItemClicked(editable: Editable, item: User): Boolean {
// Detect last '@' and remove it
var startIndex = editable.lastIndexOf("@")
if (startIndex == -1) {
startIndex = 0
}
// Detect next word separator
var endIndex = editable.indexOf(" ", startIndex)
if (endIndex == -1) {
endIndex = editable.length
}
// Replace the word by its completion
val matrixItem = item.toMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
matrixItem
)
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {
}
})
.build()
composerLayout.callback = object : TextComposerView.Callback { composerLayout.callback = object : TextComposerView.Callback {
override fun onAddAttachment() { override fun onAddAttachment() {
@ -834,9 +639,7 @@ class RoomDetailFragment @Inject constructor(
} }
private fun renderTextComposerState(state: TextComposerViewState) { private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers) autoCompleter.render(state)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
} }
private fun renderTombstoneEventHandling(async: Async<String>) { private fun renderTombstoneEventHandling(async: Async<String>) {

View file

@ -42,6 +42,8 @@ import im.vector.riotx.features.home.room.detail.timeline.format.NoticeEventForm
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.reactions.data.EmojiDataSource
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -86,7 +88,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private val htmlCompressor: VectorHtmlCompressor, private val htmlCompressor: VectorHtmlCompressor,
private val session: Session, private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter, private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences
) : VectorViewModel<MessageActionState, MessageActionsAction>(initialState) { ) : VectorViewModel<MessageActionState, MessageActionsAction>(initialState) {
private val eventId = initialState.eventId private val eventId = initialState.eventId
@ -99,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> { companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
@ -159,7 +159,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
RxRoom(room) RxRoom(room)
.liveAnnotationSummary(eventId) .liveAnnotationSummary(eventId)
.map { annotations -> .map { annotations ->
quickEmojis.map { emoji -> EmojiDataSource.quickEmojis.map { emoji ->
ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false) ToggleState(emoji, annotations.getOrNull()?.reactionsSummary?.firstOrNull { it.key == emoji }?.addedByMe ?: false)
} }
} }
@ -268,12 +268,15 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
} }
add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent())) if (vectorPreferences.developerMode()) {
if (event.isEncrypted()) { add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent()))
val decryptedContent = event.root.toClearContentStringWithIndent() if (event.isEncrypted()) {
?: stringProvider.getString(R.string.encryption_information_decryption_error) val decryptedContent = event.root.toClearContentStringWithIndent()
add(EventSharedAction.ViewDecryptedSource(decryptedContent)) ?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(EventSharedAction.ViewDecryptedSource(decryptedContent))
}
} }
add(EventSharedAction.CopyPermalink(eventId)) add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != event.root.senderId) { if (session.myUserId != event.root.senderId) {

View file

@ -120,8 +120,8 @@ class DefaultNavigator @Inject constructor(
context.startActivity(intent) context.startActivity(intent)
} }
override fun openSettings(context: Context) { override fun openSettings(context: Context, directAccess: Int) {
val intent = VectorSettingsActivity.getIntent(context) val intent = VectorSettingsActivity.getIntent(context, directAccess)
context.startActivity(intent) context.startActivity(intent)
} }

View file

@ -19,6 +19,7 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
interface Navigator { interface Navigator {
@ -39,7 +40,7 @@ interface Navigator {
fun openRoomsFiltering(context: Context) fun openRoomsFiltering(context: Context)
fun openSettings(context: Context) fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT)
fun openDebug(context: Context) fun openDebug(context: Context)

View file

@ -19,33 +19,32 @@ package im.vector.riotx.features.rageshake
import android.content.Context import android.content.Context
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorManager import android.hardware.SensorManager
import android.preference.PreferenceManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.squareup.seismic.ShakeDetector import com.squareup.seismic.ShakeDetector
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.hardware.vibrate
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.settings.VectorSettingsActivity
import javax.inject.Inject import javax.inject.Inject
class RageShake @Inject constructor(private val activity: AppCompatActivity, class RageShake @Inject constructor(private val activity: AppCompatActivity,
private val bugReporter: BugReporter) : ShakeDetector.Listener { private val bugReporter: BugReporter,
private val navigator: Navigator,
private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener {
private var shakeDetector: ShakeDetector? = null private var shakeDetector: ShakeDetector? = null
private var dialogDisplayed = false private var dialogDisplayed = false
var interceptor: (() -> Unit)? = null
fun start() { fun start() {
if (!isEnable(activity)) { val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager ?: return
return
}
val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager
if (sensorManager == null) {
return
}
shakeDetector = ShakeDetector(this).apply { shakeDetector = ShakeDetector(this).apply {
setSensitivity(vectorPreferences.getRageshakeSensitivity())
start(sensorManager) start(sensorManager)
} }
} }
@ -54,52 +53,43 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
shakeDetector?.stop() shakeDetector?.stop()
} }
/** fun setSensitivity(sensitivity: Int) {
* Enable the feature, and start it shakeDetector?.setSensitivity(sensitivity)
*/
fun enable() {
PreferenceManager.getDefaultSharedPreferences(activity).edit {
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
}
start()
}
/**
* Disable the feature, and stop it
*/
fun disable() {
PreferenceManager.getDefaultSharedPreferences(activity).edit {
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, false)
}
stop()
} }
override fun hearShake() { override fun hearShake() {
if (dialogDisplayed) { val i = interceptor
// Filtered! if (i != null) {
return vibrate(activity)
i.invoke()
} else {
if (dialogDisplayed) {
// Filtered!
return
}
vibrate(activity)
dialogDisplayed = true
AlertDialog.Builder(activity)
.setMessage(R.string.send_bug_report_alert_message)
.setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
.setNeutralButton(R.string.settings) { _, _ -> openSettings() }
.setOnDismissListener { dialogDisplayed = false }
.setNegativeButton(R.string.no, null)
.show()
} }
dialogDisplayed = true
AlertDialog.Builder(activity)
.setMessage(R.string.send_bug_report_alert_message)
.setPositiveButton(R.string.yes) { _, _ -> openBugReportScreen() }
.setNeutralButton(R.string.disable) { _, _ -> disable() }
.setOnDismissListener { dialogDisplayed = false }
.setNegativeButton(R.string.no, null)
.show()
} }
private fun openBugReportScreen() { private fun openBugReportScreen() {
bugReporter.openBugReportScreen(activity) bugReporter.openBugReportScreen(activity)
} }
companion object { private fun openSettings() {
private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS)
}
companion object {
/** /**
* Check if the feature is available * Check if the feature is available
*/ */
@ -107,12 +97,5 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager) return (context.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager)
?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null
} }
/**
* Check if the feature is enable (enabled by default)
*/
private fun isEnable(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true)
}
} }
} }

View file

@ -56,26 +56,10 @@ class EmojiSearchResultViewModel @AssistedInject constructor(
} }
private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { private fun updateQuery(action: EmojiSearchAction.UpdateQuery) {
val words = action.queryString.split("\\s".toRegex())
setState { setState {
copy( copy(
query = action.queryString, query = action.queryString,
// First add emojis with name matching query, sorted by name results = dataSource.filterWith(action.queryString)
// Then emojis with keyword matching any of the word in the query, sorted by name
results = dataSource.rawData.emojis
.values
.filter { emojiItem ->
emojiItem.name.contains(action.queryString, true)
}
.sortedBy { it.name }
+ dataSource.rawData.emojis
.values
.filter { emojiItem ->
words.fold(true, { prev, word ->
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
})
}
.sortedBy { it.name }
) )
} }
} }

View file

@ -33,4 +33,49 @@ class EmojiDataSource @Inject constructor(
.fromJson(input.bufferedReader().use { it.readText() }) .fromJson(input.bufferedReader().use { it.readText() })
} }
?: EmojiData(emptyList(), emptyMap(), emptyMap()) ?: EmojiData(emptyList(), emptyMap(), emptyMap())
private val quickReactions = mutableListOf<EmojiItem>()
fun filterWith(query: String): List<EmojiItem> {
val words = query.split("\\s".toRegex())
// First add emojis with name matching query, sorted by name
return (rawData.emojis.values
.filter { emojiItem ->
emojiItem.name.contains(query, true)
}
.sortedBy { it.name } +
// Then emojis with keyword matching any of the word in the query, sorted by name
rawData.emojis.values
.filter { emojiItem ->
words.fold(true, { prev, word ->
prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) }
})
}
.sortedBy { it.name })
// and ensure they will not be present twice
.distinct()
}
fun getQuickReactions(): List<EmojiItem> {
if (quickReactions.isEmpty()) {
listOf(
"+1", // 👍
"-1", // 👎
"grinning", // 😄
"tada", // 🎉
"confused", // 😕
"heart", // ❤️
"rocket", // 🚀
"eyes" // 👀
)
.mapNotNullTo(quickReactions) { rawData.emojis[it] }
}
return quickReactions
}
companion object {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
}
} }

View file

@ -48,6 +48,7 @@ class RoomDirectoryListCreator @Inject constructor(private val stringArrayProvid
if (it != userHsName) { if (it != userHsName) {
// Use the server name as a default display name // Use the server name as a default display name
result.add(RoomDirectoryData( result.add(RoomDirectoryData(
homeServer = it,
displayName = it, displayName = it,
includeAllNetworks = true includeAllNetworks = true
)) ))

View file

@ -23,6 +23,7 @@ import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.seismic.ShakeDetector
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository import im.vector.riotx.features.homeserver.ServerUrlsRepository
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
@ -62,8 +63,6 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY" const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY" const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
@ -149,12 +148,16 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS" const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
// analytics // analytics
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
// Rageshake
const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY" const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
const val SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
// other // other
const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY" const val SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY"
@ -247,8 +250,12 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
} }
fun developerMode(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY, false)
}
fun shouldShowHiddenEvents(): Boolean { fun shouldShowHiddenEvents(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false) return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY, false)
} }
fun swipeToReplyIsEnabled(): Boolean { fun swipeToReplyIsEnabled(): Boolean {
@ -256,7 +263,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
fun labAllowedExtendedLogging(): Boolean { fun labAllowedExtendedLogging(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
} }
/** /**
@ -730,14 +737,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
} }
/** /**
* Update the rage shake status. * Get the rage shake sensitivity.
*
* @param isEnabled true to enable the rage shake
*/ */
fun setUseRageshake(isEnabled: Boolean) { fun getRageshakeSensitivity(): Int {
defaultPrefs.edit { return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM)
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
}
} }
/** /**

View file

@ -54,7 +54,12 @@ class VectorSettingsActivity : VectorBaseActivity(),
if (isFirstCreation()) { if (isFirstCreation()) {
// display the fragment // display the fragment
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG) when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) {
EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS ->
replaceFragment(R.id.vector_settings_page, VectorSettingsAdvancedSettingsFragment::class.java, null, FRAGMENT_TAG)
else ->
replaceFragment(R.id.vector_settings_page, VectorSettingsRootFragment::class.java, null, FRAGMENT_TAG)
}
} }
supportFragmentManager.addOnBackStackChangedListener(this) supportFragmentManager.addOnBackStackChangedListener(this)
@ -111,7 +116,13 @@ class VectorSettingsActivity : VectorBaseActivity(),
} }
companion object { companion object {
fun getIntent(context: Context) = Intent(context, VectorSettingsActivity::class.java) fun getIntent(context: Context, directAccess: Int) = Intent(context, VectorSettingsActivity::class.java)
.apply { putExtra(EXTRA_DIRECT_ACCESS, directAccess) }
private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS"
const val EXTRA_DIRECT_ACCESS_ROOT = 0
const val EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS = 1
private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
} }

View file

@ -0,0 +1,78 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings
import androidx.preference.Preference
import androidx.preference.SeekBarPreference
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.preference.VectorSwitchPreference
import im.vector.riotx.features.rageshake.RageShake
class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() {
override var titleRes = R.string.settings_advanced_settings
override val preferenceXmlRes = R.xml.vector_settings_advanced_settings
private var rageshake: RageShake? = null
override fun onResume() {
super.onResume()
rageshake = (activity as? VectorBaseActivity)?.rageShake
rageshake?.interceptor = {
(activity as? VectorBaseActivity)?.showSnackbar(getString(R.string.rageshake_detected))
}
}
override fun onPause() {
super.onPause()
rageshake?.interceptor = null
rageshake = null
}
override fun bindPref() {
val isRageShakeAvailable = RageShake.isAvailable(requireContext())
if (isRageShakeAvailable) {
findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue as? Boolean == true) {
rageshake?.start()
} else {
rageshake?.stop()
}
true
}
findPreference<SeekBarPreference>(VectorPreferences.SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY)!!
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
(activity as? VectorBaseActivity)?.let {
val newValueAsInt = newValue as? Int ?: return@OnPreferenceChangeListener true
rageshake?.setSensitivity(newValueAsInt)
}
true
}
} else {
findPreference<VectorSwitchPreference>("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false
}
}
}

View file

@ -18,12 +18,8 @@ package im.vector.riotx.features.settings
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Typeface
import android.view.KeyEvent
import android.widget.Button import android.widget.Button
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -33,30 +29,19 @@ import androidx.preference.SwitchPreference
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.intent.ExternalIntentData import im.vector.riotx.core.intent.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.ProgressBarPreference
import im.vector.riotx.core.preference.VectorPreference import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.core.preference.VectorPreferenceDivider
import im.vector.riotx.core.utils.* import im.vector.riotx.core.utils.*
import im.vector.riotx.features.crypto.keys.KeysExporter import im.vector.riotx.features.crypto.keys.KeysExporter
import im.vector.riotx.features.crypto.keys.KeysImporter import im.vector.riotx.features.crypto.keys.KeysImporter
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor( class VectorSettingsSecurityPrivacyFragment @Inject constructor(
@ -66,9 +51,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_privacy override var titleRes = R.string.settings_security_and_privacy
override val preferenceXmlRes = R.xml.vector_settings_security_privacy override val preferenceXmlRes = R.xml.vector_settings_security_privacy
// used to avoid requesting to enter the password for each deletion
private var mAccountPassword: String = ""
// devices: device IDs and device names // devices: device IDs and device names
private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf() private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf()
@ -78,29 +60,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val mCryptographyCategory by lazy { private val mCryptographyCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
} }
private val mCryptographyCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!!
}
// cryptography manage // cryptography manage
private val mCryptographyManageCategory by lazy { private val mCryptographyManageCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!!
} }
private val mCryptographyManageCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!!
}
// displayed pushers // displayed pushers
private val mPushersSettingsDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!!
}
private val mPushersSettingsCategory by lazy { private val mPushersSettingsCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!! findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
} }
private val mDevicesListSettingsCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!!
}
private val mDevicesListSettingsCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!!
}
private val cryptoInfoDeviceNamePreference by lazy { private val cryptoInfoDeviceNamePreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
} }
@ -129,13 +96,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!! findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
} }
override fun onResume() {
super.onResume()
// My device name may have been updated
refreshMyDevice()
}
override fun bindPref() { override fun bindPref() {
// Push target // Push target
refreshPushersList() refreshPushersList()
// Device list
refreshDevicesList()
// Refresh Key Management section // Refresh Key Management section
refreshKeysManagementSection() refreshKeysManagementSection()
@ -151,16 +121,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
true true
} }
} }
// Rageshake Management
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!!.let {
it.isChecked = vectorPreferences.useRageshake()
it.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
vectorPreferences.setUseRageshake(newValue as Boolean)
true
}
}
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -353,11 +313,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private fun removeCryptographyPreference() { private fun removeCryptographyPreference() {
preferenceScreen.let { preferenceScreen.let {
it.removePreference(mCryptographyCategory) it.removePreference(mCryptographyCategory)
it.removePreference(mCryptographyCategoryDivider)
// Also remove keys management section // Also remove keys management section
it.removePreference(mCryptographyManageCategory) it.removePreference(mCryptographyManageCategory)
it.removePreference(mCryptographyManageCategoryDivider)
} }
} }
@ -375,7 +333,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDeviceRenameDialog(aMyDeviceInfo) // TODO device can be rename only from the device list screen for the moment
// displayDeviceRenameDialog(aMyDeviceInfo)
true true
} }
@ -428,342 +387,22 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// devices list // devices list
// ============================================================================================================== // ==============================================================================================================
private fun removeDevicesPreference() { private fun refreshMyDevice() {
preferenceScreen.let { // TODO Move to a ViewModel...
it.removePreference(mDevicesListSettingsCategory) session.sessionParams.credentials.deviceId?.let {
it.removePreference(mDevicesListSettingsCategoryDivider) session.getDeviceInfo(it, object : MatrixCallback<DeviceInfo> {
}
}
/**
* Force the refresh of the devices list.<br></br>
* The devices list is the list of the devices where the user as looged in.
* It can be any mobile device, as any browser.
*/
private fun refreshDevicesList() {
if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
// display a spinner while loading the devices list
if (0 == mDevicesListSettingsCategory.preferenceCount) {
activity?.let {
val preference = ProgressBarPreference(it)
mDevicesListSettingsCategory.addPreference(preference)
}
}
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
if (!isAdded) {
return
}
if (data.devices?.isEmpty() == true) {
removeDevicesPreference()
} else {
buildDevicesSettings(data.devices!!)
}
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (!isAdded) { // Ignore for this time?...
return }
}
removeDevicesPreference() override fun onSuccess(data: DeviceInfo) {
onCommonDone(failure.message) mMyDeviceInfo = data
refreshCryptographyPreference(data)
} }
}) })
} else {
removeDevicesPreference()
removeCryptographyPreference()
} }
} }
/**
* Build the devices portion of the settings.<br></br>
* Each row correspond to a device ID and its corresponding device name. Clicking on the row
* display a dialog containing: the device ID, the device name and the "last seen" information.
*
* @param aDeviceInfoList the list of the devices
*/
private fun buildDevicesSettings(aDeviceInfoList: List<DeviceInfo>) {
var preference: VectorPreference
var typeFaceHighlight: Int
var isNewList = true
val myDeviceId = session.sessionParams.credentials.deviceId
if (aDeviceInfoList.size == mDevicesNameList.size) {
isNewList = !mDevicesNameList.containsAll(aDeviceInfoList)
}
if (isNewList) {
var prefIndex = 0
mDevicesNameList.clear()
mDevicesNameList.addAll(aDeviceInfoList)
// sort before display: most recent first
mDevicesNameList.sortByLastSeen()
// start from scratch: remove the displayed ones
mDevicesListSettingsCategory.removeAll()
for (deviceInfo in mDevicesNameList) {
// set bold to distinguish current device ID
if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) {
mMyDeviceInfo = deviceInfo
typeFaceHighlight = Typeface.BOLD
} else {
typeFaceHighlight = Typeface.NORMAL
}
// add the edit text preference
preference = VectorPreference(requireActivity()).apply {
mTypeface = typeFaceHighlight
}
if (null == deviceInfo.deviceId && null == deviceInfo.displayName) {
continue
} else {
if (null != deviceInfo.deviceId) {
preference.title = deviceInfo.deviceId
}
// display name parameter can be null (new JSON API)
if (null != deviceInfo.displayName) {
preference.summary = deviceInfo.displayName
}
}
preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex
prefIndex++
// onClick handler: display device details dialog
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDeviceDetailsDialog(deviceInfo)
true
}
mDevicesListSettingsCategory.addPreference(preference)
}
refreshCryptographyPreference(mMyDeviceInfo)
}
}
/**
* Display a dialog containing the device ID, the device name and the "last seen" information.<>
* This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog])
*
* @param aDeviceInfo the device information
*/
private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) {
activity?.let {
val builder = AlertDialog.Builder(it)
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_details, null)
var textView = layout.findViewById<TextView>(R.id.device_id)
textView.text = aDeviceInfo.deviceId
// device name
textView = layout.findViewById(R.id.device_name)
val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName
textView.text = displayName
// last seen info
textView = layout.findViewById(R.id.device_last_seen)
val lastSeenIp = aDeviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = aDeviceInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
textView.text = lastSeenInfo
// title & icon
builder.setTitle(R.string.devices_details_dialog_title)
.setIcon(android.R.drawable.ic_dialog_info)
.setView(layout)
.setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) }
// disable the deletion for our own device
if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) {
builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) }
}
builder.setNeutralButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.show()
}
}
/**
* Display an alert dialog to rename a device
*
* @param aDeviceInfoToRename device info
*/
private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) {
activity?.let {
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text)
input.setText(aDeviceInfoToRename.displayName)
AlertDialog.Builder(it)
.setTitle(R.string.devices_details_device_name)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
displayLoadingView()
val newName = input.text.toString()
session.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// search which preference is updated
val count = mDevicesListSettingsCategory.preferenceCount
for (i in 0 until count) {
val pref = mDevicesListSettingsCategory.getPreference(i)
if (aDeviceInfoToRename.deviceId == pref.title) {
pref.summary = newName
}
}
// detect if the updated device is the current account one
if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) {
cryptoInfoDeviceNamePreference.summary = newName
}
// Also change the display name in aDeviceInfoToRename, in case of multiple renaming
aDeviceInfoToRename.displayName = newName
}
override fun onFailure(failure: Throwable) {
onCommonDone(failure.localizedMessage)
}
})
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
/**
* Try to delete a device.
*
* @param deviceInfo the device to delete
*/
private fun deleteDevice(deviceInfo: DeviceInfo) {
val deviceId = deviceInfo.deviceId
if (deviceId == null) {
Timber.e("## displayDeviceDeletionDialog(): sanity check failure")
return
}
displayLoadingView()
session.deleteDevice(deviceId, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
var isPasswordRequestFound = false
if (failure is Failure.RegistrationFlowError) {
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session)
}
}
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
onCommonDone(failure.localizedMessage)
}
}
})
}
/**
* Show a dialog to ask for user password, or use a previously entered password.
*/
private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) {
if (mAccountPassword.isNotEmpty()) {
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
} else {
activity?.let {
val inflater = it.layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
AlertDialog.Builder(it)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.devices_delete_dialog_title)
.setView(layout)
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
if (passwordEditText.toString().isEmpty()) {
it.toast(R.string.error_empty_field_your_password)
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
})
.setNegativeButton(R.string.cancel) { _, _ ->
hideLoadingView()
}
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
hideLoadingView()
return@OnKeyListener true
}
false
})
.show()
}
}
}
private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) {
session.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
hideLoadingView()
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
// Password is maybe not good
onCommonDone(failure.localizedMessage)
mAccountPassword = ""
}
})
}
// ============================================================================================================== // ==============================================================================================================
// pushers list management // pushers list management
// ============================================================================================================== // ==============================================================================================================
@ -860,6 +499,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE" private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
// TODO i18n // TODO i18n
private const val LABEL_UNAVAILABLE_DATA = "none" const val LABEL_UNAVAILABLE_DATA = "none"
} }
} }

View file

@ -0,0 +1,109 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.devices
import android.graphics.Typeface
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
/**
* A list item for Device.
*/
@EpoxyModelClass(layout = R.layout.item_device)
abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
@EpoxyAttribute
lateinit var deviceInfo: DeviceInfo
@EpoxyAttribute
var currentDevice = false
@EpoxyAttribute
var buttonsVisible = false
@EpoxyAttribute
var itemClickAction: (() -> Unit)? = null
@EpoxyAttribute
var renameClickAction: (() -> Unit)? = null
@EpoxyAttribute
var deleteClickAction: (() -> Unit)? = null
override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() }
holder.displayNameText.text = deviceInfo.displayName ?: ""
holder.deviceIdText.text = deviceInfo.deviceId ?: ""
val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
listOf(
holder.displayNameLabelText,
holder.displayNameText,
holder.deviceIdLabelText,
holder.deviceIdText,
holder.deviceLastSeenLabelText,
holder.deviceLastSeenText
).map {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
}
holder.buttonDelete.isVisible = !currentDevice
holder.buttons.isVisible = buttonsVisible
holder.buttonRename.setOnClickListener { renameClickAction?.invoke() }
holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() }
}
class Holder : VectorEpoxyHolder() {
val root by bind<ViewGroup>(R.id.itemDeviceRoot)
val displayNameLabelText by bind<TextView>(R.id.itemDeviceDisplayNameLabel)
val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName)
val deviceIdLabelText by bind<TextView>(R.id.itemDeviceIdLabel)
val deviceIdText by bind<TextView>(R.id.itemDeviceId)
val deviceLastSeenLabelText by bind<TextView>(R.id.itemDeviceLastSeenLabel)
val deviceLastSeenText by bind<TextView>(R.id.itemDeviceLastSeen)
val buttons by bind<View>(R.id.itemDeviceButtons)
val buttonDelete by bind<View>(R.id.itemDeviceDelete)
val buttonRename by bind<View>(R.id.itemDeviceRename)
}
}

View file

@ -0,0 +1,129 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.devices
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItemHeader
import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider) : EpoxyController() {
var callback: Callback? = null
private var viewState: DevicesViewState? = null
init {
requestModelBuild()
}
fun update(viewState: DevicesViewState) {
this.viewState = viewState
requestModelBuild()
}
override fun buildModels() {
val nonNullViewState = viewState ?: return
buildDevicesModels(nonNullViewState)
}
private fun buildDevicesModels(state: DevicesViewState) {
when (val devices = state.devices) {
is Loading,
is Uninitialized ->
loadingItem {
id("loading")
}
is Fail ->
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(devices.error))
listener { callback?.retry() }
}
is Success ->
buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId)
}
}
private fun buildDevicesList(devices: List<DeviceInfo>, myDeviceId: String, currentExpandedDeviceId: String?) {
// Current device
genericItemHeader {
id("current")
text(stringProvider.getString(R.string.devices_current_device))
}
devices
.filter {
it.deviceId == myDeviceId
}
.forEachIndexed { idx, deviceInfo ->
deviceItem {
id("myDevice$idx")
deviceInfo(deviceInfo)
currentDevice(true)
buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) }
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
}
}
// Other devices
if (devices.size > 1) {
genericItemHeader {
id("others")
text(stringProvider.getString(R.string.devices_other_devices))
}
devices
.filter {
it.deviceId != myDeviceId
}
// sort before display: most recent first
.sortByLastSeen()
.forEachIndexed { idx, deviceInfo ->
val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem {
id("device$idx")
deviceInfo(deviceInfo)
currentDevice(isCurrentDevice)
buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) }
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
}
}
}
}
interface Callback {
fun retry()
fun onDeviceClicked(deviceInfo: DeviceInfo)
fun onRenameDevice(deviceInfo: DeviceInfo)
fun onDeleteDevice(deviceInfo: DeviceInfo)
}
}

View file

@ -0,0 +1,268 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.devices
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
data class DevicesViewState(
val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized,
val currentExpandedDeviceId: String? = null,
val request: Async<Unit> = Uninitialized
) : MvRxState
sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction()
data class Delete(val deviceInfo: DeviceInfo) : DevicesAction()
data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction()
}
class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
private val session: Session)
: VectorViewModel<DevicesViewState, DevicesAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DevicesViewState): DevicesViewModel
}
companion object : MvRxViewModelFactory<DevicesViewModel, DevicesViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? {
val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.devicesViewModelFactory.create(state)
}
}
// temp storage when we ask for the user password
private var _currentDeviceId: String? = null
private var _currentSession: String? = null
private val _requestPasswordLiveData = MutableLiveData<LiveEvent<Unit>>()
val requestPasswordLiveData: LiveData<LiveEvent<Unit>>
get() = _requestPasswordLiveData
init {
refreshDevicesList()
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
private fun refreshDevicesList() {
if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
setState {
copy(
devices = Loading()
)
}
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
override fun onSuccess(data: DevicesListResponse) {
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
devices = Success(data.devices.orEmpty())
)
}
}
override fun onFailure(failure: Throwable) {
setState {
copy(
devices = Fail(failure)
)
}
}
})
} else {
// Should not happen
}
}
override fun handle(action: DevicesAction) {
return when (action) {
is DevicesAction.Retry -> refreshDevicesList()
is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action)
is DevicesAction.ToggleDevice -> handleToggleDevice(action)
}
}
private fun handleToggleDevice(action: DevicesAction.ToggleDevice) {
withState {
setState {
copy(
currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId
)
}
}
}
private fun handleRename(action: DevicesAction.Rename) {
session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
setState {
copy(
request = Success(data)
)
}
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
setState {
copy(
request = Fail(failure)
)
}
_requestErrorLiveData.postLiveEvent(failure)
}
})
}
/**
* Try to delete a device.
*/
private fun handleDelete(action: DevicesAction.Delete) {
val deviceId = action.deviceInfo.deviceId
if (deviceId == null) {
Timber.e("## handleDelete(): sanity check failure")
return
}
setState {
copy(
request = Loading()
)
}
session.deleteDevice(deviceId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
var isPasswordRequestFound = false
if (failure is Failure.RegistrationFlowError) {
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
_currentDeviceId = deviceId
_currentSession = failure.registrationFlowResponse.session
setState {
copy(
request = Success(Unit)
)
}
_requestPasswordLiveData.postLiveEvent(Unit)
}
}
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
setState {
copy(
request = Fail(failure)
)
}
_requestErrorLiveData.postLiveEvent(failure)
}
}
override fun onSuccess(data: Unit) {
setState {
copy(
request = Success(data)
)
}
// force settings update
refreshDevicesList()
}
})
}
private fun handlePassword(action: DevicesAction.Password) {
val currentDeviceId = _currentDeviceId
if (currentDeviceId.isNullOrBlank()) {
// Abort
return
}
setState {
copy(
request = Loading()
)
}
session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_currentDeviceId = null
_currentSession = null
setState {
copy(
request = Success(data)
)
}
// force settings update
refreshDevicesList()
}
override fun onFailure(failure: Throwable) {
_currentDeviceId = null
_currentSession = null
// Password is maybe not good
setState {
copy(
request = Fail(failure)
)
}
_requestErrorLiveData.postLiveEvent(failure)
}
})
}
}

View file

@ -0,0 +1,181 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.devices
import android.content.DialogInterface
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject
/**
* Display the list of the user's device
*/
class VectorSettingsDevicesFragment @Inject constructor(
val devicesViewModelFactory: DevicesViewModel.Factory,
private val devicesController: DevicesController
) : VectorBaseFragment(), DevicesController.Callback {
// used to avoid requesting to enter the password for each deletion
private var mAccountPassword: String = ""
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val devicesViewModel: DevicesViewModel by fragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
waiting_view_status_text.setText(R.string.please_wait)
waiting_view_status_text.isVisible = true
devicesController.callback = this
recyclerView.configureWith(devicesController, showDivider = true)
devicesViewModel.requestErrorLiveData.observeEvent(this) {
displayErrorDialog(it)
// Password is maybe not good, for safety measure, reset it here
mAccountPassword = ""
}
devicesViewModel.requestPasswordLiveData.observeEvent(this) {
maybeShowDeleteDeviceWithPasswordDialog()
}
}
override fun onDestroyView() {
devicesController.callback = null
recyclerView.cleanup()
super.onDestroyView()
}
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list)
}
private fun displayErrorDialog(throwable: Throwable) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(throwable))
.setPositiveButton(R.string.ok, null)
.show()
}
override fun onDeviceClicked(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo))
}
override fun onDeleteDevice(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
}
override fun onRenameDevice(deviceInfo: DeviceInfo) {
displayDeviceRenameDialog(deviceInfo)
}
override fun retry() {
devicesViewModel.handle(DevicesAction.Retry)
}
/**
* Display an alert dialog to rename a device
*
* @param deviceInfo device info
*/
private fun displayDeviceRenameDialog(deviceInfo: DeviceInfo) {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
val input = layout.findViewById<EditText>(R.id.edit_text)
input.setText(deviceInfo.displayName)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.devices_details_device_name)
.setView(layout)
.setPositiveButton(R.string.ok) { _, _ ->
val newName = input.text.toString()
devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
/**
* Show a dialog to ask for user password, or use a previously entered password.
*/
private fun maybeShowDeleteDeviceWithPasswordDialog() {
if (mAccountPassword.isNotEmpty()) {
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
} else {
val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
AlertDialog.Builder(requireActivity())
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.devices_delete_dialog_title)
.setView(layout)
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
if (passwordEditText.toString().isEmpty()) {
requireActivity().toast(R.string.error_empty_field_your_password)
return@OnClickListener
}
mAccountPassword = passwordEditText.text.toString()
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
})
.setNegativeButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.show()
}
}
override fun invalidate() = withState(devicesViewModel) { state ->
devicesController.update(state)
handleRequestStatus(state.request)
}
private fun handleRequestStatus(unIgnoreRequest: Async<Unit>) {
when (unIgnoreRequest) {
is Loading -> waiting_view.isVisible = true
else -> waiting_view.isVisible = false
}
}
}

View file

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/device_container_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:id="@+id/device_id_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device id" />
<TextView
android:id="@+id/device_name_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device name" />
<TextView
android:id="@+id/device_last_seen_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_last_seen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
</LinearLayout>
</ScrollView>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="8dp">
<TextView
android:id="@+id/itemAutocompleteEmoji"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="@color/black"
android:textSize="20dp"
tools:ignore="SpUsage"
tools:text="@sample/reactions.json/data/reaction" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/itemAutocompleteEmojiName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="12sp"
android:textStyle="bold"
tools:text="name" />
<TextView
android:id="@+id/itemAutocompleteEmojiSubname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
android:visibility="gone"
tools:text="name"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:padding="8dp"
android:text="@string/autocomplete_limited_results"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemDeviceRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/itemDeviceDisplayNameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceDisplayName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="Android phone" />
<TextView
android:id="@+id/itemDeviceIdLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="XUIDERFZAA" />
<TextView
android:id="@+id/itemDeviceLastSeenLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceLastSeen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
<LinearLayout
android:id="@+id/itemDeviceButtons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemDeviceRename"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rename" />
<com.google.android.material.button.MaterialButton
android:id="@+id/itemDeviceDelete"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/delete"
android:textColor="@color/riotx_notice"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout>

View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="5dp"
android:background="?vctr_shadow_bottom" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="?vctr_preference_divider_color" />
<View
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="?vctr_shadow_top" />
</LinearLayout>

View file

@ -63,9 +63,6 @@
<!-- room notification text color (typing, unsent...) --> <!-- room notification text color (typing, unsent...) -->
<attr name="vctr_room_notification_text_color" format="color" /> <attr name="vctr_room_notification_text_color" format="color" />
<!-- color for dividers in settings -->
<attr name="vctr_preference_divider_color" format="color" />
<!-- icon colors --> <!-- icon colors -->
<attr name="vctr_icon_tint_on_light_action_bar_color" format="color" /> <attr name="vctr_icon_tint_on_light_action_bar_color" format="color" />
<attr name="vctr_icon_tint_on_dark_action_bar_color" format="color" /> <attr name="vctr_icon_tint_on_dark_action_bar_color" format="color" />

View file

@ -5,4 +5,19 @@
<string name="notification_initial_sync">Initial Sync…</string> <string name="notification_initial_sync">Initial Sync…</string>
<string name="settings_show_devices_list">See all my devices</string>
<string name="settings_advanced_settings">Advanced settings</string>
<string name="settings_developer_mode">Developer mode</string>
<string name="settings_developer_mode_summary">The developer mode activates hidden features and may also make the application less stable. For developers only!</string>
<string name="settings_rageshake">Rageshake</string>
<string name="settings_rageshake_detection_threshold">Detection threshold</string>
<string name="settings_rageshake_detection_threshold_summary">Shake your phone to test the detection threshold</string>
<string name="rageshake_detected">Shake detected!</string>
<string name="settings">Settings</string>
<string name="devices_current_device">Current device</string>
<string name="devices_other_devices">Other devices</string>
<string name="autocomplete_limited_results">Showing only the first results, type more letters…</string>
</resources> </resources>

View file

@ -72,9 +72,6 @@
<item name="vctr_tab_home_secondary">@color/primary_color_dark_black</item> <item name="vctr_tab_home_secondary">@color/primary_color_dark_black</item>
<item name="vctr_list_divider_color">@color/list_divider_color_black</item> <item name="vctr_list_divider_color">@color/list_divider_color_black</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">@color/list_divider_color_black</item>
<item name="vctr_markdown_block_background_color">#FF4D4D4D</item> <item name="vctr_markdown_block_background_color">#FF4D4D4D</item>
<item name="vctr_pill_receipt">@drawable/pill_receipt_black</item> <item name="vctr_pill_receipt">@drawable/pill_receipt_black</item>

View file

@ -139,9 +139,6 @@
<!--Notice (secondary)--> <!--Notice (secondary)-->
<item name="vctr_room_notification_text_color">#FF61708b</item> <item name="vctr_room_notification_text_color">#FF61708b</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">@color/list_divider_color_dark</item>
<!-- icon colors --> <!-- icon colors -->
<item name="vctr_settings_icon_tint_color">@android:color/white</item> <item name="vctr_settings_icon_tint_color">@android:color/white</item>
<item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item> <item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item>

View file

@ -139,9 +139,6 @@
<!--Notice (secondary)--> <!--Notice (secondary)-->
<item name="vctr_room_notification_text_color">#FF61708b</item> <item name="vctr_room_notification_text_color">#FF61708b</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">@color/list_divider_color_light</item>
<!-- icon colors --> <!-- icon colors -->
<item name="vctr_settings_icon_tint_color">@android:color/black</item> <item name="vctr_settings_icon_tint_color">@android:color/black</item>
<item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item> <item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item>

View file

@ -88,9 +88,6 @@
<!-- room notification text color (typing, unsent...) --> <!-- room notification text color (typing, unsent...) -->
<item name="vctr_room_notification_text_color">#a0a29f</item> <item name="vctr_room_notification_text_color">#a0a29f</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">#e1e1e1</item>
<!-- icon colors --> <!-- icon colors -->
<item name="vctr_settings_icon_tint_color">@color/accent_color_status</item> <item name="vctr_settings_icon_tint_color">@color/accent_color_status</item>
<item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item> <item name="vctr_icon_tint_on_light_action_bar_color">@color/riotx_accent</item>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_developer_mode">
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:summary="@string/settings_developer_mode_summary"
android:title="@string/settings_developer_mode" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:dependency="SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY"
android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
android:summary="@string/labs_allow_extended_logging_summary"
android:title="@string/labs_allow_extended_logging" />
<!-- TODO Display unsupported events -->
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_RAGE_SHAKE_CATEGORY_KEY"
android:title="@string/settings_rageshake">
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_USE_RAGE_SHAKE_KEY"
android:title="@string/send_bug_report_rage_shake" />
<androidx.preference.SeekBarPreference
android:defaultValue="13"
android:dependency="SETTINGS_USE_RAGE_SHAKE_KEY"
android:key="SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
android:max="15"
android:summary="@string/settings_rageshake_detection_threshold_summary"
android:title="@string/settings_rageshake_detection_threshold"
app:min="11" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_notifications">
<im.vector.riotx.core.preference.VectorPreference
android:persistent="false"
android:title="@string/settings_notifications_targets"
app:fragment="im.vector.riotx.features.settings.push.PushGatewaysFragment" />
<im.vector.riotx.core.preference.VectorPreference
android:persistent="false"
android:title="@string/settings_push_rules"
app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen>

View file

@ -45,11 +45,10 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CONTACT_PREFERENCE_KEYS" android:key="SETTINGS_CONTACT_PREFERENCE_KEYS"
android:title="@string/settings_contact"> android:title="@string/settings_contact"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="CONTACT_BOOK_ACCESS_KEY" android:key="CONTACT_BOOK_ACCESS_KEY"
@ -62,8 +61,6 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_advanced"> <im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_advanced">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
@ -74,11 +71,12 @@
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_HOME_SERVER_PREFERENCE_KEY" android:key="SETTINGS_HOME_SERVER_PREFERENCE_KEY"
android:title="@string/settings_home_server" android:title="@string/settings_home_server"
tools:summary="@string/default_hs_server_url" /> tools:summary="https://homeserver.org" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY" android:key="SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY"
android:title="@string/settings_identity_server" android:title="@string/settings_identity_server"
app:isPreferenceVisible="@bool/false_not_implemented"
tools:summary="https://identity.server.url" /> tools:summary="https://identity.server.url" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
@ -91,10 +89,7 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider /> <im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/action_sign_out">
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:title="@string/action_sign_out">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SIGN_OUT_KEY" android:key="SETTINGS_SIGN_OUT_KEY"
@ -104,7 +99,8 @@
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY" android:key="SETTINGS_DEACTIVATE_ACCOUNT_CATEGORY_KEY"
android:title="@string/settings_deactivate_account_section"> android:title="@string/settings_deactivate_account_section"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY" android:key="SETTINGS_DEACTIVATE_ACCOUNT_KEY"

View file

@ -2,50 +2,44 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_OTHERS_PREFERENCE_KEY" android:key="APP_INFO_LINK_PREFERENCE_KEY"
android:title="@string/settings_other"> android:summary="@string/settings_app_info_link_summary"
android:title="@string/settings_app_info_link_title" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="APP_INFO_LINK_PREFERENCE_KEY" android:key="SETTINGS_VERSION_PREFERENCE_KEY"
android:summary="@string/settings_app_info_link_summary" android:title="@string/settings_version"
android:title="@string/settings_app_info_link_title" /> tools:summary="1.2.3" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_VERSION_PREFERENCE_KEY" android:key="SETTINGS_SDK_VERSION_PREFERENCE_KEY"
android:title="@string/settings_version" android:title="@string/settings_sdk_version"
tools:summary="1.2.3" /> tools:summary="4.5.6" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SDK_VERSION_PREFERENCE_KEY" android:key="SETTINGS_OLM_VERSION_PREFERENCE_KEY"
android:title="@string/settings_sdk_version" android:title="@string/settings_olm_version"
tools:summary="4.5.6" /> tools:summary="7.8.9" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_OLM_VERSION_PREFERENCE_KEY" android:key="SETTINGS_COPYRIGHT_PREFERENCE_KEY"
android:title="@string/settings_olm_version" android:title="@string/settings_copyright" />
tools:summary="7.8.9" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_COPYRIGHT_PREFERENCE_KEY" android:key="SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY"
android:title="@string/settings_copyright" /> android:title="@string/settings_app_term_conditions" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY" android:key="SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY"
android:title="@string/settings_app_term_conditions" /> android:title="@string/settings_privacy_policy" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY" android:key="SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
android:title="@string/settings_privacy_policy" /> android:title="@string/settings_third_party_notices" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY" android:key="SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
android:title="@string/settings_third_party_notices" /> android:title="@string/settings_other_third_party_notices" />
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_OTHER_THIRD_PARTY_NOTICES_PREFERENCE_KEY"
android:title="@string/settings_other_third_party_notices" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -34,24 +34,12 @@
<!--android:summary="@string/settings_labs_enable_send_voice_summary"--> <!--android:summary="@string/settings_labs_enable_send_voice_summary"-->
<!--android:title="@string/settings_labs_enable_send_voice" />--> <!--android:title="@string/settings_labs_enable_send_voice" />-->
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
android:title="@string/settings_labs_show_hidden_events_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
android:title="@string/labs_swipe_to_reply_in_timeline" /> android:title="@string/labs_swipe_to_reply_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_LABS_ALLOW_EXTENDED_LOGS"
android:summary="@string/labs_allow_extended_logging_summary"
android:title="@string/labs_allow_extended_logging" />
<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>--> <!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -36,8 +36,6 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<!-- For API < 26 --> <!-- For API < 26 -->
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:dialogTitle="@string/settings_notification_ringtone" android:dialogTitle="@string/settings_notification_ringtone"

View file

@ -67,27 +67,8 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY" />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY" android:key="SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY"
android:title="@string/settings_notifications_targets" /> android:title="@string/settings_notifications_targets" /-->
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY" /-->
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_expert">
<im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:persistent="false"
android:title="@string/settings_notifications_targets"
app:fragment="im.vector.riotx.features.settings.push.PushGatewaysFragment" />
<im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:persistent="false"
android:title="@string/settings_push_rules"
app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>

View file

@ -30,13 +30,15 @@
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
android:summary="@string/settings_inline_url_preview_summary" android:summary="@string/settings_inline_url_preview_summary"
android:title="@string/settings_inline_url_preview" /> android:title="@string/settings_inline_url_preview"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SEND_TYPING_NOTIF_KEY" android:key="SETTINGS_SEND_TYPING_NOTIF_KEY"
android:summary="@string/settings_send_typing_notifs_summary" android:summary="@string/settings_send_typing_notifs_summary"
android:title="@string/settings_send_typing_notifs" /> android:title="@string/settings_send_typing_notifs"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
@ -46,38 +48,45 @@
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY" android:key="SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
android:title="@string/settings_always_show_timestamps" /> android:title="@string/settings_always_show_timestamps"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_12_24_TIMESTAMPS_KEY" android:key="SETTINGS_12_24_TIMESTAMPS_KEY"
android:title="@string/settings_12_24_timestamps" /> android:title="@string/settings_12_24_timestamps"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_READ_RECEIPTS_KEY" android:key="SETTINGS_SHOW_READ_RECEIPTS_KEY"
android:summary="@string/settings_show_read_receipts_summary" android:summary="@string/settings_show_read_receipts_summary"
android:title="@string/settings_show_read_receipts" /> android:title="@string/settings_show_read_receipts"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"
android:summary="@string/settings_show_join_leave_messages_summary" android:summary="@string/settings_show_join_leave_messages_summary"
android:title="@string/settings_show_join_leave_messages" /> android:title="@string/settings_show_join_leave_messages"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY" android:key="SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY"
android:summary="@string/settings_show_avatar_display_name_changes_messages_summary" android:summary="@string/settings_show_avatar_display_name_changes_messages_summary"
android:title="@string/settings_show_avatar_display_name_changes_messages" /> android:title="@string/settings_show_avatar_display_name_changes_messages"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_VIBRATE_ON_MENTION_KEY" android:key="SETTINGS_VIBRATE_ON_MENTION_KEY"
android:title="@string/settings_vibrate_on_mention" /> android:title="@string/settings_vibrate_on_mention"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_SEND_MESSAGE_WITH_ENTER" android:key="SETTINGS_SEND_MESSAGE_WITH_ENTER"
android:summary="@string/settings_send_message_with_enter_summary" android:summary="@string/settings_send_message_with_enter_summary"
android:title="@string/settings_send_message_with_enter" /> android:title="@string/settings_send_message_with_enter"
app:isPreferenceVisible="@bool/false_not_implemented" />
<im.vector.riotx.core.preference.VectorListPreference <im.vector.riotx.core.preference.VectorListPreference
android:defaultValue="always" android:defaultValue="always"
@ -85,15 +94,15 @@
android:entryValues="@array/show_info_area_values" android:entryValues="@array/show_info_area_values"
android:key="SETTINGS_SHOW_INFO_AREA_KEY" android:key="SETTINGS_SHOW_INFO_AREA_KEY"
android:summary="%s" android:summary="%s"
android:title="@string/settings_info_area_show" /> android:title="@string/settings_info_area_show"
app:isPreferenceVisible="@bool/false_not_implemented" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_HOME_DISPLAY_KEY" android:key="SETTINGS_HOME_DISPLAY_KEY"
android:title="@string/settings_home_display"> android:title="@string/settings_home_display"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
@ -107,9 +116,9 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider /> <im.vector.riotx.core.preference.VectorPreferenceCategory
android:title="@string/settings_media"
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_media"> app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_MEDIA_SAVING_PERIOD_KEY" android:key="SETTINGS_MEDIA_SAVING_PERIOD_KEY"

View file

@ -3,58 +3,54 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_general" android:icon="@drawable/ic_settings_root_general"
android:title="@string/settings_general_title" android:title="@string/settings_general_title"
app:fragment="im.vector.riotx.features.settings.VectorSettingsGeneralFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsGeneralFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:enabled="@bool/false_not_implemented" android:enabled="@bool/false_not_implemented"
android:icon="@drawable/ic_settings_root_flair" android:icon="@drawable/ic_settings_root_flair"
android:title="@string/settings_flair" android:title="@string/settings_flair"
app:fragment="im.vector.riotx.features.settings.VectorSettingsFlairFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsFlairFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_notification" android:icon="@drawable/ic_settings_root_notification"
android:key="SETTINGS_NOTIFICATIONS_KEY" android:key="SETTINGS_NOTIFICATIONS_KEY"
android:title="@string/settings_notifications" android:title="@string/settings_notifications"
app:fragment="im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_preferences" android:icon="@drawable/ic_settings_root_preferences"
android:title="@string/settings_preferences" android:title="@string/settings_preferences"
app:fragment="im.vector.riotx.features.settings.VectorSettingsPreferencesFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsPreferencesFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:enabled="@bool/false_not_implemented" android:enabled="@bool/false_not_implemented"
android:icon="@drawable/ic_settings_root_call" android:icon="@drawable/ic_settings_root_call"
android:title="@string/preference_voice_and_video" android:title="@string/preference_voice_and_video"
app:fragment="im.vector.riotx.features.settings.VectorSettingsVoiceVideoFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsVoiceVideoFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_ignored_users" android:icon="@drawable/ic_settings_root_ignored_users"
android:title="@string/settings_ignored_users" android:title="@string/settings_ignored_users"
app:fragment="im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment" /> app:fragment="im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_security_privacy" android:icon="@drawable/ic_settings_root_security_privacy"
android:title="@string/settings_security_and_privacy" android:title="@string/settings_security_and_privacy"
app:fragment="im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent"
android:icon="@drawable/ic_settings_root_labs" android:icon="@drawable/ic_settings_root_labs"
android:title="@string/room_settings_labs_pref_title" android:title="@string/room_settings_labs_pref_title"
app:fragment="im.vector.riotx.features.settings.VectorSettingsLabsFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsLabsFragment" />
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:layout_width="match_parent" android:icon="@drawable/ic_settings_root_general"
android:title="@string/settings_advanced_settings"
app:fragment="im.vector.riotx.features.settings.VectorSettingsAdvancedSettingsFragment" />
<im.vector.riotx.core.preference.VectorPreference
android:icon="@drawable/ic_settings_root_help_about" android:icon="@drawable/ic_settings_root_help_about"
android:title="@string/preference_root_help_about" android:title="@string/preference_root_help_about"
app:fragment="im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment" /> app:fragment="im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment" />

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- ************ Cryptography section ************ --> <!-- ************ Cryptography section ************ -->
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
@ -21,11 +22,22 @@
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
android:summary="@string/encryption_never_send_to_unverified_devices_summary" android:summary="@string/encryption_never_send_to_unverified_devices_summary"
android:title="@string/encryption_never_send_to_unverified_devices_title" /> android:title="@string/encryption_never_send_to_unverified_devices_title"
app:isPreferenceVisible="@bool/false_not_implemented" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" /> <!-- devices list entry point -->
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_devices_list">
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_show_devices_list"
app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY" android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
@ -48,18 +60,10 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" />
<!-- devices list: device ids + device names -->
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_devices_list" />
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY" />
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY" android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY"
android:title="@string/settings_analytics"> android:title="@string/settings_analytics"
app:isPreferenceVisible="@bool/false_not_implemented">
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="false" android:defaultValue="false"
@ -67,9 +71,6 @@
android:summary="@string/settings_opt_in_of_analytics_summary" android:summary="@string/settings_opt_in_of_analytics_summary"
android:title="@string/settings_opt_in_of_analytics" /> android:title="@string/settings_opt_in_of_analytics" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_USE_RAGE_SHAKE_KEY"
android:title="@string/send_bug_report_rage_shake" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>