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<Unit> = 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<Unit>() 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<DeviceFullInfo>?) { + 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<Unit>) { 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<OtherSessionItem.Holder>(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<ImageView>(R.id.otherSessionDeviceTypeImageView) + val otherSessionVerificationStatusImageView by bind<ShieldImageView>(R.id.otherSessionVerificationStatusImageView) + val otherSessionNameTextView by bind<TextView>(R.id.otherSessionNameTextView) + val otherSessionDescriptionTextView by bind<TextView>(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<List<DeviceFullInfo>>() { + + override fun buildModels(data: List<DeviceFullInfo>?) { + 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<DeviceFullInfo>) { + 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="ring" + android:innerRadius="0dp" + android:thicknessRatio="2" + android:useLevel="false"> + + <solid android:color="?android:colorBackground" /> + + <stroke + android:width="1dp" + android:color="?vctr_content_quinary" /> + +</shape> 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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> + <path + android:pathData="M12,23.5C18.075,23.5 23,18.575 23,12.5C23,6.425 18.075,1.5 12,1.5C5.925,1.5 1,6.425 1,12.5C1,18.575 5.925,23.5 12,23.5ZM12,19.26C12.76,19.26 13.375,18.645 13.375,17.885C13.375,17.126 12.76,16.51 12,16.51C11.241,16.51 10.625,17.126 10.625,17.885C10.625,18.645 11.241,19.26 12,19.26ZM10.09,10.428C10.09,9.37 10.949,8.518 12,8.518C13.048,8.518 13.909,9.38 13.909,10.428C13.909,10.913 13.702,11.087 12.884,11.652C12.521,11.902 12.025,12.25 11.638,12.76C11.223,13.306 10.968,13.987 10.968,14.853H13.031C13.031,14.429 13.144,14.187 13.281,14.007C13.445,13.79 13.683,13.606 14.056,13.349C14.095,13.321 14.137,13.293 14.18,13.264C14.856,12.804 15.972,12.045 15.972,10.428C15.972,8.241 14.187,6.456 12,6.456C9.816,6.456 8.027,8.225 8.027,10.428H10.09Z" + android:fillColor="#737D8C" + android:fillType="evenOdd"/> +</vector> 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 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> - <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView - android:id="@+id/deviceListHeaderSectionCurrent" - android:layout_width="0dp" - android:layout_height="wrap_content" - app:devicesListHeaderDescription="" - app:devicesListHeaderTitle="@string/device_manager_header_section_current_session" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <im.vector.app.features.settings.devices.v2.list.CurrentSessionView - android:id="@+id/deviceListCurrentSession" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginHorizontal="16dp" - android:layout_marginVertical="24dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/deviceListHeaderSectionCurrent" /> + <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView + android:id="@+id/deviceListHeaderCurrentSession" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:devicesListHeaderDescription="" + app:devicesListHeaderTitle="@string/device_manager_header_section_current_session" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView - android:id="@+id/deviceListHeaderSectionOther" - android:layout_width="0dp" - android:layout_height="wrap_content" - app:devicesListHeaderDescription="@string/settings_sessions_other_description" - app:devicesListHeaderTitle="@string/settings_sessions_other_title" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/deviceListCurrentSession" /> + <im.vector.app.features.settings.devices.v2.list.CurrentSessionView + android:id="@+id/deviceListCurrentSession" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/deviceListHeaderCurrentSession" /> - <include - android:id="@+id/waiting_view" - layout="@layout/merge_overlay_waiting_view" /> + <View + android:id="@+id/deviceListDividerCurrentSession" + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_marginTop="24dp" + android:background="@drawable/divider_horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/deviceListCurrentSession" /> -</androidx.constraintlayout.widget.ConstraintLayout> + <im.vector.app.features.settings.devices.v2.list.DevicesListHeaderView + android:id="@+id/deviceListHeaderOtherSessions" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:devicesListHeaderDescription="@string/settings_sessions_other_description" + app:devicesListHeaderTitle="@string/settings_sessions_other_title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" /> + + <im.vector.app.features.settings.devices.v2.list.OtherSessionsView + android:id="@+id/deviceListOtherSessions" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions" /> + + <include + android:id="@+id/waiting_view" + layout="@layout/merge_overlay_waiting_view" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="16dp"> + + <ImageView + android:id="@+id/otherSessionDeviceTypeImageView" + android:layout_width="40dp" + android:layout_height="40dp" + android:background="@drawable/bg_device_type" + android:contentDescription="@string/a11y_device_manager_device_type_mobile" + android:padding="8dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_device_type_mobile" /> + + <im.vector.app.core.ui.views.ShieldImageView + android:id="@+id/otherSessionVerificationStatusImageView" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginStart="24dp" + android:layout_marginTop="24dp" + android:background="@drawable/circle_with_border" + android:importantForAccessibility="no" + android:padding="6dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_shield_trusted" /> + + <TextView + android:id="@+id/otherSessionNameTextView" + style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/otherSessionDeviceTypeImageView" + app:layout_constraintTop_toTopOf="parent" + tools:text="Element Mobile: Android" /> + + <TextView + android:id="@+id/otherSessionDescriptionTextView" + style="@style/TextAppearance.Vector.Body.DevicesManagement" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="2dp" + app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView" + app:layout_constraintTop_toBottomOf="@id/otherSessionNameTextView" + tools:text="@string/device_manager_verification_status_verified" /> + + <View + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_marginTop="16dp" + android:background="?vctr_content_quinary" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/otherSessionNameTextView" + app:layout_constraintTop_toBottomOf="@id/otherSessionDescriptionTextView" /> + +</androidx.constraintlayout.widget.ConstraintLayout> 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" /> <TextView - android:id="@+id/currentSessionDeviceTypeTextView" - style="@style/TextAppearance.Vector.Headline.Medium" + android:id="@+id/currentSessionNameTextView" + style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/currentSessionDeviceTypeImageView" - tools:text="@string/device_manager_device_type_android" /> + tools:text="Element Mobile: Android" /> <LinearLayout android:id="@+id/currentSessionVerificationStatusContainer" @@ -40,7 +40,7 @@ android:orientation="horizontal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/currentSessionDeviceTypeTextView"> + app:layout_constraintTop_toBottomOf="@id/currentSessionNameTextView"> <im.vector.app.core.ui.views.ShieldImageView android:id="@+id/currentSessionVerificationStatusImageView" @@ -61,7 +61,7 @@ <TextView android:id="@+id/currentSessionVerificationStatusDetailTextView" - style="@style/TextAppearance.Vector.Caption" + style="@style/TextAppearance.Vector.Body.DevicesManagement" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="32dp" diff --git a/vector/src/main/res/layout/view_other_sessions.xml b/vector/src/main/res/layout/view_other_sessions.xml new file mode 100644 index 0000000000..aacbbe8ffe --- /dev/null +++ b/vector/src/main/res/layout/view_other_sessions.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/otherSessionsRecyclerView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:listitem="@layout/item_other_session" /> + + <Button + android:id="@+id/otherSessionsViewAllButton" + style="@style/Widget.Vector.Button.Text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="0dp" + app:layout_constraintStart_toStartOf="@id/otherSessionsRecyclerView" + app:layout_constraintTop_toBottomOf="@id/otherSessionsRecyclerView" + tools:text="@string/device_manager_other_sessions_view_all" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 4d39dcae87..564f1f5bb6 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3209,9 +3209,9 @@ <!-- Device Manager --> <string name="device_manager_settings_active_sessions_show_all">Show All Sessions (V2, WIP)</string> <string name="a11y_device_manager_device_type_mobile">Mobile</string> - <string name="a11y_device_manager_device_type_web" tools:ignore="UnusedResources">Web</string> - <string name="a11y_device_manager_device_type_desktop" tools:ignore="UnusedResources">Desktop</string> - <string name="device_manager_device_type_android">${app_name} Mobile: Android</string> + <string name="a11y_device_manager_device_type_web">Web</string> + <string name="a11y_device_manager_device_type_desktop">Desktop</string> + <string name="a11y_device_manager_device_type_unknown">Unknown device type</string> <string name="device_manager_verification_status_verified">Verified session</string> <string name="device_manager_verification_status_unverified">Unverified session</string> <string name="device_manager_verification_status_detail_verified">Your current session is ready for secure messaging.</string> @@ -3219,5 +3219,8 @@ <string name="device_manager_verify_session">Verify Session</string> <string name="device_manager_view_details">View Details</string> <string name="device_manager_header_section_current_session">Current Session</string> + <string name="device_manager_other_sessions_view_all">View All (%1$d)</string> + <string name="device_manager_other_sessions_description_verified">Verified · Last activity %1$s</string> + <string name="device_manager_other_sessions_description_unverified">Unverified · Last activity %1$s</string> </resources>