Skip to content

Commit 6341cf9

Browse files
authored
Merge pull request #6946 from vector-im/feature/ons/device_manager_other_session_list
[Device Manager] Render other sessions (PSG-668)
2 parents 0950e41 + 357a859 commit 6341cf9

16 files changed

+511
-85
lines changed

changelog.d/6945.wip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Device Manager] Other Sessions Section

vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import org.matrix.android.sdk.api.session.Session
5757
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
5858
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
5959
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
60+
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
6061
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
6162
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
6263
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
@@ -79,12 +80,13 @@ data class DevicesViewState(
7980
// TODO Replace by isLoading boolean
8081
val request: Async<Unit> = Uninitialized,
8182
val hasAccountCrossSigning: Boolean = false,
82-
val accountCrossSigningIsTrusted: Boolean = false
83+
val accountCrossSigningIsTrusted: Boolean = false,
8384
) : MavericksState
8485

8586
data class DeviceFullInfo(
8687
val deviceInfo: DeviceInfo,
87-
val cryptoDeviceInfo: CryptoDeviceInfo?
88+
val cryptoDeviceInfo: CryptoDeviceInfo?,
89+
val trustLevelForShield: RoomEncryptionTrustLevel,
8890
)
8991

9092
class DevicesViewModel @AssistedInject constructor(
@@ -108,11 +110,13 @@ class DevicesViewModel @AssistedInject constructor(
108110
private val refreshSource = PublishDataSource<Unit>()
109111

110112
init {
113+
val hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized()
114+
val accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
111115

112116
setState {
113117
copy(
114-
hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(),
115-
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
118+
hasAccountCrossSigning = hasAccountCrossSigning,
119+
accountCrossSigningIsTrusted = accountCrossSigningIsTrusted,
116120
myDeviceId = session.sessionParams.deviceId ?: ""
117121
)
118122
}
@@ -125,7 +129,13 @@ class DevicesViewModel @AssistedInject constructor(
125129
.sortedByDescending { it.lastSeenTs }
126130
.map { deviceInfo ->
127131
val cryptoDeviceInfo = cryptoList.firstOrNull { it.deviceId == deviceInfo.deviceId }
128-
DeviceFullInfo(deviceInfo, cryptoDeviceInfo)
132+
val trustLevelForShield = computeTrustLevelForShield(
133+
currentSessionCrossTrusted = accountCrossSigningIsTrusted,
134+
legacyMode = !hasAccountCrossSigning,
135+
deviceTrustLevel = cryptoDeviceInfo?.trustLevel,
136+
isCurrentDevice = deviceInfo.deviceId == session.sessionParams.deviceId
137+
)
138+
DeviceFullInfo(deviceInfo, cryptoDeviceInfo, trustLevelForShield)
129139
}
130140
}
131141
.distinctUntilChanged()
@@ -243,6 +253,20 @@ class DevicesViewModel @AssistedInject constructor(
243253
}
244254
}
245255

256+
private fun computeTrustLevelForShield(
257+
currentSessionCrossTrusted: Boolean,
258+
legacyMode: Boolean,
259+
deviceTrustLevel: DeviceTrustLevel?,
260+
isCurrentDevice: Boolean,
261+
): RoomEncryptionTrustLevel {
262+
return TrustUtils.shieldForTrust(
263+
currentDevice = isCurrentDevice,
264+
trustMSK = currentSessionCrossTrusted,
265+
legacyMode = legacyMode,
266+
deviceTrustLevel = deviceTrustLevel
267+
)
268+
}
269+
246270
private fun handleInteractiveVerification(action: DevicesAction.VerifyMyDevice) {
247271
val txID = session.cryptoService()
248272
.verificationService()

vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ import im.vector.app.core.platform.VectorBaseFragment
3636
import im.vector.app.databinding.FragmentSettingsDevicesBinding
3737
import im.vector.app.features.crypto.recover.SetupMode
3838
import im.vector.app.features.crypto.verification.VerificationBottomSheet
39+
import im.vector.app.features.settings.devices.DeviceFullInfo
3940
import im.vector.app.features.settings.devices.DevicesAction
4041
import im.vector.app.features.settings.devices.DevicesViewEvents
4142
import im.vector.app.features.settings.devices.DevicesViewModel
42-
import im.vector.app.features.settings.devices.DevicesViewState
4343

4444
/**
4545
* Display the list of the user's devices and sessions.
@@ -114,42 +114,61 @@ class VectorSettingsDevicesFragment :
114114
}
115115

116116
private fun initLearnMoreButtons() {
117-
views.deviceListHeaderSectionOther.onLearnMoreClickListener = {
117+
views.deviceListHeaderOtherSessions.onLearnMoreClickListener = {
118118
Toast.makeText(context, "Learn more other", Toast.LENGTH_LONG).show()
119119
}
120120
}
121121

122122
private fun cleanUpLearnMoreButtonsListeners() {
123-
views.deviceListHeaderSectionOther.onLearnMoreClickListener = null
123+
views.deviceListHeaderOtherSessions.onLearnMoreClickListener = null
124124
}
125125

126126
override fun invalidate() = withState(viewModel) { state ->
127-
val currentDeviceInfo = state.devices()
128-
?.firstOrNull {
129-
it.deviceInfo.deviceId == state.myDeviceId
130-
}
127+
if (state.devices is Success) {
128+
val devices = state.devices()
129+
val currentDeviceInfo = devices?.firstOrNull {
130+
it.deviceInfo.deviceId == state.myDeviceId
131+
}
132+
val otherDevices = devices?.filter { it.deviceInfo.deviceId != state.myDeviceId }
131133

132-
if (state.devices is Success && currentDeviceInfo != null) {
133-
renderCurrentDevice(state)
134+
renderCurrentDevice(currentDeviceInfo)
135+
renderOtherSessionsView(otherDevices)
134136
} else {
135137
hideCurrentSessionView()
138+
hideOtherSessionsView()
136139
}
137140

138141
handleRequestStatus(state.request)
139142
}
140143

141-
private fun hideCurrentSessionView() {
142-
views.deviceListHeaderSectionCurrent.isVisible = false
143-
views.deviceListCurrentSession.isVisible = false
144+
private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?) {
145+
if (otherDevices.isNullOrEmpty()) {
146+
hideOtherSessionsView()
147+
} else {
148+
views.deviceListHeaderOtherSessions.isVisible = true
149+
views.deviceListOtherSessions.isVisible = true
150+
views.deviceListOtherSessions.render(otherDevices)
151+
}
152+
}
153+
154+
private fun hideOtherSessionsView() {
155+
views.deviceListHeaderOtherSessions.isVisible = false
156+
views.deviceListOtherSessions.isVisible = false
144157
}
145158

146-
private fun renderCurrentDevice(state: DevicesViewState) {
147-
views.deviceListHeaderSectionCurrent.isVisible = true
148-
views.deviceListCurrentSession.isVisible = true
149-
views.deviceListCurrentSession.update(
150-
accountCrossSigningIsTrusted = state.accountCrossSigningIsTrusted,
151-
legacyMode = !state.hasAccountCrossSigning
152-
)
159+
private fun renderCurrentDevice(currentDeviceInfo: DeviceFullInfo?) {
160+
currentDeviceInfo?.let {
161+
views.deviceListHeaderCurrentSession.isVisible = true
162+
views.deviceListCurrentSession.isVisible = true
163+
views.deviceListCurrentSession.render(it)
164+
} ?: run {
165+
hideCurrentSessionView()
166+
}
167+
}
168+
169+
private fun hideCurrentSessionView() {
170+
views.deviceListHeaderCurrentSession.isVisible = false
171+
views.deviceListCurrentSession.isVisible = false
153172
}
154173

155174
private fun handleRequestStatus(unIgnoreRequest: Async<Unit>) {

vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
2222
import androidx.core.view.isVisible
2323
import im.vector.app.R
2424
import im.vector.app.databinding.ViewCurrentSessionBinding
25-
import im.vector.app.features.settings.devices.TrustUtils
25+
import im.vector.app.features.settings.devices.DeviceFullInfo
2626
import im.vector.app.features.themes.ThemeUtils
27-
import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
27+
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
2828

2929
class CurrentSessionView @JvmOverloads constructor(
3030
context: Context,
@@ -39,21 +39,14 @@ class CurrentSessionView @JvmOverloads constructor(
3939
views = ViewCurrentSessionBinding.bind(this)
4040
}
4141

42-
fun update(accountCrossSigningIsTrusted: Boolean, legacyMode: Boolean) {
43-
renderDeviceType()
44-
renderVerificationStatus(accountCrossSigningIsTrusted, legacyMode)
42+
fun render(currentDeviceInfo: DeviceFullInfo) {
43+
renderDeviceInfo(currentDeviceInfo.deviceInfo.displayName.orEmpty())
44+
renderVerificationStatus(currentDeviceInfo.trustLevelForShield)
4545
}
4646

47-
private fun renderVerificationStatus(accountCrossSigningIsTrusted: Boolean, legacyMode: Boolean) {
48-
val deviceTrustLevel = DeviceTrustLevel(crossSigningVerified = accountCrossSigningIsTrusted, locallyVerified = true)
49-
val shield = TrustUtils.shieldForTrust(
50-
currentDevice = true,
51-
trustMSK = accountCrossSigningIsTrusted,
52-
legacyMode = legacyMode,
53-
deviceTrustLevel = deviceTrustLevel
54-
)
55-
views.currentSessionVerificationStatusImageView.render(shield)
56-
if (deviceTrustLevel.crossSigningVerified) {
47+
private fun renderVerificationStatus(trustLevelForShield: RoomEncryptionTrustLevel) {
48+
views.currentSessionVerificationStatusImageView.render(trustLevelForShield)
49+
if (trustLevelForShield == RoomEncryptionTrustLevel.Trusted) {
5750
renderCrossSigningVerified()
5851
} else {
5952
renderCrossSigningUnverified()
@@ -75,9 +68,9 @@ class CurrentSessionView @JvmOverloads constructor(
7568
}
7669

7770
// TODO. We don't have this info yet. Update later accordingly.
78-
private fun renderDeviceType() {
71+
private fun renderDeviceInfo(sessionName: String) {
7972
views.currentSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile)
8073
views.currentSessionDeviceTypeImageView.contentDescription = context.getString(R.string.a11y_device_manager_device_type_mobile)
81-
views.currentSessionDeviceTypeTextView.text = context.getString(R.string.device_manager_device_type_android)
74+
views.currentSessionNameTextView.text = sessionName
8275
}
8376
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.list
18+
19+
enum class DeviceType {
20+
MOBILE,
21+
WEB,
22+
DESKTOP,
23+
UNKNOWN,
24+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.list
18+
19+
import android.widget.ImageView
20+
import android.widget.TextView
21+
import com.airbnb.epoxy.EpoxyAttribute
22+
import com.airbnb.epoxy.EpoxyModelClass
23+
import im.vector.app.R
24+
import im.vector.app.core.epoxy.VectorEpoxyHolder
25+
import im.vector.app.core.epoxy.VectorEpoxyModel
26+
import im.vector.app.core.resources.StringProvider
27+
import im.vector.app.core.ui.views.ShieldImageView
28+
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
29+
30+
@EpoxyModelClass
31+
abstract class OtherSessionItem : VectorEpoxyModel<OtherSessionItem.Holder>(R.layout.item_other_session) {
32+
33+
@EpoxyAttribute
34+
var deviceType: DeviceType = DeviceType.UNKNOWN
35+
36+
@EpoxyAttribute
37+
var roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
38+
39+
@EpoxyAttribute
40+
var sessionName: String? = null
41+
42+
@EpoxyAttribute
43+
var sessionDescription: String? = null
44+
45+
@EpoxyAttribute
46+
lateinit var stringProvider: StringProvider
47+
48+
override fun bind(holder: Holder) {
49+
super.bind(holder)
50+
when (deviceType) {
51+
DeviceType.MOBILE -> {
52+
holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_mobile)
53+
holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_mobile)
54+
}
55+
DeviceType.WEB -> {
56+
holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_web)
57+
holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_web)
58+
}
59+
DeviceType.DESKTOP -> {
60+
holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_desktop)
61+
holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_desktop)
62+
}
63+
DeviceType.UNKNOWN -> {
64+
holder.otherSessionDeviceTypeImageView.setImageResource(R.drawable.ic_device_type_unknown)
65+
holder.otherSessionDeviceTypeImageView.contentDescription = stringProvider.getString(R.string.a11y_device_manager_device_type_unknown)
66+
}
67+
}
68+
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
69+
holder.otherSessionNameTextView.text = sessionName
70+
holder.otherSessionDescriptionTextView.text = sessionDescription
71+
}
72+
73+
class Holder : VectorEpoxyHolder() {
74+
val otherSessionDeviceTypeImageView by bind<ImageView>(R.id.otherSessionDeviceTypeImageView)
75+
val otherSessionVerificationStatusImageView by bind<ShieldImageView>(R.id.otherSessionVerificationStatusImageView)
76+
val otherSessionNameTextView by bind<TextView>(R.id.otherSessionNameTextView)
77+
val otherSessionDescriptionTextView by bind<TextView>(R.id.otherSessionDescriptionTextView)
78+
}
79+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.settings.devices.v2.list
18+
19+
import com.airbnb.epoxy.TypedEpoxyController
20+
import im.vector.app.R
21+
import im.vector.app.core.date.DateFormatKind
22+
import im.vector.app.core.date.VectorDateFormatter
23+
import im.vector.app.core.epoxy.noResultItem
24+
import im.vector.app.core.resources.StringProvider
25+
import im.vector.app.features.settings.devices.DeviceFullInfo
26+
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
27+
import javax.inject.Inject
28+
29+
class OtherSessionsController @Inject constructor(
30+
private val stringProvider: StringProvider,
31+
private val dateFormatter: VectorDateFormatter,
32+
) : TypedEpoxyController<List<DeviceFullInfo>>() {
33+
34+
override fun buildModels(data: List<DeviceFullInfo>?) {
35+
val host = this
36+
37+
if (data.isNullOrEmpty()) {
38+
noResultItem {
39+
id("empty")
40+
text(host.stringProvider.getString(R.string.no_result_placeholder))
41+
}
42+
} else {
43+
data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device ->
44+
val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
45+
val description = if (device.trustLevelForShield == RoomEncryptionTrustLevel.Trusted) {
46+
stringProvider.getString(R.string.device_manager_other_sessions_description_verified, formattedLastActivityDate)
47+
} else {
48+
stringProvider.getString(R.string.device_manager_other_sessions_description_unverified, formattedLastActivityDate)
49+
}
50+
51+
otherSessionItem {
52+
id(device.deviceInfo.deviceId)
53+
deviceType(DeviceType.UNKNOWN) // TODO. We don't have this info yet. Update accordingly.
54+
roomEncryptionTrustLevel(device.trustLevelForShield)
55+
sessionName(device.deviceInfo.displayName)
56+
sessionDescription(description)
57+
stringProvider(this@OtherSessionsController.stringProvider)
58+
}
59+
}
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)