diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index fd18ee8992..5eaeae4d86 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3295,5 +3295,14 @@ Other sessions Filter + Verified + For best security, sign out from any session that you don’t recognize or use anymore. + Unverified + Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore. + Inactive + + Consider signing out from old sessions (%1$d day or more) you don’t use anymore. + Consider signing out from old sessions (%1$d days or more) you don’t use anymore. + diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 01af740d43..3d6bc91f2e 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -141,6 +141,7 @@ #0DBD8B + #0F0DBD8B #17191C #FF4B55 #0FFF4B55 diff --git a/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml b/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml new file mode 100644 index 0000000000..6a46132b13 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_other_sessions_security_recommendation_view.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt index 8c7718bfcf..3c459ca992 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesAction.kt @@ -17,8 +17,10 @@ package im.vector.app.features.settings.devices.v2 import im.vector.app.core.platform.VectorViewModelAction +import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo sealed class DevicesAction : VectorViewModelAction { data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() + data class FilterDevices(val filterType: DeviceManagerFilterType) : DevicesAction() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt index e0b6368fc1..4bdadda815 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/DevicesViewModel.kt @@ -144,9 +144,19 @@ class DevicesViewModel @AssistedInject constructor( override fun handle(action: DevicesAction) { when (action) { is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction() + is DevicesAction.FilterDevices -> handleFilterDevices(action) } } + private fun handleFilterDevices(action: DevicesAction.FilterDevices) { + setState { + copy( + currentFilter = action.filterType + ) + } + queryRefreshDevicesList() + } + private fun handleMarkAsManuallyVerifiedAction() { // TODO implement when needed } 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 8b014c4ba8..2996de5658 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,25 +20,31 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.core.view.isVisible 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.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider import im.vector.app.databinding.FragmentOtherSessionsBinding import im.vector.app.features.settings.devices.v2.DeviceFullInfo +import im.vector.app.features.settings.devices.v2.DevicesAction import im.vector.app.features.settings.devices.v2.DevicesViewModel import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType +import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS +import im.vector.app.features.themes.ThemeUtils +import javax.inject.Inject @AndroidEntryPoint class OtherSessionsFragment : VectorBaseFragment(), VectorBaseBottomSheetDialogFragment.ResultListener { private val viewModel: DevicesViewModel by fragmentViewModel() + @Inject lateinit var colorProvider: ColorProvider override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding { return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false) @@ -59,8 +65,8 @@ class OtherSessionsFragment : VectorBaseFragment() } override fun onBottomSheetResult(resultCode: Int, data: Any?) { - if (resultCode == RESULT_OK && data != null) { - Toast.makeText(requireContext(), data.toString(), Toast.LENGTH_LONG).show() + if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) { + viewModel.handle(DevicesAction.FilterDevices(data)) } } @@ -77,10 +83,51 @@ class OtherSessionsFragment : VectorBaseFragment() private fun renderDevices(devices: List?, currentFilter: DeviceManagerFilterType) { views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS + views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS + views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS + + when (currentFilter) { + DeviceManagerFilterType.VERIFIED -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_verified), + description = getString(R.string.device_manager_other_sessions_recommendation_description_verified), + imageResourceId = R.drawable.ic_shield_trusted_no_border, + imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_trust_background) + ) + ) + } + DeviceManagerFilterType.UNVERIFIED -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_unverified), + description = getString(R.string.device_manager_other_sessions_recommendation_description_unverified), + imageResourceId = R.drawable.ic_shield_warning_no_border, + imageTintColorResourceId = colorProvider.getColor(R.color.shield_color_warning_background) + ) + ) + } + DeviceManagerFilterType.INACTIVE -> { + views.otherSessionsSecurityRecommendationView.render( + OtherSessionsSecurityRecommendationViewState( + title = getString(R.string.device_manager_other_sessions_recommendation_title_inactive), + description = resources.getQuantityString( + R.plurals.device_manager_other_sessions_recommendation_description_inactive, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS, + SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS + ), + imageResourceId = R.drawable.ic_inactive_sessions, + imageTintColorResourceId = ThemeUtils.getColor(requireContext(), R.attr.vctr_system) + ) + ) + } + DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ } + } if (devices.isNullOrEmpty()) { - // TODO. Render empty state + views.deviceListOtherSessions.isVisible = false } else { + views.deviceListOtherSessions.isVisible = true views.deviceListOtherSessions.render(devices, devices.size) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt new file mode 100644 index 0000000000..c72dc30a93 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationView.kt @@ -0,0 +1,107 @@ +/* + * 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.othersessions + +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.TypedArray +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.res.use +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.extensions.setTextWithColoredPart +import im.vector.app.databinding.ViewOtherSessionSecurityRecommendationBinding + +@AndroidEntryPoint +class OtherSessionsSecurityRecommendationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val views: ViewOtherSessionSecurityRecommendationBinding + var onLearnMoreClickListener: (() -> Unit)? = null + + init { + inflate(context, R.layout.view_other_session_security_recommendation, this) + views = ViewOtherSessionSecurityRecommendationBinding.bind(this) + + context.obtainStyledAttributes( + attrs, + R.styleable.OtherSessionsSecurityRecommendationView, + 0, + 0 + ).use { + setTitle(it) + setDescription(it) + setImage(it) + } + } + + private fun setTitle(typedArray: TypedArray) { + val title = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationTitle) + setTitle(title) + } + + private fun setTitle(title: String?) { + views.recommendationTitleTextView.text = title + } + + private fun setDescription(typedArray: TypedArray) { + val description = typedArray.getString(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationDescription) + setDescription(description) + } + + private fun setImage(typedArray: TypedArray) { + val imageResource = typedArray.getResourceId(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageResource, 0) + val backgroundTint = typedArray.getColor(R.styleable.OtherSessionsSecurityRecommendationView_otherSessionsRecommendationImageBackgroundTint, 0) + setImageResource(imageResource) + setImageBackgroundTint(backgroundTint) + } + + private fun setImageResource(resourceId: Int) { + views.recommendationShieldImageView.setImageResource(resourceId) + } + + private fun setImageBackgroundTint(backgroundTintColor: Int) { + views.recommendationShieldImageView.backgroundTintList = ColorStateList.valueOf(backgroundTintColor) + } + + private fun setDescription(description: String?) { + val learnMore = context.getString(R.string.action_learn_more) + val stringBuilder = StringBuilder() + stringBuilder.append(description) + stringBuilder.append(" ") + stringBuilder.append(learnMore) + + views.recommendationDescriptionTextView.setTextWithColoredPart( + fullText = stringBuilder.toString(), + coloredPart = learnMore, + underline = false + ) { + onLearnMoreClickListener?.invoke() + } + } + + fun render(viewState: OtherSessionsSecurityRecommendationViewState) { + setTitle(viewState.title) + setDescription(viewState.description) + setImageResource(viewState.imageResourceId) + setImageBackgroundTint(viewState.imageTintColorResourceId) + } +} diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.kt new file mode 100644 index 0000000000..2b17cb26b3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsSecurityRecommendationViewState.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.othersessions + +data class OtherSessionsSecurityRecommendationViewState( + val title: String, + val description: String, + val imageResourceId: Int, + val imageTintColorResourceId: Int, +) diff --git a/vector/src/main/res/layout/fragment_other_sessions.xml b/vector/src/main/res/layout/fragment_other_sessions.xml index 8b504ca903..ae9ca5ae50 100644 --- a/vector/src/main/res/layout/fragment_other_sessions.xml +++ b/vector/src/main/res/layout/fragment_other_sessions.xml @@ -1,6 +1,7 @@ @@ -24,7 +25,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="end" - android:layout_marginEnd="16dp"> + android:padding="8dp" + android:layout_marginEnd="8dp"> - + app:layout_constraintTop_toBottomOf="@id/appBarLayout" /> + + + app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView" /> diff --git a/vector/src/main/res/layout/view_other_session_security_recommendation.xml b/vector/src/main/res/layout/view_other_session_security_recommendation.xml new file mode 100644 index 0000000000..d7597aea35 --- /dev/null +++ b/vector/src/main/res/layout/view_other_session_security_recommendation.xml @@ -0,0 +1,46 @@ + + + + + + + + + +