diff --git a/changelog.d/7100.wip b/changelog.d/7100.wip new file mode 100644 index 0000000000..47e7a6f810 --- /dev/null +++ b/changelog.d/7100.wip @@ -0,0 +1 @@ +[Device Management] Learn more bottom sheets diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 4ff7aae750..7925ac02c4 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -406,6 +406,7 @@ Reset Learn more Next + Got it Copied to clipboard @@ -2366,9 +2367,6 @@ Manage Sessions Sign out of this session Sessions - Other sessions - For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. - Server name Server version Server file upload limit @@ -3229,6 +3227,8 @@ Show All Sessions (V2, WIP) + Other sessions + For best security, verify your sessions and sign out from any session that you don’t recognize or use anymore. Mobile Web Desktop @@ -3302,6 +3302,14 @@ Session name Custom session names can help you recognize your devices more easily. Please be aware that session names are also visible to people you communicate with. + Inactive sessions + Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.\n\nRemoving inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious. + Unverified sessions + Unverified sessions are sessions that have logged in with your credentials but not been cross-verified.\n\nYou should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account. + Verified sessions + Verified sessions have logged in with your credentials and then been verified, either using your secure passphrase or by cross-verifying.\n\nThis means they hold encryption keys for your previous messages, and confirm to other users you are communicating with that these sessions are really you. + Renaming sessions + Other users in direct messages and rooms that you join are able to view a full list of your sessions.\n\nThis provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here. %s\nis looking a little empty. diff --git a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml index d3b931e44a..098ec263fc 100644 --- a/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml +++ b/library/ui-styles/src/main/res/values/stylable_sessions_list_header_view.xml @@ -4,6 +4,7 @@ + diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index 62e7140742..38b62e1511 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -89,6 +89,7 @@ import im.vector.app.features.settings.crosssigning.CrossSigningSettingsViewMode import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheetViewModel import im.vector.app.features.settings.devices.DevicesViewModel import im.vector.app.features.settings.devices.v2.details.SessionDetailsViewModel +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreViewModel import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsViewModel import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel import im.vector.app.features.settings.devices.v2.rename.RenameSessionViewModel @@ -659,4 +660,9 @@ interface MavericksViewModelModule { @IntoMap @MavericksViewModelKey(RenameSessionViewModel::class) fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @MavericksViewModelKey(SessionLearnMoreViewModel::class) + fun sessionLearnMoreViewModelFactory(factory: SessionLearnMoreViewModel.Factory): MavericksAssistedViewModelFactory<*, *> } 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 1e5c4d88e0..0fdbd40178 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 @@ -21,7 +21,6 @@ import android.os.Bundle import android.view.LayoutInflater 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.Success @@ -82,7 +81,6 @@ class VectorSettingsDevicesFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - initLearnMoreButtons() initWaitingView() initOtherSessionsView() initSecurityRecommendationsView() @@ -155,12 +153,6 @@ class VectorSettingsDevicesFragment : super.onDestroyView() } - private fun initLearnMoreButtons() { - views.deviceListHeaderOtherSessions.onLearnMoreClickListener = { - Toast.makeText(context, "Learn more other", Toast.LENGTH_LONG).show() - } - } - private fun cleanUpLearnMoreButtonsListeners() { views.deviceListHeaderOtherSessions.onLearnMoreClickListener = null } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt index ef8682df01..0660e7d642 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/list/SessionsListHeaderView.kt @@ -65,14 +65,23 @@ class SessionsListHeaderView @JvmOverloads constructor( return } + val hasLearnMoreLink = typedArray.getBoolean(R.styleable.SessionsListHeaderView_sessionsListHeaderHasLearnMoreLink, true) + if (hasLearnMoreLink) { + setDescriptionWithLearnMore(description) + } else { + binding.sessionsListHeaderDescription.text = description + } + + binding.sessionsListHeaderDescription.isVisible = true + } + + private fun setDescriptionWithLearnMore(description: String) { val learnMore = context.getString(R.string.action_learn_more) val fullDescription = buildString { append(description) append(" ") append(learnMore) } - - binding.sessionsListHeaderDescription.isVisible = true binding.sessionsListHeaderDescription.setTextWithColoredPart( fullText = fullDescription, coloredPart = learnMore, diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt new file mode 100644 index 0000000000..22ca06eb1e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreBottomSheet.kt @@ -0,0 +1,75 @@ +/* + * 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.more + +import android.os.Bundle +import android.os.Parcelable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.FragmentManager +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetSessionLearnMoreBinding +import kotlinx.parcelize.Parcelize + +@AndroidEntryPoint +class SessionLearnMoreBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @Parcelize + data class Args( + val title: String, + val description: String, + ) : Parcelable + + private val viewModel: SessionLearnMoreViewModel by fragmentViewModel() + + override val showExpanded = true + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetSessionLearnMoreBinding { + return BottomSheetSessionLearnMoreBinding.inflate(inflater, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initCloseButton() + } + + private fun initCloseButton() { + views.bottomSheetSessionLearnMoreCloseButton.debouncedClicks { + dismiss() + } + } + + override fun invalidate() = withState(viewModel) { viewState -> + super.invalidate() + views.bottomSheetSessionLearnMoreTitle.text = viewState.title + views.bottomSheetSessionLearnMoreDescription.text = viewState.description + } + + companion object { + + fun show(fragmentManager: FragmentManager, args: Args) { + val bottomSheet = SessionLearnMoreBottomSheet() + bottomSheet.isCancelable = true + bottomSheet.setArguments(args) + bottomSheet.show(fragmentManager, "SessionLearnMoreBottomSheet") + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewModel.kt new file mode 100644 index 0000000000..09ca2df15d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewModel.kt @@ -0,0 +1,43 @@ +/* + * 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.more + +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.EmptyAction +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel + +class SessionLearnMoreViewModel @AssistedInject constructor( + @Assisted initialState: SessionLearnMoreViewState, +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: SessionLearnMoreViewState): SessionLearnMoreViewModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: EmptyAction) { + // do nothing + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewState.kt new file mode 100644 index 0000000000..cade2ce861 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/more/SessionLearnMoreViewState.kt @@ -0,0 +1,29 @@ +/* + * 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.more + +import com.airbnb.mvrx.MavericksState + +data class SessionLearnMoreViewState( + val title: String, + val description: String, +) : MavericksState { + constructor(args: SessionLearnMoreBottomSheet.Args) : this( + title = args.title, + description = args.description, + ) +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt index 5734b04089..610776e22e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt @@ -20,6 +20,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.StringRes import androidx.core.view.isVisible import com.airbnb.mvrx.Success import com.airbnb.mvrx.args @@ -37,6 +38,7 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBott import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import im.vector.app.features.settings.devices.v2.list.OtherSessionsView import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.themes.ThemeUtils import javax.inject.Inject @@ -121,6 +123,7 @@ class OtherSessionsFragment : ) ) views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found) + updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_verified_title, R.string.device_manager_learn_more_sessions_verified) } DeviceManagerFilterType.UNVERIFIED -> { views.otherSessionsSecurityRecommendationView.render( @@ -132,6 +135,10 @@ class OtherSessionsFragment : ) ) views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_unverified_sessions_found) + updateSecurityLearnMoreButton( + R.string.device_manager_learn_more_sessions_unverified_title, + R.string.device_manager_learn_more_sessions_unverified + ) } DeviceManagerFilterType.INACTIVE -> { views.otherSessionsSecurityRecommendationView.render( @@ -147,8 +154,10 @@ class OtherSessionsFragment : ) ) views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found) + updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_inactive_title, R.string.device_manager_learn_more_sessions_inactive) + } + DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ } - DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ } } if (devices.isNullOrEmpty()) { @@ -161,6 +170,26 @@ class OtherSessionsFragment : } } + private fun updateSecurityLearnMoreButton( + @StringRes titleResId: Int, + @StringRes descriptionResId: Int, + ) { + views.otherSessionsSecurityRecommendationView.onLearnMoreClickListener = { + showLearnMoreInfo(titleResId, getString(descriptionResId)) + } + } + + private fun showLearnMoreInfo( + @StringRes titleResId: Int, + description: String, + ) { + val args = SessionLearnMoreBottomSheet.Args( + title = getString(titleResId), + description = description, + ) + SessionLearnMoreBottomSheet.show(childFragmentManager, args) + } + override fun onOtherSessionClicked(deviceId: String) { viewNavigator.navigateToSessionOverview( context = requireActivity(), diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt index 4af4913183..8c3b907070 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/overview/SessionOverviewFragment.kt @@ -41,9 +41,11 @@ import im.vector.app.databinding.FragmentSessionOverviewBinding import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import im.vector.app.features.workers.signout.SignOutUiWorker import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import javax.inject.Inject /** @@ -204,6 +206,9 @@ class SessionOverviewFragment : isLastSeenDetailsVisible = true, ) views.sessionOverviewInfo.render(infoViewState, dateFormatter, drawableProvider, colorProvider) + views.sessionOverviewInfo.onLearnMoreClickListener = { + showLearnMoreInfoVerificationStatus(deviceInfo.roomEncryptionTrustLevel == RoomEncryptionTrustLevel.Trusted) + } } else { views.sessionOverviewInfo.isVisible = false } @@ -249,4 +254,22 @@ class SessionOverviewFragment : reAuthActivityResultLauncher.launch(intent) } } + + private fun showLearnMoreInfoVerificationStatus(isVerified: Boolean) { + val titleResId = if (isVerified) { + R.string.device_manager_verification_status_verified + } else { + R.string.device_manager_verification_status_unverified + } + val descriptionResId = if (isVerified) { + R.string.device_manager_learn_more_sessions_verified + } else { + R.string.device_manager_learn_more_sessions_unverified + } + val args = SessionLearnMoreBottomSheet.Args( + title = getString(titleResId), + description = getString(descriptionResId), + ) + SessionLearnMoreBottomSheet.show(childFragmentManager, args) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt index df92bee100..2f671492e3 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/rename/RenameSessionFragment.kt @@ -24,9 +24,11 @@ import androidx.core.widget.doOnTextChanged 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.extensions.showKeyboard import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.databinding.FragmentSessionRenameBinding +import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet import javax.inject.Inject /** @@ -51,6 +53,7 @@ class RenameSessionFragment : initEditText() initSaveButton() initWithLastEditedName() + initInfoView() } private fun initToolbar() { @@ -75,6 +78,20 @@ class RenameSessionFragment : viewModel.handle(RenameSessionAction.InitWithLastEditedName) } + private fun initInfoView() { + views.renameSessionInfo.onLearnMoreClickListener = { + showLearnMoreInfo() + } + } + + private fun showLearnMoreInfo() { + val args = SessionLearnMoreBottomSheet.Args( + title = getString(R.string.device_manager_learn_more_session_rename_title), + description = getString(R.string.device_manager_learn_more_session_rename), + ) + SessionLearnMoreBottomSheet.show(childFragmentManager, args) + } + private fun observeViewEvents() { viewModel.observeViewEvents { when (it) { diff --git a/vector/src/main/res/layout/bottom_sheet_session_learn_more.xml b/vector/src/main/res/layout/bottom_sheet_session_learn_more.xml new file mode 100644 index 0000000000..466ab5af49 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_session_learn_more.xml @@ -0,0 +1,59 @@ + + + + + + + + + +