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
This commit is contained in:
parent
90f2199eb7
commit
6b2703f6ce
@ -28,6 +28,12 @@ fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
|
|||||||
?.chunked(4)
|
?.chunked(4)
|
||||||
?.joinToString(separator = " ")
|
?.joinToString(separator = " ")
|
||||||
|
|
||||||
fun MutableList<DeviceInfo>.sortByLastSeen() {
|
/* ==========================================================================================
|
||||||
sortWith(DatedObjectComparators.descComparator)
|
* DeviceInfo
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
fun List<DeviceInfo>.sortByLastSeen(): List<DeviceInfo> {
|
||||||
|
val list = toMutableList()
|
||||||
|
list.sortWith(DatedObjectComparators.descComparator)
|
||||||
|
return list
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|||||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
|
|
||||||
@ -89,6 +90,8 @@ interface CryptoService {
|
|||||||
|
|
||||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||||
|
|
||||||
|
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
|
||||||
|
|
||||||
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
|
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
|
||||||
|
|
||||||
fun isRoomEncrypted(roomId: String): Boolean
|
fun isRoomEncrypted(roomId: String): Boolean
|
||||||
|
@ -123,6 +123,9 @@ internal abstract class CryptoModule {
|
|||||||
@Binds
|
@Binds
|
||||||
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
|
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask
|
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
|||||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||||
@ -127,6 +128,7 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
|
private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
|
||||||
// Tasks
|
// Tasks
|
||||||
private val getDevicesTask: GetDevicesTask,
|
private val getDevicesTask: GetDevicesTask,
|
||||||
|
private val getDeviceInfoTask: GetDeviceInfoTask,
|
||||||
private val setDeviceNameTask: SetDeviceNameTask,
|
private val setDeviceNameTask: SetDeviceNameTask,
|
||||||
private val uploadKeysTask: UploadKeysTask,
|
private val uploadKeysTask: UploadKeysTask,
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
@ -199,6 +201,14 @@ internal class DefaultCryptoService @Inject constructor(
|
|||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
|
||||||
|
getDeviceInfoTask
|
||||||
|
.configureWith(GetDeviceInfoTask.Params(deviceId)) {
|
||||||
|
this.callback = callback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
|
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
|
||||||
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
|
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
|
||||||
}
|
}
|
||||||
|
@ -25,11 +25,18 @@ internal interface CryptoApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the devices list
|
* Get the devices list
|
||||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices
|
* Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices
|
||||||
*/
|
*/
|
||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
|
||||||
fun getDevices(): Call<DevicesListResponse>
|
fun getDevices(): Call<DevicesListResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the device info by id
|
||||||
|
* Doc: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-devices-deviceid
|
||||||
|
*/
|
||||||
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{deviceId}")
|
||||||
|
fun getDeviceInfo(@Path("deviceId") deviceId: String): Call<DeviceInfo>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload device and/or one-time keys.
|
* Upload device and/or one-time keys.
|
||||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload
|
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.matrix.android.internal.crypto.tasks
|
||||||
|
|
||||||
|
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface GetDeviceInfoTask : Task<GetDeviceInfoTask.Params, DeviceInfo> {
|
||||||
|
data class Params(val deviceId: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultGetDeviceInfoTask @Inject constructor(private val cryptoApi: CryptoApi)
|
||||||
|
: GetDeviceInfoTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: GetDeviceInfoTask.Params): DeviceInfo {
|
||||||
|
return executeRequest {
|
||||||
|
apiCall = cryptoApi.getDeviceInfo(params.deviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -45,6 +45,7 @@ import im.vector.riotx.features.roomdirectory.createroom.CreateRoomFragment
|
|||||||
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
||||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
||||||
import im.vector.riotx.features.settings.*
|
import im.vector.riotx.features.settings.*
|
||||||
|
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||||
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
||||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||||
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
|
||||||
@ -226,7 +227,12 @@ interface FragmentModule {
|
|||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(VectorSettingsIgnoredUsersFragment::class)
|
@FragmentKey(VectorSettingsIgnoredUsersFragment::class)
|
||||||
fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
|
fun bindVectorSettingsIgnoredUsersFragment(fragment: VectorSettingsIgnoredUsersFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(VectorSettingsDevicesFragment::class)
|
||||||
|
fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
|
@ -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_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY"
|
||||||
const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
|
const val SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
|
||||||
const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
|
const val SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY"
|
||||||
const val SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
|
|
||||||
const val SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY"
|
|
||||||
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
|
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY"
|
||||||
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
|
const val SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY"
|
||||||
const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
|
const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"
|
||||||
|
@ -18,12 +18,8 @@ package im.vector.riotx.features.settings
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.EditText
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@ -33,30 +29,20 @@ import androidx.preference.SwitchPreference
|
|||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
|
import im.vector.matrix.android.api.extensions.getFingerprintHumanReadable
|
||||||
import im.vector.matrix.android.api.extensions.sortByLastSeen
|
|
||||||
import im.vector.matrix.android.api.failure.Failure
|
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.dialogs.ExportKeysDialog
|
import im.vector.riotx.core.dialogs.ExportKeysDialog
|
||||||
import im.vector.riotx.core.intent.ExternalIntentData
|
import im.vector.riotx.core.intent.ExternalIntentData
|
||||||
import im.vector.riotx.core.intent.analyseIntent
|
import im.vector.riotx.core.intent.analyseIntent
|
||||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||||
import im.vector.riotx.core.platform.SimpleTextWatcher
|
import im.vector.riotx.core.platform.SimpleTextWatcher
|
||||||
import im.vector.riotx.core.preference.ProgressBarPreference
|
|
||||||
import im.vector.riotx.core.preference.VectorPreference
|
import im.vector.riotx.core.preference.VectorPreference
|
||||||
import im.vector.riotx.core.preference.VectorPreferenceDivider
|
import im.vector.riotx.core.preference.VectorPreferenceDivider
|
||||||
import im.vector.riotx.core.utils.*
|
import im.vector.riotx.core.utils.*
|
||||||
import im.vector.riotx.features.crypto.keys.KeysExporter
|
import im.vector.riotx.features.crypto.keys.KeysExporter
|
||||||
import im.vector.riotx.features.crypto.keys.KeysImporter
|
import im.vector.riotx.features.crypto.keys.KeysImporter
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import timber.log.Timber
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
||||||
@ -66,9 +52,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
override var titleRes = R.string.settings_security_and_privacy
|
override var titleRes = R.string.settings_security_and_privacy
|
||||||
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
|
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
|
||||||
|
|
||||||
// used to avoid requesting to enter the password for each deletion
|
|
||||||
private var mAccountPassword: String = ""
|
|
||||||
|
|
||||||
// devices: device IDs and device names
|
// devices: device IDs and device names
|
||||||
private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf()
|
private val mDevicesNameList: MutableList<DeviceInfo> = mutableListOf()
|
||||||
|
|
||||||
@ -95,12 +78,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
private val mPushersSettingsCategory by lazy {
|
private val mPushersSettingsCategory by lazy {
|
||||||
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
|
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY)!!
|
||||||
}
|
}
|
||||||
private val mDevicesListSettingsCategory by lazy {
|
|
||||||
findPreference<PreferenceCategory>(VectorPreferences.SETTINGS_DEVICES_LIST_PREFERENCE_KEY)!!
|
|
||||||
}
|
|
||||||
private val mDevicesListSettingsCategoryDivider by lazy {
|
|
||||||
findPreference<VectorPreferenceDivider>(VectorPreferences.SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY)!!
|
|
||||||
}
|
|
||||||
private val cryptoInfoDeviceNamePreference by lazy {
|
private val cryptoInfoDeviceNamePreference by lazy {
|
||||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
|
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
|
||||||
}
|
}
|
||||||
@ -129,13 +106,16 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
|
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// My device name may have been updated
|
||||||
|
refreshMyDevice()
|
||||||
|
}
|
||||||
|
|
||||||
override fun bindPref() {
|
override fun bindPref() {
|
||||||
// Push target
|
// Push target
|
||||||
refreshPushersList()
|
refreshPushersList()
|
||||||
|
|
||||||
// Device list
|
|
||||||
refreshDevicesList()
|
|
||||||
|
|
||||||
// Refresh Key Management section
|
// Refresh Key Management section
|
||||||
refreshKeysManagementSection()
|
refreshKeysManagementSection()
|
||||||
|
|
||||||
@ -375,7 +355,8 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
|
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
|
||||||
|
|
||||||
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
displayDeviceRenameDialog(aMyDeviceInfo)
|
// TODO device can be rename only from the device list screen for the moment
|
||||||
|
// displayDeviceRenameDialog(aMyDeviceInfo)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,342 +409,22 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
// devices list
|
// devices list
|
||||||
// ==============================================================================================================
|
// ==============================================================================================================
|
||||||
|
|
||||||
private fun removeDevicesPreference() {
|
private fun refreshMyDevice() {
|
||||||
preferenceScreen.let {
|
// TODO Move to a ViewModel...
|
||||||
it.removePreference(mDevicesListSettingsCategory)
|
session.sessionParams.credentials.deviceId?.let {
|
||||||
it.removePreference(mDevicesListSettingsCategoryDivider)
|
session.getDeviceInfo(it, object : MatrixCallback<DeviceInfo> {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force the refresh of the devices list.<br></br>
|
|
||||||
* The devices list is the list of the devices where the user as looged in.
|
|
||||||
* It can be any mobile device, as any browser.
|
|
||||||
*/
|
|
||||||
private fun refreshDevicesList() {
|
|
||||||
if (session.isCryptoEnabled() && !session.sessionParams.credentials.deviceId.isNullOrEmpty()) {
|
|
||||||
// display a spinner while loading the devices list
|
|
||||||
if (0 == mDevicesListSettingsCategory.preferenceCount) {
|
|
||||||
activity?.let {
|
|
||||||
val preference = ProgressBarPreference(it)
|
|
||||||
mDevicesListSettingsCategory.addPreference(preference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
|
|
||||||
override fun onSuccess(data: DevicesListResponse) {
|
|
||||||
if (!isAdded) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.devices?.isEmpty() == true) {
|
|
||||||
removeDevicesPreference()
|
|
||||||
} else {
|
|
||||||
buildDevicesSettings(data.devices!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
override fun onFailure(failure: Throwable) {
|
||||||
if (!isAdded) {
|
// Ignore for this time?...
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
removeDevicesPreference()
|
override fun onSuccess(data: DeviceInfo) {
|
||||||
onCommonDone(failure.message)
|
mMyDeviceInfo = data
|
||||||
|
refreshCryptographyPreference(data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
removeDevicesPreference()
|
|
||||||
removeCryptographyPreference()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the devices portion of the settings.<br></br>
|
|
||||||
* Each row correspond to a device ID and its corresponding device name. Clicking on the row
|
|
||||||
* display a dialog containing: the device ID, the device name and the "last seen" information.
|
|
||||||
*
|
|
||||||
* @param aDeviceInfoList the list of the devices
|
|
||||||
*/
|
|
||||||
private fun buildDevicesSettings(aDeviceInfoList: List<DeviceInfo>) {
|
|
||||||
var preference: VectorPreference
|
|
||||||
var typeFaceHighlight: Int
|
|
||||||
var isNewList = true
|
|
||||||
val myDeviceId = session.sessionParams.credentials.deviceId
|
|
||||||
|
|
||||||
if (aDeviceInfoList.size == mDevicesNameList.size) {
|
|
||||||
isNewList = !mDevicesNameList.containsAll(aDeviceInfoList)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNewList) {
|
|
||||||
var prefIndex = 0
|
|
||||||
mDevicesNameList.clear()
|
|
||||||
mDevicesNameList.addAll(aDeviceInfoList)
|
|
||||||
|
|
||||||
// sort before display: most recent first
|
|
||||||
mDevicesNameList.sortByLastSeen()
|
|
||||||
|
|
||||||
// start from scratch: remove the displayed ones
|
|
||||||
mDevicesListSettingsCategory.removeAll()
|
|
||||||
|
|
||||||
for (deviceInfo in mDevicesNameList) {
|
|
||||||
// set bold to distinguish current device ID
|
|
||||||
if (null != myDeviceId && myDeviceId == deviceInfo.deviceId) {
|
|
||||||
mMyDeviceInfo = deviceInfo
|
|
||||||
typeFaceHighlight = Typeface.BOLD
|
|
||||||
} else {
|
|
||||||
typeFaceHighlight = Typeface.NORMAL
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the edit text preference
|
|
||||||
preference = VectorPreference(requireActivity()).apply {
|
|
||||||
mTypeface = typeFaceHighlight
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null == deviceInfo.deviceId && null == deviceInfo.displayName) {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
if (null != deviceInfo.deviceId) {
|
|
||||||
preference.title = deviceInfo.deviceId
|
|
||||||
}
|
|
||||||
|
|
||||||
// display name parameter can be null (new JSON API)
|
|
||||||
if (null != deviceInfo.displayName) {
|
|
||||||
preference.summary = deviceInfo.displayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.key = DEVICES_PREFERENCE_KEY_BASE + prefIndex
|
|
||||||
prefIndex++
|
|
||||||
|
|
||||||
// onClick handler: display device details dialog
|
|
||||||
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
displayDeviceDetailsDialog(deviceInfo)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
mDevicesListSettingsCategory.addPreference(preference)
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshCryptographyPreference(mMyDeviceInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a dialog containing the device ID, the device name and the "last seen" information.<>
|
|
||||||
* This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog])
|
|
||||||
*
|
|
||||||
* @param aDeviceInfo the device information
|
|
||||||
*/
|
|
||||||
private fun displayDeviceDetailsDialog(aDeviceInfo: DeviceInfo) {
|
|
||||||
activity?.let {
|
|
||||||
val builder = AlertDialog.Builder(it)
|
|
||||||
val inflater = it.layoutInflater
|
|
||||||
val layout = inflater.inflate(R.layout.dialog_device_details, null)
|
|
||||||
var textView = layout.findViewById<TextView>(R.id.device_id)
|
|
||||||
|
|
||||||
textView.text = aDeviceInfo.deviceId
|
|
||||||
|
|
||||||
// device name
|
|
||||||
textView = layout.findViewById(R.id.device_name)
|
|
||||||
val displayName = if (aDeviceInfo.displayName.isNullOrEmpty()) LABEL_UNAVAILABLE_DATA else aDeviceInfo.displayName
|
|
||||||
textView.text = displayName
|
|
||||||
|
|
||||||
// last seen info
|
|
||||||
textView = layout.findViewById(R.id.device_last_seen)
|
|
||||||
|
|
||||||
val lastSeenIp = aDeviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
|
|
||||||
|
|
||||||
val lastSeenTime = aDeviceInfo.lastSeenTs?.let { ts ->
|
|
||||||
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
|
|
||||||
val date = Date(ts)
|
|
||||||
|
|
||||||
val time = dateFormatTime.format(date)
|
|
||||||
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
|
|
||||||
|
|
||||||
dateFormat.format(date) + ", " + time
|
|
||||||
} ?: "-"
|
|
||||||
|
|
||||||
val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
|
|
||||||
textView.text = lastSeenInfo
|
|
||||||
|
|
||||||
// title & icon
|
|
||||||
builder.setTitle(R.string.devices_details_dialog_title)
|
|
||||||
.setIcon(android.R.drawable.ic_dialog_info)
|
|
||||||
.setView(layout)
|
|
||||||
.setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(aDeviceInfo) }
|
|
||||||
|
|
||||||
// disable the deletion for our own device
|
|
||||||
if (session.getMyDevice().deviceId != aDeviceInfo.deviceId) {
|
|
||||||
builder.setNegativeButton(R.string.delete) { _, _ -> deleteDevice(aDeviceInfo) }
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setNeutralButton(R.string.cancel, null)
|
|
||||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
|
||||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
dialog.cancel()
|
|
||||||
return@OnKeyListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display an alert dialog to rename a device
|
|
||||||
*
|
|
||||||
* @param aDeviceInfoToRename device info
|
|
||||||
*/
|
|
||||||
private fun displayDeviceRenameDialog(aDeviceInfoToRename: DeviceInfo) {
|
|
||||||
activity?.let {
|
|
||||||
val inflater = it.layoutInflater
|
|
||||||
val layout = inflater.inflate(R.layout.dialog_base_edit_text, null)
|
|
||||||
|
|
||||||
val input = layout.findViewById<EditText>(R.id.edit_text)
|
|
||||||
input.setText(aDeviceInfoToRename.displayName)
|
|
||||||
|
|
||||||
AlertDialog.Builder(it)
|
|
||||||
.setTitle(R.string.devices_details_device_name)
|
|
||||||
.setView(layout)
|
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
|
||||||
displayLoadingView()
|
|
||||||
|
|
||||||
val newName = input.text.toString()
|
|
||||||
|
|
||||||
session.setDeviceName(aDeviceInfoToRename.deviceId!!, newName, object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
hideLoadingView()
|
|
||||||
|
|
||||||
// search which preference is updated
|
|
||||||
val count = mDevicesListSettingsCategory.preferenceCount
|
|
||||||
|
|
||||||
for (i in 0 until count) {
|
|
||||||
val pref = mDevicesListSettingsCategory.getPreference(i)
|
|
||||||
|
|
||||||
if (aDeviceInfoToRename.deviceId == pref.title) {
|
|
||||||
pref.summary = newName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect if the updated device is the current account one
|
|
||||||
if (cryptoInfoDeviceIdPreference.summary == aDeviceInfoToRename.deviceId) {
|
|
||||||
cryptoInfoDeviceNamePreference.summary = newName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also change the display name in aDeviceInfoToRename, in case of multiple renaming
|
|
||||||
aDeviceInfoToRename.displayName = newName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
onCommonDone(failure.localizedMessage)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to delete a device.
|
|
||||||
*
|
|
||||||
* @param deviceInfo the device to delete
|
|
||||||
*/
|
|
||||||
private fun deleteDevice(deviceInfo: DeviceInfo) {
|
|
||||||
val deviceId = deviceInfo.deviceId
|
|
||||||
if (deviceId == null) {
|
|
||||||
Timber.e("## displayDeviceDeletionDialog(): sanity check failure")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
displayLoadingView()
|
|
||||||
session.deleteDevice(deviceId, object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
hideLoadingView()
|
|
||||||
// force settings update
|
|
||||||
refreshDevicesList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
var isPasswordRequestFound = false
|
|
||||||
|
|
||||||
if (failure is Failure.RegistrationFlowError) {
|
|
||||||
// We only support LoginFlowTypes.PASSWORD
|
|
||||||
// Check if we can provide the user password
|
|
||||||
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
|
|
||||||
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPasswordRequestFound) {
|
|
||||||
maybeShowDeleteDeviceWithPasswordDialog(deviceId, failure.registrationFlowResponse.session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isPasswordRequestFound) {
|
|
||||||
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
|
|
||||||
onCommonDone(failure.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a dialog to ask for user password, or use a previously entered password.
|
|
||||||
*/
|
|
||||||
private fun maybeShowDeleteDeviceWithPasswordDialog(deviceId: String, authSession: String?) {
|
|
||||||
if (mAccountPassword.isNotEmpty()) {
|
|
||||||
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
|
|
||||||
} else {
|
|
||||||
activity?.let {
|
|
||||||
val inflater = it.layoutInflater
|
|
||||||
val layout = inflater.inflate(R.layout.dialog_device_delete, null)
|
|
||||||
val passwordEditText = layout.findViewById<EditText>(R.id.delete_password)
|
|
||||||
|
|
||||||
AlertDialog.Builder(it)
|
|
||||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
||||||
.setTitle(R.string.devices_delete_dialog_title)
|
|
||||||
.setView(layout)
|
|
||||||
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
|
|
||||||
if (passwordEditText.toString().isEmpty()) {
|
|
||||||
it.toast(R.string.error_empty_field_your_password)
|
|
||||||
return@OnClickListener
|
|
||||||
}
|
|
||||||
mAccountPassword = passwordEditText.text.toString()
|
|
||||||
deleteDeviceWithPassword(deviceId, authSession, mAccountPassword)
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
|
||||||
hideLoadingView()
|
|
||||||
}
|
|
||||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
|
||||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
dialog.cancel()
|
|
||||||
hideLoadingView()
|
|
||||||
return@OnKeyListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteDeviceWithPassword(deviceId: String, authSession: String?, accountPassword: String) {
|
|
||||||
session.deleteDeviceWithUserPassword(deviceId, authSession, accountPassword, object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
hideLoadingView()
|
|
||||||
// force settings update
|
|
||||||
refreshDevicesList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
// Password is maybe not good
|
|
||||||
onCommonDone(failure.localizedMessage)
|
|
||||||
mAccountPassword = ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==============================================================================================================
|
// ==============================================================================================================
|
||||||
// pushers list management
|
// pushers list management
|
||||||
// ==============================================================================================================
|
// ==============================================================================================================
|
||||||
@ -860,6 +521,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
|
private const val DEVICES_PREFERENCE_KEY_BASE = "DEVICES_PREFERENCE_KEY_BASE"
|
||||||
|
|
||||||
// TODO i18n
|
// TODO i18n
|
||||||
private const val LABEL_UNAVAILABLE_DATA = "none"
|
const val LABEL_UNAVAILABLE_DATA = "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<DeviceItem.Holder>() {
|
||||||
|
|
||||||
|
@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<View>(R.id.itemDeviceRoot)
|
||||||
|
val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName)
|
||||||
|
val deviceIdText by bind<TextView>(R.id.itemDeviceId)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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<List<DeviceInfo>> = Uninitialized,
|
||||||
|
val request: Async<Unit> = Uninitialized
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
sealed class DevicesAction : VectorViewModelAction {
|
||||||
|
object Retry : DevicesAction()
|
||||||
|
data class Delete(val deviceInfo: DeviceInfo) : DevicesAction()
|
||||||
|
data class Password(val password: String) : DevicesAction()
|
||||||
|
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
|
||||||
|
private val session: Session)
|
||||||
|
: VectorViewModel<DevicesViewState, DevicesAction>(initialState) {
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: DevicesViewState): DevicesViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<DevicesViewModel, DevicesViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: DevicesViewState): DevicesViewModel? {
|
||||||
|
val fragment: VectorSettingsDevicesFragment = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
|
return fragment.devicesViewModelFactory.create(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// temp storage when we ask for the user password
|
||||||
|
private var _currentDeviceId: String? = null
|
||||||
|
private var _currentSession: String? = null
|
||||||
|
|
||||||
|
private val _requestPasswordLiveData = MutableLiveData<LiveEvent<Unit>>()
|
||||||
|
val requestPasswordLiveData: LiveData<LiveEvent<Unit>>
|
||||||
|
get() = _requestPasswordLiveData
|
||||||
|
|
||||||
|
init {
|
||||||
|
refreshDevicesList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force the refresh of the devices list.
|
||||||
|
* The devices list is the list of the devices where the user 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<DevicesListResponse> {
|
||||||
|
override fun onSuccess(data: DevicesListResponse) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
|
||||||
|
devices = Success(data.devices.orEmpty())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
devices = Fail(failure)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Should not happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: DevicesAction) {
|
||||||
|
return when (action) {
|
||||||
|
is DevicesAction.Retry -> refreshDevicesList()
|
||||||
|
is DevicesAction.Delete -> handleDelete(action)
|
||||||
|
is DevicesAction.Password -> handlePassword(action)
|
||||||
|
is DevicesAction.Rename -> handleRename(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRename(action: DevicesAction.Rename) {
|
||||||
|
session.setDeviceName(action.deviceInfo.deviceId!!, action.newName, object : MatrixCallback<Unit> {
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Success(data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// force settings update
|
||||||
|
refreshDevicesList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Fail(failure)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestErrorLiveData.postLiveEvent(failure)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to delete a device.
|
||||||
|
*/
|
||||||
|
private fun handleDelete(action: DevicesAction.Delete) {
|
||||||
|
val deviceId = action.deviceInfo.deviceId
|
||||||
|
if (deviceId == null) {
|
||||||
|
Timber.e("## handleDelete(): sanity check failure")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Loading()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.deleteDevice(deviceId, object : MatrixCallback<Unit> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
var isPasswordRequestFound = false
|
||||||
|
|
||||||
|
if (failure is Failure.RegistrationFlowError) {
|
||||||
|
// We only support LoginFlowTypes.PASSWORD
|
||||||
|
// Check if we can provide the user password
|
||||||
|
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
|
||||||
|
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPasswordRequestFound) {
|
||||||
|
_currentDeviceId = deviceId
|
||||||
|
_currentSession = failure.registrationFlowResponse.session
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Success(Unit)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestPasswordLiveData.postLiveEvent(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPasswordRequestFound) {
|
||||||
|
// LoginFlowTypes.PASSWORD not supported, and this is the only one RiotX supports so far...
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Fail(failure)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestErrorLiveData.postLiveEvent(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Success(data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// force settings update
|
||||||
|
refreshDevicesList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePassword(action: DevicesAction.Password) {
|
||||||
|
val currentDeviceId = _currentDeviceId
|
||||||
|
if (currentDeviceId.isNullOrBlank()) {
|
||||||
|
// Abort
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Loading()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback<Unit> {
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
_currentDeviceId = null
|
||||||
|
_currentSession = null
|
||||||
|
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Success(data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// force settings update
|
||||||
|
refreshDevicesList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
_currentDeviceId = null
|
||||||
|
_currentSession = null
|
||||||
|
|
||||||
|
// Password is maybe not good
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
request = Fail(failure)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_requestErrorLiveData.postLiveEvent(failure)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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<TextView>(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<EditText>(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<EditText>(R.id.delete_password)
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setTitle(R.string.devices_delete_dialog_title)
|
||||||
|
.setView(layout)
|
||||||
|
.setPositiveButton(R.string.devices_delete_submit_button_label, DialogInterface.OnClickListener { _, _ ->
|
||||||
|
if (passwordEditText.toString().isEmpty()) {
|
||||||
|
requireActivity().toast(R.string.error_empty_field_your_password)
|
||||||
|
return@OnClickListener
|
||||||
|
}
|
||||||
|
mAccountPassword = passwordEditText.text.toString()
|
||||||
|
devicesViewModel.handle(DevicesAction.Password(mAccountPassword))
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||||
|
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
|
dialog.cancel()
|
||||||
|
return@OnKeyListener true
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun invalidate() = withState(devicesViewModel) { state ->
|
||||||
|
devicesController.update(state)
|
||||||
|
|
||||||
|
handleRequestStatus(state.request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRequestStatus(unIgnoreRequest: Async<Unit>) {
|
||||||
|
when (unIgnoreRequest) {
|
||||||
|
is Loading -> waiting_view.isVisible = true
|
||||||
|
else -> waiting_view.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
vector/src/main/res/layout/item_device.xml
Normal file
44
vector/src/main/res/layout/item_device.xml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/itemDeviceRoot"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?riotx_background"
|
||||||
|
android:foreground="?attr/selectableItemBackground"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/itemDeviceDisplayName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?riotx_text_primary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/itemUserName"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/itemUserAvatar"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="Android phone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/itemDeviceId"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="15sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/itemUserAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/itemUserId"
|
||||||
|
tools:text="XUIDERFZAA" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -5,4 +5,8 @@
|
|||||||
|
|
||||||
<string name="notification_initial_sync">Initial Sync…</string>
|
<string name="notification_initial_sync">Initial Sync…</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<string name="settings_show_devices_list">See all my devices</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<!-- ************ Cryptography section ************ -->
|
<!-- ************ Cryptography section ************ -->
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||||
@ -27,6 +28,20 @@
|
|||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" />
|
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY" />
|
||||||
|
|
||||||
|
<!-- devices list entry point -->
|
||||||
|
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||||
|
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
|
||||||
|
android:title="@string/settings_devices_list">
|
||||||
|
|
||||||
|
<im.vector.riotx.core.preference.VectorPreference
|
||||||
|
android:key="SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY"
|
||||||
|
android:title="@string/settings_show_devices_list"
|
||||||
|
app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment"/>
|
||||||
|
|
||||||
|
</im.vector.riotx.core.preference.VectorPreferenceCategory>
|
||||||
|
|
||||||
|
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY" />
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||||
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
|
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
|
||||||
android:title="@string/settings_cryptography_manage_keys">
|
android:title="@string/settings_cryptography_manage_keys">
|
||||||
@ -50,13 +65,6 @@
|
|||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" />
|
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY" />
|
||||||
|
|
||||||
<!-- devices list: device ids + device names -->
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
|
||||||
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
|
|
||||||
android:title="@string/settings_devices_list" />
|
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceDivider android:key="SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY" />
|
|
||||||
|
|
||||||
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
<im.vector.riotx.core.preference.VectorPreferenceCategory
|
||||||
android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY"
|
android:key="SETTINGS_ANALYTICS_PREFERENCE_KEY"
|
||||||
android:title="@string/settings_analytics">
|
android:title="@string/settings_analytics">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user