diff --git a/CHANGES.md b/CHANGES.md index 4e39b72de7..560350dddf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,9 @@ 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 Other changes: - @@ -16,6 +19,7 @@ Bugfix 🐛: - Fix crash when opening room creation screen from the room filtering screen - Fix avatar image disappearing (#777) - Fix read marker banner when permalink + - Hide non working settings (#751) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt index 685a522f60..bada3f86a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt @@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint() ?.chunked(4) ?.joinToString(separator = " ") -fun MutableList.sortByLastSeen() { - sortWith(DatedObjectComparators.descComparator) +/* ========================================================================================== + * DeviceInfo + * ========================================================================================== */ + +fun List.sortByLastSeen(): List { + val list = toMutableList() + list.sortWith(DatedObjectComparators.descComparator) + return list } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 706f89dfc9..986cbb698b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -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) + fun getDeviceInfo(deviceId: String, callback: MatrixCallback) + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int fun isRoomEncrypted(roomId: String): Boolean diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index a12f6e40ce..fd180a93b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt index 28a9fad35f..be8918dac7 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -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) { + getDeviceInfoTask + .configureWith(GetDeviceInfoTask.Params(deviceId)) { + this.callback = callback + } + .executeBy(taskExecutor) + } + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { return cryptoStore.inboundGroupSessionsCount(onlyBackedUp) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt index f4821f8ef3..b2e880c2f3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt @@ -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 + /** + * 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 + /** * 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt new file mode 100644 index 0000000000..f97e86a57d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt @@ -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 { + 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) + } + } +} diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 124763916b..308ca60094 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + = Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(durationMillis, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(durationMillis) + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt index f70aed9393..f4e3631b8a 100644 --- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt b/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt deleted file mode 100644 index f9f9da644b..0000000000 --- a/vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt +++ /dev/null @@ -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 - } -} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index d537b66ec3..d08a891081 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -42,6 +42,7 @@ 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 java.text.SimpleDateFormat import java.util.* @@ -86,7 +87,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(initialState) { private val eventId = initialState.eventId @@ -268,12 +270,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) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 08ff11217d..48422056b4 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -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) } diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 278c8fdba0..60045984c3 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt index 39749be8c2..effed19c59 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt @@ -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) - } } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index dd99488465..0ec67789fe 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt @@ -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) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt index 16484224af..490805ea3c 100755 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsActivity.kt @@ -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" } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt new file mode 100644 index 0000000000..43adcf6335 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt @@ -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(VectorPreferences.SETTINGS_USE_RAGE_SHAKE_KEY)!! + .onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue -> + + if (newValue as? Boolean == true) { + rageshake?.start() + } else { + rageshake?.stop() + } + + true + } + + findPreference(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("SETTINGS_RAGE_SHAKE_CATEGORY_KEY")!!.isVisible = false + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index b7ec443ea0..cf5273d5a4 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -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 = mutableListOf() @@ -78,29 +60,14 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( private val mCryptographyCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY)!! } - private val mCryptographyCategoryDivider by lazy { - findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY)!! - } // cryptography manage private val mCryptographyManageCategory by lazy { findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY)!! } - private val mCryptographyManageCategoryDivider by lazy { - findPreference(VectorPreferences.SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY)!! - } // displayed pushers - private val mPushersSettingsDivider by lazy { - findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY)!! - } private val mPushersSettingsCategory by lazy { findPreference(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!! } - private val mDevicesListSettingsCategory by lazy { - findPreference(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!! - } - private val mDevicesListSettingsCategoryDivider by lazy { - findPreference(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!! - } private val cryptoInfoDeviceNamePreference by lazy { findPreference(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!! } @@ -129,13 +96,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( findPreference(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(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, 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.

- * 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 { - 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 { 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.

- * 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) { - 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(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(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 { - 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 { - 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(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 { - 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" } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt new file mode 100644 index 0000000000..b6c84ade9a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -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() { + + @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(R.id.itemDeviceRoot) + val displayNameLabelText by bind(R.id.itemDeviceDisplayNameLabel) + val displayNameText by bind(R.id.itemDeviceDisplayName) + val deviceIdLabelText by bind(R.id.itemDeviceIdLabel) + val deviceIdText by bind(R.id.itemDeviceId) + val deviceLastSeenLabelText by bind(R.id.itemDeviceLastSeenLabel) + val deviceLastSeenText by bind(R.id.itemDeviceLastSeen) + val buttons by bind(R.id.itemDeviceButtons) + val buttonDelete by bind(R.id.itemDeviceDelete) + val buttonRename by bind(R.id.itemDeviceRename) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt new file mode 100644 index 0000000000..18c0965f86 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -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, 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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt new file mode 100644 index 0000000000..b2b015a3f0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -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> = Uninitialized, + val currentExpandedDeviceId: String? = null, + val request: Async = 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(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: DevicesViewState): DevicesViewModel + } + + companion object : MvRxViewModelFactory { + + @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>() + val requestPasswordLiveData: LiveData> + 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 { + 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 { + 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 { + 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 { + 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) + } + }) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt new file mode 100644 index 0000000000..465b3ba0fb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -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(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(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) { + when (unIgnoreRequest) { + is Loading -> waiting_view.isVisible = true + else -> waiting_view.isVisible = false + } + } +} diff --git a/vector/src/main/res/layout/dialog_device_details.xml b/vector/src/main/res/layout/dialog_device_details.xml deleted file mode 100644 index b3b5c5aff7..0000000000 --- a/vector/src/main/res/layout/dialog_device_details.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_device.xml b/vector/src/main/res/layout/item_device.xml new file mode 100644 index 0000000000..bebaf156d9 --- /dev/null +++ b/vector/src/main/res/layout/item_device.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/vector_preference_divider.xml b/vector/src/main/res/layout/vector_preference_divider.xml deleted file mode 100644 index 81f7a091e5..0000000000 --- a/vector/src/main/res/layout/vector_preference_divider.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index c30a1d99d9..aca2a7fa5f 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -63,9 +63,6 @@ - - - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8ee63bc628..1502740112 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,4 +5,17 @@ Initial Sync… + + See all my devices + Advanced settings + Developer mode + The developer mode activates hidden features and may also make the application less stable. For developers only! + Rageshake + Detection threshold + Shake your phone to test the detection threshold + Shake detected! + Settings + Current device + Other devices + diff --git a/vector/src/main/res/values/theme_black.xml b/vector/src/main/res/values/theme_black.xml index 7398a4bcb7..7bce009429 100644 --- a/vector/src/main/res/values/theme_black.xml +++ b/vector/src/main/res/values/theme_black.xml @@ -72,9 +72,6 @@ @color/primary_color_dark_black @color/list_divider_color_black - - @color/list_divider_color_black - #FF4D4D4D @drawable/pill_receipt_black diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index f61a89482a..a05081eec7 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_dark - @android:color/white @color/riotx_accent diff --git a/vector/src/main/res/values/theme_light.xml b/vector/src/main/res/values/theme_light.xml index aa343a11fc..9cea0e52b7 100644 --- a/vector/src/main/res/values/theme_light.xml +++ b/vector/src/main/res/values/theme_light.xml @@ -139,9 +139,6 @@ #FF61708b - - @color/list_divider_color_light - @android:color/black @color/riotx_accent diff --git a/vector/src/main/res/values/theme_status.xml b/vector/src/main/res/values/theme_status.xml index 322522c723..421632e64c 100644 --- a/vector/src/main/res/values/theme_status.xml +++ b/vector/src/main/res/values/theme_status.xml @@ -88,9 +88,6 @@ #a0a29f - - #e1e1e1 - @color/accent_color_status @color/riotx_accent diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml new file mode 100644 index 0000000000..93ff104a81 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index d1ffe5bcf1..c49eca825a 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -45,11 +45,10 @@ - - + android:title="@string/settings_contact" + app:isPreferenceVisible="@bool/false_not_implemented"> - - + tools:summary="https://homeserver.org" /> - - - + + android:title="@string/settings_deactivate_account_section" + app:isPreferenceVisible="@bool/false_not_implemented"> - + - + - + - + - + - + - + - + - - - - - + \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index e9e5e27198..2661568f77 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -34,24 +34,12 @@ - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml index 32b6a2b499..b5f01d98f6 100644 --- a/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_notification_advanced_preferences.xml @@ -36,8 +36,6 @@ - - - - + android:title="@string/settings_notifications_targets" /--> - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 96471cfebe..7698372053 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -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" /> + android:title="@string/settings_send_typing_notifs" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_always_show_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_12_24_timestamps" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_read_receipts" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_join_leave_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_show_avatar_display_name_changes_messages" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_vibrate_on_mention" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_send_message_with_enter" + app:isPreferenceVisible="@bool/false_not_implemented" /> + android:title="@string/settings_info_area_show" + app:isPreferenceVisible="@bool/false_not_implemented" /> - - + android:title="@string/settings_home_display" + app:isPreferenceVisible="@bool/false_not_implemented"> - - - + + + diff --git a/vector/src/main/res/xml/vector_settings_security_privacy.xml b/vector/src/main/res/xml/vector_settings_security_privacy.xml index 9e88da34a1..234ecbe647 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -1,5 +1,6 @@ - + + android:title="@string/encryption_never_send_to_unverified_devices_title" + app:isPreferenceVisible="@bool/false_not_implemented" /> - + + + + + + - - - - - - - + android:title="@string/settings_analytics" + app:isPreferenceVisible="@bool/false_not_implemented"> - \ No newline at end of file