diff --git a/changelog.d/6902.wip b/changelog.d/6902.wip new file mode 100644 index 0000000000..8c982cc9ae --- /dev/null +++ b/changelog.d/6902.wip @@ -0,0 +1 @@ +[Device Manager] Current Session Section 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 a5281d1b5c..41d93cb957 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 @@ -23,10 +23,23 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.dialogs.ManuallyVerifyDialog 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.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. @@ -35,6 +48,8 @@ import im.vector.app.databinding.FragmentSettingsDevicesBinding class VectorSettingsDevicesFragment : VectorBaseFragment() { + private val viewModel: DevicesViewModel by fragmentViewModel() + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding { return FragmentSettingsDevicesBinding.inflate(inflater, container, false) } @@ -52,7 +67,45 @@ class VectorSettingsDevicesFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + initLearnMoreButtons() + initWaitingView() + observerViewEvents() + } + + private fun observerViewEvents() { + viewModel.observeViewEvents { + when (it) { + is DevicesViewEvents.Loading -> showLoading(it.message) + is DevicesViewEvents.Failure -> showFailure(it.throwable) + is DevicesViewEvents.RequestReAuth -> Unit // TODO. Next PR + is DevicesViewEvents.PromptRenameDevice -> Unit // TODO. Next PR + is DevicesViewEvents.ShowVerifyDevice -> { + VerificationBottomSheet.withArgs( + roomId = null, + otherUserId = it.userId, + transactionId = it.transactionId + ).show(childFragmentManager, "REQPOP") + } + is DevicesViewEvents.SelfVerification -> { + VerificationBottomSheet.forSelfVerification(it.session) + .show(childFragmentManager, "REQPOP") + } + is DevicesViewEvents.ShowManuallyVerify -> { + ManuallyVerifyDialog.show(requireActivity(), it.cryptoDeviceInfo) { + viewModel.handle(DevicesAction.MarkAsManuallyVerified(it.cryptoDeviceInfo)) + } + } + is DevicesViewEvents.PromptResetSecrets -> { + navigator.open4SSetup(requireContext(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) + } + } + } + } + + private fun initWaitingView() { + views.waitingView.waitingStatusText.setText(R.string.please_wait) + views.waitingView.waitingStatusText.isVisible = true } override fun onDestroyView() { @@ -61,12 +114,48 @@ class VectorSettingsDevicesFragment : } private fun initLearnMoreButtons() { - views.devicesListHeaderSectionOther.onLearnMoreClickListener = { + views.deviceListHeaderSectionOther.onLearnMoreClickListener = { Toast.makeText(context, "Learn more other", Toast.LENGTH_LONG).show() } } private fun cleanUpLearnMoreButtonsListeners() { - views.devicesListHeaderSectionOther.onLearnMoreClickListener = null + views.deviceListHeaderSectionOther.onLearnMoreClickListener = null + } + + override fun invalidate() = withState(viewModel) { state -> + val currentDeviceInfo = state.devices() + ?.firstOrNull { + it.deviceInfo.deviceId == state.myDeviceId + } + + if (state.devices is Success && currentDeviceInfo != null) { + renderCurrentDevice(state) + } else { + hideCurrentSessionView() + } + + handleRequestStatus(state.request) + } + + private fun hideCurrentSessionView() { + views.deviceListHeaderSectionCurrent.isVisible = false + views.deviceListCurrentSession.isVisible = false + } + + private fun renderCurrentDevice(state: DevicesViewState) { + views.deviceListHeaderSectionCurrent.isVisible = true + views.deviceListCurrentSession.isVisible = true + views.deviceListCurrentSession.update( + accountCrossSigningIsTrusted = state.accountCrossSigningIsTrusted, + legacyMode = !state.hasAccountCrossSigning + ) + } + + private fun handleRequestStatus(unIgnoreRequest: Async) { + views.waitingView.root.isVisible = when (unIgnoreRequest) { + is Loading -> true + else -> false + } } } 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 new file mode 100644 index 0000000000..baf30e35b5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/CurrentSessionView.kt @@ -0,0 +1,83 @@ +/* + * 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 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.themes.ThemeUtils +import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel + +class CurrentSessionView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val views: ViewCurrentSessionBinding + + init { + inflate(context, R.layout.view_current_session, this) + views = ViewCurrentSessionBinding.bind(this) + } + + fun update(accountCrossSigningIsTrusted: Boolean, legacyMode: Boolean) { + renderDeviceType() + renderVerificationStatus(accountCrossSigningIsTrusted, legacyMode) + } + + 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) { + renderCrossSigningVerified() + } else { + renderCrossSigningUnverified() + } + } + + private fun renderCrossSigningVerified() { + views.currentSessionVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_verified) + views.currentSessionVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) + views.currentSessionVerificationStatusDetailTextView.text = context.getString(R.string.device_manager_verification_status_detail_verified) + views.currentSessionVerifySessionButton.isVisible = false + } + + private fun renderCrossSigningUnverified() { + views.currentSessionVerificationStatusTextView.text = context.getString(R.string.device_manager_verification_status_unverified) + views.currentSessionVerificationStatusTextView.setTextColor(ThemeUtils.getColor(context, R.attr.colorError)) + views.currentSessionVerificationStatusDetailTextView.text = context.getString(R.string.device_manager_verification_status_detail_unverified) + views.currentSessionVerifySessionButton.isVisible = true + } + + // TODO. We don't have this info yet. Update later accordingly. + private fun renderDeviceType() { + 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) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt index cdd6b58526..d6c7dbe273 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/DevicesListHeaderView.kt @@ -22,6 +22,7 @@ import android.util.AttributeSet import android.view.LayoutInflater import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.use +import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.databinding.ViewDevicesListHeaderBinding @@ -58,12 +59,18 @@ class DevicesListHeaderView @JvmOverloads constructor( private fun setDescription(typedArray: TypedArray) { val description = typedArray.getString(R.styleable.DevicesListHeaderView_devicesListHeaderDescription) + if (description.isNullOrEmpty()) { + binding.devicesListHeaderDescription.isVisible = false + return + } + val learnMore = context.getString(R.string.action_learn_more) val stringBuilder = StringBuilder() stringBuilder.append(description) stringBuilder.append(" ") stringBuilder.append(learnMore) + binding.devicesListHeaderDescription.isVisible = true binding.devicesListHeaderDescription.setTextWithColoredPart( fullText = stringBuilder.toString(), coloredPart = learnMore, diff --git a/vector/src/main/res/drawable/bg_current_session.xml b/vector/src/main/res/drawable/bg_current_session.xml new file mode 100644 index 0000000000..02728b022f --- /dev/null +++ b/vector/src/main/res/drawable/bg_current_session.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/vector/src/main/res/drawable/bg_device_type.xml b/vector/src/main/res/drawable/bg_device_type.xml new file mode 100644 index 0000000000..88a90ccbe6 --- /dev/null +++ b/vector/src/main/res/drawable/bg_device_type.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/vector/src/main/res/drawable/ic_device_type_desktop.xml b/vector/src/main/res/drawable/ic_device_type_desktop.xml new file mode 100644 index 0000000000..922073459d --- /dev/null +++ b/vector/src/main/res/drawable/ic_device_type_desktop.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/drawable/ic_device_type_mobile.xml b/vector/src/main/res/drawable/ic_device_type_mobile.xml new file mode 100644 index 0000000000..fd825c77f0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_device_type_mobile.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_device_type_web.xml b/vector/src/main/res/drawable/ic_device_type_web.xml new file mode 100644 index 0000000000..dde3ce3a1f --- /dev/null +++ b/vector/src/main/res/drawable/ic_device_type_web.xml @@ -0,0 +1,11 @@ + + + diff --git a/vector/src/main/res/layout/fragment_settings_devices.xml b/vector/src/main/res/layout/fragment_settings_devices.xml index 11c04eacf8..860e395c1f 100644 --- a/vector/src/main/res/layout/fragment_settings_devices.xml +++ b/vector/src/main/res/layout/fragment_settings_devices.xml @@ -5,13 +5,37 @@ android:layout_height="match_parent"> + + + + + app:layout_constraintTop_toBottomOf="@id/deviceListCurrentSession" /> + + diff --git a/vector/src/main/res/layout/view_current_session.xml b/vector/src/main/res/layout/view_current_session.xml new file mode 100644 index 0000000000..61641cbbe7 --- /dev/null +++ b/vector/src/main/res/layout/view_current_session.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + +