Update My device list + action to verify

This commit is contained in:
Valere 2020-01-23 13:57:17 +01:00
parent a0aa1f34d3
commit 1276d1f39d
19 changed files with 692 additions and 250 deletions

View File

@ -184,7 +184,17 @@ internal class DefaultCryptoService @Inject constructor(
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) { override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
setDeviceNameTask setDeviceNameTask
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) { .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
this.callback = callback this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
//bg refresh of crypto device
downloadKeys(listOf(credentials.userId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {})
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View File

@ -130,6 +130,10 @@ internal class DefaultCrossSigningService @Inject constructor(
listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() } listOf(masterPkSigning, userPkSigning, selfSigningPkSigning).forEach { it?.releaseSigning() }
} }
protected fun finalize() {
release()
}
/** /**
* - Make 3 key pairs (MSK, USK, SSK) * - Make 3 key pairs (MSK, USK, SSK)
* - Save the private keys with proper security * - Save the private keys with proper security

View File

@ -52,6 +52,7 @@ import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.settings.devices.DeviceVerificationInfoBottomSheet
import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
@ -142,6 +143,8 @@ interface ScreenComponent {
fun inject(activity: DebugMenuActivity) fun inject(activity: DebugMenuActivity)
fun inject(deviceVerificationInfoBottomSheet: DeviceVerificationInfoBottomSheet)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View File

@ -60,6 +60,10 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
@DrawableRes @DrawableRes
var endIconResourceId: Int = -1 var endIconResourceId: Int = -1
@EpoxyAttribute
@DrawableRes
var titleIconResourceId: Int = -1
@EpoxyAttribute @EpoxyAttribute
var hasIndeterminateProcess = false var hasIndeterminateProcess = false
@ -72,6 +76,13 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.titleText.setTextOrHide(title) holder.titleText.setTextOrHide(title)
if (titleIconResourceId != -1) {
holder.titleIcon.setImageResource(titleIconResourceId)
holder.titleIcon.isVisible = true
} else {
holder.titleIcon.isVisible = false
}
when (style) { when (style) {
STYLE.BIG_TEXT -> holder.titleText.textSize = 18f STYLE.BIG_TEXT -> holder.titleText.textSize = 18f
STYLE.NORMAL_TEXT -> holder.titleText.textSize = 14f STYLE.NORMAL_TEXT -> holder.titleText.textSize = 14f
@ -104,7 +115,7 @@ abstract class GenericItem : VectorEpoxyModel<GenericItem.Holder>() {
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val root by bind<View>(R.id.item_generic_root) val root by bind<View>(R.id.item_generic_root)
val titleIcon by bind<ImageView>(R.id.item_generic_title_image)
val titleText by bind<TextView>(R.id.item_generic_title_text) val titleText by bind<TextView>(R.id.item_generic_title_text)
val descriptionText by bind<TextView>(R.id.item_generic_description_text) val descriptionText by bind<TextView>(R.id.item_generic_description_text)
val accessoryImage by bind<ImageView>(R.id.item_generic_accessory_image) val accessoryImage by bind<ImageView>(R.id.item_generic_accessory_image)

View File

@ -66,13 +66,11 @@ class VectorPreferences @Inject constructor(private val context: Context) {
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_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_CROSS_SIGNING_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" const val SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
const val SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY" const val SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY"
const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY" const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"

View File

@ -1,5 +1,6 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2019 New Vector Ltd
* Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,10 +29,10 @@ import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreference 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.internal.crypto.crosssigning.isVerified import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
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
@ -43,7 +44,6 @@ import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.openFileSelection import im.vector.riotx.core.utils.openFileSelection
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter import im.vector.riotx.features.crypto.keys.KeysExporter
@ -80,14 +80,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY)!!
} }
private val cryptoInfoDeviceNamePreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY)!!
}
private val cryptoInfoDeviceIdPreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY)!!
}
private val manageBackupPref by lazy { private val manageBackupPref by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY)!!
} }
@ -100,9 +92,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY)!!
} }
private val cryptoInfoTextPreference by lazy { private val showDeviceListPref by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY)!!
} }
// encrypt to unverified devices // encrypt to unverified devices
private val sendToUnverifiedDevicesPref by lazy { private val sendToUnverifiedDevicesPref by lazy {
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!! findPreference<SwitchPreference>(VectorPreferences.SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY)!!
@ -112,6 +105,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
super.onResume() super.onResume()
// My device name may have been updated // My device name may have been updated
refreshMyDevice() refreshMyDevice()
mCryptographyCategory.isVisible = vectorPreferences.developerMode()
} }
override fun bindPref() { override fun bindPref() {
@ -137,13 +131,13 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
if (vectorPreferences.developerMode()) { if (vectorPreferences.developerMode()) {
val crossSigningKeys = session.getCrossSigningService().getMyCrossSigningKeys() val crossSigningKeys = session.getCrossSigningService().getMyCrossSigningKeys()
val xSigningIsEnableInAccount = crossSigningKeys != null val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningCaseAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified() val xSigningKeysAreTrusted = session.getCrossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign() val xSigningKeyCanSign = session.getCrossSigningService().canCrossSign()
if (xSigningKeyCanSign) { if (xSigningKeyCanSign) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningCaseAreTrusted) { } else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_warning) mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_warning)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted) mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) { } else if (xSigningIsEnableInAccount) {
@ -361,53 +355,55 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
* *
* @param aMyDeviceInfo the device info * @param aMyDeviceInfo the device info
*/ */
private fun refreshCryptographyPreference(aMyDeviceInfo: DeviceInfo?) { private fun refreshCryptographyPreference(devices: List<DeviceInfo>) {
val userId = session.myUserId showDeviceListPref.isEnabled = devices.size > 0
val deviceId = session.sessionParams.credentials.deviceId showDeviceListPref.summary = resources.getQuantityString(R.plurals.settings_active_sessions_count, devices.size, devices.size)
// val userId = session.myUserId
// val deviceId = session.sessionParams.credentials.deviceId
// device name // device name
if (null != aMyDeviceInfo) { // if (null != aMyDeviceInfo) {
cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName // cryptoInfoDeviceNamePreference.summary = aMyDeviceInfo.displayName
//
cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { // cryptoInfoDeviceNamePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
// TODO device can be rename only from the device list screen for the moment // // TODO device can be rename only from the device list screen for the moment
// displayDeviceRenameDialog(aMyDeviceInfo) // // displayDeviceRenameDialog(aMyDeviceInfo)
true // true
} // }
//
cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener { // cryptoInfoDeviceNamePreference.onPreferenceLongClickListener = object : VectorPreference.OnPreferenceLongClickListener {
override fun onPreferenceLongClick(preference: Preference): Boolean { // override fun onPreferenceLongClick(preference: Preference): Boolean {
activity?.let { copyToClipboard(it, aMyDeviceInfo.displayName!!) } // activity?.let { copyToClipboard(it, aMyDeviceInfo.displayName!!) }
return true // return true
} // }
} // }
} // }
//
// crypto section: device ID // // crypto section: device ID
if (!deviceId.isNullOrEmpty()) { // if (!deviceId.isNullOrEmpty()) {
cryptoInfoDeviceIdPreference.summary = deviceId // cryptoInfoDeviceIdPreference.summary = deviceId
//
cryptoInfoDeviceIdPreference.setOnPreferenceClickListener { // cryptoInfoDeviceIdPreference.setOnPreferenceClickListener {
activity?.let { copyToClipboard(it, deviceId) } // activity?.let { copyToClipboard(it, deviceId) }
true // true
} // }
} // }
//
// crypto section: device key (fingerprint) // // crypto section: device key (fingerprint)
if (!deviceId.isNullOrEmpty() && userId.isNotEmpty()) { // if (!deviceId.isNullOrEmpty() && userId.isNotEmpty()) {
val deviceInfo = session.getDeviceInfo(userId, deviceId) // val deviceInfo = session.getDeviceInfo(userId, deviceId)
//
if (null != deviceInfo && !deviceInfo.fingerprint().isNullOrEmpty()) { // if (null != deviceInfo && !deviceInfo.fingerprint().isNullOrEmpty()) {
cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable() // cryptoInfoTextPreference.summary = deviceInfo.getFingerprintHumanReadable()
//
cryptoInfoTextPreference.setOnPreferenceClickListener { // cryptoInfoTextPreference.setOnPreferenceClickListener {
deviceInfo.fingerprint()?.let { // deviceInfo.fingerprint()?.let {
copyToClipboard(requireActivity(), it) // copyToClipboard(requireActivity(), it)
} // }
true // true
} // }
} // }
} // }
sendToUnverifiedDevicesPref.isChecked = false sendToUnverifiedDevicesPref.isChecked = false
@ -426,18 +422,15 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
private fun refreshMyDevice() { private fun refreshMyDevice() {
// TODO Move to a ViewModel... // TODO Move to a ViewModel...
session.sessionParams.credentials.deviceId?.let { session.getDevicesList(object : MatrixCallback<DevicesListResponse> {
session.getDeviceInfo(it, object : MatrixCallback<DeviceInfo> { override fun onSuccess(data: DevicesListResponse) {
override fun onFailure(failure: Throwable) { refreshCryptographyPreference(data.devices ?: emptyList())
// Ignore for this time?... }
}
override fun onSuccess(data: DeviceInfo) { override fun onFailure(failure: Throwable) {
mMyDeviceInfo = data refreshCryptographyPreference(emptyList())
refreshCryptographyPreference(data) }
} })
})
}
} }
// ============================================================================================================== // ==============================================================================================================

View File

@ -19,7 +19,10 @@ package im.vector.riotx.features.settings.devices
import android.graphics.Typeface import android.graphics.Typeface
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
@ -29,7 +32,8 @@ import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.util.Locale
/** /**
* A list item for Device. * A list item for Device.
@ -43,67 +47,82 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var currentDevice = false var currentDevice = false
@EpoxyAttribute
var buttonsVisible = false
@EpoxyAttribute @EpoxyAttribute
var itemClickAction: (() -> Unit)? = null var itemClickAction: (() -> Unit)? = null
@EpoxyAttribute @EpoxyAttribute
var renameClickAction: (() -> Unit)? = null var detailedMode = false
@EpoxyAttribute @EpoxyAttribute
var deleteClickAction: (() -> Unit)? = null var trusted : Boolean? = false
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() } holder.root.setOnClickListener { itemClickAction?.invoke() }
holder.displayNameText.text = deviceInfo.displayName ?: "" if (trusted != null) {
holder.deviceIdText.text = deviceInfo.deviceId ?: "" holder.trustIcon.setImageDrawable(
ContextCompat.getDrawable(
holder.view.context,
if (trusted!!) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
)
)
holder.trustIcon.isInvisible = false
} else {
holder.trustIcon.isInvisible = true
}
val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" val detailedModeLabels = listOf(
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.displayNameLabelText,
holder.displayNameText, holder.displayNameText,
holder.deviceIdLabelText, holder.deviceIdLabelText,
holder.deviceIdText, holder.deviceIdText,
holder.deviceLastSeenLabelText, holder.deviceLastSeenLabelText,
holder.deviceLastSeenText holder.deviceLastSeenText
).map { )
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL) if (detailedMode) {
holder.summaryLabelText.isVisible = false
holder.displayNameText.text = deviceInfo.displayName ?: ""
holder.deviceIdText.text = deviceInfo.deviceId ?: ""
val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
detailedModeLabels.map {
it.isVisible = true
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
}
} else {
holder.summaryLabelText.text = deviceInfo.displayName ?: deviceInfo.deviceId ?: ""
holder.summaryLabelText.isVisible = true
detailedModeLabels.map {
it.isVisible = false
}
} }
holder.buttonDelete.isVisible = !currentDevice
holder.buttons.isVisible = buttonsVisible
holder.buttonRename.setOnClickListener { renameClickAction?.invoke() }
holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() }
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val root by bind<ViewGroup>(R.id.itemDeviceRoot) val root by bind<ViewGroup>(R.id.itemDeviceRoot)
val summaryLabelText by bind<TextView>(R.id.itemDeviceSimpleSummary)
val displayNameLabelText by bind<TextView>(R.id.itemDeviceDisplayNameLabel) val displayNameLabelText by bind<TextView>(R.id.itemDeviceDisplayNameLabel)
val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName) val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName)
val deviceIdLabelText by bind<TextView>(R.id.itemDeviceIdLabel) val deviceIdLabelText by bind<TextView>(R.id.itemDeviceIdLabel)
val deviceIdText by bind<TextView>(R.id.itemDeviceId) val deviceIdText by bind<TextView>(R.id.itemDeviceId)
val deviceLastSeenLabelText by bind<TextView>(R.id.itemDeviceLastSeenLabel) val deviceLastSeenLabelText by bind<TextView>(R.id.itemDeviceLastSeenLabel)
val deviceLastSeenText by bind<TextView>(R.id.itemDeviceLastSeen) val deviceLastSeenText by bind<TextView>(R.id.itemDeviceLastSeen)
val buttons by bind<View>(R.id.itemDeviceButtons)
val buttonDelete by bind<View>(R.id.itemDeviceDelete) val trustIcon by bind<ImageView>(R.id.itemDeviceTrustLevelIcon)
val buttonRename by bind<View>(R.id.itemDeviceRename)
} }
} }

View File

@ -0,0 +1,95 @@
/*
* 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.settings.devices
import android.os.Bundle
import android.os.Parcelable
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_generic_list_with_title.*
import javax.inject.Inject
@Parcelize
data class DeviceVerificationInfoArgs(
val userId: String,
val deviceId: String
) : Parcelable
class DeviceVerificationInfoBottomSheet : VectorBaseBottomSheetDialogFragment(), DeviceVerificationInfoEpoxyController.Callback {
private val viewModel: DeviceVerificationInfoBottomSheetViewModel by fragmentViewModel(DeviceVerificationInfoBottomSheetViewModel::class)
private val sharedViewModel: DevicesViewModel by parentFragmentViewModel (DevicesViewModel::class)
@Inject lateinit var deviceVerificationInfoViewModelFactory: DeviceVerificationInfoBottomSheetViewModel.Factory
@BindView(R.id.bottomSheetRecyclerView)
lateinit var recyclerView: RecyclerView
override fun injectWith(injector: ScreenComponent) {
injector.inject(this)
}
@Inject lateinit var epoxyController: DeviceVerificationInfoEpoxyController
override fun getLayoutResId() = R.layout.bottom_sheet_generic_list_with_title
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recyclerView.configureWith(
epoxyController,
showDivider = true,
hasFixedSize = false)
epoxyController.callback = this
bottomSheetTitle.isVisible = false
}
override fun onDestroyView() {
recyclerView.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) {
epoxyController.setData(it)
super.invalidate()
}
companion object {
fun newInstance(userId: String, deviceId: String): DeviceVerificationInfoBottomSheet {
val args = Bundle()
val parcelableArgs = DeviceVerificationInfoArgs(userId, deviceId)
args.putParcelable(MvRx.KEY_ARG, parcelableArgs)
return DeviceVerificationInfoBottomSheet().apply { arguments = args }
}
}
override fun onAction(action: DevicesAction) {
dismiss()
sharedViewModel.handle(action)
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.settings.devices
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
data class DeviceVerificationInfoBottomSheetViewState(
val cryptoDeviceInfo: Async<CryptoDeviceInfo> = Uninitialized
) : MvRxState
class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@Assisted initialState: DeviceVerificationInfoBottomSheetViewState,
val session: Session
) : VectorViewModel<DeviceVerificationInfoBottomSheetViewState, EmptyAction>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: DeviceVerificationInfoBottomSheetViewState): DeviceVerificationInfoBottomSheetViewModel
}
companion object : MvRxViewModelFactory<DeviceVerificationInfoBottomSheetViewModel, DeviceVerificationInfoBottomSheetViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: DeviceVerificationInfoBottomSheetViewState): DeviceVerificationInfoBottomSheetViewModel? {
val fragment: DeviceVerificationInfoBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.deviceVerificationInfoViewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): DeviceVerificationInfoBottomSheetViewState? {
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
val args = viewModelContext.args<DeviceVerificationInfoArgs>()
session.getDeviceInfo(args.userId, args.deviceId)?.let {
return DeviceVerificationInfoBottomSheetViewState(cryptoDeviceInfo = Success(it))
}
return super.initialState(viewModelContext)
}
}
override fun handle(action: EmptyAction) {
}
}

View File

@ -0,0 +1,123 @@
/*
* 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.settings.devices
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.dividerItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.GenericItem
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
class DeviceVerificationInfoEpoxyController @Inject constructor(private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val session: Session,
private val avatarRender: AvatarRenderer)
: TypedEpoxyController<DeviceVerificationInfoBottomSheetViewState>() {
var callback: Callback? = null
override fun buildModels(data: DeviceVerificationInfoBottomSheetViewState?) {
val device = data?.cryptoDeviceInfo?.invoke()
if (device == null) {
loadingItem {
id("loading")
}
} else {
if (device.isVerified) {
genericItem {
id("trust${device.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)
titleIconResourceId(R.drawable.ic_shield_trusted)
title(stringProvider.getString(R.string.encryption_information_verified))
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
genericItem {
id("trust${device.deviceId}")
titleIconResourceId(R.drawable.ic_shield_warning)
style(GenericItem.STYLE.BIG_TEXT)
title(stringProvider.getString(R.string.encryption_information_not_verified))
description(stringProvider.getString(R.string.settings_active_sessions_unverified_device_desc))
}
}
genericItem {
id("info${device.deviceId}")
title(device.displayName() ?: "")
description("(${device.deviceId})")
}
if (!device.isVerified) {
dividerItem {
id("d1")
}
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.verification_verify_device))
titleColor(colorProvider.getColor(R.color.riotx_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_accent))
listener {
callback?.onAction(DevicesAction.VerifyMyDevice(device.deviceId))
}
}
}
if (device.deviceId != session.sessionParams.credentials.deviceId) {
//Add the delete option
dividerItem {
id("d2")
}
bottomSheetVerificationActionItem {
id("delete")
title(stringProvider.getString(R.string.settings_active_sessions_signout_device))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
callback?.onAction(DevicesAction.Delete(device.deviceId))
}
}
}
dividerItem {
id("d3")
}
bottomSheetVerificationActionItem {
id("rename")
title(stringProvider.getString(R.string.rename))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
listener {
callback?.onAction(DevicesAction.PromptRename(device.deviceId))
}
}
}
}
interface Callback {
fun onAction(action: DevicesAction)
}
}

View File

@ -22,17 +22,21 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.extensions.sortByLastSeen import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItemHeader import im.vector.riotx.core.ui.list.genericItemHeader
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider) : EpoxyController() { private val stringProvider: StringProvider,
private val vectorPreferences: VectorPreferences) : EpoxyController() {
var callback: Callback? = null var callback: Callback? = null
private var viewState: DevicesViewState? = null private var viewState: DevicesViewState? = null
@ -65,11 +69,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
listener { callback?.retry() } listener { callback?.retry() }
} }
is Success -> is Success ->
buildDevicesList(devices(), state.myDeviceId, state.currentExpandedDeviceId) buildDevicesList(devices(), state.cryptoDevices(), state.myDeviceId)
} }
} }
private fun buildDevicesList(devices: List<DeviceInfo>, myDeviceId: String, currentExpandedDeviceId: String?) { private fun buildDevicesList(devices: List<DeviceInfo>, cryptoDevices: List<CryptoDeviceInfo>?, myDeviceId: String) {
// Current device // Current device
genericItemHeader { genericItemHeader {
id("current") id("current")
@ -83,17 +87,17 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
.forEachIndexed { idx, deviceInfo -> .forEachIndexed { idx, deviceInfo ->
deviceItem { deviceItem {
id("myDevice$idx") id("myDevice$idx")
detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(true) currentDevice(true)
buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) } trusted(true)
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
} }
} }
// Other devices // Other devices
if (devices.size > 1) { if (devices.size > 1) {
genericItemHeader { genericItemHeader {
id("others") id("others")
text(stringProvider.getString(R.string.devices_other_devices)) text(stringProvider.getString(R.string.devices_other_devices))
@ -109,12 +113,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
val isCurrentDevice = deviceInfo.deviceId == myDeviceId val isCurrentDevice = deviceInfo.deviceId == myDeviceId
deviceItem { deviceItem {
id("device$idx") id("device$idx")
detailedMode(vectorPreferences.developerMode())
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(isCurrentDevice) currentDevice(isCurrentDevice)
buttonsVisible(deviceInfo.deviceId == currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) } itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) } trusted(cryptoDevices?.firstOrNull { it.deviceId == deviceInfo.deviceId }?.isVerified ?: false)
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
} }
} }
} }
@ -123,7 +126,5 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
interface Callback { interface Callback {
fun retry() fun retry()
fun onDeviceClicked(deviceInfo: DeviceInfo) fun onDeviceClicked(deviceInfo: DeviceInfo)
fun onRenameDevice(deviceInfo: DeviceInfo)
fun onDeleteDevice(deviceInfo: DeviceInfo)
} }
} }

View File

@ -18,39 +18,53 @@ package im.vector.riotx.features.settings.devices
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.* import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
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.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
import im.vector.riotx.core.utils.LiveEvent import im.vector.riotx.core.utils.LiveEvent
import timber.log.Timber
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized, val devices: Async<List<DeviceInfo>> = Uninitialized,
val currentExpandedDeviceId: String? = null, val cryptoDevices: Async<List<CryptoDeviceInfo>> = Uninitialized,
val request: Async<Unit> = Uninitialized val request: Async<Unit> = Uninitialized
) : MvRxState ) : MvRxState
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object Retry : DevicesAction() object Retry : DevicesAction()
data class Delete(val deviceInfo: DeviceInfo) : DevicesAction() data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction() data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction() data class PromptRename(val deviceId: String, val deviceInfo: DeviceInfo? = null) : DevicesAction()
data class VerifyMyDevice(val deviceId: String, val userId: String? = null, val transactionId: String? = null) : DevicesAction()
} }
class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState, class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
private val session: Session) private val session: Session)
: VectorViewModel<DevicesViewState, DevicesAction>(initialState) { : VectorViewModel<DevicesViewState, DevicesAction>(initialState), SasVerificationService.SasVerificationListener {
@AssistedInject.Factory @AssistedInject.Factory
interface Factory { interface Factory {
@ -74,8 +88,26 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
val requestPasswordLiveData: LiveData<LiveEvent<Unit>> val requestPasswordLiveData: LiveData<LiveEvent<Unit>>
get() = _requestPasswordLiveData get() = _requestPasswordLiveData
// Used to communicate back from model to fragment
private val _requestLiveData = MutableLiveData<LiveEvent<Async<DevicesAction>>>()
val fragmentActionLiveData: LiveData<LiveEvent<Async<DevicesAction>>>
get() = _requestLiveData
init { init {
refreshDevicesList() refreshDevicesList()
session.getSasVerificationService().addListener(this)
}
override fun onCleared() {
session.getSasVerificationService().removeListener(this)
super.onCleared()
}
override fun transactionCreated(tx: SasVerificationTransaction) {}
override fun transactionUpdated(tx: SasVerificationTransaction) {
if(tx.state == SasVerificationTxState.Verified) {
refreshDevicesList()
}
} }
/** /**
@ -109,6 +141,25 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
} }
} }
}) })
// Put cached state
setState {
copy(
myDeviceId = session.sessionParams.credentials.deviceId ?: "",
cryptoDevices = Success(session.getUserDevices(session.myUserId))
)
}
// then force download
session.downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
setState {
copy(
cryptoDevices = Success(session.getUserDevices(session.myUserId))
)
}
}
})
} else { } else {
// Should not happen // Should not happen
} }
@ -116,21 +167,34 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
return when (action) { return when (action) {
is DevicesAction.Retry -> refreshDevicesList() is DevicesAction.Retry -> refreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action) is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
is DevicesAction.ToggleDevice -> handleToggleDevice(action) is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleVerify(action)
} }
} }
private fun handleToggleDevice(action: DevicesAction.ToggleDevice) { private fun handleVerify(action: DevicesAction.VerifyMyDevice) {
withState { // TODO Implement request in to DEVICE!!!
setState { val txID = session.getSasVerificationService().beginKeyVerification(VerificationMethod.SAS, session.myUserId, action.deviceId)
copy( if (txID != null) {
currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId _requestLiveData.postValue(LiveEvent(Success(
) action.copy(
} userId = session.myUserId,
transactionId = txID
)
)))
}
}
private fun handlePromptRename(action: DevicesAction.PromptRename) = withState { state ->
val info = state.devices.invoke()?.firstOrNull { it.deviceId == action.deviceId }
if (info == null) {
_requestLiveData.postValue(LiveEvent(Uninitialized))
} else {
_requestLiveData.postValue(LiveEvent(Success(action.copy(deviceInfo = info))))
} }
} }
@ -162,11 +226,7 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
* Try to delete a device. * Try to delete a device.
*/ */
private fun handleDelete(action: DevicesAction.Delete) { private fun handleDelete(action: DevicesAction.Delete) {
val deviceId = action.deviceInfo.deviceId val deviceId = action.deviceId
if (deviceId == null) {
Timber.e("## handleDelete(): sanity check failure")
return
}
setState { setState {
copy( copy(

View File

@ -25,6 +25,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
@ -35,6 +36,7 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import javax.inject.Inject import javax.inject.Inject
@ -69,6 +71,29 @@ class VectorSettingsDevicesFragment @Inject constructor(
devicesViewModel.requestPasswordLiveData.observeEvent(this) { devicesViewModel.requestPasswordLiveData.observeEvent(this) {
maybeShowDeleteDeviceWithPasswordDialog() maybeShowDeleteDeviceWithPasswordDialog()
} }
devicesViewModel.fragmentActionLiveData.observeEvent(this) { async ->
when (async) {
is Success -> {
when (val action = async.invoke()) {
is DevicesAction.PromptRename -> {
action.deviceInfo?.let { deviceInfo ->
displayDeviceRenameDialog(deviceInfo)
}
}
is DevicesAction.VerifyMyDevice -> {
if (context is VectorBaseActivity) {
VerificationBottomSheet.withArgs(
roomId = null,
otherUserId = action.userId!!,
transactionId = action.transactionId!!
).show(childFragmentManager, "REQPOP")
}
}
}
}
}
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -80,7 +105,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_devices_list) (activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_active_sessions_manage)
} }
private fun displayErrorDialog(throwable: Throwable) { private fun displayErrorDialog(throwable: Throwable) {
@ -92,16 +117,19 @@ class VectorSettingsDevicesFragment @Inject constructor(
} }
override fun onDeviceClicked(deviceInfo: DeviceInfo) { override fun onDeviceClicked(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo)) DeviceVerificationInfoBottomSheet.newInstance(deviceInfo.user_id ?: "", deviceInfo.deviceId ?: "").show(
childFragmentManager,
"VERIF_INFO"
)
} }
override fun onDeleteDevice(deviceInfo: DeviceInfo) { // override fun onDeleteDevice(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) // devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
} // }
//
override fun onRenameDevice(deviceInfo: DeviceInfo) { // override fun onRenameDevice(deviceInfo: DeviceInfo) {
displayDeviceRenameDialog(deviceInfo) // displayDeviceRenameDialog(deviceInfo)
} // }
override fun retry() { override fun retry() {
devicesViewModel.handle(DevicesAction.Retry) devicesViewModel.handle(DevicesAction.Retry)

View File

@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:background="?vctr_list_header_background_color"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

View File

@ -6,93 +6,90 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?riotx_background" android:background="?riotx_background"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:orientation="vertical" android:orientation="horizontal"
android:paddingStart="16dp" android:padding="8dp">
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/itemDeviceDisplayNameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceDisplayName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="Android phone" />
<TextView
android:id="@+id/itemDeviceIdLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="XUIDERFZAA" />
<TextView
android:id="@+id/itemDeviceLastSeenLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceLastSeen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
<LinearLayout <LinearLayout
android:id="@+id/itemDeviceButtons" android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_weight="1"
android:layout_marginTop="4dp" android:orientation="vertical">
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton <!-- In compact mode only this is shown-->
android:id="@+id/itemDeviceRename" <TextView
style="@style/VectorButtonStyleText" android:id="@+id/itemDeviceSimpleSummary"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/rename" /> android:gravity="center_vertical"
android:minHeight="40dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="Riot X" />
<com.google.android.material.button.MaterialButton <!---
android:id="@+id/itemDeviceDelete" The following detailed informations are displayed in developper mode
style="@style/VectorButtonStyleText" -->
android:layout_width="wrap_content" <TextView
android:id="@+id/itemDeviceDisplayNameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginTop="6dp"
android:text="@string/delete" android:text="@string/devices_details_name_title"
android:textColor="@color/riotx_notice" android:textColor="?riotx_text_secondary"
android:visibility="gone" android:textSize="12sp" />
tools:visibility="visible" />
<TextView
android:id="@+id/itemDeviceDisplayName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
tools:text="Android phone" />
<TextView
android:id="@+id/itemDeviceIdLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="XUIDERFZAA" />
<TextView
android:id="@+id/itemDeviceLastSeenLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceLastSeen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
</LinearLayout> </LinearLayout>
</LinearLayout> <ImageView
android:id="@+id/itemDeviceTrustLevelIcon"
android:layout_width="50dp"
android:layout_height="match_parent"
android:padding="8dp"
tools:src="@drawable/ic_shield_trusted" />
</LinearLayout>

View File

@ -8,24 +8,36 @@
android:background="?android:attr/colorBackground" android:background="?android:attr/colorBackground"
android:minHeight="50dp"> android:minHeight="50dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:id="@+id/item_generic_title_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/item_generic_title_text"
app:layout_constraintBottom_toBottomOf="@id/item_generic_title_text"
android:layout_marginStart="16dp"
android:scaleType="centerInside"
tools:src="@drawable/ic_shield_trusted"
tools:visibility="visible"
android:visibility="gone"/>
<TextView <TextView
android:id="@+id/item_generic_title_text" android:id="@+id/item_generic_title_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginRight="16dp" android:layout_marginStart="8dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textStyle="bold" android:textStyle="bold"
android:visibility="gone" android:visibility="gone"
app:layout_constrainedWidth="true" app:layout_constrainedWidth="true"
app:layout_goneMarginStart="16dp"
app:layout_constraintBottom_toTopOf="@+id/item_generic_description_text" app:layout_constraintBottom_toTopOf="@+id/item_generic_description_text"
app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier" app:layout_constraintEnd_toStartOf="@+id/item_generic_barrier"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@id/item_generic_title_image"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Item Title" tools:text="Item Title"
tools:textSize="14sp" tools:textSize="14sp"

View File

@ -7,7 +7,7 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:minHeight="72dp" android:minHeight="64dp"
android:paddingLeft="@dimen/layout_horizontal_margin" android:paddingLeft="@dimen/layout_horizontal_margin"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingRight="@dimen/layout_horizontal_margin" android:paddingRight="@dimen/layout_horizontal_margin"

View File

@ -32,6 +32,9 @@
<string name="verification_sent">Verification Sent</string> <string name="verification_sent">Verification Sent</string>
<string name="verification_request">Verification Request</string> <string name="verification_request">Verification Request</string>
<string name="verification_verify_device">Verify this device</string>
<!-- Sender name of a message when it is send by you, e.g. You: Hello!--> <!-- Sender name of a message when it is send by you, e.g. You: Hello!-->
<string name="you">You</string> <string name="you">You</string>
@ -102,4 +105,18 @@
<string name="encryption_information_dg_xsigning_not_trusted">Cross-Signing is enabled\nKeys are not trusted</string> <string name="encryption_information_dg_xsigning_not_trusted">Cross-Signing is enabled\nKeys are not trusted</string>
<string name="encryption_information_dg_xsigning_disabled">Cross-Signing is not enabled</string> <string name="encryption_information_dg_xsigning_disabled">Cross-Signing is not enabled</string>
<string name="settings_active_sessions_list">Active Sessions</string>
<string name="settings_active_sessions_show_all">Show All Sessions</string>
<string name="settings_active_sessions_manage">Manage Sessions</string>
<string name="settings_active_sessions_signout_device">Sign out this device</string>
<string name="settings_active_sessions_verified_device_desc">This session is trusted for secure messaging because you verified it:</string>
<string name="settings_active_sessions_unverified_device_desc">Verify this session to mark it as trusted &amp; grant it access to encrypted messages. If you didnt sign in to this device your account may be compromised:</string>
<plurals name="settings_active_sessions_count">
<item quantity="one">%d active session</item>
<item quantity="other">%d active sessions</item>
</plurals>
</resources> </resources>

View File

@ -6,42 +6,46 @@
<!-- ************ Cryptography section ************ --> <!-- ************ Cryptography section ************ -->
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY" android:key="SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY"
tools:isPreferenceVisible="true"
app:isPreferenceVisible="false"
android:title="@string/settings_cryptography"> android:title="@string/settings_cryptography">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY" android:key="SETTINGS_ENCRYPTION_CROSS_SIGNING_PREFERENCE_KEY"
tools:icon="@drawable/ic_shield_trusted" tools:icon="@drawable/ic_shield_trusted"
android:persistent="false" android:persistent="false"
android:title="@string/encryption_information_cross_signing_state" /> android:title="@string/encryption_information_cross_signing_state"
tools:summary="@string/encryption_information_dg_xsigning_complete"
/>
<im.vector.riotx.core.preference.VectorPreference <!-- <im.vector.riotx.core.preference.VectorPreference-->
android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY" <!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY"-->
android:title="@string/encryption_information_device_name" /> <!-- android:title="@string/encryption_information_device_name" />-->
<im.vector.riotx.core.preference.VectorPreference <!-- <im.vector.riotx.core.preference.VectorPreference-->
android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY" <!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY"-->
android:title="@string/encryption_information_device_id" /> <!-- android:title="@string/encryption_information_device_id" />-->
<im.vector.riotx.core.preference.VectorPreference <!-- <im.vector.riotx.core.preference.VectorPreference-->
android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY" <!-- android:key="SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY"-->
android:title="@string/encryption_information_device_key" /> <!-- android:title="@string/encryption_information_device_key" />-->
<im.vector.riotx.core.preference.VectorSwitchPreference <im.vector.riotx.core.preference.VectorSwitchPreference
android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY" android:key="SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY"
android:summary="@string/encryption_never_send_to_unverified_devices_summary" android:summary="@string/encryption_never_send_to_unverified_devices_summary"
android:title="@string/encryption_never_send_to_unverified_devices_title" android:title="@string/encryption_never_send_to_unverified_devices_title"
app:isPreferenceVisible="@bool/false_not_implemented" /> android:enabled="false" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>
<!-- devices list entry point --> <!-- devices list entry point -->
<im.vector.riotx.core.preference.VectorPreferenceCategory <im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY" android:key="SETTINGS_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_devices_list"> android:title="@string/settings_active_sessions_list">
<im.vector.riotx.core.preference.VectorPreference <im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY" android:key="SETTINGS_SHOW_DEVICES_LIST_PREFERENCE_KEY"
android:title="@string/settings_show_devices_list" android:title="@string/settings_active_sessions_show_all"
app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment" /> app:fragment="im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>