From 6b2703f6ce0eec44dacf360bec87fa9996d1ffc6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 15:05:17 +0100 Subject: [PATCH 01/32] Device list is now on a dedicated Fragment New request to get info on the current device for VectorSettingsSecurityPrivacyFragment. The whole device list is only retrieved in the new Fragment --- .../api/extensions/MatrixSdkExtensions.kt | 10 +- .../api/session/crypto/CryptoService.kt | 3 + .../android/internal/crypto/CryptoModule.kt | 3 + .../internal/crypto/DefaultCryptoService.kt | 10 + .../android/internal/crypto/api/CryptoApi.kt | 9 +- .../crypto/tasks/GetDeviceInfoTask.kt | 37 ++ .../im/vector/riotx/core/di/FragmentModule.kt | 8 +- .../features/settings/VectorPreferences.kt | 2 - .../VectorSettingsSecurityPrivacyFragment.kt | 375 +----------------- .../features/settings/devices/DeviceItem.kt | 64 +++ .../settings/devices/DevicesController.kt | 88 ++++ .../settings/devices/DevicesViewModel.kt | 255 ++++++++++++ .../devices/VectorSettingsDevicesFragment.kt | 234 +++++++++++ vector/src/main/res/layout/item_device.xml | 44 ++ vector/src/main/res/values/strings_riotX.xml | 4 + .../xml/vector_settings_security_privacy.xml | 24 +- 16 files changed, 799 insertions(+), 371 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDeviceInfoTask.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt create mode 100644 vector/src/main/res/layout/item_device.xml 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/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c86436d56a..88af219c78 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -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 @@ -226,7 +227,12 @@ interface FragmentModule { @Binds @IntoMap @FragmentKey(VectorSettingsIgnoredUsersFragment::class) - fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment + fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(VectorSettingsDevicesFragment::class) + fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment @Binds @IntoMap 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..c3b07c7496 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 @@ -62,8 +62,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" 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..72cc0884c9 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,20 @@ 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 +52,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() @@ -95,12 +78,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( 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 +106,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() @@ -375,7 +355,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 +409,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 +521,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..201d4c88dd --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DeviceItem.kt @@ -0,0 +1,64 @@ +/* + * 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.widget.TextView +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 + +/** + * A list item for Device. + */ +@EpoxyModelClass(layout = R.layout.item_device) +abstract class DeviceItem : VectorEpoxyModel() { + + @EpoxyAttribute + lateinit var deviceInfo: DeviceInfo + + @EpoxyAttribute + var bold = false + + @EpoxyAttribute + var itemClickAction: (() -> Unit)? = null + + override fun bind(holder: Holder) { + holder.root.setOnClickListener { itemClickAction?.invoke() } + + holder.displayNameText.text = deviceInfo.displayName ?: "" + holder.deviceIdText.text = deviceInfo.deviceId ?: "" + + if (bold) { + holder.displayNameText.setTypeface(null, Typeface.BOLD) + holder.deviceIdText.setTypeface(null, Typeface.BOLD) + } else { + holder.displayNameText.setTypeface(null, Typeface.NORMAL) + holder.deviceIdText.setTypeface(null, Typeface.NORMAL) + } + } + + class Holder : VectorEpoxyHolder() { + val root by bind(R.id.itemDeviceRoot) + val displayNameText by bind(R.id.itemDeviceDisplayName) + val deviceIdText by bind(R.id.itemDeviceId) + } +} 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..6fe25335df --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesController.kt @@ -0,0 +1,88 @@ +/* + * 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.core.epoxy.errorWithRetryItem +import im.vector.riotx.core.epoxy.loadingItem +import im.vector.riotx.core.error.ErrorFormatter +import javax.inject.Inject + +class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter) : 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 -> + // 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. + devices() + // sort before display: most recent first + .sortByLastSeen() + .forEachIndexed { idx, deviceInfo -> + val isCurrentDevice = deviceInfo.deviceId == state.myDeviceId + deviceItem { + id("device$idx") + deviceInfo(deviceInfo) + bold(isCurrentDevice) + itemClickAction { callback?.onDeviceClicked(deviceInfo, isCurrentDevice) } + } + } + } + } + + interface Callback { + fun retry() + fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) + } +} + + 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..c06912cdeb --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/DevicesViewModel.kt @@ -0,0 +1,255 @@ +/* + * 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 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() +} + +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 as logged in. + * It can be any mobile device, as any browser. + */ + 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) + } + } + + 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..96a6d11e06 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -0,0 +1,234 @@ +/* + * 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 android.widget.TextView +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 im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment +import kotlinx.android.synthetic.main.fragment_generic_recycler.* +import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +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) + 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() + } + + /** + * 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 deviceInfo the device information + * @param isCurrentDevice true if this is the current device + */ + override fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) { + val builder = AlertDialog.Builder(requireActivity()) + val inflater = requireActivity().layoutInflater + val layout = inflater.inflate(R.layout.dialog_device_details, null) + var textView = layout.findViewById(R.id.device_id) + + textView.text = deviceInfo.deviceId + + // device name + textView = layout.findViewById(R.id.device_name) + val displayName = if (deviceInfo.displayName.isNullOrEmpty()) VectorSettingsSecurityPrivacyFragment.LABEL_UNAVAILABLE_DATA else deviceInfo.displayName + textView.text = displayName + + // last seen info + textView = layout.findViewById(R.id.device_last_seen) + + 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 + } ?: "-" + + 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(deviceInfo) } + + // disable the deletion for our own device + if (!isCurrentDevice) { + builder.setNegativeButton(R.string.delete) { _, _ -> devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) } + } + + 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() + } + + override fun retry() { + devicesViewModel.handle(DevicesAction.Retry) + } + + /** + * Display an alert dialog to rename a device + * + * @param aDeviceInfoToRename device info + */ + private fun displayDeviceRenameDialog(aDeviceInfoToRename: 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(aDeviceInfoToRename.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(aDeviceInfoToRename, 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/item_device.xml b/vector/src/main/res/layout/item_device.xml new file mode 100644 index 0000000000..59b7ba92c0 --- /dev/null +++ b/vector/src/main/res/layout/item_device.xml @@ -0,0 +1,44 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8ee63bc628..754b1b6631 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -5,4 +5,8 @@ Initial Sync… + + + See all my devices + 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..49d782479d 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 @@ - + + + + + + + + + + @@ -50,13 +65,6 @@ - - - - - From 8dff196716a32683eebe32fe797d3b6146b7f3c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 15:42:42 +0100 Subject: [PATCH 02/32] Device list: remove the detail dialog: handle the actions directly in the list --- .../features/settings/devices/DeviceItem.kt | 61 +++++++++++++-- .../settings/devices/DevicesController.kt | 13 ++-- .../settings/devices/DevicesViewModel.kt | 21 ++++- .../devices/VectorSettingsDevicesFragment.kt | 64 +++------------ .../main/res/layout/dialog_device_details.xml | 66 ---------------- vector/src/main/res/layout/item_device.xml | 78 ++++++++++++++++--- 6 files changed, 153 insertions(+), 150 deletions(-) delete mode 100644 vector/src/main/res/layout/dialog_device_details.xml 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 index 201d4c88dd..b6c84ade9a 100644 --- 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 @@ -18,13 +18,18 @@ 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. @@ -36,29 +41,69 @@ abstract class DeviceItem : VectorEpoxyModel() { lateinit var deviceInfo: DeviceInfo @EpoxyAttribute - var bold = false + 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 ?: "" - if (bold) { - holder.displayNameText.setTypeface(null, Typeface.BOLD) - holder.deviceIdText.setTypeface(null, Typeface.BOLD) - } else { - holder.displayNameText.setTypeface(null, Typeface.NORMAL) - holder.deviceIdText.setTypeface(null, Typeface.NORMAL) + 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 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 index 6fe25335df..db61db429b 100644 --- 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 @@ -72,8 +72,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor deviceItem { id("device$idx") deviceInfo(deviceInfo) - bold(isCurrentDevice) - itemClickAction { callback?.onDeviceClicked(deviceInfo, isCurrentDevice) } + currentDevice(isCurrentDevice) + buttonsVisible(deviceInfo.deviceId == state.currentExpandedDeviceId) + itemClickAction { callback?.onDeviceClicked(deviceInfo) } + renameClickAction { callback?.onRenameDevice(deviceInfo) } + deleteClickAction { callback?.onDeleteDevice(deviceInfo) } } } } @@ -81,8 +84,8 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor interface Callback { fun retry() - fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) + 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 index c06912cdeb..1fc56e76fd 100644 --- 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 @@ -36,6 +36,7 @@ import timber.log.Timber data class DevicesViewState( val myDeviceId: String = "", val devices: Async> = Uninitialized, + val currentExpandedDeviceId: String? = null, val request: Async = Uninitialized ) : MvRxState @@ -44,6 +45,7 @@ sealed class DevicesAction : VectorViewModelAction { 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, @@ -114,10 +116,21 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic 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.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 + ) + } } } 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 index 96a6d11e06..4168e3eeb7 100644 --- 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 @@ -21,7 +21,6 @@ import android.os.Bundle import android.view.KeyEvent import android.view.View import android.widget.EditText -import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import com.airbnb.mvrx.Async @@ -36,12 +35,8 @@ 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 im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* import javax.inject.Inject /** @@ -97,63 +92,22 @@ class VectorSettingsDevicesFragment @Inject constructor( } /** - * Display a dialog containing the device ID, the device name and the "last seen" information.<> + * 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 deviceInfo the device information * @param isCurrentDevice true if this is the current device */ - override fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) { - val builder = AlertDialog.Builder(requireActivity()) - val inflater = requireActivity().layoutInflater - val layout = inflater.inflate(R.layout.dialog_device_details, null) - var textView = layout.findViewById(R.id.device_id) + override fun onDeviceClicked(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) + } - textView.text = deviceInfo.deviceId + override fun onDeleteDevice(deviceInfo: DeviceInfo) { + devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) + } - // device name - textView = layout.findViewById(R.id.device_name) - val displayName = if (deviceInfo.displayName.isNullOrEmpty()) VectorSettingsSecurityPrivacyFragment.LABEL_UNAVAILABLE_DATA else deviceInfo.displayName - textView.text = displayName - - // last seen info - textView = layout.findViewById(R.id.device_last_seen) - - 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 - } ?: "-" - - 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(deviceInfo) } - - // disable the deletion for our own device - if (!isCurrentDevice) { - builder.setNegativeButton(R.string.delete) { _, _ -> devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) } - } - - 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() + override fun onRenameDevice(deviceInfo: DeviceInfo) { + displayDeviceRenameDialog(deviceInfo) } override fun retry() { 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 index 59b7ba92c0..bebaf156d9 100644 --- a/vector/src/main/res/layout/item_device.xml +++ b/vector/src/main/res/layout/item_device.xml @@ -1,6 +1,5 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From 7d744f7d7faa18683e165ca541a8c488b38ef211 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 16:01:47 +0100 Subject: [PATCH 03/32] Developer mode: UI And some cleanup --- .../VectorSettingsDeveloperModeFragment.kt | 29 +++++++++++++ vector/src/main/res/values/strings_riotX.xml | 3 +- .../xml/vector_settings_developer_mode.xml | 42 +++++++++++++++++++ .../src/main/res/xml/vector_settings_labs.xml | 12 ------ .../res/xml/vector_settings_notifications.xml | 15 ------- .../src/main/res/xml/vector_settings_root.xml | 14 +++---- 6 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt create mode 100644 vector/src/main/res/xml/vector_settings_developer_mode.xml diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt new file mode 100644 index 0000000000..00f71192bf --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt @@ -0,0 +1,29 @@ +/* + * 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 im.vector.riotx.R + +class VectorSettingsDeveloperModeFragment : VectorSettingsBaseFragment() { + + override var titleRes = R.string.settings_developer_mode + override val preferenceXmlRes = R.xml.vector_settings_developer_mode + + override fun bindPref() { + // Nothing to do + } +} diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 754b1b6631..8c8e36be16 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -6,7 +6,8 @@ Initial Sync… - See all my devices + Developer mode + The developer mode activates hidden features and may also make the application less stable. For developers only! diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml new file mode 100644 index 0000000000..1f81e2e659 --- /dev/null +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + \ 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_notifications.xml b/vector/src/main/res/xml/vector_settings_notifications.xml index 26f204c17c..4cfbaa0d7b 100644 --- a/vector/src/main/res/xml/vector_settings_notifications.xml +++ b/vector/src/main/res/xml/vector_settings_notifications.xml @@ -75,19 +75,4 @@ - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 894784767a..0a59570050 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -3,58 +3,54 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + From 703a1a034d00a06132cff3cb429e333820d3775e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 16:10:55 +0100 Subject: [PATCH 04/32] Developer mode: hide show (decrypted) source actions --- CHANGES.md | 2 ++ .../timeline/action/MessageActionsViewModel.kt | 17 +++++++++++------ .../features/settings/VectorPreferences.kt | 9 +++++++-- .../res/xml/vector_settings_developer_mode.xml | 8 ++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e2cd2915b5..35f4a23804 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,8 @@ 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 Other changes: - 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/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index c3b07c7496..63b7c60bef 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 @@ -147,6 +147,7 @@ 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" @@ -245,8 +246,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 { @@ -254,7 +259,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) } /** diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index 1f81e2e659..8ceb03f323 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -4,19 +4,19 @@ @@ -24,7 +24,7 @@ Date: Thu, 2 Jan 2020 16:24:31 +0100 Subject: [PATCH 05/32] Hide non working settings (#751) --- CHANGES.md | 1 + vector/src/main/res/values/strings_riotX.xml | 1 + .../main/res/xml/vector_settings_general.xml | 9 ++-- .../res/xml/vector_settings_preferences.xml | 41 ++++++++++++------- .../xml/vector_settings_security_privacy.xml | 20 ++++++--- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 35f4a23804..67b430e79b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ Other changes: Bugfix πŸ›: - Fix avatar image disappearing (#777) - Fix read marker banner when permalink + - Hide non working settings (#751) Translations πŸ—£: - diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 8c8e36be16..d0019bfca5 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -9,5 +9,6 @@ See all my devices Developer mode The developer mode activates hidden features and may also make the application less stable. For developers only! + Rageshake diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index d1ffe5bcf1..cd94da2d15 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -74,11 +74,12 @@ + tools:summary="https://homeserver.org" /> - + + android:title="@string/settings_deactivate_account_section" + app:isPreferenceVisible="@bool/false_not_implemented"> + 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"> - + - + + android:title="@string/encryption_never_send_to_unverified_devices_title" + app:isPreferenceVisible="@bool/false_not_implemented" /> @@ -36,7 +37,7 @@ + app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment" /> @@ -63,11 +64,14 @@ - + + android:title="@string/settings_analytics" + app:isPreferenceVisible="@bool/false_not_implemented"> + + + + + + - + \ No newline at end of file From 5c26f66523ad5bd72677215a65d6a045604f13d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 17:42:44 +0100 Subject: [PATCH 06/32] Rageshake: settings for sensitivity --- .../riotx/core/platform/VectorBaseActivity.kt | 11 ++- .../riotx/features/rageshake/RageShake.kt | 85 +++++++------------ .../features/settings/VectorPreferences.kt | 11 +++ .../VectorSettingsDeveloperModeFragment.kt | 51 ++++++++++- vector/src/main/res/values/strings_riotX.xml | 4 + .../xml/vector_settings_developer_mode.xml | 85 ++++++++++++------- .../xml/vector_settings_security_privacy.xml | 9 -- 7 files changed, 158 insertions(+), 98 deletions(-) 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/features/rageshake/RageShake.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/RageShake.kt index 39749be8c2..bbd1090d98 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,28 @@ 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.features.settings.VectorPreferences import javax.inject.Inject class RageShake @Inject constructor(private val activity: AppCompatActivity, - private val bugReporter: BugReporter) : ShakeDetector.Listener { + private val bugReporter: BugReporter, + 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 +49,41 @@ 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) { + i.invoke() + } else { + if (dialogDisplayed) { + // Filtered! + return + } + + 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() { + // TODO + } + companion object { /** * Check if the feature is available */ @@ -107,12 +91,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 63b7c60bef..a86c3625ce 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 @@ -153,7 +154,10 @@ class VectorPreferences @Inject constructor(private val context: Context) { // 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" @@ -732,6 +736,13 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true) } + /** + * Get the rage shake sensitivity. + */ + fun getRageshakeSensitivity(): Int { + return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM) + } + /** * Update the rage shake status. * diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt index 00f71192bf..f700759fd0 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt @@ -16,14 +16,63 @@ 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 VectorSettingsDeveloperModeFragment : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_developer_mode override val preferenceXmlRes = R.xml.vector_settings_developer_mode + 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() { - // Nothing to do + 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/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index d0019bfca5..1134b4be0f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -10,5 +10,9 @@ 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 diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index 8ceb03f323..6cae5fad06 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -2,40 +2,63 @@ - - - - - - - - + android:key="SETTINGS_RAGE_SHAKE_CATEGORY_KEY" + android:title="@string/settings_rageshake"> - + - + + + + + + + + + + + + + + + + + + + + + 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 dd6a89ff1f..9a9185cdc7 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -81,13 +81,4 @@ - - - - - - - \ No newline at end of file From 96c9293edc69c62497d254bd1bfdc7b76b5df016 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 18:12:44 +0100 Subject: [PATCH 07/32] Rageshake: vibrate --- CHANGES.md | 1 + vector/src/main/AndroidManifest.xml | 1 + .../im/vector/riotx/core/hardware/vibrator.kt | 32 +++++++++++++++++++ .../features/navigation/DefaultNavigator.kt | 4 +-- .../riotx/features/navigation/Navigator.kt | 3 +- .../riotx/features/rageshake/RageShake.kt | 8 ++++- .../settings/VectorSettingsActivity.kt | 15 +++++++-- .../xml/vector_settings_developer_mode.xml | 4 +-- 8 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/hardware/vibrator.kt diff --git a/CHANGES.md b/CHANGES.md index 67b430e79b..adbba84260 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements πŸ™Œ: - 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: - 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/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 bbd1090d98..6d5c780202 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 @@ -23,11 +23,15 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity 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, + private val navigator: Navigator, private val vectorPreferences: VectorPreferences) : ShakeDetector.Listener { private var shakeDetector: ShakeDetector? = null @@ -56,6 +60,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, override fun hearShake() { val i = interceptor if (i != null) { + vibrate(activity) i.invoke() } else { if (dialogDisplayed) { @@ -63,6 +68,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, return } + vibrate(activity) dialogDisplayed = true AlertDialog.Builder(activity) @@ -80,7 +86,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, } private fun openSettings() { - // TODO + navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DEVELOPER) } companion object { 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..810547f610 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_DEVELOPER -> + replaceFragment(R.id.vector_settings_page, VectorSettingsDeveloperModeFragment::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_DEVELOPER = 1 private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment" } diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index 6cae5fad06..d3f960b31b 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -14,10 +14,10 @@ android:defaultValue="13" android:dependency="SETTINGS_USE_RAGE_SHAKE_KEY" android:key="SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY" - android:max="17" + android:max="15" android:summary="@string/settings_rageshake_detection_threshold_summary" android:title="@string/settings_rageshake_detection_threshold" - app:min="9" /> + app:min="11" /> From 8e478e78e13783482d1b75bd847830df0d788a96 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 18:17:54 +0100 Subject: [PATCH 08/32] Disable pref unused --- vector/src/main/res/xml/vector_settings_general.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/res/xml/vector_settings_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index cd94da2d15..327815cb1d 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -49,7 +49,8 @@ + android:title="@string/settings_contact" + app:isPreferenceVisible="@bool/false_not_implemented"> Date: Thu, 2 Jan 2020 18:27:46 +0100 Subject: [PATCH 09/32] Remove Preference divider and cleanup prefs --- .../preference/VectorPreferenceDivider.kt | 36 ---------- .../features/settings/VectorPreferences.kt | 11 --- .../VectorSettingsSecurityPrivacyFragment.kt | 22 ------ .../res/layout/vector_preference_divider.xml | 21 ------ vector/src/main/res/values/attrs.xml | 3 - vector/src/main/res/values/theme_black.xml | 3 - vector/src/main/res/values/theme_dark.xml | 3 - vector/src/main/res/values/theme_light.xml | 3 - vector/src/main/res/values/theme_status.xml | 3 - .../main/res/xml/vector_settings_general.xml | 6 -- .../res/xml/vector_settings_help_about.xml | 68 +++++++++---------- ...ings_notification_advanced_preferences.xml | 2 - .../res/xml/vector_settings_notifications.xml | 6 +- .../res/xml/vector_settings_preferences.xml | 4 -- .../xml/vector_settings_security_privacy.xml | 8 --- 15 files changed, 32 insertions(+), 167 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/core/preference/VectorPreferenceDivider.kt delete mode 100644 vector/src/main/res/layout/vector_preference_divider.xml 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/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorPreferences.kt index a86c3625ce..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 @@ -743,17 +743,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getInt(SETTINGS_RAGE_SHAKE_DETECTION_THRESHOLD_KEY, ShakeDetector.SENSITIVITY_MEDIUM) } - /** - * Update the rage shake status. - * - * @param isEnabled true to enable the rage shake - */ - fun setUseRageshake(isEnabled: Boolean) { - defaultPrefs.edit { - putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled) - } - } - /** * Tells if all the events must be displayed ie even the redacted events. * 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 72cc0884c9..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 @@ -38,7 +38,6 @@ 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.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 @@ -61,20 +60,11 @@ 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)!! } @@ -131,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) { @@ -333,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) } } 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/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_general.xml b/vector/src/main/res/xml/vector_settings_general.xml index 327815cb1d..c49eca825a 100644 --- a/vector/src/main/res/xml/vector_settings_general.xml +++ b/vector/src/main/res/xml/vector_settings_general.xml @@ -45,8 +45,6 @@ - - - - - - - + - + - + - + - + - + - + - + - - - - - + \ 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 7d490d124b..7698372053 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -99,8 +99,6 @@ - - - - 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 9a9185cdc7..234ecbe647 100644 --- a/vector/src/main/res/xml/vector_settings_security_privacy.xml +++ b/vector/src/main/res/xml/vector_settings_security_privacy.xml @@ -27,8 +27,6 @@ - - - - @@ -64,10 +60,6 @@ - - Date: Thu, 2 Jan 2020 18:45:44 +0100 Subject: [PATCH 10/32] Auto-review --- .../im/vector/riotx/core/di/FragmentModule.kt | 2 +- .../settings/devices/DevicesController.kt | 3 --- .../features/settings/devices/DevicesViewModel.kt | 4 ++-- .../devices/VectorSettingsDevicesFragment.kt | 15 ++++----------- .../res/xml/vector_settings_developer_mode.xml | 2 +- 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index 88af219c78..5565369006 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -227,7 +227,7 @@ interface FragmentModule { @Binds @IntoMap @FragmentKey(VectorSettingsIgnoredUsersFragment::class) - fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment + fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment @Binds @IntoMap 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 index db61db429b..e0a379be83 100644 --- 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 @@ -61,9 +61,6 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor listener { callback?.retry() } } is Success -> - // 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. devices() // sort before display: most recent first .sortByLastSeen() 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 index 1fc56e76fd..b2b015a3f0 100644 --- 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 @@ -80,8 +80,8 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic /** * Force the refresh of the devices list. - * The devices list is the list of the devices where the user as logged in. - * It can be any mobile device, as any browser. + * 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()) { 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 index 4168e3eeb7..4d14930fce 100644 --- 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 @@ -91,13 +91,6 @@ class VectorSettingsDevicesFragment @Inject constructor( .show() } - /** - * 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 deviceInfo the device information - * @param isCurrentDevice true if this is the current device - */ override fun onDeviceClicked(deviceInfo: DeviceInfo) { devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) } @@ -117,14 +110,14 @@ class VectorSettingsDevicesFragment @Inject constructor( /** * Display an alert dialog to rename a device * - * @param aDeviceInfoToRename device info + * @param deviceInfo device info */ - private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) { + 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(aDeviceInfoToRename.displayName) + input.setText(deviceInfo.displayName) AlertDialog.Builder(requireActivity()) .setTitle(R.string.devices_details_device_name) @@ -132,7 +125,7 @@ class VectorSettingsDevicesFragment @Inject constructor( .setPositiveButton(R.string.ok) { _, _ -> val newName = input.text.toString() - devicesViewModel.handle(DevicesAction.Rename(aDeviceInfoToRename, newName)) + devicesViewModel.handle(DevicesAction.Rename(deviceInfo, newName)) } .setNegativeButton(R.string.cancel, null) .show() diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_developer_mode.xml index d3f960b31b..b7b800052f 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_developer_mode.xml @@ -10,7 +10,7 @@ android:key="SETTINGS_USE_RAGE_SHAKE_KEY" android:title="@string/send_bug_report_rage_shake" /> - Date: Fri, 3 Jan 2020 16:20:00 +0100 Subject: [PATCH 11/32] Update SessionParamsEntity primaryKey to include deviceId --- CHANGES.md | 2 +- .../matrix/android/internal/auth/SessionId.kt | 23 ++++++++++++++++ .../internal/auth/db/AuthRealmMigration.kt | 26 ++++++++++++++++++- .../internal/auth/db/SessionParamsEntity.kt | 3 ++- .../internal/auth/db/SessionParamsMapper.kt | 2 ++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt diff --git a/CHANGES.md b/CHANGES.md index 4e39b72de7..638ba9bf49 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,7 @@ Improvements πŸ™Œ: - Fix autocompletion issues and add support for rooms and groups 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt new file mode 100644 index 0000000000..2f39806aa5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionId.kt @@ -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() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt index 83bf7b7822..36f3bce6b6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt @@ -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,7 +26,7 @@ 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") @@ -53,5 +56,26 @@ internal object AuthRealmMigration : RealmMigration { ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } } + + if (oldVersion <= 2) { + 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) + } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt index 92511dccf7..72eed95fcc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt index 72e8087f3f..d4ba1eb818 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt @@ -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, From 160927e7b53323d8bf74d379dacd191cfa7c7484 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 14:25:19 +0100 Subject: [PATCH 12/32] Split code into several methods --- .../internal/auth/db/AuthRealmMigration.kt | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt index 36f3bce6b6..7d7f8cc22c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt @@ -31,51 +31,55 @@ internal object AuthRealmMigration : RealmMigration { 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") - if (oldVersion <= 2) { - Timber.d("Step 2 -> 3") - Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") + realm.schema.get("SessionParamsEntity") + ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java) + ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) } + } - 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) + private fun migrateTo3(realm: DynamicRealm) { + Timber.d("Step 2 -> 3") + Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId") - val credentials = MoshiProvider.providesMoshi() - .adapter(Credentials::class.java) - .fromJson(credentialsJson) + 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) - it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId)) - } - ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) - } + val credentials = MoshiProvider.providesMoshi() + .adapter(Credentials::class.java) + .fromJson(credentialsJson) + + it.set(SessionParamsEntityFields.SESSION_ID, createSessionId(userId, credentials?.deviceId)) + } + ?.addPrimaryKey(SessionParamsEntityFields.SESSION_ID) } } From 215abea10a7af80dfac28ba5299272c0531903e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 14:49:26 +0100 Subject: [PATCH 13/32] Introduce @SessionId --- .../SessionRealmConfigurationFactory.kt | 8 +++++--- .../android/internal/di/StringQualifiers.kt | 8 ++++++++ .../internal/session/DefaultFileService.kt | 8 ++++---- .../android/internal/session/SessionModule.kt | 20 +++++++++++++++++-- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index bc806a56a4..0d0c351a2b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -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,7 +55,7 @@ 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() @@ -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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 0e38618590..7d2610903a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -31,3 +31,11 @@ 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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt index c160ac9b31..66b94cf68d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultFileService.kt @@ -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): - // /MF/// + // /MF/// val tmpFolderRoot = File(context.cacheDir, "MF") - val tmpFolderUser = File(tmpFolderRoot, userMd5) + val tmpFolderUser = File(tmpFolderRoot, sessionId) File(tmpFolderUser, id.md5()) } FileService.DownloadMode.TO_EXPORT -> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 0e88894969..4c135b960c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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.* @@ -83,11 +84,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 From f432d15757b01707285fe6498a444aba228271cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 3 Jan 2020 14:55:06 +0100 Subject: [PATCH 14/32] Ensure key aliases are always computed the same way --- .../im/vector/matrix/android/internal/crypto/CryptoModule.kt | 4 ++-- .../internal/database/SessionRealmConfigurationFactory.kt | 2 +- .../vector/matrix/android/internal/session/SessionModule.kt | 3 +-- .../matrix/android/internal/session/signout/SignOutTask.kt | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) 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..443f748528 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 @@ -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()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt index 0d0c351a2b..b9d95035d2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/SessionRealmConfigurationFactory.kt @@ -62,7 +62,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val .directory(directory) .name(REALM_NAME) .apply { - realmKeysUtils.configureEncryption(this, "${SessionModule.DB_ALIAS_PREFIX}$userMd5") + realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } .modules(SessionRealmModule()) .deleteRealmIfMigrationNeeded() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 4c135b960c..437a559ea1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -55,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 diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt index 51cb22c988..b43bfa603c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt @@ -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) { From a00f51a26447aae53a330d80b9f17799997a31f8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 6 Jan 2020 10:32:36 +0100 Subject: [PATCH 15/32] Settings: rename "developer mode" to "advanced settings" --- .../riotx/features/rageshake/RageShake.kt | 2 +- .../settings/VectorSettingsActivity.kt | 8 +-- ...VectorSettingsAdvancedSettingsFragment.kt} | 6 +- vector/src/main/res/values/strings_riotX.xml | 1 + ... => vector_settings_advanced_settings.xml} | 60 +++++++++---------- .../src/main/res/xml/vector_settings_root.xml | 4 +- 6 files changed, 40 insertions(+), 41 deletions(-) rename vector/src/main/java/im/vector/riotx/features/settings/{VectorSettingsDeveloperModeFragment.kt => VectorSettingsAdvancedSettingsFragment.kt} (92%) rename vector/src/main/res/xml/{vector_settings_developer_mode.xml => vector_settings_advanced_settings.xml} (73%) 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 6d5c780202..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 @@ -86,7 +86,7 @@ class RageShake @Inject constructor(private val activity: AppCompatActivity, } private fun openSettings() { - navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DEVELOPER) + navigator.openSettings(activity, VectorSettingsActivity.EXTRA_DIRECT_ACCESS_ADVANCED_SETTINGS) } companion object { 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 810547f610..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 @@ -55,9 +55,9 @@ class VectorSettingsActivity : VectorBaseActivity(), if (isFirstCreation()) { // display the fragment when (intent.getIntExtra(EXTRA_DIRECT_ACCESS, EXTRA_DIRECT_ACCESS_ROOT)) { - EXTRA_DIRECT_ACCESS_DEVELOPER -> - replaceFragment(R.id.vector_settings_page, VectorSettingsDeveloperModeFragment::class.java, null, FRAGMENT_TAG) - else -> + 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) } } @@ -122,7 +122,7 @@ class VectorSettingsActivity : VectorBaseActivity(), private const val EXTRA_DIRECT_ACCESS = "EXTRA_DIRECT_ACCESS" const val EXTRA_DIRECT_ACCESS_ROOT = 0 - const val EXTRA_DIRECT_ACCESS_DEVELOPER = 1 + 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/VectorSettingsDeveloperModeFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt similarity index 92% rename from vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt rename to vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt index f700759fd0..43adcf6335 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsDeveloperModeFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsAdvancedSettingsFragment.kt @@ -23,10 +23,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.preference.VectorSwitchPreference import im.vector.riotx.features.rageshake.RageShake -class VectorSettingsDeveloperModeFragment : VectorSettingsBaseFragment() { +class VectorSettingsAdvancedSettingsFragment : VectorSettingsBaseFragment() { - override var titleRes = R.string.settings_developer_mode - override val preferenceXmlRes = R.xml.vector_settings_developer_mode + override var titleRes = R.string.settings_advanced_settings + override val preferenceXmlRes = R.xml.vector_settings_advanced_settings private var rageshake: RageShake? = null diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1134b4be0f..5b2b2e99e8 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -7,6 +7,7 @@ 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 diff --git a/vector/src/main/res/xml/vector_settings_developer_mode.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml similarity index 73% rename from vector/src/main/res/xml/vector_settings_developer_mode.xml rename to vector/src/main/res/xml/vector_settings_advanced_settings.xml index b7b800052f..93ff104a81 100644 --- a/vector/src/main/res/xml/vector_settings_developer_mode.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -2,25 +2,6 @@ - - - - - - - - - + - + - + - + + + + + + + + + diff --git a/vector/src/main/res/xml/vector_settings_root.xml b/vector/src/main/res/xml/vector_settings_root.xml index 0a59570050..f2e13d8111 100644 --- a/vector/src/main/res/xml/vector_settings_root.xml +++ b/vector/src/main/res/xml/vector_settings_root.xml @@ -47,8 +47,8 @@ + android:title="@string/settings_advanced_settings" + app:fragment="im.vector.riotx.features.settings.VectorSettingsAdvancedSettingsFragment" /> Date: Mon, 6 Jan 2020 10:51:30 +0100 Subject: [PATCH 16/32] Improve (a bit) the devices list UX/UI --- .../settings/devices/DevicesController.kt | 71 +++++++++++++++---- .../devices/VectorSettingsDevicesFragment.kt | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 + 3 files changed, 59 insertions(+), 16 deletions(-) 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 index e0a379be83..18c0965f86 100644 --- 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 @@ -23,12 +23,16 @@ 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) : EpoxyController() { +class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, + private val stringProvider: StringProvider) : EpoxyController() { var callback: Callback? = null private var viewState: DevicesViewState? = null @@ -61,21 +65,58 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor listener { callback?.retry() } } is Success -> - devices() - // sort before display: most recent first - .sortByLastSeen() - .forEachIndexed { idx, deviceInfo -> - val isCurrentDevice = deviceInfo.deviceId == state.myDeviceId - deviceItem { - id("device$idx") - deviceInfo(deviceInfo) - currentDevice(isCurrentDevice) - buttonsVisible(deviceInfo.deviceId == state.currentExpandedDeviceId) - itemClickAction { callback?.onDeviceClicked(deviceInfo) } - renameClickAction { callback?.onRenameDevice(deviceInfo) } - deleteClickAction { callback?.onDeleteDevice(deviceInfo) } - } + 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) } } + } } } 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 index 4d14930fce..465b3ba0fb 100644 --- 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 @@ -60,7 +60,7 @@ class VectorSettingsDevicesFragment @Inject constructor( waiting_view_status_text.setText(R.string.please_wait) waiting_view_status_text.isVisible = true devicesController.callback = this - recyclerView.configureWith(devicesController) + recyclerView.configureWith(devicesController, showDivider = true) devicesViewModel.requestErrorLiveData.observeEvent(this) { displayErrorDialog(it) // Password is maybe not good, for safety measure, reset it here diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 5b2b2e99e8..1502740112 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -15,5 +15,7 @@ Shake your phone to test the detection threshold Shake detected! Settings + Current device + Other devices From d73a1135ae8afbe1246a52bd6bd991698f83f35e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:01:07 +0100 Subject: [PATCH 17/32] Extract AutoComplete feature from RoomDetailFragment --- .../home/room/detail/AutoCompleter.kt | 241 ++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 192 +------------- 2 files changed, 247 insertions(+), 186 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt new file mode 100644 index 0000000000..5e8a87ba51 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -0,0 +1,241 @@ +/* + * 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.text.Editable +import android.text.Spannable +import android.widget.EditText +import androidx.fragment.app.Fragment +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.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.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 lateinit var fragment: Fragment + + fun enterSpecialMode() { + commandAutocompletePolicy.enabled = false + } + + fun exitSpecialMode() { + commandAutocompletePolicy.enabled = true + } + + private val glideRequests by lazy { + GlideApp.with(fragment) + } + + fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { + this.fragment = fragment + + val elevation = 6f + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + Autocomplete.on(editText) + .with(commandAutocompletePolicy) + .with(autocompleteCommandPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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 = listener + Autocomplete.on(editText) + .with(CharPolicy('#', true)) + .with(autocompleteRoomPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteGroupPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('+', true)) + .with(autocompleteGroupPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + + autocompleteUserPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(elevation) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + + fun render(state: TextComposerViewState) { + autocompleteUserPresenter.render(state.asyncUsers) + autocompleteRoomPresenter.render(state.asyncRooms) + autocompleteGroupPresenter.render(state.asyncGroups) + } + + interface AutoCompleterListener : + AutocompleteUserPresenter.Callback, + AutocompleteRoomPresenter.Callback, + AutocompleteGroupPresenter.Callback +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 334445870c..4ae79e8215 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -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, @@ -397,7 +376,7 @@ class RoomDetailFragment @Inject constructor( } private fun renderRegularMode(text: String) { - commandAutocompletePolicy.enabled = true + autoCompleter.exitSpecialMode() composerLayout.collapse() updateComposerText(text) @@ -408,7 +387,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() @@ -580,164 +559,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - val elevation = 6f - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) - Autocomplete.on(composerLayout.composerEditText) - .with(commandAutocompletePolicy) - .with(autocompleteCommandPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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(composerLayout.composerEditText) - .with(CharPolicy('#', true)) - .with(autocompleteRoomPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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(composerLayout.composerEditText) - .with(CharPolicy('+', true)) - .with(autocompleteGroupPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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(composerLayout.composerEditText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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(this, composerLayout.composerEditText, this) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { @@ -834,9 +656,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) { From c4fe0bdb7f74695c501d7895c3db199ea892fac5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:08:28 +0100 Subject: [PATCH 18/32] Split into small methods --- .../home/room/detail/AutoCompleter.kt | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 5e8a87ba51..7edba64a05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -17,6 +17,7 @@ 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 @@ -68,12 +69,19 @@ class AutoCompleter @Inject constructor( fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { this.fragment = fragment - val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + + setupCommands(backgroundDrawable, editText) + setupUsers(backgroundDrawable, editText, listener) + setupRooms(backgroundDrawable, editText, listener) + setupGroups(backgroundDrawable, editText, listener) + } + + private fun setupCommands(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: Command): Boolean { @@ -88,12 +96,62 @@ class AutoCompleter @Inject constructor( } }) .build() + } + private fun setupUsers(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteUserPresenter.Callback) { + autocompleteUserPresenter.callback = listener + Autocomplete.on(editText) + .with(CharPolicy('@', true)) + .with(autocompleteUserPresenter) + .with(ELEVATION) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + 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, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + + return true + } + + override fun onPopupVisibilityChanged(shown: Boolean) { + } + }) + .build() + } + + private fun setupRooms(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteRoomPresenter.Callback) { autocompleteRoomPresenter.callback = listener Autocomplete.on(editText) .with(CharPolicy('#', true)) .with(autocompleteRoomPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: RoomSummary): Boolean { @@ -134,12 +192,14 @@ class AutoCompleter @Inject constructor( } }) .build() + } + private fun setupGroups(backgroundDrawable: ColorDrawable, editText: EditText, listener: AutocompleteGroupPresenter.Callback) { autocompleteGroupPresenter.callback = listener Autocomplete.on(editText) .with(CharPolicy('+', true)) .with(autocompleteGroupPresenter) - .with(elevation) + .with(ELEVATION) .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: GroupSummary): Boolean { @@ -180,52 +240,6 @@ class AutoCompleter @Inject constructor( } }) .build() - - autocompleteUserPresenter.callback = listener - Autocomplete.on(editText) - .with(CharPolicy('@', true)) - .with(autocompleteUserPresenter) - .with(elevation) - .with(backgroundDrawable) - .with(object : AutocompleteCallback { - 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, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - - return true - } - - override fun onPopupVisibilityChanged(shown: Boolean) { - } - }) - .build() } fun render(state: TextComposerViewState) { @@ -238,4 +252,8 @@ class AutoCompleter @Inject constructor( AutocompleteUserPresenter.Callback, AutocompleteRoomPresenter.Callback, AutocompleteGroupPresenter.Callback + + companion object { + private const val ELEVATION = 6f + } } From d88e5d8af8b7afea2c5dfc6c33068d5355d3fabb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:14:26 +0100 Subject: [PATCH 19/32] DRY --- .../home/room/detail/AutoCompleter.kt | 133 +++++------------- 1 file changed, 39 insertions(+), 94 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 7edba64a05..ef7715f91a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -28,6 +28,7 @@ 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 @@ -77,6 +78,12 @@ class AutoCompleter @Inject constructor( setupGroups(backgroundDrawable, editText, listener) } + 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(editText) .with(commandAutocompletePolicy) @@ -107,36 +114,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { 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, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "@", item.toMatrixItem()) return true } @@ -155,36 +133,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { 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, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "#", item.toRoomAliasMatrixItem()) return true } @@ -203,36 +152,7 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { 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, - fragment.requireContext(), - matrixItem - ) - span.bind(editText) - - editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - + insertMatrixItem(editText, editable, "+", item.toMatrixItem()) return true } @@ -242,10 +162,35 @@ class AutoCompleter @Inject constructor( .build() } - fun render(state: TextComposerViewState) { - autocompleteUserPresenter.render(state.asyncUsers) - autocompleteRoomPresenter.render(state.asyncRooms) - autocompleteGroupPresenter.render(state.asyncGroups) + 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, + fragment.requireContext(), + matrixItem + ) + span.bind(editText) + + editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } interface AutoCompleterListener : From 8597c2b9a2d99d5edcbf3fa2dd9ef89ba30f955e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 07:23:30 +0100 Subject: [PATCH 20/32] Improve API --- .../riotx/features/home/room/detail/AutoCompleter.kt | 12 ++++++------ .../features/home/room/detail/RoomDetailFragment.kt | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index ef7715f91a..ba643e2d7d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -53,7 +53,7 @@ class AutoCompleter @Inject constructor( private val autocompleteRoomPresenter: AutocompleteRoomPresenter, private val autocompleteGroupPresenter: AutocompleteGroupPresenter ) { - private lateinit var fragment: Fragment + private lateinit var editText: EditText fun enterSpecialMode() { commandAutocompletePolicy.enabled = false @@ -64,13 +64,13 @@ class AutoCompleter @Inject constructor( } private val glideRequests by lazy { - GlideApp.with(fragment) + GlideApp.with(editText) } - fun setup(fragment: Fragment, editText: EditText, listener: AutoCompleterListener) { - this.fragment = fragment + fun setup(editText: EditText, listener: AutoCompleterListener) { + this.editText = editText - val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(fragment.requireContext(), R.attr.riotx_background)) + val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(editText.context, R.attr.riotx_background)) setupCommands(backgroundDrawable, editText) setupUsers(backgroundDrawable, editText, listener) @@ -185,7 +185,7 @@ class AutoCompleter @Inject constructor( val span = PillImageSpan( glideRequests, avatarRenderer, - fragment.requireContext(), + editText.context, matrixItem ) span.bind(editText) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4ae79e8215..4414c48205 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -559,7 +559,7 @@ class RoomDetailFragment @Inject constructor( } private fun setupComposer() { - autoCompleter.setup(this, composerLayout.composerEditText, this) + autoCompleter.setup(composerLayout.composerEditText, this) composerLayout.callback = object : TextComposerView.Callback { override fun onAddAttachment() { From 8b4c51139df1406d15fa2e6e76eca58eae7e4020 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:00:02 +0100 Subject: [PATCH 21/32] Completion on emoji WIP --- .../emoji/AutocompleteEmojiController.kt | 71 +++++++++++++++++++ .../emoji/AutocompleteEmojiItem.kt | 52 ++++++++++++++ .../emoji/AutocompleteEmojiPresenter.kt | 54 ++++++++++++++ .../home/room/detail/AutoCompleter.kt | 27 ++++++- .../reactions/EmojiSearchResultViewModel.kt | 16 +---- .../reactions/data/EmojiDataSource.kt | 21 ++++++ 6 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt new file mode 100644 index 0000000000..e0d7bdd888 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -0,0 +1,71 @@ +/* + * 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 im.vector.riotx.features.reactions.emojiSearchResultItem +import javax.inject.Inject + +class AutocompleteEmojiController @Inject constructor( + private val fontProvider: EmojiCompatFontProvider +) : TypedEpoxyController>() { + + 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? = null + + override fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEach { emojiItem -> + emojiSearchResultItem { + id(emojiItem.name) + emojiItem(emojiItem) + emojiTypeFace(emojiTypeface) + //currentQuery(data.query) + onClickListener(object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } + ) + } + } + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + fontProvider.removeListener(fontProviderListener) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt new file mode 100644 index 0000000000..e5c53315ab --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt @@ -0,0 +1,52 @@ +/* + * 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.view.View +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 + +//@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) +//abstract class AutocompleteEmojiItem : VectorEpoxyModel() { +// +// @EpoxyAttribute +// var name: CharSequence? = null +// @EpoxyAttribute +// var parameters: CharSequence? = null +// @EpoxyAttribute +// var description: CharSequence? = null +// @EpoxyAttribute +// var clickListener: View.OnClickListener? = null +// +// override fun bind(holder: Holder) { +// holder.view.setOnClickListener(clickListener) +// +// holder.nameView.text = name +// holder.parametersView.text = parameters +// holder.descriptionView.text = description +// } +// +// class Holder : VectorEpoxyHolder() { +// val nameView by bind(R.id.commandName) +// val parametersView by bind(R.id.commandParameter) +// val descriptionView by bind(R.id.commandDescription) +// } +//} diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt new file mode 100644 index 0000000000..731b48af86 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -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(context), AutocompleteClickListener { + + 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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index ba643e2d7d..9fe2249450 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable import android.text.Editable import android.text.Spannable import android.widget.EditText -import androidx.fragment.app.Fragment import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy @@ -35,6 +34,7 @@ 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 @@ -51,7 +51,8 @@ class AutoCompleter @Inject constructor( private val autocompleteCommandPresenter: AutocompleteCommandPresenter, private val autocompleteUserPresenter: AutocompleteUserPresenter, private val autocompleteRoomPresenter: AutocompleteRoomPresenter, - private val autocompleteGroupPresenter: AutocompleteGroupPresenter + private val autocompleteGroupPresenter: AutocompleteGroupPresenter, + private val autocompleteEmojiPresenter: AutocompleteEmojiPresenter ) { private lateinit var editText: EditText @@ -76,6 +77,7 @@ class AutoCompleter @Inject constructor( setupUsers(backgroundDrawable, editText, listener) setupRooms(backgroundDrawable, editText, listener) setupGroups(backgroundDrawable, editText, listener) + setupEmojis(backgroundDrawable, editText) } fun render(state: TextComposerViewState) { @@ -162,6 +164,27 @@ class AutoCompleter @Inject constructor( .build() } + private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { + Autocomplete.on(editText) + .with(CharPolicy(':', true)) + .with(autocompleteEmojiPresenter) + .with(ELEVATION) + .with(backgroundDrawable) + .with(object : AutocompleteCallback { + override fun onPopupItemClicked(editable: Editable, item: String): Boolean { + editable.clear() + editable + .append(item) + .append(" ") + 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) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index 01debac5ed..aa5e79ed29 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -56,26 +56,12 @@ 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) ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index a326828112..873ab90254 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -33,4 +33,25 @@ class EmojiDataSource @Inject constructor( .fromJson(input.bufferedReader().use { it.readText() }) } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + + fun filterWith(query: String): List { + val words = query.split("\\s".toRegex()) + + return rawData.emojis.values + .filter { emojiItem -> + emojiItem.name.contains(query, true) + } + .sortedBy { it.name } + + rawData.emojis.values + .filter { emojiItem -> + words.fold(true, { prev, word -> + prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } + }) + } + .sortedBy { it.name } + } + + fun getQuickReactions(): List { + return listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€").mapNotNull { rawData.emojis[it] } + } } From 9e73e95f55f1206bb2387831e740bab37635dff3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:36:54 +0100 Subject: [PATCH 22/32] Ensure there is never twice the same emoji --- .../vector/riotx/features/reactions/data/EmojiDataSource.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 873ab90254..8abea21667 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -37,7 +37,7 @@ class EmojiDataSource @Inject constructor( fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) - return rawData.emojis.values + return (rawData.emojis.values .filter { emojiItem -> emojiItem.name.contains(query, true) } @@ -48,7 +48,8 @@ class EmojiDataSource @Inject constructor( prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } }) } - .sortedBy { it.name } + .sortedBy { it.name }) + .distinct() } fun getQuickReactions(): List { From 5fa2acf60b197a945e68f370c1e2d9585d63826d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 08:46:55 +0100 Subject: [PATCH 23/32] Completion on emoji --- .../riotx/features/reactions/data/EmojiDataSource.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 8abea21667..b8ab24da4e 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -53,6 +53,15 @@ class EmojiDataSource @Inject constructor( } fun getQuickReactions(): List { - return listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€").mapNotNull { rawData.emojis[it] } + return listOf( + "+1", // πŸ‘ + "-1", // πŸ‘Ž + "grinning", // πŸ˜„ + "tada", // πŸŽ‰ + "confused", // πŸ˜• + "heart", // ❀️ + "rocket", // πŸš€ + "eyes" // πŸ‘€ + ).mapNotNull { rawData.emojis[it] } } } From c8e67f8ab4d6781a0e7833c71b04b3491e55a9cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:06:28 +0100 Subject: [PATCH 24/32] Completion on emoji WIP --- .../emoji/AutocompleteEmojiController.kt | 15 +++-- .../emoji/AutocompleteEmojiItem.kt | 60 ++++++++++--------- .../home/room/detail/AutoCompleter.kt | 18 ++++-- .../res/layout/item_autocomplete_emoji.xml | 56 +++++++++++++++++ 4 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 vector/src/main/res/layout/item_autocomplete_emoji.xml diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index e0d7bdd888..bef43d8b14 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -23,7 +23,6 @@ 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 im.vector.riotx.features.reactions.emojiSearchResultItem import javax.inject.Inject class AutocompleteEmojiController @Inject constructor( @@ -49,16 +48,16 @@ class AutocompleteEmojiController @Inject constructor( return } data.forEach { emojiItem -> - emojiSearchResultItem { + autocompleteEmojiItem { id(emojiItem.name) emojiItem(emojiItem) emojiTypeFace(emojiTypeface) - //currentQuery(data.query) - onClickListener(object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) - } - } + onClickListener( + object : ReactionClickListener { + override fun onReactionSelected(reaction: String) { + listener?.onItemClick(reaction) + } + } ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt index e5c53315ab..36759f9271 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiItem.kt @@ -16,37 +16,43 @@ package im.vector.riotx.features.autocomplete.emoji -import android.view.View +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() { -// -// @EpoxyAttribute -// var name: CharSequence? = null -// @EpoxyAttribute -// var parameters: CharSequence? = null -// @EpoxyAttribute -// var description: CharSequence? = null -// @EpoxyAttribute -// var clickListener: View.OnClickListener? = null -// -// override fun bind(holder: Holder) { -// holder.view.setOnClickListener(clickListener) -// -// holder.nameView.text = name -// holder.parametersView.text = parameters -// holder.descriptionView.text = description -// } -// -// class Holder : VectorEpoxyHolder() { -// val nameView by bind(R.id.commandName) -// val parametersView by bind(R.id.commandParameter) -// val descriptionView by bind(R.id.commandDescription) -// } -//} +@EpoxyModelClass(layout = R.layout.item_autocomplete_emoji) +abstract class AutocompleteEmojiItem : VectorEpoxyModel() { + + @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(R.id.itemAutocompleteEmoji) + val emojiNameText by bind(R.id.itemAutocompleteEmojiName) + val emojiKeywordText by bind(R.id.itemAutocompleteEmojiSubname) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 9fe2249450..78ddf80a84 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -172,10 +172,20 @@ class AutoCompleter @Inject constructor( .with(backgroundDrawable) .with(object : AutocompleteCallback { override fun onPopupItemClicked(editable: Editable, item: String): Boolean { - editable.clear() - editable - .append(item) - .append(" ") + // 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 } diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml new file mode 100644 index 0000000000..650a405f34 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file From 0e5fcd071cffd4621101f33947b09fd92ea491c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:21:56 +0100 Subject: [PATCH 25/32] Completion on emoji: display the first 50 results --- .../emoji/AutocompleteEmojiController.kt | 36 ++++++++++++------- .../emoji/AutocompleteMoreResultItem.kt | 28 +++++++++++++++ .../home/room/detail/AutoCompleter.kt | 2 +- .../layout/item_autocomplete_more_result.xml | 9 +++++ vector/src/main/res/values/strings_riotX.xml | 2 ++ 5 files changed, 64 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt create mode 100644 vector/src/main/res/layout/item_autocomplete_more_result.xml diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt index bef43d8b14..010b362b68 100644 --- a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteEmojiController.kt @@ -47,18 +47,26 @@ class AutocompleteEmojiController @Inject constructor( if (data.isNullOrEmpty()) { return } - data.forEach { emojiItem -> - autocompleteEmojiItem { - id(emojiItem.name) - emojiItem(emojiItem) - emojiTypeFace(emojiTypeface) - onClickListener( - object : ReactionClickListener { - override fun onReactionSelected(reaction: String) { - listener?.onItemClick(reaction) - } - } - ) + 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") } } } @@ -67,4 +75,8 @@ class AutocompleteEmojiController @Inject constructor( super.onDetachedFromRecyclerView(recyclerView) fontProvider.removeListener(fontProviderListener) } + + companion object { + const val MAX = 50 + } } diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt new file mode 100644 index 0000000000..844cc96035 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/emoji/AutocompleteMoreResultItem.kt @@ -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() { + + class Holder : VectorEpoxyHolder() +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt index 78ddf80a84..609e7e2183 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/AutoCompleter.kt @@ -166,7 +166,7 @@ class AutoCompleter @Inject constructor( private fun setupEmojis(backgroundDrawable: Drawable, editText: EditText) { Autocomplete.on(editText) - .with(CharPolicy(':', true)) + .with(CharPolicy(':', false)) .with(autocompleteEmojiPresenter) .with(ELEVATION) .with(backgroundDrawable) diff --git a/vector/src/main/res/layout/item_autocomplete_more_result.xml b/vector/src/main/res/layout/item_autocomplete_more_result.xml new file mode 100644 index 0000000000..d04f515ed0 --- /dev/null +++ b/vector/src/main/res/layout/item_autocomplete_more_result.xml @@ -0,0 +1,9 @@ + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 1502740112..317511b921 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,4 +18,6 @@ Current device Other devices + Limited results, please type more letters… + From 92d7ebe94f6be8f0fcb25eba4182da1aefaaa05c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 09:22:43 +0100 Subject: [PATCH 26/32] Update changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 560350dddf..b11be910db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ Improvements πŸ™Œ: - 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) Other changes: - From 9ecceafb960d2155f423c0a40099b8bb0d37aa64 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sun, 22 Dec 2019 21:56:35 +0100 Subject: [PATCH 27/32] Move comment --- .../riotx/features/reactions/EmojiSearchResultViewModel.kt | 2 -- .../im/vector/riotx/features/reactions/data/EmojiDataSource.kt | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt index aa5e79ed29..8aa03d9b22 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/EmojiSearchResultViewModel.kt @@ -59,8 +59,6 @@ class EmojiSearchResultViewModel @AssistedInject constructor( 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.filterWith(action.queryString) ) } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index b8ab24da4e..8a279a7d4d 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -37,11 +37,13 @@ class EmojiDataSource @Inject constructor( fun filterWith(query: String): List { 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 -> @@ -49,6 +51,7 @@ class EmojiDataSource @Inject constructor( }) } .sortedBy { it.name }) + // and ensure they will not be present twice .distinct() } From 448552d287e79f1d4035ef918668c504a9d9838f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 2 Jan 2020 10:42:01 +0100 Subject: [PATCH 28/32] Move list of Quick Emoji to Emoji Data Source --- .../action/MessageActionsViewModel.kt | 6 ++-- .../reactions/data/EmojiDataSource.kt | 31 +++++++++++++------ .../res/layout/item_autocomplete_emoji.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) 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 d08a891081..aad73e12f4 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 @@ -43,6 +43,7 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformatio 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.* @@ -101,9 +102,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } companion object : MvRxViewModelFactory { - - val quickEmojis = listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€") - @JvmStatic override fun create(viewModelContext: ViewModelContext, state: MessageActionState): MessageActionsViewModel? { val fragment: MessageActionsBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() @@ -161,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) } } diff --git a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt index 8a279a7d4d..9317c645c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/riotx/features/reactions/data/EmojiDataSource.kt @@ -34,6 +34,8 @@ class EmojiDataSource @Inject constructor( } ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + private val quickReactions = mutableListOf() + fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) @@ -56,15 +58,24 @@ class EmojiDataSource @Inject constructor( } fun getQuickReactions(): List { - return listOf( - "+1", // πŸ‘ - "-1", // πŸ‘Ž - "grinning", // πŸ˜„ - "tada", // πŸŽ‰ - "confused", // πŸ˜• - "heart", // ❀️ - "rocket", // πŸš€ - "eyes" // πŸ‘€ - ).mapNotNull { rawData.emojis[it] } + if (quickReactions.isEmpty()) { + listOf( + "+1", // πŸ‘ + "-1", // πŸ‘Ž + "grinning", // πŸ˜„ + "tada", // πŸŽ‰ + "confused", // πŸ˜• + "heart", // ❀️ + "rocket", // πŸš€ + "eyes" // πŸ‘€ + ) + .mapNotNullTo(quickReactions) { rawData.emojis[it] } + } + + return quickReactions + } + + companion object { + val quickEmojis = listOf("πŸ‘", "πŸ‘Ž", "πŸ˜„", "πŸŽ‰", "πŸ˜•", "❀️", "πŸš€", "πŸ‘€") } } diff --git a/vector/src/main/res/layout/item_autocomplete_emoji.xml b/vector/src/main/res/layout/item_autocomplete_emoji.xml index 650a405f34..c34ab0d452 100644 --- a/vector/src/main/res/layout/item_autocomplete_emoji.xml +++ b/vector/src/main/res/layout/item_autocomplete_emoji.xml @@ -53,4 +53,4 @@ - \ No newline at end of file + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 317511b921..3e8485ebcc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -18,6 +18,6 @@ Current device Other devices - Limited results, please type more letters… + Showing only the first results, type more letters… From 5a7f4bed43e1ed6c8144609e677fc71898cbc4ef Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 7 Jan 2020 14:24:26 +0100 Subject: [PATCH 29/32] ktlint --- .../im/vector/matrix/android/internal/di/StringQualifiers.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt index 7d2610903a..32649443db 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt @@ -32,7 +32,6 @@ internal annotation class UserId @Retention(AnnotationRetention.RUNTIME) internal annotation class UserMd5 - /** * Used to inject the sessionId, which is defined as md5(userId|deviceId) */ From 8ef5c60e2e3b490550e141e19946793bcf034d24 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 11:43:21 +0100 Subject: [PATCH 30/32] RageShake is enabled by default --- vector/src/main/res/xml/vector_settings_advanced_settings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/xml/vector_settings_advanced_settings.xml b/vector/src/main/res/xml/vector_settings_advanced_settings.xml index 93ff104a81..11ca97870d 100644 --- a/vector/src/main/res/xml/vector_settings_advanced_settings.xml +++ b/vector/src/main/res/xml/vector_settings_advanced_settings.xml @@ -32,6 +32,7 @@ android:title="@string/settings_rageshake"> From 7575cb286ed9056469a2962e3e80a29e992f0902 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 14:49:26 +0100 Subject: [PATCH 31/32] Show skip to bottom FAB while scrolling down (#752) --- CHANGES.md | 1 + .../im/vector/riotx/core/utils/Debouncer.kt | 16 +++++--- .../home/room/detail/RoomDetailFragment.kt | 38 +++++++++++++------ 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb132982d8..3087b7405c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Improvements πŸ™Œ: - 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) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index b02e3c9366..2f5db57ccb 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -24,11 +24,9 @@ internal class Debouncer(private val handler: Handler) { private val runnables = HashMap() 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) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4414c48205..4e29aa2f89 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -474,21 +474,31 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) updateJumpToReadMarkerViewVisibility() - updateJumpToBottomViewVisibility() + maybeShowJumpToBottomViewVisibilityWithDelay() } timelineEventController.addModelBuildListener(modelBuildListener) recyclerView.adapter = timelineEventController.adapter 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 -> { - updateJumpToBottomViewVisibility() + maybeShowJumpToBottomViewVisibilityWithDelay() } RecyclerView.SCROLL_STATE_DRAGGING, - RecyclerView.SCROLL_STATE_SETTLING -> { - jumpToBottomView.hide() - } + RecyclerView.SCROLL_STATE_SETTLING -> Unit } } }) @@ -547,17 +557,21 @@ class RoomDetailFragment @Inject constructor( } } - private fun updateJumpToBottomViewVisibility() { + private fun maybeShowJumpToBottomViewVisibilityWithDelay() { debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { - Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstVisibleItemPosition() != 0) { - jumpToBottomView.show() - } else { - jumpToBottomView.hide() - } + maybeShowJumpToBottomViewVisibility() }) } + private fun maybeShowJumpToBottomViewVisibility() { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstVisibleItemPosition() != 0) { + jumpToBottomView.show() + } else { + jumpToBottomView.hide() + } + } + private fun setupComposer() { autoCompleter.setup(composerLayout.composerEditText, this) From 501ac36040d5d1ecf960c6b5ad19ed6b8e8afc75 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 8 Jan 2020 15:03:58 +0100 Subject: [PATCH 32/32] Reduce size of RoomDetailFragment --- .../im/vector/riotx/core/utils/Debouncer.kt | 2 +- .../JumpToBottomViewVisibilityManager.kt | 77 +++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 49 +++--------- 3 files changed, 87 insertions(+), 41 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index 2f5db57ccb..627d757574 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -19,7 +19,7 @@ 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() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt new file mode 100644 index 0000000000..4be5502678 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -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() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4e29aa2f89..a956d0e2e9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -181,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 @@ -312,6 +313,13 @@ class RoomDetailFragment @Inject constructor( } } } + + jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( + jumpToBottomView, + debouncer, + recyclerView, + layoutManager + ) } private fun setupJumpToReadMarkerView() { @@ -474,35 +482,11 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) updateJumpToReadMarkerViewVisibility() - maybeShowJumpToBottomViewVisibilityWithDelay() + jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() } timelineEventController.addModelBuildListener(modelBuildListener) recyclerView.adapter = timelineEventController.adapter - 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 - } - } - }) - timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { @@ -557,21 +541,6 @@ class RoomDetailFragment @Inject constructor( } } - private 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() - } - } - private fun setupComposer() { autoCompleter.setup(composerLayout.composerEditText, this)