diff --git a/changelog.d/6945.wip b/changelog.d/6945.wip new file mode 100644 index 0000000000..6f5916a8c2 --- /dev/null +++ b/changelog.d/6945.wip @@ -0,0 +1 @@ +[Device Manager] Other Sessions Section diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index 1840a97098..11fc6ecd64 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -57,6 +57,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction @@ -79,12 +80,13 @@ data class DevicesViewState( // TODO Replace by isLoading boolean val request: Async = Uninitialized, val hasAccountCrossSigning: Boolean = false, - val accountCrossSigningIsTrusted: Boolean = false + val accountCrossSigningIsTrusted: Boolean = false, ) : MavericksState data class DeviceFullInfo( val deviceInfo: DeviceInfo, - val cryptoDeviceInfo: CryptoDeviceInfo? + val cryptoDeviceInfo: CryptoDeviceInfo?, + val trustLevelForShield: RoomEncryptionTrustLevel, ) class DevicesViewModel @AssistedInject constructor( @@ -108,11 +110,13 @@ class DevicesViewModel @AssistedInject constructor( private val refreshSource = PublishDataSource() init { + val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized() + val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified() setState { copy( - hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(), - accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(), + hasAccountCrossSigning = hasAccountCrossSigning, + accountCrossSigningIsTrusted = accountCrossSigningIsTrusted, myDeviceId = session.sessionParams.deviceId ?: "" ) } @@ -125,7 +129,13 @@ class DevicesViewModel @AssistedInject constructor( .sortedByDescending { it.lastSeenTs } .map { deviceInfo -> val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId } - DeviceFullInfo(deviceInfo, cryptoDeviceInfo) + val trustLevelForShield = computeTrustLevelForShield( + currentSessionCrossTrusted = accountCrossSigningIsTrusted, + legacyMode = !hasAccountCrossSigning, + deviceTrustLevel = cryptoDeviceInfo?.trustLevel, + isCurrentDevice = deviceInfo.deviceId == session.sessionParams.deviceId + ) + DeviceFullInfo(deviceInfo, cryptoDeviceInfo, trustLevelForShield) } } .distinctUntilChanged() @@ -243,6 +253,20 @@ class DevicesViewModel @AssistedInject constructor( } } + private fun computeTrustLevelForShield( + currentSessionCrossTrusted: Boolean, + legacyMode: Boolean, + deviceTrustLevel: DeviceTrustLevel?, + isCurrentDevice: Boolean, + ): RoomEncryptionTrustLevel { + return TrustUtils.shieldForTrust( + currentDevice = isCurrentDevice, + trustMSK = currentSessionCrossTrusted, + legacyMode = legacyMode, + deviceTrustLevel = deviceTrustLevel + ) + } + private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) { val txID = session.cryptoService() .verificationService() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt index 41d93cb957..80dfe25c77 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt @@ -36,10 +36,10 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.VerificationBottomSheet +import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.DevicesAction import im.vector.app.features.settings.devices.DevicesViewEvents import im.vector.app.features.settings.devices.DevicesViewModel -import im.vector.app.features.settings.devices.DevicesViewState /** * Display the list of the user's devices and sessions. @@ -114,42 +114,61 @@ class VectorSettingsDevicesFragment : } private fun initLearnMoreButtons() { - views.deviceListHeaderSectionOther.onLearnMoreClickListener = { + views.deviceListHeaderOtherSessions.onLearnMoreClickListener = { Toast.makeText(context, "Learn more other", Toast.LENGTH_LONG).show() } } private fun cleanUpLearnMoreButtonsListeners() { - views.deviceListHeaderSectionOther.onLearnMoreClickListener = null + views.deviceListHeaderOtherSessions.onLearnMoreClickListener = null } override fun invalidate() = withState(viewModel) { state -> - val currentDeviceInfo = state.devices() - ?.firstOrNull { - it.deviceInfo.deviceId == state.myDeviceId - } + if (state.devices is Success) { + val devices = state.devices() + val currentDeviceInfo = devices?.firstOrNull { + it.deviceInfo.deviceId == state.myDeviceId + } + val otherDevices = devices?.filter { it.deviceInfo.deviceId != state.myDeviceId } - if (state.devices is Success && currentDeviceInfo != null) { - renderCurrentDevice(state) + renderCurrentDevice(currentDeviceInfo) + renderOtherSessionsView(otherDevices) } else { hideCurrentSessionView() + hideOtherSessionsView() } handleRequestStatus(state.request) } - private fun hideCurrentSessionView() { - views.deviceListHeaderSectionCurrent.isVisible = false - views.deviceListCurrentSession.isVisible = false + private fun renderOtherSessionsView(otherDevices: List?) { + if (otherDevices.isNullOrEmpty()) { + hideOtherSessionsView() + } else { + views.deviceListHeaderOtherSessions.isVisible = true + views.deviceListOtherSessions.isVisible = true + views.deviceListOtherSessions.render(otherDevices) + } } - private fun renderCurrentDevice(state: DevicesViewState) { - views.deviceListHeaderSectionCurrent.isVisible = true - views.deviceListCurrentSession.isVisible = true - views.deviceListCurrentSession.update( - accountCrossSigningIsTrusted = state.accountCrossSigningIsTrusted, - legacyMode = !state.hasAccountCrossSigning - ) + private fun hideOtherSessionsView() { + views.deviceListHeaderOtherSessions.isVisible = false + views.deviceListOtherSessions.isVisible = false + } + + private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) { + currentDeviceInfo?.let { + views.deviceListHeaderCurrentSession.isVisible = true + views.deviceListCurrentSession.isVisible = true + views.deviceListCurrentSession.render(it) + } ?: run { + hideCurrentSessionView() + } + } + + private fun hideCurrentSessionView() { + views.deviceListHeaderCurrentSession.isVisible = false + views.deviceListCurrentSession.isVisible = false } private fun handleRequestStatus(unIgnoreRequest: Async) { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt index baf30e35b5..d6f81f4f79 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt @@ -22,9 +22,9 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.databinding.ViewCurrentSessionBinding -import im.vector.app.features.settings.devices.TrustUtils +import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.themes.ThemeUtils -import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel class CurrentSessionView @JvmOverloads constructor( context: Context, @@ -39,21 +39,14 @@ class CurrentSessionView @JvmOverloads constructor( views = ViewCurrentSessionBinding.bind(this) } - fun update(accountCrossSigningIsTrusted: Boolean, legacyMode: Boolean) { - renderDeviceType() - renderVerificationStatus(accountCrossSigningIsTrusted, legacyMode) + fun render(currentDeviceInfo: DeviceFullInfo) { + renderDeviceInfo(currentDeviceInfo.deviceInfo.displayName.orEmpty()) + renderVerificationStatus(currentDeviceInfo.trustLevelForShield) } - private fun renderVerificationStatus(accountCrossSigningIsTrusted: Boolean, legacyMode: Boolean) { - val deviceTrustLevel = DeviceTrustLevel(crossSigningVerified = accountCrossSigningIsTrusted, locallyVerified = true) - val shield = TrustUtils.shieldForTrust( - currentDevice = true, - trustMSK = accountCrossSigningIsTrusted, - legacyMode = legacyMode, - deviceTrustLevel = deviceTrustLevel - ) - views.currentSessionVerificationStatusImageView.render(shield) - if (deviceTrustLevel.crossSigningVerified) { + private fun renderVerificationStatus(trustLevelForShield: RoomEncryptionTrustLevel) { + views.currentSessionVerificationStatusImageView.render(trustLevelForShield) + if (trustLevelForShield == RoomEncryptionTrustLevel.Trusted) { renderCrossSigningVerified() } else { renderCrossSigningUnverified() @@ -75,9 +68,9 @@ class CurrentSessionView @JvmOverloads constructor( } // TODO. We don't have this info yet. Update later accordingly. - private fun renderDeviceType() { + private fun renderDeviceInfo(sessionName: String) { views.currentSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile) views.currentSessionDeviceTypeImageView.contentDescription = context.getString(R.string.a11y_device_manager_device_type_mobile) - views.currentSessionDeviceTypeTextView.text = context.getString(R.string.device_manager_device_type_android) + views.currentSessionNameTextView.text = sessionName } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DeviceType.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DeviceType.kt new file mode 100644 index 0000000000..3082901375 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DeviceType.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022 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.app.features.settings.devices.v2.list + +enum class DeviceType { + MOBILE, + WEB, + DESKTOP, + UNKNOWN, +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt new file mode 100644 index 0000000000..2a62100994 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 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.app.features.settings.devices.v2.list + +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.views.ShieldImageView +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel + +@EpoxyModelClass +abstract class OtherSessionItem : VectorEpoxyModel(R.layout.item_other_session) { + + @EpoxyAttribute + var deviceType: DeviceType = DeviceType.UNKNOWN + + @EpoxyAttribute + var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null + + @EpoxyAttribute + var sessionName: String? = null + + @EpoxyAttribute + var sessionDescription: String? = null + + @EpoxyAttribute + lateinit var stringProvider: StringProvider + + override fun bind(holder: Holder) { + super.bind(holder) + when (deviceType) { + DeviceType.MOBILE -> { + holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile) + holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_mobile) + } + DeviceType.WEB -> { + holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_web) + holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_web) + } + DeviceType.DESKTOP -> { + holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_desktop) + holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_desktop) + } + DeviceType.UNKNOWN -> { + holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_unknown) + holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_unknown) + } + } + holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel) + holder.otherSessionNameTextView.text = sessionName + holder.otherSessionDescriptionTextView.text = sessionDescription + } + + class Holder : VectorEpoxyHolder() { + val otherSessionDeviceTypeImageView by bind(R.id.otherSessionDeviceTypeImageView) + val otherSessionVerificationStatusImageView by bind(R.id.otherSessionVerificationStatusImageView) + val otherSessionNameTextView by bind(R.id.otherSessionNameTextView) + val otherSessionDescriptionTextView by bind(R.id.otherSessionDescriptionTextView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt new file mode 100644 index 0000000000..44c73e6eb7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 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.app.features.settings.devices.v2.list + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.epoxy.noResultItem +import im.vector.app.core.resources.StringProvider +import im.vector.app.features.settings.devices.DeviceFullInfo +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel +import javax.inject.Inject + +class OtherSessionsController @Inject constructor( + private val stringProvider: StringProvider, + private val dateFormatter: VectorDateFormatter, +) : TypedEpoxyController>() { + + override fun buildModels(data: List?) { + val host = this + + if (data.isNullOrEmpty()) { + noResultItem { + id("empty") + text(host.stringProvider.getString(R.string.no_result_placeholder)) + } + } else { + data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device -> + val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME) + val description = if (device.trustLevelForShield == RoomEncryptionTrustLevel.Trusted) { + stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate) + } else { + stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate) + } + + otherSessionItem { + id(device.deviceInfo.deviceId) + deviceType(DeviceType.UNKNOWN) // TODO. We don't have this info yet. Update accordingly. + roomEncryptionTrustLevel(device.trustLevelForShield) + sessionName(device.deviceInfo.displayName) + sessionDescription(description) + stringProvider(this@OtherSessionsController.stringProvider) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt new file mode 100644 index 0000000000..55978e61fd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 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.app.features.settings.devices.v2.list + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.databinding.ViewOtherSessionsBinding +import im.vector.app.features.settings.devices.DeviceFullInfo +import javax.inject.Inject + +@AndroidEntryPoint +class OtherSessionsView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + @Inject lateinit var otherSessionsController: OtherSessionsController + + private val views: ViewOtherSessionsBinding + + init { + inflate(context, R.layout.view_other_sessions, this) + views = ViewOtherSessionsBinding.bind(this) + } + + fun render(devices: List) { + views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = true) + views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, devices.size) + otherSessionsController.setData(devices) + } + + override fun onDetachedFromWindow() { + views.otherSessionsRecyclerView.cleanup() + super.onDetachedFromWindow() + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt new file mode 100644 index 0000000000..c1dbbdff4f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionListConstants.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022 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.app.features.settings.devices.v2.list + +internal const val NUMBER_OF_OTHER_DEVICES_TO_RENDER = 5 diff --git a/vector/src/main/res/drawable/circle_with_border.xml b/vector/src/main/res/drawable/circle_with_border.xml new file mode 100644 index 0000000000..7b9bad44f7 --- /dev/null +++ b/vector/src/main/res/drawable/circle_with_border.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/vector/src/main/res/drawable/ic_device_type_unknown.xml b/vector/src/main/res/drawable/ic_device_type_unknown.xml new file mode 100644 index 0000000000..8368dcbd0b --- /dev/null +++ b/vector/src/main/res/drawable/ic_device_type_unknown.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 860e395c1f..1367835d2c 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -1,41 +1,69 @@ - - + - + - + - + - + + + + + + + + diff --git a/vector/src/main/res/layout/item_other_session.xml b/vector/src/main/res/layout/item_other_session.xml new file mode 100644 index 0000000000..84b813c977 --- /dev/null +++ b/vector/src/main/res/layout/item_other_session.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_current_session.xml b/vector/src/main/res/layout/view_current_session.xml index 61641cbbe7..31ad3cce2c 100644 --- a/vector/src/main/res/layout/view_current_session.xml +++ b/vector/src/main/res/layout/view_current_session.xml @@ -21,15 +21,15 @@ tools:src="@drawable/ic_device_type_mobile" /> + tools:text="Element Mobile: Android" /> + app:layout_constraintTop_toBottomOf="@id/currentSessionNameTextView"> + + + + +