Merge branch 'develop' into fix_807

This commit is contained in:
Benoit Marty 2020-01-08 18:12:22 +01:00 committed by GitHub
commit 943be39e1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 2043 additions and 1003 deletions

View File

@ -8,15 +8,21 @@ Improvements 🙌:
- The initial sync is now handled by a foreground service
- Render aliases and canonical alias change in the timeline
- 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:
-
- Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800)
Bugfix 🐛:
- Fix crash when opening room creation screen from the room filtering screen
- Fix avatar image disappearing (#777)
- Fix read marker banner when permalink
- Fix matrix.org room directory not being browsable
- Fix matrix.org room directory not being browsable (#807)
- Hide non working settings (#751)
Translations 🗣:
-

View File

@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
?.chunked(4)
?.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.MXEncryptEventContentResult
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.RoomKeyRequestBody
@ -89,6 +90,8 @@ interface CryptoService {
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
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
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.RealmMigration
import timber.log.Timber
@ -23,35 +26,60 @@ import timber.log.Timber
internal object AuthRealmMigration : RealmMigration {
// Current schema version
const val SCHEMA_VERSION = 2L
const val SCHEMA_VERSION = 3L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
}
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.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)
}
private fun migrateTo1(realm: DynamicRealm) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
if (oldVersion <= 1) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.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")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2")
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
internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "",
@PrimaryKey var sessionId: String = "",
var userId: String = "",
var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "",
// 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.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.createSessionId
import javax.inject.Inject
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
@ -49,6 +50,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
return null
}
return SessionParamsEntity(
createSessionId(sessionParams.credentials.userId, sessionParams.credentials.deviceId),
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,

View File

@ -47,7 +47,7 @@ internal abstract class CryptoModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "crypto_module_"
internal fun getKeyAlias(userMd5: String) = "crypto_module_$userMd5"
@JvmStatic
@Provides
@ -59,7 +59,7 @@ internal abstract class CryptoModule {
return RealmConfiguration.Builder()
.directory(directory)
.apply {
realmKeysUtils.configureEncryption(this, "$DB_ALIAS_PREFIX$userMd5")
realmKeysUtils.configureEncryption(this, getKeyAlias(userMd5))
}
.name("crypto_store.realm")
.modules(RealmCryptoStoreModule())
@ -123,6 +123,9 @@ internal abstract class CryptoModule {
@Binds
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
@Binds
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.MXUsersDevicesMap
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.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
// Tasks
private val getDevicesTask: GetDevicesTask,
private val getDeviceInfoTask: GetDeviceInfoTask,
private val setDeviceNameTask: SetDeviceNameTask,
private val uploadKeysTask: UploadKeysTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
.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 {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}

View File

@ -25,11 +25,18 @@ internal interface CryptoApi {
/**
* 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")
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.
* 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 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.UserMd5
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,
@UserCacheDirectory val directory: File,
@SessionId val sessionId: String,
@UserMd5 val userMd5: String,
context: Context) {
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
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) {
Timber.v("************************************************************")
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
.edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", true)
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.apply()
val realmConfiguration = RealmConfiguration.Builder()
.directory(directory)
.name(REALM_NAME)
.apply {
realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5")
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(SessionRealmModule())
.deleteRealmIfMigrationNeeded()
@ -71,7 +73,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
Timber.v("Successfully create realm instance")
sharedPreferences
.edit()
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$userMd5", false)
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.apply()
}
return realmConfiguration

View File

@ -31,3 +31,10 @@ internal annotation class UserId
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
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.internal.crypto.attachments.ElementToDecrypt
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.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
@ -42,7 +42,7 @@ import java.io.IOException
import javax.inject.Inject
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 coroutineDispatchers: MatrixCoroutineDispatchers) : FileService {
@ -103,9 +103,9 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> {
// 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 tmpFolderUser = File(tmpFolderRoot, userMd5)
val tmpFolderUser = File(tmpFolderRoot, sessionId)
File(tmpFolderUser, id.md5())
}
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.homeserver.HomeServerCapabilitiesService
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.SessionRealmConfigurationFactory
import im.vector.matrix.android.internal.di.*
@ -54,8 +55,7 @@ internal abstract class SessionModule {
@Module
companion object {
internal const val DB_ALIAS_PREFIX = "session_db_"
internal fun getKeyAlias(userMd5: String) = "session_db_$userMd5"
@JvmStatic
@Provides
@ -83,11 +83,26 @@ internal abstract class SessionModule {
return userId.md5()
}
@JvmStatic
@SessionId
@Provides
fun providesSessionId(credentials: Credentials): String {
return createSessionId(credentials.userId, credentials.deviceId)
}
@JvmStatic
@Provides
@UserCacheDirectory
fun providesFilesDir(@UserMd5 userMd5: String, context: Context): File {
return File(context.filesDir, userMd5)
fun providesFilesDir(@UserMd5 userMd5: String,
@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

View File

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

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
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.roompreview.RoomPreviewNoPreviewFragment
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.push.PushGatewaysFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@ -228,6 +229,11 @@ interface FragmentModule {
@FragmentKey(VectorSettingsIgnoredUsersFragment::class)
fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
@Binds
@IntoMap
@FragmentKey(VectorSettingsDevicesFragment::class)
fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
@Binds
@IntoMap
@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.RageShake
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.ThemeUtils
import im.vector.riotx.receivers.DebugReceiver
@ -88,9 +89,11 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
private lateinit var configurationViewModel: ConfigurationViewModel
private lateinit var sessionListener: SessionListener
protected lateinit var bugReporter: BugReporter
private lateinit var rageShake: RageShake
lateinit var rageShake: RageShake
private set
protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder
private lateinit var vectorPreferences: VectorPreferences
// Filter for multiple invalid token error
private var mainActivityStarted = false
@ -135,7 +138,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
override fun onCreate(savedInstanceState: Bundle?) {
screenComponent = DaggerScreenComponent.factory().create(getVectorComponent(), this)
val vectorComponent = getVectorComponent()
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
val timeForInjection = measureTimeMillis {
injectWith(screenComponent)
}
@ -150,6 +154,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
rageShake = screenComponent.rageShake()
navigator = screenComponent.navigator()
activeSessionHolder = screenComponent.activeSessionHolder()
vectorPreferences = vectorComponent.vectorPreferences()
configurationViewModel.activityRestarter.observe(this, Observer {
if (!it.hasBeenHandled) {
// Recreate the Activity because configuration has changed
@ -226,7 +231,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
configurationViewModel.onActivityResumed()
if (this !is BugReportActivity) {
if (this !is BugReportActivity && vectorPreferences.useRageshake()) {
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
internal class Debouncer(private val handler: Handler) {
class Debouncer(private val handler: Handler) {
private val runnables = HashMap<String, Runnable>()
fun debounce(identifier: String, millis: Long, r: Runnable): Boolean {
if (runnables.containsKey(identifier)) {
// debounce
val old = runnables[identifier]
handler.removeCallbacks(old)
}
// debounce
cancel(identifier)
insertRunnable(identifier, r, millis)
return true
}
@ -37,6 +35,14 @@ internal class Debouncer(private val handler: Handler) {
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) {
val chained = Runnable {
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.content.DialogInterface
import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.Spannable
import android.view.*
import android.widget.TextView
@ -52,25 +50,18 @@ import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
import com.google.android.material.snackbar.Snackbar
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.session.Session
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.group.model.GroupSummary
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.send.SendState
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.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.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
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.AttachmentsHelper
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.home.AvatarRenderer
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.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize
@ -142,11 +127,7 @@ class RoomDetailFragment @Inject constructor(
private val session: Session,
private val avatarRenderer: AvatarRenderer,
private val timelineEventController: TimelineEventController,
private val commandAutocompletePolicy: CommandAutocompletePolicy,
private val autocompleteCommandPresenter: AutocompleteCommandPresenter,
private val autocompleteUserPresenter: AutocompleteUserPresenter,
private val autocompleteRoomPresenter: AutocompleteRoomPresenter,
private val autocompleteGroupPresenter: AutocompleteGroupPresenter,
private val autoCompleter: AutoCompleter,
private val permalinkHandler: PermalinkHandler,
private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
@ -156,9 +137,7 @@ class RoomDetailFragment @Inject constructor(
) :
VectorBaseFragment(),
TimelineEventController.Callback,
AutocompleteUserPresenter.Callback,
AutocompleteRoomPresenter.Callback,
AutocompleteGroupPresenter.Callback,
AutoCompleter.AutoCompleterListener,
VectorInviteView.Callback,
JumpToReadMarkerView.Callback,
AttachmentTypeSelectorView.Callback,
@ -202,6 +181,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
private lateinit var layoutManager: LinearLayoutManager
private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
private var modelBuildListener: OnModelBuildFinishedListener? = null
private lateinit var attachmentsHelper: AttachmentsHelper
@ -333,6 +313,13 @@ class RoomDetailFragment @Inject constructor(
}
}
}
jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager(
jumpToBottomView,
debouncer,
recyclerView,
layoutManager
)
}
private fun setupJumpToReadMarkerView() {
@ -397,7 +384,7 @@ class RoomDetailFragment @Inject constructor(
}
private fun renderRegularMode(text: String) {
commandAutocompletePolicy.enabled = true
autoCompleter.exitSpecialMode()
composerLayout.collapse()
updateComposerText(text)
@ -408,7 +395,7 @@ class RoomDetailFragment @Inject constructor(
@DrawableRes iconRes: Int,
@StringRes descriptionRes: Int,
defaultContent: String) {
commandAutocompletePolicy.enabled = false
autoCompleter.enterSpecialMode()
// switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName()
@ -495,25 +482,11 @@ class RoomDetailFragment @Inject constructor(
it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback)
updateJumpToReadMarkerViewVisibility()
updateJumpToBottomViewVisibility()
jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay()
}
timelineEventController.addModelBuildListener(modelBuildListener)
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
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() {
val elevation = 6f
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()
autoCompleter.setup(composerLayout.composerEditText, this)
composerLayout.callback = object : TextComposerView.Callback {
override fun onAddAttachment() {
@ -834,9 +639,7 @@ class RoomDetailFragment @Inject constructor(
}
private fun renderTextComposerState(state: TextComposerViewState) {
autocompleteUserPresenter.render(state.asyncUsers)
autocompleteRoomPresenter.render(state.asyncRooms)
autocompleteGroupPresenter.render(state.asyncGroups)
autoCompleter.render(state)
}
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.html.EventHtmlRenderer
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.util.*
@ -86,7 +88,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
private val htmlCompressor: VectorHtmlCompressor,
private val session: Session,
private val noticeEventFormatter: NoticeEventFormatter,
private val stringProvider: StringProvider
private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences
) : VectorViewModel<MessageActionState, MessageActionsAction>(initialState) {
private val eventId = initialState.eventId
@ -99,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
}
companion object : MvRxViewModelFactory<MessageActionsViewModel, MessageActionState> {
val quickEmojis = listOf("👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀")
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? {
val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
@ -159,7 +159,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
RxRoom(room)
.liveAnnotationSummary(eventId)
.map { annotations ->
quickEmojis.map { emoji ->
EmojiDataSource.quickEmojis.map { emoji ->
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 (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(EventSharedAction.ViewDecryptedSource(decryptedContent))
if (vectorPreferences.developerMode()) {
add(EventSharedAction.ViewSource(event.root.toContentStringWithIndent()))
if (event.isEncrypted()) {
val decryptedContent = event.root.toClearContentStringWithIndent()
?: stringProvider.getString(R.string.encryption_information_decryption_error)
add(EventSharedAction.ViewDecryptedSource(decryptedContent))
}
}
add(EventSharedAction.CopyPermalink(eventId))
if (session.myUserId != event.root.senderId) {

View File

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

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.navigation
import android.app.Activity
import android.content.Context
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
interface Navigator {
@ -39,7 +40,7 @@ interface Navigator {
fun openRoomsFiltering(context: Context)
fun openSettings(context: Context)
fun openSettings(context: Context, directAccess: Int = VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ROOT)
fun openDebug(context: Context)

View File

@ -19,33 +19,32 @@ package im.vector.riotx.features.rageshake
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import android.preference.PreferenceManager
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import com.squareup.seismic.ShakeDetector
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
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 dialogDisplayed = false
var interceptor: (() -> Unit)? = null
fun start() {
if (!isEnable(activity)) {
return
}
val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager
if (sensorManager == null) {
return
}
val sensorManager = activity.getSystemService(AppCompatActivity.SENSOR_SERVICE) as? SensorManager ?: return
shakeDetector = ShakeDetector(this).apply {
setSensitivity(vectorPreferences.getRageshakeSensitivity())
start(sensorManager)
}
}
@ -54,52 +53,43 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity,
shakeDetector?.stop()
}
/**
* Enable the feature, and start it
*/
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()
fun setSensitivity(sensitivity: Int) {
shakeDetector?.setSensitivity(sensitivity)
}
override fun hearShake() {
if (dialogDisplayed) {
// Filtered!
return
val i = interceptor
if (i != null) {
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() {
bugReporter.openBugReportScreen(activity)
}
companion object {
private const val SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY"
private fun openSettings() {
navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS)
}
companion object {
/**
* 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)
?.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) {
val words = action.queryString.split("\\s".toRegex())
setState {
copy(
query = action.queryString,
// First add emojis with name matching query, sorted by name
// 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 }
results = dataSource.filterWith(action.queryString)
)
}
}

View File

@ -33,4 +33,49 @@ class EmojiDataSource @Inject constructor(
.fromJson(input.bufferedReader().use { it.readText() })
}
?: 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

@ -23,6 +23,7 @@ import android.net.Uri
import android.provider.MediaStore
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.squareup.seismic.ShakeDetector
import im.vector.riotx.R
import im.vector.riotx.features.homeserver.ServerUrlsRepository
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_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_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_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"
@ -149,12 +148,16 @@ class VectorPreferences @Inject constructor(private val context: Context) {
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_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
// analytics
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_RAGE_SHAKE_DETECTION_THRESHOLD_KEY = "SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY"
// other
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 {
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 {
@ -256,7 +263,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
}
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.
*
* @param isEnabled true to enable the rage shake
* Get the rage shake sensitivity.
*/
fun setUseRageshake(isEnabled: Boolean) {
defaultPrefs.edit {
putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
}
fun getRageshakeSensitivity(): Int {
return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM)
}
/**

View File

@ -54,7 +54,12 @@ class VectorSettingsActivity : VectorBaseActivity(),
if (isFirstCreation()) {
// 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)
@ -111,7 +116,13 @@ class VectorSettingsActivity : VectorBaseActivity(),
}
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"
}

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.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Typeface
import android.view.KeyEvent
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
@ -33,30 +29,19 @@ import androidx.preference.SwitchPreference
import com.google.android.material.textfield.TextInputEditText
import im.vector.matrix.android.api.MatrixCallback
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.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.intent.ExternalIntentData
import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri
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.VectorPreferenceDivider
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.crypto.keys.KeysExporter
import im.vector.riotx.features.crypto.keys.KeysImporter
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
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
@ -66,9 +51,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_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
private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf()
@ -78,29 +60,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private val mCryptographyCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!!
}
private val mCryptographyCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!!
}
// cryptography manage
private val mCryptographyManageCategory by lazy {
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!!
}
private val mCryptographyManageCategoryDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!!
}
// displayed pushers
private val mPushersSettingsDivider by lazy {
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!!
}
private val mPushersSettingsCategory by lazy {
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 {
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)!!
}
override fun onResume() {
super.onResume()
// My device name may have been updated
refreshMyDevice()
}
override fun bindPref() {
// Push target
refreshPushersList()
// Device list
refreshDevicesList()
// Refresh Key Management section
refreshKeysManagementSection()
@ -151,16 +121,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
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) {
@ -353,11 +313,9 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private fun removeCryptographyPreference() {
preferenceScreen.let {
it.removePreference(mCryptographyCategory)
it.removePreference(mCryptographyCategoryDivider)
// Also remove keys management section
it.removePreference(mCryptographyManageCategory)
it.removePreference(mCryptographyManageCategoryDivider)
}
}
@ -375,7 +333,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
displayDeviceRenameDialog(aMyDeviceInfo)
// TODO device can be rename only from the device list screen for the moment
// displayDeviceRenameDialog(aMyDeviceInfo)
true
}
@ -428,342 +387,22 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// devices list
// ==============================================================================================================
private fun removeDevicesPreference() {
preferenceScreen.let {
it.removePreference(mDevicesListSettingsCategory)
it.removePreference(mDevicesListSettingsCategoryDivider)
}
}
/**
* 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!!)
}
}
private fun refreshMyDevice() {
// TODO Move to a ViewModel...
session.sessionParams.credentials.deviceId?.let {
session.getDeviceInfo(it, object : MatrixCallback<DeviceInfo> {
override fun onFailure(failure: Throwable) {
if (!isAdded) {
return
}
// Ignore for this time?...
}
removeDevicesPreference()
onCommonDone(failure.message)
override fun onSuccess(data: DeviceInfo) {
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
// ==============================================================================================================
@ -860,6 +499,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
// 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...) -->
<attr name="vctr_room_notification_text_color" format="color" />
<!-- color for dividers in settings -->
<attr name="vctr_preference_divider_color" format="color" />
<!-- icon colors -->
<attr name="vctr_icon_tint_on_light_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="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>

View File

@ -72,9 +72,6 @@
<item name="vctr_tab_home_secondary">@color/primary_color_dark_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_pill_receipt">@drawable/pill_receipt_black</item>

View File

@ -139,9 +139,6 @@
<!--Notice (secondary)-->
<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 -->
<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>

View File

@ -139,9 +139,6 @@
<!--Notice (secondary)-->
<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 -->
<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>

View File

@ -88,9 +88,6 @@
<!-- room notification text color (typing, unsent...) -->
<item name="vctr_room_notification_text_color">#a0a29f</item>
<!-- color for dividers in settings -->
<item name="vctr_preference_divider_color">#e1e1e1</item>
<!-- icon colors -->
<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>

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

View File

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

View File

@ -34,24 +34,12 @@
<!--android:summary="@string/settings_labs_enable_send_voice_summary"-->
<!--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
android:defaultValue="true"
android:key="SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
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>-->
</androidx.preference.PreferenceScreen>

View File

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

View File

@ -67,27 +67,8 @@
</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
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>

View File

@ -30,13 +30,15 @@
android:defaultValue="true"
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
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
android:defaultValue="true"
android:key="SETTINGS_SEND_TYPING_NOTIF_KEY"
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
android:defaultValue="false"
@ -46,38 +48,45 @@
<im.vector.riotx.core.preference.VectorSwitchPreference
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
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
android:defaultValue="true"
android:key="SETTINGS_SHOW_READ_RECEIPTS_KEY"
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
android:defaultValue="true"
android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"
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
android:defaultValue="true"
android:key="SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY"
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
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
android:key="SETTINGS_SEND_MESSAGE_WITH_ENTER"
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
android:defaultValue="always"
@ -85,15 +94,15 @@
android:entryValues="@array/show_info_area_values"
android:key="SETTINGS_SHOW_INFO_AREA_KEY"
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.VectorPreferenceDivider />
<im.vector.riotx.core.preference.VectorPreferenceCategory
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
android:defaultValue="true"
@ -107,9 +116,9 @@
</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
android:key="SETTINGS_MEDIA_SAVING_PERIOD_KEY"

View File

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

View File

@ -1,5 +1,6 @@
<?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 ************ -->
<im.vector.riotx.core.preference.VectorPreferenceCategory
@ -21,11 +22,22 @@
<im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
android:summary="@string/encryption_never_send_to_unverified_devices_summary"
android:title="@string/encryption_never_send_to_unverified_devices_title" />
android: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.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
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
@ -48,18 +60,10 @@
</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
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
android:defaultValue="false"
@ -67,9 +71,6 @@
android:summary="@string/settings_opt_in_of_analytics_summary"
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>
</androidx.preference.PreferenceScreen>