Merge pull request #7046 from vector-im/feature/ons/device_manager_filter

[Device Manager] Filter Other Sessions (PSG-684)
This commit is contained in:
Onuray Sahin 2022-09-19 14:24:42 +03:00 committed by GitHub
commit 5902c9cd83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1450 additions and 122 deletions

1
changelog.d/7045.wip Normal file
View File

@ -0,0 +1 @@
[Device Manager] Filter Other Sessions

View File

@ -2602,8 +2602,8 @@
<string name="all_chats">Tots els xats</string>
<string name="home_layout_preferences">Preferències de disseny</string>
<string name="explore_rooms">Explora sales</string>
<string name="settings_sessions_other_description">Per estar més segur, verifica les teves sessions i tanca qualsevol sessió que no reconeguis o ja no utilitzis.</string>
<string name="settings_sessions_other_title">Altres sessions</string>
<string name="device_manager_sessions_other_description">Per estar més segur, verifica les teves sessions i tanca qualsevol sessió que no reconeguis o ja no utilitzis.</string>
<string name="device_manager_sessions_other_title">Altres sessions</string>
<string name="settings_sessions_list">Sessions</string>
<string name="a11y_open_spaces">Obre la llista d\'espais</string>
<string name="a11y_create_message">Crea un nou xat o sala</string>

View File

@ -2651,8 +2651,8 @@
<string name="a11y_open_settings">Otevřít nastavení</string>
<string name="all_chats">Všechny konverzace</string>
<string name="device_manager_settings_active_sessions_show_all">Zobrazit všechny relace (V2, WIP)</string>
<string name="settings_sessions_other_description">V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte.</string>
<string name="settings_sessions_other_title">Ostatní relace</string>
<string name="device_manager_sessions_other_description">V zájmu co nejlepšího zabezpečení ověřujte své relace a odhlašujte se ze všech relací, které již nepoznáváte nebo nepoužíváte.</string>
<string name="device_manager_sessions_other_title">Ostatní relace</string>
<string name="settings_sessions_list">Relace</string>
<string name="a11y_open_spaces">Seznam otevřených prostorů</string>
<string name="a11y_create_message">Vytvořit novou konverzaci nebo místnost</string>

View File

@ -2587,8 +2587,8 @@
<string name="room_list_filter_people">Personen</string>
<string name="send_your_first_msg_to_invite">Schreibe deine erste Nachricht, um %s zur Konversation einzuladen</string>
<string name="device_manager_settings_active_sessions_show_all">Alle Sitzungen anzeigen (V2, in Arbeit)</string>
<string name="settings_sessions_other_description">Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt.</string>
<string name="settings_sessions_other_title">Andere Sitzungen</string>
<string name="device_manager_sessions_other_description">Für bestmögliche Sicherheit verifiziere deine Sitzungen und melde dich von allen ab, die du nicht erkennst oder nutzt.</string>
<string name="device_manager_sessions_other_title">Andere Sitzungen</string>
<string name="settings_sessions_list">Sitzungen</string>
<string name="a11y_open_spaces">Space-Liste öffnen</string>
<string name="a11y_create_message">Beginne ein Gespräch oder erstelle einen Raum</string>

View File

@ -2592,8 +2592,8 @@
<string name="a11y_open_settings">Ava seadistused</string>
<string name="all_chats">Kõik vestlused</string>
<string name="device_manager_settings_active_sessions_show_all">Näita kõiki sessioone (V2, WIP)</string>
<string name="settings_sessions_other_description">Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta.</string>
<string name="settings_sessions_other_title">Muud sessioonid</string>
<string name="device_manager_sessions_other_description">Parima turvalisuse nimel verifitseeri kõik oma sessioonid ning logi välja neist, mida sa enam ei kasuta.</string>
<string name="device_manager_sessions_other_title">Muud sessioonid</string>
<string name="settings_sessions_list">Sessionid</string>
<string name="a11y_open_spaces">Ava kogukondade loend</string>
<string name="a11y_create_message">Alusta uut vestlust või loo uus jututuba</string>

View File

@ -2601,8 +2601,8 @@
<string name="a11y_open_settings">گشودن تنظیمات</string>
<string name="all_chats">تمامی گپ‌ها</string>
<string name="device_manager_settings_active_sessions_show_all">نمایش تمامی نشست‌ها (ن۲، دح‌ت)</string>
<string name="settings_sessions_other_description">برای امنیت بیش‌تر، نشست‌هایتان را تأیید و از هر نشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید خارج شوید.</string>
<string name="settings_sessions_other_title">دیگر نشست‌ها</string>
<string name="device_manager_sessions_other_description">برای امنیت بیش‌تر، نشست‌هایتان را تأیید و از هر نشستی که تشخیصش نمی‌دهید یا دیگر استفاده نمی‌کنید خارج شوید.</string>
<string name="device_manager_sessions_other_title">دیگر نشست‌ها</string>
<string name="settings_sessions_list">نشست‌ها</string>
<string name="a11y_open_spaces">گشودن سیاههٔ فضاها</string>
<string name="a11y_create_message">ایجاد اتاق یا گفت‌وگویی جدید</string>

View File

@ -2601,8 +2601,8 @@
<string name="a11y_open_settings">Ouvrir les paramètres</string>
<string name="all_chats">Toutes les conversations</string>
<string name="device_manager_settings_active_sessions_show_all">Afficher toutes les sessions (V2, en cours)</string>
<string name="settings_sessions_other_description">Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous nutilisez plus.</string>
<string name="settings_sessions_other_title">Autres sessions</string>
<string name="device_manager_sessions_other_description">Pour une meilleure sécurité, vérifiez vos sessions et déconnectez toutes les sessions que vous ne connaissez pas ou que vous nutilisez plus.</string>
<string name="device_manager_sessions_other_title">Autres sessions</string>
<string name="settings_sessions_list">Sessions</string>
<string name="a11y_open_spaces">Ouvrir la liste des espaces</string>
<string name="a11y_create_message">Créer une nouvelle conversation ou salon</string>

View File

@ -2615,8 +2615,8 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze
<string name="a11y_device_manager_device_type_web">Web</string>
<string name="a11y_device_manager_device_type_mobile">Mobil</string>
<string name="device_manager_settings_active_sessions_show_all">Minden munkamenet megjelenítése (V2, WIP)</string>
<string name="settings_sessions_other_description">A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz.</string>
<string name="settings_sessions_other_title">Más munkamenetek</string>
<string name="device_manager_sessions_other_description">A legjobb biztonság érdekében ellenőrizd a munkameneteket, és jelentkezz ki minden olyan munkamenetből, melyet már nem ismersz fel vagy nem használsz.</string>
<string name="device_manager_sessions_other_title">Más munkamenetek</string>
<string name="settings_sessions_list">Munkamenetek</string>
<string name="a11y_open_spaces">Nyitott területek listája</string>
<string name="a11y_create_message">Új beszélgetés vagy szoba létrehozása</string>

View File

@ -2553,8 +2553,8 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan.</string>
<string name="auth_reset_password_error_unverified">Email belum diverifikasi, periksa kotak masuk Anda</string>
<string name="all_chats">Semua Obrolan</string>
<string name="device_manager_settings_active_sessions_show_all">Tampilkan Semua Sesi (V2, Dalam Pengembangan)</string>
<string name="settings_sessions_other_description">Untuk keamanan terbaik, verifikasi sesi Anda dan keluarkan sesi apa pun yang Anda tidak kenal atau Anda tidak gunakan lagi.</string>
<string name="settings_sessions_other_title">Sesi lainnya</string>
<string name="device_manager_sessions_other_description">Untuk keamanan terbaik, verifikasi sesi Anda dan keluarkan sesi apa pun yang Anda tidak kenal atau Anda tidak gunakan lagi.</string>
<string name="device_manager_sessions_other_title">Sesi lainnya</string>
<string name="settings_sessions_list">Sesi</string>
<string name="a11y_open_spaces">Buka daftar space</string>
<string name="a11y_create_message">Buat percakapan atau ruangan baru</string>

View File

@ -2592,8 +2592,8 @@
<string name="a11y_open_settings">Apri le impostazioni</string>
<string name="all_chats">Tutte le chat</string>
<string name="device_manager_settings_active_sessions_show_all">Mostra tutte le sessioni (V2, WIP)</string>
<string name="settings_sessions_other_description">Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più.</string>
<string name="settings_sessions_other_title">Altre sessioni</string>
<string name="device_manager_sessions_other_description">Per una maggiore sicurezza, verifica le tue sessioni e disconnetti quelle che non riconosci o che non usi più.</string>
<string name="device_manager_sessions_other_title">Altre sessioni</string>
<string name="settings_sessions_list">Sessioni</string>
<string name="a11y_open_spaces">Apri elenco spazi</string>
<string name="a11y_create_message">Crea una nuova conversazione o stanza</string>

View File

@ -2600,8 +2600,8 @@
<string name="location_share_loading_map_error">Kan kaart niet laden
\nDeze server is mogelijk niet geconfigureerd om kaarten weer te geven.</string>
<string name="a11y_open_settings">Open instellingen</string>
<string name="settings_sessions_other_description">Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt.</string>
<string name="settings_sessions_other_title">Andere sessies</string>
<string name="device_manager_sessions_other_description">Voor de beste beveiliging verifieert u uw sessies en meldt u zich af bij elke sessie die u niet meer herkent of gebruikt.</string>
<string name="device_manager_sessions_other_title">Andere sessies</string>
<string name="settings_sessions_list">Sessies</string>
<string name="a11y_open_spaces">Lijst met publieke spaces</string>
<string name="a11y_create_message">Maak een nieuw gesprek of een nieuwe kamer</string>

View File

@ -2697,8 +2697,8 @@
<string name="location_share_loading_map_error">Nie można wczytać mapy.
\nTen serwer macierzysty może nie być skonfigurowany do wyświetlania map.</string>
<string name="a11y_open_settings">Otwórz ustawienia</string>
<string name="settings_sessions_other_description">Aby zapewnić najlepsze bezpieczeństwo, zweryfikuj swoje sesje i wyloguj się z każdej sesji, której już nie rozpoznajesz lub której już nie używasz.</string>
<string name="settings_sessions_other_title">Inne sesje</string>
<string name="device_manager_sessions_other_description">Aby zapewnić najlepsze bezpieczeństwo, zweryfikuj swoje sesje i wyloguj się z każdej sesji, której już nie rozpoznajesz lub której już nie używasz.</string>
<string name="device_manager_sessions_other_title">Inne sesje</string>
<string name="settings_sessions_list">Sesje</string>
<string name="a11y_open_spaces">Lista otwartych przestrzeni</string>
<string name="a11y_create_message">Utwórz nową rozmowę lub pokój</string>

View File

@ -2601,8 +2601,8 @@
<string name="a11y_open_settings">Abrir configurações</string>
<string name="all_chats">Todos os Chats</string>
<string name="device_manager_settings_active_sessions_show_all">Mostrar Todas Sessões (V2, WIP)</string>
<string name="settings_sessions_other_description">Para a melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais.</string>
<string name="settings_sessions_other_title">Outras sessões</string>
<string name="device_manager_sessions_other_description">Para a melhor segurança, verifique suas sessões e faça signout de qualquer sessão que você não reconhece ou usa mais.</string>
<string name="device_manager_sessions_other_title">Outras sessões</string>
<string name="settings_sessions_list">Sessões</string>
<string name="a11y_open_spaces">Abrir lista de espaços</string>
<string name="a11y_create_message">Criar uma nova conversa ou sala</string>

View File

@ -2660,8 +2660,8 @@
<string name="location_share_loading_map_error">Не удалось загрузить карту
\nВозможно, этот домашний сервер не настроен для отображения карт.</string>
<string name="all_chats">Все беседы</string>
<string name="settings_sessions_other_description">Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете.</string>
<string name="settings_sessions_other_title">Другие сессии</string>
<string name="device_manager_sessions_other_description">Для лучшей безопасности заверьте свои сессии и выйдите из тех, которые более не признаёте или не используете.</string>
<string name="device_manager_sessions_other_title">Другие сессии</string>
<string name="settings_sessions_list">Сессии</string>
<string name="a11y_create_message">Создать беседу или комнату</string>
<string name="device_manager_settings_active_sessions_show_all">Показать все сессии (V2, в разработке)</string>

View File

@ -2651,8 +2651,8 @@
<string name="a11y_open_settings">Otvoriť nastavenia</string>
<string name="all_chats">Všetky konverzácie</string>
<string name="device_manager_settings_active_sessions_show_all">Zobraziť všetky relácie (V2, WIP)</string>
<string name="settings_sessions_other_description">V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate.</string>
<string name="settings_sessions_other_title">Iné relácie</string>
<string name="device_manager_sessions_other_description">V záujme čo najlepšieho zabezpečenia overte svoje relácie a odhláste sa z každej relácie, ktorú už nepoznáte alebo nepoužívate.</string>
<string name="device_manager_sessions_other_title">Iné relácie</string>
<string name="settings_sessions_list">Relácie</string>
<string name="a11y_open_spaces">Otvoriť zoznam priestorov</string>
<string name="a11y_create_message">Vytvoriť novú konverzáciu alebo miestnosť</string>

View File

@ -2701,8 +2701,8 @@
<string name="a11y_open_settings">Відкрити налаштування</string>
<string name="all_chats">Усі бесіди</string>
<string name="device_manager_settings_active_sessions_show_all">Показати всі сеанси (V2, WIP)</string>
<string name="settings_sessions_other_description">Для найкращої безпеки перевірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте.</string>
<string name="settings_sessions_other_title">Інші сеанси</string>
<string name="device_manager_sessions_other_description">Для найкращої безпеки перевірте свої сеанси та вийдіть з усіх сеансів, які ви більше не розпізнаєте або не використовуєте.</string>
<string name="device_manager_sessions_other_title">Інші сеанси</string>
<string name="settings_sessions_list">Сеанси</string>
<string name="a11y_open_spaces">Відкрити список кімнат</string>
<string name="a11y_create_message">Створити нову розмову або кімнату</string>

View File

@ -2551,8 +2551,8 @@
<string name="a11y_open_settings">打开设置</string>
<string name="all_chats">全部聊天</string>
<string name="device_manager_settings_active_sessions_show_all">显示全部会话V2, WIP</string>
<string name="settings_sessions_other_description">为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。</string>
<string name="settings_sessions_other_title">其他会话</string>
<string name="device_manager_sessions_other_description">为获得最佳安全性,请验证你的会话,并从任何你不认识或不再使用的会话登出。</string>
<string name="device_manager_sessions_other_title">其他会话</string>
<string name="settings_sessions_list">会话</string>
<string name="a11y_open_spaces">打开空间列表</string>
<string name="a11y_create_message">创建新对话或房间</string>

View File

@ -2551,8 +2551,8 @@
<string name="a11y_open_settings">開啟設定</string>
<string name="all_chats">所有聊天</string>
<string name="device_manager_settings_active_sessions_show_all">顯示所有工作階段 (V2, WIP)</string>
<string name="settings_sessions_other_description">為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。</string>
<string name="settings_sessions_other_title">其他工作階段</string>
<string name="device_manager_sessions_other_description">為了取得最佳安全性,請驗證您的工作階段並登出任何您無法識別或不再使用的工作階段。</string>
<string name="device_manager_sessions_other_title">其他工作階段</string>
<string name="settings_sessions_list">工作階段</string>
<string name="a11y_open_spaces">開啟空間清單</string>
<string name="a11y_create_message">建立新的對話或聊天室</string>

View File

@ -2361,8 +2361,8 @@
<string name="settings_active_sessions_manage">Manage Sessions</string>
<string name="settings_active_sessions_signout_device">Sign out of this session</string>
<string name="settings_sessions_list">Sessions</string>
<string name="settings_sessions_other_title">Other sessions</string>
<string name="settings_sessions_other_description">For best security, verify your sessions and sign out from any session that you dont recognize or use anymore.</string>
<string name="device_manager_sessions_other_title">Other sessions</string>
<string name="device_manager_sessions_other_description">For best security, verify your sessions and sign out from any session that you dont recognize or use anymore.</string>
<string name="settings_server_name">Server name</string>
<string name="settings_server_version">Server version</string>
@ -3265,6 +3265,31 @@
<string name="device_manager_device_title">Device</string>
<!-- Examples: Last activity Yesterday at 6PM, Last activity Aug 31 at 5:47PM -->
<string name="device_manager_session_last_activity">Last activity %1$s</string>
<string name="device_manager_filter_bottom_sheet_title">Filter</string>
<string name="device_manager_filter_option_all_sessions">All sessions</string>
<string name="device_manager_filter_option_verified">Verified</string>
<string name="device_manager_filter_option_verified_description">Ready for secure messaging</string>
<string name="device_manager_filter_option_unverified">Unverified</string>
<string name="device_manager_filter_option_unverified_description">Not ready for secure messaging</string>
<string name="device_manager_filter_option_inactive">Inactive</string>
<plurals name="device_manager_filter_option_inactive_description">
<item quantity="one">Inactive for %1$d day or longer</item>
<item quantity="other">Inactive for %1$d days or longer</item>
</plurals>
<string name="a11y_device_manager_filter">Filter</string>
<string name="device_manager_other_sessions_recommendation_title_verified">Verified</string>
<string name="device_manager_other_sessions_recommendation_description_verified">For best security, sign out from any session that you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_unverified">Unverified</string>
<string name="device_manager_other_sessions_recommendation_description_unverified">Verify your sessions for enhanced secure messaging or sign out from those you dont recognize or use anymore.</string>
<string name="device_manager_other_sessions_recommendation_title_inactive">Inactive</string>
<plurals name="device_manager_other_sessions_recommendation_description_inactive">
<item quantity="one">Consider signing out from old sessions (%1$d day or more) you dont use anymore.</item>
<item quantity="other">Consider signing out from old sessions (%1$d days or more) you dont use anymore.</item>
</plurals>
<string name="device_manager_other_sessions_no_verified_sessions_found">No verified sessions found.</string>
<string name="device_manager_other_sessions_no_unverified_sessions_found">No unverified sessions found.</string>
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
<string name="device_manager_session_details_title">Session details</string>
<string name="device_manager_session_details_description">Application, device, and activity information.</string>
<string name="device_manager_session_details_session_name">Session name</string>

View File

@ -141,6 +141,7 @@
<!-- Shield colors -->
<color name="shield_color_trust">#0DBD8B</color>
<color name="shield_color_trust_background">#0F0DBD8B</color>
<color name="shield_color_black">#17191C</color>
<color name="shield_color_warning">#FF4B55</color>
<color name="shield_color_warning_background">#0FFF4B55</color>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="OtherSessionsSecurityRecommendationView">
<attr name="otherSessionsRecommendationTitle" format="string" />
<attr name="otherSessionsRecommendationDescription" format="string" />
<attr name="otherSessionsRecommendationImageResource" format="reference" />
<attr name="otherSessionsRecommendationImageBackgroundTint" format="color" />
</declare-styleable>
</resources>

View File

@ -323,6 +323,7 @@
<activity android:name=".features.home.room.list.home.invites.InvitesActivity"/>
<activity android:name=".features.home.room.list.home.release.ReleaseNotesActivity"/>
<activity android:name=".features.settings.devices.v2.overview.SessionOverviewActivity"/>
<activity android:name=".features.settings.devices.v2.othersessions.OtherSessionsActivity" />
<activity android:name=".features.settings.devices.v2.details.SessionDetailsActivity"/>
<!-- Services -->

View File

@ -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.othersessions.OtherSessionsViewModel
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewViewModel
import im.vector.app.features.settings.devtools.AccountDataViewModel
import im.vector.app.features.settings.devtools.GossipingEventsPaperTrailViewModel
@ -643,6 +644,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(SessionOverviewViewModel::class)
fun sessionOverviewViewModelFactory(factory: SessionOverviewViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(OtherSessionsViewModel::class)
fun otherSessionsViewModelFactory(factory: OtherSessionsViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(SessionDetailsViewModel::class)

View File

@ -24,26 +24,20 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.utils.PublishDataSource
import im.vector.lib.core.utils.flow.throttleFirst
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import kotlin.time.Duration.Companion.seconds
class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState,
private val activeSessionHolder: ActiveSessionHolder,
activeSessionHolder: ActiveSessionHolder,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesUseCase: RefreshDevicesUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState), VerificationService.Listener {
refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorSessionsListViewModel<DevicesViewState, DevicesAction, DevicesViewEvent>(initialState, activeSessionHolder, refreshDevicesUseCase) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<DevicesViewModel, DevicesViewState> {
@ -52,35 +46,11 @@ class DevicesViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<DevicesViewModel, DevicesViewState> by hiltMavericksViewModelFactory()
private val refreshSource = PublishDataSource<Unit>()
private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds
init {
addVerificationListener()
observeCurrentSessionCrossSigningInfo()
observeDevices()
observeRefreshSource()
refreshDevicesOnCryptoDevicesChange()
queryRefreshDevicesList()
}
override fun onCleared() {
removeVerificationListener()
super.onCleared()
}
private fun addVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.addListener(this)
}
private fun removeVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.removeListener(this)
refreshDeviceList()
}
private fun observeCurrentSessionCrossSigningInfo() {
@ -94,7 +64,10 @@ class DevicesViewModel @AssistedInject constructor(
}
private fun observeDevices() {
getDeviceFullInfoListUseCase.execute()
getDeviceFullInfoListUseCase.execute(
filterType = DeviceManagerFilterType.ALL_SESSIONS,
excludeCurrentDevice = false
)
.execute { async ->
if (async is Success) {
val deviceFullInfoList = async.invoke()
@ -119,28 +92,6 @@ class DevicesViewModel @AssistedInject constructor(
}
}
private fun observeRefreshSource() {
refreshSource.stream()
.throttleFirst(refreshThrottleDelayMs)
.onEach { refreshDevicesUseCase.execute() }
.launchIn(viewModelScope)
}
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) {
queryRefreshDevicesList()
}
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
private fun queryRefreshDevicesList() {
refreshSource.post(Unit)
}
override fun handle(action: DevicesAction) {
when (action) {
is DevicesAction.MarkAsManuallyVerified -> handleMarkAsManuallyVerifiedAction()

View File

@ -17,6 +17,8 @@
package im.vector.app.features.settings.devices.v2
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@ -32,16 +34,23 @@ class GetDeviceFullInfoListUseCase @Inject constructor(
private val checkIfSessionIsInactiveUseCase: CheckIfSessionIsInactiveUseCase,
private val getEncryptionTrustLevelForDeviceUseCase: GetEncryptionTrustLevelForDeviceUseCase,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val filterDevicesUseCase: FilterDevicesUseCase,
) {
fun execute(): Flow<List<DeviceFullInfo>> {
fun execute(filterType: DeviceManagerFilterType, excludeCurrentDevice: Boolean = false): Flow<List<DeviceFullInfo>> {
return activeSessionHolder.getSafeActiveSession()?.let { session ->
val deviceFullInfoFlow = combine(
getCurrentSessionCrossSigningInfoUseCase.execute(),
session.flow().liveUserCryptoDevices(session.myUserId),
session.flow().liveMyDevicesInfo()
) { currentSessionCrossSigningInfo, cryptoList, infoList ->
convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList)
val deviceFullInfoList = convertToDeviceFullInfoList(currentSessionCrossSigningInfo, cryptoList, infoList)
val excludedDeviceIds = if (excludeCurrentDevice) {
listOf(currentSessionCrossSigningInfo.deviceId)
} else {
emptyList()
}
filterDevicesUseCase.execute(deviceFullInfoList, filterType, excludedDeviceIds)
}
deviceFullInfoFlow.distinctUntilChanged()

View File

@ -0,0 +1,87 @@
/*
* 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
import com.airbnb.mvrx.MavericksState
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.utils.PublishDataSource
import im.vector.lib.core.utils.flow.throttleFirst
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import kotlin.time.Duration.Companion.seconds
abstract class VectorSessionsListViewModel<S : MavericksState, VA : VectorViewModelAction, VE : VectorViewEvents>(
initialState: S,
private val activeSessionHolder: ActiveSessionHolder,
private val refreshDevicesUseCase: RefreshDevicesUseCase,
) : VectorViewModel<S, VA, VE>(initialState), VerificationService.Listener {
private val refreshSource = PublishDataSource<Unit>()
private val refreshThrottleDelayMs = 4.seconds.inWholeMilliseconds
init {
addVerificationListener()
observeRefreshSource()
}
override fun onCleared() {
removeVerificationListener()
super.onCleared()
}
private fun addVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.addListener(this)
}
private fun removeVerificationListener() {
activeSessionHolder.getSafeActiveSession()
?.cryptoService()
?.verificationService()
?.removeListener(this)
}
private fun observeRefreshSource() {
refreshSource.stream()
.throttleFirst(refreshThrottleDelayMs)
.onEach { refreshDevicesUseCase.execute() }
.launchIn(viewModelScope)
}
override fun transactionUpdated(tx: VerificationTransaction) {
if (tx.state == VerificationTxState.Verified) {
refreshDeviceList()
}
}
/**
* Force the refresh of the devices list.
* The devices list is the list of the devices where the user is logged in.
* It can be any mobile devices, and any browsers.
*/
fun refreshDeviceList() {
refreshSource.post(Unit)
}
}

View File

@ -37,7 +37,8 @@ import im.vector.app.core.resources.DrawableProvider
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.v2.list.OtherSessionsController
import im.vector.app.features.settings.devices.v2.list.NUMBER_OF_OTHER_DEVICES_TO_RENDER
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.list.SecurityRecommendationViewState
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
@ -48,7 +49,8 @@ import javax.inject.Inject
*/
@AndroidEntryPoint
class VectorSettingsDevicesFragment :
VectorBaseFragment<FragmentSettingsDevicesBinding>() {
VectorBaseFragment<FragmentSettingsDevicesBinding>(),
OtherSessionsView.Callback {
@Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator
@ -120,11 +122,7 @@ class VectorSettingsDevicesFragment :
}
private fun initOtherSessionsView() {
views.deviceListOtherSessions.setCallback(object : OtherSessionsController.Callback {
override fun onItemClicked(deviceId: String) {
navigateToSessionOverview(deviceId)
}
})
views.deviceListOtherSessions.callback = this
}
override fun onDestroyView() {
@ -201,7 +199,11 @@ class VectorSettingsDevicesFragment :
} else {
views.deviceListHeaderOtherSessions.isVisible = true
views.deviceListOtherSessions.isVisible = true
views.deviceListOtherSessions.render(otherDevices)
views.deviceListOtherSessions.render(
devices = otherDevices.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER),
totalNumberOfDevices = otherDevices.size,
showViewAll = otherDevices.size > NUMBER_OF_OTHER_DEVICES_TO_RENDER
)
}
}
@ -252,4 +254,12 @@ class VectorSettingsDevicesFragment :
private fun handleLoadingStatus(isLoading: Boolean) {
views.waitingView.root.isVisible = isLoading
}
override fun onOtherSessionClicked(deviceId: String) {
navigateToSessionOverview(deviceId)
}
override fun onViewAllOtherSessionsClicked() {
viewNavigator.navigateToOtherSessions(requireActivity())
}
}

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2
import android.content.Context
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import javax.inject.Inject
@ -25,4 +26,8 @@ class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToSessionOverview(context: Context, deviceId: String) {
context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
}
fun navigateToOtherSessions(context: Context) {
context.startActivity(OtherSessionsActivity.newIntent(context))
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.filter
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.args
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.databinding.BottomSheetDeviceManagerFilterBinding
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
import kotlinx.parcelize.Parcelize
@Parcelize
data class DeviceManagerFilterBottomSheetArgs(
val initialFilterType: DeviceManagerFilterType,
) : Parcelable
@AndroidEntryPoint
class DeviceManagerFilterBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetDeviceManagerFilterBinding>() {
private val args: DeviceManagerFilterBottomSheetArgs by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetDeviceManagerFilterBinding {
return BottomSheetDeviceManagerFilterBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initFilterRadioGroup()
}
private fun initFilterRadioGroup() {
views.filterOptionInactiveTextView.text = resources.getQuantityString(
R.plurals.device_manager_filter_option_inactive_description,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS,
SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
)
val radioButtonId = when (args.initialFilterType) {
DeviceManagerFilterType.ALL_SESSIONS -> R.id.filterOptionAllSessionsRadioButton
DeviceManagerFilterType.VERIFIED -> R.id.filterOptionVerifiedRadioButton
DeviceManagerFilterType.UNVERIFIED -> R.id.filterOptionUnverifiedRadioButton
DeviceManagerFilterType.INACTIVE -> R.id.filterOptionInactiveRadioButton
}
views.filterOptionsRadioGroup.check(radioButtonId)
views.filterOptionVerifiedTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionVerifiedRadioButton)
}
views.filterOptionUnverifiedTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionUnverifiedRadioButton)
}
views.filterOptionInactiveTextView.debouncedClicks {
views.filterOptionsRadioGroup.check(R.id.filterOptionInactiveRadioButton)
}
views.filterOptionsRadioGroup.setOnCheckedChangeListener { _, checkedId ->
onFilterTypeChanged(checkedId)
}
}
private fun onFilterTypeChanged(checkedId: Int) {
val filterType = when (checkedId) {
R.id.filterOptionAllSessionsRadioButton -> DeviceManagerFilterType.ALL_SESSIONS
R.id.filterOptionVerifiedRadioButton -> DeviceManagerFilterType.VERIFIED
R.id.filterOptionUnverifiedRadioButton -> DeviceManagerFilterType.UNVERIFIED
R.id.filterOptionInactiveRadioButton -> DeviceManagerFilterType.INACTIVE
else -> DeviceManagerFilterType.ALL_SESSIONS
}
resultListener?.onBottomSheetResult(RESULT_OK, filterType)
dismiss()
}
companion object {
fun newInstance(initialFilterType: DeviceManagerFilterType, resultListener: ResultListener): DeviceManagerFilterBottomSheet {
return DeviceManagerFilterBottomSheet().apply {
this.resultListener = resultListener
setArguments(DeviceManagerFilterBottomSheetArgs(initialFilterType))
}
}
}
}

View File

@ -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.filter
enum class DeviceManagerFilterType {
ALL_SESSIONS,
VERIFIED,
UNVERIFIED,
INACTIVE,
}

View File

@ -0,0 +1,41 @@
/*
* 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.filter
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
class FilterDevicesUseCase @Inject constructor() {
fun execute(
devices: List<DeviceFullInfo>,
filterType: DeviceManagerFilterType,
excludedDeviceIds: List<String> = emptyList(),
): List<DeviceFullInfo> {
return devices
.filter {
when (filterType) {
DeviceManagerFilterType.ALL_SESSIONS -> true
DeviceManagerFilterType.VERIFIED -> it.cryptoDeviceInfo?.isVerified.orFalse()
DeviceManagerFilterType.UNVERIFIED -> !it.cryptoDeviceInfo?.isVerified.orFalse()
DeviceManagerFilterType.INACTIVE -> it.isInactive
}
}
.filter { it.deviceInfo.deviceId !in excludedDeviceIds }
}
}

View File

@ -50,7 +50,7 @@ class OtherSessionsController @Inject constructor(
text(host.stringProvider.getString(R.string.no_result_placeholder))
}
} else {
data.take(NUMBER_OF_OTHER_DEVICES_TO_RENDER).forEach { device ->
data.forEach { device ->
val dateFormatKind = if (device.isInactive) DateFormatKind.TIMELINE_DAY_DIVIDER else DateFormatKind.DEFAULT_DATE_AND_TIME
val formattedLastActivityDate = host.dateFormatter.format(device.deviceInfo.lastSeenTs, dateFormatKind)
val description = if (device.isInactive) {

View File

@ -19,8 +19,13 @@ 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 androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.OnModelBuildFinishedListener
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.databinding.ViewOtherSessionsBinding
@ -32,30 +37,74 @@ class OtherSessionsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback {
interface Callback {
fun onOtherSessionClicked(deviceId: String)
fun onViewAllOtherSessionsClicked()
}
@Inject lateinit var otherSessionsController: OtherSessionsController
private val views: ViewOtherSessionsBinding
private lateinit var recyclerViewDataObserver: RecyclerView.AdapterDataObserver
private lateinit var stateRestorer: LayoutManagerStateRestorer
private var modelBuildListener: OnModelBuildFinishedListener? = null
var callback: Callback? = null
init {
inflate(context, R.layout.view_other_sessions, this)
views = ViewOtherSessionsBinding.bind(this)
configureOtherSessionsRecyclerView()
views.otherSessionsViewAllButton.setOnClickListener {
callback?.onViewAllOtherSessionsClicked()
}
}
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)
private fun configureOtherSessionsRecyclerView() {
views.otherSessionsRecyclerView.configureWith(otherSessionsController, hasFixedSize = false)
val layoutManager = LinearLayoutManager(context)
stateRestorer = LayoutManagerStateRestorer(layoutManager)
views.otherSessionsRecyclerView.layoutManager = layoutManager
layoutManager.recycleChildrenOnDetach = true
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
otherSessionsController.addModelBuildListener(modelBuildListener)
recyclerViewDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
views.otherSessionsRecyclerView.scrollToPosition(0)
}
}
otherSessionsController.adapter.registerAdapterDataObserver(recyclerViewDataObserver)
otherSessionsController.callback = this
}
fun render(devices: List<DeviceFullInfo>, totalNumberOfDevices: Int, showViewAll: Boolean) {
if (showViewAll) {
views.otherSessionsViewAllButton.isVisible = true
views.otherSessionsViewAllButton.text = context.getString(R.string.device_manager_other_sessions_view_all, totalNumberOfDevices)
} else {
views.otherSessionsViewAllButton.isVisible = false
}
otherSessionsController.setData(devices)
}
fun setCallback(callback: OtherSessionsController.Callback) {
otherSessionsController.callback = callback
}
override fun onDetachedFromWindow() {
otherSessionsController.removeModelBuildListener(modelBuildListener)
modelBuildListener = null
otherSessionsController.callback = null
otherSessionsController.adapter.unregisterAdapterDataObserver(recyclerViewDataObserver)
views.otherSessionsRecyclerView.cleanup()
super.onDetachedFromWindow()
}
override fun onItemClicked(deviceId: String) {
callback?.onOtherSessionClicked(deviceId)
}
}

View File

@ -24,6 +24,7 @@ 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.setTextOrHide
import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.databinding.ViewSessionsListHeaderBinding
@ -54,7 +55,7 @@ class SessionsListHeaderView @JvmOverloads constructor(
private fun setTitle(typedArray: TypedArray) {
val title = typedArray.getString(R.styleable.SessionsListHeaderView_sessionsListHeaderTitle)
binding.sessionsListHeaderTitle.text = title
binding.sessionsListHeaderTitle.setTextOrHide(title)
}
private fun setDescription(typedArray: TypedArray) {

View File

@ -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
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
sealed class OtherSessionsAction : VectorViewModelAction {
data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction()
}

View File

@ -0,0 +1,48 @@
/*
* 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.Intent
import android.os.Bundle
import android.view.View
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity
@AndroidEntryPoint
class OtherSessionsActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
if (isFirstCreation()) {
addFragment(
container = views.container,
fragmentClass = OtherSessionsFragment::class.java
)
}
}
companion object {
fun newIntent(context: Context): Intent {
return Intent(context, OtherSessionsActivity::class.java)
}
}
}

View File

@ -0,0 +1,168 @@
/*
* 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.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.filter.DeviceManagerFilterBottomSheet
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.themes.ThemeUtils
import javax.inject.Inject
@AndroidEntryPoint
class OtherSessionsFragment :
VectorBaseFragment<FragmentOtherSessionsBinding>(),
VectorBaseBottomSheetDialogFragment.ResultListener,
OtherSessionsView.Callback {
private val viewModel: OtherSessionsViewModel by fragmentViewModel()
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var viewNavigator: OtherSessionsViewNavigator
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.otherSessionsToolbar).allowBack()
observeViewEvents()
initFilterView()
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is OtherSessionsViewEvents.Loading -> showLoading(it.message)
is OtherSessionsViewEvents.Failure -> showFailure(it.throwable)
}
}
}
private fun initFilterView() {
views.otherSessionsFilterFrameLayout.debouncedClicks {
withState(viewModel) { state ->
DeviceManagerFilterBottomSheet
.newInstance(state.currentFilter, this)
.show(requireActivity().supportFragmentManager, "SHOW_DEVICE_MANAGER_FILTER_BOTTOM_SHEET")
}
}
views.otherSessionsClearFilterButton.debouncedClicks {
viewModel.handle(OtherSessionsAction.FilterDevices(DeviceManagerFilterType.ALL_SESSIONS))
}
views.deviceListOtherSessions.callback = this
}
override fun onBottomSheetResult(resultCode: Int, data: Any?) {
if (resultCode == RESULT_OK && data != null && data is DeviceManagerFilterType) {
viewModel.handle(OtherSessionsAction.FilterDevices(data))
}
}
override fun invalidate() = withState(viewModel) { state ->
if (state.devices is Success) {
renderDevices(state.devices(), state.currentFilter)
}
}
private fun renderDevices(devices: List<DeviceFullInfo>?, 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)
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_verified_sessions_found)
}
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)
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_unverified_sessions_found)
}
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)
)
)
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found)
}
DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ }
}
if (devices.isNullOrEmpty()) {
views.deviceListOtherSessions.isVisible = false
views.otherSessionsNotFoundLayout.isVisible = true
} else {
views.deviceListOtherSessions.isVisible = true
views.otherSessionsNotFoundLayout.isVisible = false
views.deviceListOtherSessions.render(devices = devices, totalNumberOfDevices = devices.size, showViewAll = false)
}
}
override fun onOtherSessionClicked(deviceId: String) {
viewNavigator.navigateToSessionOverview(
context = requireActivity(),
deviceId = deviceId
)
}
override fun onViewAllOtherSessionsClicked() {
// NOOP. We don't have this button in this screen
}
}

View File

@ -0,0 +1,108 @@
/*
* 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 formattedDescription = buildString {
append(description)
append(" ")
append(learnMore)
}
views.recommendationDescriptionTextView.setTextWithColoredPart(
fullText = formattedDescription,
coloredPart = learnMore,
underline = false
) {
onLearnMoreClickListener?.invoke()
}
}
fun render(viewState: OtherSessionsSecurityRecommendationViewState) {
setTitle(viewState.title)
setDescription(viewState.description)
setImageResource(viewState.imageResourceId)
setImageBackgroundTint(viewState.imageTintColorResourceId)
}
}

View File

@ -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,
)

View File

@ -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
import im.vector.app.core.platform.VectorViewEvents
sealed class OtherSessionsViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : OtherSessionsViewEvents()
data class Failure(val throwable: Throwable) : OtherSessionsViewEvents()
}

View File

@ -0,0 +1,81 @@
/*
* 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 com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.features.settings.devices.v2.GetDeviceFullInfoListUseCase
import im.vector.app.features.settings.devices.v2.RefreshDevicesUseCase
import im.vector.app.features.settings.devices.v2.VectorSessionsListViewModel
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import kotlinx.coroutines.Job
class OtherSessionsViewModel @AssistedInject constructor(
@Assisted initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
refreshDevicesUseCase: RefreshDevicesUseCase
) : VectorSessionsListViewModel<OtherSessionsViewState, OtherSessionsAction, OtherSessionsViewEvents>(
initialState, activeSessionHolder, refreshDevicesUseCase
) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> {
override fun create(initialState: OtherSessionsViewState): OtherSessionsViewModel
}
companion object : MavericksViewModelFactory<OtherSessionsViewModel, OtherSessionsViewState> by hiltMavericksViewModelFactory()
private var observeDevicesJob: Job? = null
init {
observeDevices(initialState.currentFilter)
}
private fun observeDevices(currentFilter: DeviceManagerFilterType) {
observeDevicesJob?.cancel()
observeDevicesJob = getDeviceFullInfoListUseCase.execute(
filterType = currentFilter,
excludeCurrentDevice = true
)
.execute { async ->
copy(
devices = async,
)
}
}
override fun handle(action: OtherSessionsAction) {
when (action) {
is OtherSessionsAction.FilterDevices -> handleFilterDevices(action)
}
}
private fun handleFilterDevices(action: OtherSessionsAction.FilterDevices) {
setState {
copy(
currentFilter = action.filterType
)
}
observeDevices(action.filterType)
}
}

View File

@ -0,0 +1,28 @@
/*
* 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 im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import javax.inject.Inject
class OtherSessionsViewNavigator @Inject constructor() {
fun navigateToSessionOverview(context: Context, deviceId: String) {
context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
}
}

View File

@ -0,0 +1,28 @@
/*
* 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 com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
data class OtherSessionsViewState(
val devices: Async<List<DeviceFullInfo>> = Uninitialized,
val currentFilter: DeviceManagerFilterType = DeviceManagerFilterType.ALL_SESSIONS,
) : MavericksState

View File

@ -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="?colorPrimary" />
<stroke
android:width="3dp"
android:color="?vctr_toolbar_background" />
</shape>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingBottom="32dp">
<View
android:layout_width="36dp"
android:layout_height="6dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:background="@drawable/ic_bottom_sheet_handle" />
<TextView
style="@style/TextAppearance.Vector.Subtitle.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/device_manager_filter_bottom_sheet_title" />
<RadioGroup
android:id="@+id/filterOptionsRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layoutDirection="rtl"
android:showDividers="none">
<RadioButton
android:id="@+id/filterOptionAllSessionsRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_all_sessions" />
<RadioButton
android:id="@+id/filterOptionVerifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_verified" />
<TextView
android:id="@+id/filterOptionVerifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_verified_description" />
<RadioButton
android:id="@+id/filterOptionUnverifiedRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_unverified" />
<TextView
android:id="@+id/filterOptionUnverifiedTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/device_manager_filter_option_unverified_description" />
<RadioButton
android:id="@+id/filterOptionInactiveRadioButton"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:minHeight="0dp"
android:text="@string/device_manager_filter_option_inactive" />
<TextView
android:id="@+id/filterOptionInactiveTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
</RadioGroup>
</LinearLayout>

View File

@ -0,0 +1,119 @@
<?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">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/otherSessionsToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_back_24dp"
app:title="@string/device_manager_sessions_other_title">
<FrameLayout
android:id="@+id/otherSessionsFilterFrameLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:padding="8dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/a11y_device_manager_filter"
android:src="@drawable/ic_filter" />
<ImageView
android:id="@+id/otherSessionsFilterBadgeImageView"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginStart="12dp"
android:importantForAccessibility="no"
android:src="@drawable/circle_with_transparent_border" />
</FrameLayout>
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<im.vector.app.features.settings.devices.v2.list.SessionsListHeaderView
android:id="@+id/deviceListHeaderOtherSessions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderTitle=""
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsSecurityRecommendationView
android:id="@+id/otherSessionsSecurityRecommendationView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListHeaderOtherSessions"
app:otherSessionsRecommendationDescription="@string/device_manager_other_sessions_recommendation_description_unverified"
app:otherSessionsRecommendationImageBackgroundTint="@color/shield_color_warning_background"
app:otherSessionsRecommendationImageResource="@drawable/ic_shield_warning_no_border"
app:otherSessionsRecommendationTitle="@string/device_manager_other_sessions_recommendation_title_unverified"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/otherSessionsNotFoundLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView">
<TextView
android:id="@+id/otherSessionsNotFoundTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/device_manager_other_sessions_no_verified_sessions_found" />
<Button
android:id="@+id/otherSessionsClearFilterButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="start"
android:padding="0dp"
android:text="@string/device_manager_other_sessions_clear_filter" />
</LinearLayout>
<im.vector.app.features.settings.devices.v2.list.OtherSessionsView
android:id="@+id/deviceListOtherSessions"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherSessionsSecurityRecommendationView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -90,8 +90,8 @@
android:id="@+id/deviceListHeaderOtherSessions"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:sessionsListHeaderDescription="@string/settings_sessions_other_description"
app:sessionsListHeaderTitle="@string/settings_sessions_other_title"
app:sessionsListHeaderDescription="@string/device_manager_sessions_other_description"
app:sessionsListHeaderTitle="@string/device_manager_sessions_other_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/deviceListDividerCurrentSession" />

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<ImageView
android:id="@+id/recommendationShieldImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/bg_security_recommendation_shield"
android:importantForAccessibility="no"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:backgroundTint="@color/shield_color_warning_background"
tools:src="@drawable/ic_shield_warning_no_border" />
<TextView
android:id="@+id/recommendationTitleTextView"
style="@style/TextAppearance.Vector.Subtitle.Medium.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/recommendationShieldImageView"
app:layout_constraintTop_toTopOf="@id/recommendationShieldImageView"
app:layout_constraintBottom_toBottomOf="@id/recommendationShieldImageView"
tools:text="@string/device_manager_other_sessions_recommendation_title_unverified" />
<TextView
android:id="@+id/recommendationDescriptionTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="40dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/recommendationTitleTextView"
app:layout_constraintTop_toBottomOf="@id/recommendationTitleTextView"
tools:text="@string/device_manager_other_sessions_recommendation_description_unverified" />
</merge>

View File

@ -23,10 +23,10 @@
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/layout_horizontal_margin"
android:layout_marginTop="18.5dp"
android:layout_marginEnd="40dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/sessions_list_header_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessions_list_header_title"
tools:text="For best security, verify your sessions and sign out from any session that you dont recognize or use anymore. Learn More." />
</merge>

View File

@ -52,8 +52,8 @@ class DevicesViewModelTest {
fakeActiveSessionHolder.instance,
getCurrentSessionCrossSigningInfoUseCase,
getDeviceFullInfoListUseCase,
refreshDevicesUseCase,
refreshDevicesOnCryptoDevicesChangeUseCase,
refreshDevicesUseCase,
)
}
@ -181,7 +181,7 @@ class DevicesViewModelTest {
)
val deviceFullInfoList = listOf(deviceFullInfo1, deviceFullInfo2)
val deviceFullInfoListFlow = flowOf(deviceFullInfoList)
every { getDeviceFullInfoListUseCase.execute() } returns deviceFullInfoListFlow
every { getDeviceFullInfoListUseCase.execute(any(), any()) } returns deviceFullInfoListFlow
return deviceFullInfoList
}

View File

@ -16,6 +16,8 @@
package im.vector.app.features.settings.devices.v2
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
import im.vector.app.features.settings.devices.v2.filter.FilterDevicesUseCase
import im.vector.app.features.settings.devices.v2.list.CheckIfSessionIsInactiveUseCase
import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.test
@ -47,12 +49,14 @@ class GetDeviceFullInfoListUseCaseTest {
private val checkIfSessionIsInactiveUseCase = mockk<CheckIfSessionIsInactiveUseCase>()
private val getEncryptionTrustLevelForDeviceUseCase = mockk<GetEncryptionTrustLevelForDeviceUseCase>()
private val getCurrentSessionCrossSigningInfoUseCase = mockk<GetCurrentSessionCrossSigningInfoUseCase>()
private val filterDevicesUseCase = mockk<FilterDevicesUseCase>()
private val getDeviceFullInfoListUseCase = GetDeviceFullInfoListUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance,
checkIfSessionIsInactiveUseCase = checkIfSessionIsInactiveUseCase,
getEncryptionTrustLevelForDeviceUseCase = getEncryptionTrustLevelForDeviceUseCase,
getCurrentSessionCrossSigningInfoUseCase = getCurrentSessionCrossSigningInfoUseCase,
filterDevicesUseCase = filterDevicesUseCase,
)
@Before
@ -117,9 +121,10 @@ class GetDeviceFullInfoListUseCaseTest {
isInactive = false
)
val expectedResult = listOf(expectedResult3, expectedResult2, expectedResult1)
every { filterDevicesUseCase.execute(any(), any()) } returns expectedResult
// When
val result = getDeviceFullInfoListUseCase.execute()
val result = getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.ALL_SESSIONS, excludeCurrentDevice = false)
.test(this)
// Then
@ -144,7 +149,7 @@ class GetDeviceFullInfoListUseCaseTest {
fakeActiveSessionHolder.givenGetSafeActiveSessionReturns(null)
// When
val result = getDeviceFullInfoListUseCase.execute()
val result = getDeviceFullInfoListUseCase.execute(DeviceManagerFilterType.ALL_SESSIONS, excludeCurrentDevice = false)
.test(this)
// Then

View File

@ -17,6 +17,7 @@
package im.vector.app.features.settings.devices.v2
import android.content.Intent
import im.vector.app.features.settings.devices.v2.othersessions.OtherSessionsActivity
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import im.vector.app.test.fakes.FakeContext
import io.mockk.every
@ -38,6 +39,7 @@ class VectorSettingsDevicesViewNavigatorTest {
@Before
fun setUp() {
mockkObject(SessionOverviewActivity.Companion)
mockkObject(OtherSessionsActivity.Companion)
}
@After
@ -57,9 +59,27 @@ class VectorSettingsDevicesViewNavigatorTest {
}
}
@Test
fun `given an intent when navigating to other sessions list then it starts the correct activity`() {
val intent = givenIntentForOtherSessions()
context.givenStartActivity(intent)
vectorSettingsDevicesViewNavigator.navigateToOtherSessions(context.instance)
verify {
context.instance.startActivity(intent)
}
}
private fun givenIntentForSessionOverview(sessionId: String): Intent {
val intent = mockk<Intent>()
every { SessionOverviewActivity.newIntent(context.instance, sessionId) } returns intent
return intent
}
private fun givenIntentForOtherSessions(): Intent {
val intent = mockk<Intent>()
every { OtherSessionsActivity.newIntent(context.instance) } returns intent
return intent
}
}

View File

@ -0,0 +1,110 @@
/*
* 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.filter
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.shouldContainAll
import org.junit.Test
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
private val activeVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_VERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "ACTIVE_VERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = false
)
private val inactiveVerifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_VERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "INACTIVE_VERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = true, locallyVerified = true)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Trusted,
isInactive = true
)
private val activeUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "ACTIVE_UNVERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "ACTIVE_UNVERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = false
)
private val inactiveUnverifiedDevice = DeviceFullInfo(
deviceInfo = DeviceInfo(deviceId = "INACTIVE_UNVERIFIED_DEVICE"),
cryptoDeviceInfo = CryptoDeviceInfo(
userId = "USER_ID_1",
deviceId = "INACTIVE_UNVERIFIED_DEVICE",
trustLevel = DeviceTrustLevel(crossSigningVerified = false, locallyVerified = false)
),
roomEncryptionTrustLevel = RoomEncryptionTrustLevel.Warning,
isInactive = true
)
private val devices = listOf(
activeVerifiedDevice,
inactiveVerifiedDevice,
activeUnverifiedDevice,
inactiveUnverifiedDevice,
)
class FilterDevicesUseCaseTest {
private val filterDevicesUseCase = FilterDevicesUseCase()
@Test
fun `given a device list when filter type is ALL_SESSIONS then returns the same list`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.ALL_SESSIONS, emptyList())
filteredDeviceList.size shouldBeEqualTo devices.size
}
@Test
fun `given a device list when filter type is VERIFIED then returns only verified devices`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.VERIFIED, emptyList())
filteredDeviceList.size shouldBeEqualTo 2
filteredDeviceList shouldContainAll listOf(activeVerifiedDevice, inactiveVerifiedDevice)
}
@Test
fun `given a device list when filter type is UNVERIFIED then returns only unverified devices`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.UNVERIFIED, emptyList())
filteredDeviceList.size shouldBeEqualTo 2
filteredDeviceList shouldContainAll listOf(activeUnverifiedDevice, inactiveUnverifiedDevice)
}
@Test
fun `given a device list when filter type is INACTIVE then returns only inactive devices`() {
val filteredDeviceList = filterDevicesUseCase.execute(devices, DeviceManagerFilterType.INACTIVE, emptyList())
filteredDeviceList.size shouldBeEqualTo 2
filteredDeviceList shouldContainAll listOf(inactiveVerifiedDevice, inactiveUnverifiedDevice)
}
}

View File

@ -0,0 +1,65 @@
/*
* 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.Intent
import im.vector.app.features.settings.devices.v2.overview.SessionOverviewActivity
import im.vector.app.test.fakes.FakeContext
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.unmockkAll
import io.mockk.verify
import org.junit.After
import org.junit.Before
import org.junit.Test
private const val A_DEVICE_ID = "A_DEVICE_ID"
class OtherSessionsViewNavigatorTest {
private val context = FakeContext()
private val otherSessionsViewNavigator = OtherSessionsViewNavigator()
@Before
fun setUp() {
mockkObject(SessionOverviewActivity)
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a device id when navigating to overview then it starts the correct activity`() {
val intent = givenIntentForDeviceOverview(A_DEVICE_ID)
context.givenStartActivity(intent)
otherSessionsViewNavigator.navigateToSessionOverview(context.instance, A_DEVICE_ID)
verify {
context.instance.startActivity(intent)
}
}
private fun givenIntentForDeviceOverview(deviceId: String): Intent {
val intent = mockk<Intent>()
every { SessionOverviewActivity.newIntent(context.instance, deviceId) } returns intent
return intent
}
}