summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2024-05-29 17:18:29 +0200
committerDavid Göransson <david.goransson@mullvad.net>2024-05-29 17:18:29 +0200
commitad90145a5d86d8c1e6e70f2238f11edf5e50f8d8 (patch)
tree9d085bc81caed9409e3a4360490c06c2da4fbba8 /android/app/src
parent8e14a8d4287af66a57a98db79d3ac320c2dad4a1 (diff)
parent767b97eda756f4ec4e67fb5fa2ae664277291e8f (diff)
downloadmullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.tar.xz
mullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.zip
Merge branch 'android-grpc'
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt117
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt14
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt16
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt27
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt23
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt47
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt160
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt2
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt10
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt12
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt77
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt2
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt8
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt20
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt18
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt3
-rw-r--r--android/app/src/main/AndroidManifest.xml23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/MullvadApplication.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomListCell.kt31
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomPortCell.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DropdownMenuCell.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterCell.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt88
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListSuccess.kt36
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/DnsDialogResult.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CustomListNameTextField.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Text.kt46
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt27
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt48
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt60
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RemoveDeviceConfirmationDialog.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt38
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardPortInfoDialog.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialog.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/SnackbarHostExtensions.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DeviceListPreviewParameterProvider.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewData.kt29
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewParameterProvider.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt80
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt38
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt84
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt38
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt28
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt329
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt49
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt210
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt25
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt53
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt86
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CreateCustomListUiState.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListState.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterState.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/PreviewData.kt81
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PathConstant.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt99
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/provider/MullvadFileProvider.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Provider.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt86
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt82
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt216
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt31
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AccountRepository.kt83
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt115
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/DeviceRepository.kt129
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepository.kt39
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt53
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt113
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplitTunnelingRepository.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt75
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoCache.kt55
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AuthTokenCache.kt38
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt143
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/CustomDns.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/EmptyServiceConnection.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt61
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt90
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSource.kt56
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt150
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt75
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt48
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VoucherRedeemer.kt41
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VpnPermission.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCase.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AvailableProvidersUseCase.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt32
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/LastKnownLocationUseCase.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceNotificationUseCase.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PortRangeUseCase.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListFilterUseCase.kt44
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListUseCase.kt90
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationTitleUseCase.kt56
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt29
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt183
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListRelayItemsUseCase.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsRelayItemUseCase.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt33
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortConstraintExtensions.kt)14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortRangeExtensions.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/StringExtensions.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt46
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt232
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt42
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt115
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt43
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt141
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt37
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt33
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt41
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt147
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt59
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/NoDaemonViewModel.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt75
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModel.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt152
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt39
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt42
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt82
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt64
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt142
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt88
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt256
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepositoryTest.kt323
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepositoryTest.kt174
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt82
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt70
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt14
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt178
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt23
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt71
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt35
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt52
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt52
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt25
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt210
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt53
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt159
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModelTest.kt14
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt10
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt101
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt34
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt35
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt85
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt93
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt79
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModelTest.kt12
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt178
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt37
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt69
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt106
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt71
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt68
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt81
219 files changed, 4282 insertions, 5938 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt
index 2adfa22220..0218e06afd 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/data/DummyRelayItems.kt
@@ -1,61 +1,108 @@
package net.mullvad.mullvadvpn.compose.data
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.model.RelayEndpointData
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.RelayListCity
-import net.mullvad.mullvadvpn.model.RelayListCountry
-import net.mullvad.mullvadvpn.model.WireguardEndpointData
-import net.mullvad.mullvadvpn.model.WireguardRelayEndpointData
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.relaylist.toRelayCountries
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.ProviderId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayList
+import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData
private val DUMMY_RELAY_1 =
- net.mullvad.mullvadvpn.model.Relay(
- hostname = "Relay host 1",
+ RelayItem.Location.Relay(
+ id =
+ GeoLocationId.Hostname(
+ city = GeoLocationId.City(GeoLocationId.Country("RCo1"), "Relay City 1"),
+ "Relay host 1"
+ ),
active = true,
- endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData),
- owned = true,
- provider = "PROVIDER"
+ provider =
+ Provider(
+ providerId = ProviderId("PROVIDER RENTED"),
+ ownership = Ownership.Rented,
+ )
)
private val DUMMY_RELAY_2 =
- net.mullvad.mullvadvpn.model.Relay(
- hostname = "Relay host 2",
+ RelayItem.Location.Relay(
+ id =
+ GeoLocationId.Hostname(
+ city = GeoLocationId.City(GeoLocationId.Country("RCo2"), "Relay City 2"),
+ "Relay host 2"
+ ),
active = true,
- endpointData = RelayEndpointData.Wireguard(WireguardRelayEndpointData),
- owned = true,
- provider = "PROVIDER"
+ provider =
+ Provider(providerId = ProviderId("PROVIDER OWNED"), ownership = Ownership.MullvadOwned)
+ )
+private val DUMMY_RELAY_CITY_1 =
+ RelayItem.Location.City(
+ name = "Relay City 1",
+ id = GeoLocationId.City(countryCode = GeoLocationId.Country("RCo1"), cityCode = "RCi1"),
+ relays = listOf(DUMMY_RELAY_1),
+ expanded = false
+ )
+private val DUMMY_RELAY_CITY_2 =
+ RelayItem.Location.City(
+ name = "Relay City 2",
+ id = GeoLocationId.City(countryCode = GeoLocationId.Country("RCo2"), cityCode = "RCi2"),
+ relays = listOf(DUMMY_RELAY_2),
+ expanded = false
)
-private val DUMMY_RELAY_CITY_1 = RelayListCity("Relay City 1", "RCi1", arrayListOf(DUMMY_RELAY_1))
-private val DUMMY_RELAY_CITY_2 = RelayListCity("Relay City 2", "RCi2", arrayListOf(DUMMY_RELAY_2))
private val DUMMY_RELAY_COUNTRY_1 =
- RelayListCountry("Relay Country 1", "RCo1", arrayListOf(DUMMY_RELAY_CITY_1))
+ RelayItem.Location.Country(
+ name = "Relay Country 1",
+ id = GeoLocationId.Country("RCo1"),
+ expanded = false,
+ cities = listOf(DUMMY_RELAY_CITY_1)
+ )
private val DUMMY_RELAY_COUNTRY_2 =
- RelayListCountry("Relay Country 2", "RCo2", arrayListOf(DUMMY_RELAY_CITY_2))
+ RelayItem.Location.Country(
+ name = "Relay Country 2",
+ id = GeoLocationId.Country("RCo2"),
+ expanded = false,
+ cities = listOf(DUMMY_RELAY_CITY_2)
+ )
private val DUMMY_WIREGUARD_PORT_RANGES = ArrayList<PortRange>()
private val DUMMY_WIREGUARD_ENDPOINT_DATA = WireguardEndpointData(DUMMY_WIREGUARD_PORT_RANGES)
-val DUMMY_RELAY_COUNTRIES =
+val DUMMY_RELAY_COUNTRIES = listOf(DUMMY_RELAY_COUNTRY_1, DUMMY_RELAY_COUNTRY_2)
+
+val DUMMY_RELAY_LIST =
RelayList(
- arrayListOf(DUMMY_RELAY_COUNTRY_1, DUMMY_RELAY_COUNTRY_2),
- DUMMY_WIREGUARD_ENDPOINT_DATA,
- )
- .toRelayCountries()
+ DUMMY_RELAY_COUNTRIES,
+ DUMMY_WIREGUARD_ENDPOINT_DATA,
+ )
-val DUMMY_CUSTOM_LISTS =
+val DUMMY_RELAY_ITEM_CUSTOM_LISTS =
listOf(
RelayItem.CustomList(
- CustomListName.fromString("First list"),
- false,
- "1",
+ customListName = CustomListName.fromString("First list"),
+ expanded = false,
+ id = CustomListId("1"),
locations = DUMMY_RELAY_COUNTRIES
),
RelayItem.CustomList(
- CustomListName.fromString("Empty list"),
+ customListName = CustomListName.fromString("Empty list"),
expanded = false,
- "2",
+ id = CustomListId("2"),
+ locations = emptyList()
+ )
+ )
+
+val DUMMY_CUSTOM_LISTS =
+ listOf(
+ CustomList(
+ name = CustomListName.fromString("First list"),
+ id = CustomListId("1"),
+ locations = DUMMY_RELAY_COUNTRIES.map { it.id }
+ ),
+ CustomList(
+ name = CustomListName.fromString("Empty list"),
+ id = CustomListId("2"),
locations = emptyList()
)
)
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt
index baeb5902d7..915db82438 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialogTest.kt
@@ -12,7 +12,9 @@ import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState
import net.mullvad.mullvadvpn.compose.test.CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.UnknownCustomListError
+import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -44,7 +46,10 @@ class CreateCustomListDialogTest {
fun givenCustomListExistsShouldShowCustomListExitsErrorText() =
composeExtension.use {
// Arrange
- val state = CreateCustomListUiState(error = CustomListsError.CustomListExists)
+ val state =
+ CreateCustomListUiState(
+ error = CreateWithLocationsError.Create(CustomListAlreadyExists)
+ )
setContentWithTheme { CreateCustomListDialog(state = state) }
// Assert
@@ -56,7 +61,10 @@ class CreateCustomListDialogTest {
fun givenOtherCustomListErrorShouldShowAnErrorOccurredErrorText() =
composeExtension.use {
// Arrange
- val state = CreateCustomListUiState(error = CustomListsError.OtherError)
+ val state =
+ CreateCustomListUiState(
+ error = CreateWithLocationsError.Create(UnknownCustomListError(Throwable()))
+ )
setContentWithTheme { CreateCustomListDialog(state = state) }
// Assert
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt
index f9c7ec2143..bcb3908fae 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/CustomPortDialogTest.kt
@@ -9,7 +9,8 @@ import io.mockk.MockKAnnotations
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
-import net.mullvad.mullvadvpn.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.onNodeWithTagAndText
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -29,9 +30,9 @@ class CustomPortDialogTest {
@SuppressLint("ComposableNaming")
@Composable
private fun testWireguardCustomPortDialog(
- initialPort: Int? = null,
+ initialPort: Port? = null,
allowedPortRanges: List<PortRange> = emptyList(),
- onSave: (Int?) -> Unit = { _ -> },
+ onSave: (Port?) -> Unit = { _ -> },
onDismiss: () -> Unit = {},
) {
@@ -47,21 +48,20 @@ class CustomPortDialogTest {
fun testShowWireguardCustomPortDialogInvalidInt() =
composeExtension.use {
// Input a number to make sure that a too long number does not show and it does not
- // crash
- // the app
+ // crash the app
// Arrange
setContentWithTheme { testWireguardCustomPortDialog() }
// Act
- onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).performTextInput(invalidCustomPort)
+ onNodeWithTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).performTextInput(INVALID_CUSTOM_PORT)
// Assert
- onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, invalidCustomPort)
+ onNodeWithTagAndText(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG, INVALID_CUSTOM_PORT)
.assertDoesNotExist()
}
companion object {
- const val invalidCustomPort = "21474836471"
+ const val INVALID_CUSTOM_PORT = "21474836471"
}
}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt
index e79c5a2fe7..ee347c246a 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt
@@ -8,6 +8,8 @@ import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
+import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState
+import net.mullvad.mullvadvpn.lib.model.CustomListName
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -27,8 +29,13 @@ class DeleteCustomListConfirmationDialogTest {
fun givenNameShouldShowDeleteNameTitle() =
composeExtension.use {
// Arrange
- val name = "List should be deleted"
- setContentWithTheme { DeleteCustomListConfirmationDialog(name = name) }
+ val name = CustomListName.fromString("List should be deleted")
+ setContentWithTheme {
+ DeleteCustomListConfirmationDialog(
+ name = name,
+ state = DeleteCustomListUiState(null)
+ )
+ }
// Assert
onNodeWithText(DELETE_TITLE.format(name)).assertExists()
@@ -38,10 +45,14 @@ class DeleteCustomListConfirmationDialogTest {
fun whenDeleteIsClickedShouldCallOnDelete() =
composeExtension.use {
// Arrange
- val name = "List should be deleted"
+ val name = CustomListName.fromString("List should be deleted")
val mockedOnDelete: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
- DeleteCustomListConfirmationDialog(name = name, onDelete = mockedOnDelete)
+ DeleteCustomListConfirmationDialog(
+ name = name,
+ state = DeleteCustomListUiState(null),
+ onDelete = mockedOnDelete
+ )
}
// Act
@@ -55,10 +66,14 @@ class DeleteCustomListConfirmationDialogTest {
fun whenCancelIsClickedShouldCallOnBack() =
composeExtension.use {
// Arrange
- val name = "List should be deleted"
+ val name = CustomListName.fromString("List should be deleted")
val mockedOnBack: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
- DeleteCustomListConfirmationDialog(name = name, onBack = mockedOnBack)
+ DeleteCustomListConfirmationDialog(
+ name = name,
+ state = DeleteCustomListUiState(null),
+ onBack = mockedOnBack
+ )
}
// Act
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt
index cbd6ae09d7..3128bbc508 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt
@@ -10,9 +10,11 @@ import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
-import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState
+import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState
import net.mullvad.mullvadvpn.compose.test.EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.UnknownCustomListError
+import net.mullvad.mullvadvpn.usecase.customlists.RenameError
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -32,7 +34,7 @@ class EditCustomListNameDialogTest {
fun givenNoErrorShouldShowNoErrorMessage() =
composeExtension.use {
// Arrange
- val state = UpdateCustomListUiState(error = null)
+ val state = EditCustomListNameUiState(error = null)
setContentWithTheme { EditCustomListNameDialog(state = state) }
// Assert
@@ -44,7 +46,7 @@ class EditCustomListNameDialogTest {
fun givenCustomListExistsShouldShowCustomListExitsErrorText() =
composeExtension.use {
// Arrange
- val state = UpdateCustomListUiState(error = CustomListsError.CustomListExists)
+ val state = EditCustomListNameUiState(error = RenameError(NameAlreadyExists("name")))
setContentWithTheme { EditCustomListNameDialog(state = state) }
// Assert
@@ -56,7 +58,10 @@ class EditCustomListNameDialogTest {
fun givenOtherCustomListErrorShouldShowAnErrorOccurredErrorText() =
composeExtension.use {
// Arrange
- val state = UpdateCustomListUiState(error = CustomListsError.OtherError)
+ val state =
+ EditCustomListNameUiState(
+ error = RenameError(UnknownCustomListError(RuntimeException("")))
+ )
setContentWithTheme { EditCustomListNameDialog(state = state) }
// Assert
@@ -69,7 +74,7 @@ class EditCustomListNameDialogTest {
composeExtension.use {
// Arrange
val mockedOnDismiss: () -> Unit = mockk(relaxed = true)
- val state = UpdateCustomListUiState()
+ val state = EditCustomListNameUiState()
setContentWithTheme {
EditCustomListNameDialog(state = state, onDismiss = mockedOnDismiss)
}
@@ -86,7 +91,7 @@ class EditCustomListNameDialogTest {
composeExtension.use {
// Arrange
val mockedUpdateName: (String) -> Unit = mockk(relaxed = true)
- val state = UpdateCustomListUiState()
+ val state = EditCustomListNameUiState()
setContentWithTheme {
EditCustomListNameDialog(state = state, updateName = mockedUpdateName)
}
@@ -104,7 +109,7 @@ class EditCustomListNameDialogTest {
// Arrange
val mockedUpdateName: (String) -> Unit = mockk(relaxed = true)
val inputText = "NEW NAME"
- val state = UpdateCustomListUiState()
+ val state = EditCustomListNameUiState()
setContentWithTheme {
EditCustomListNameDialog(state = state, updateName = mockedUpdateName)
}
@@ -123,7 +128,7 @@ class EditCustomListNameDialogTest {
// Arrange
val mockedOnInputChanged: () -> Unit = mockk(relaxed = true)
val inputText = "NEW NAME"
- val state = UpdateCustomListUiState()
+ val state = EditCustomListNameUiState()
setContentWithTheme {
EditCustomListNameDialog(state = state, onInputChanged = mockedOnInputChanged)
}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt
index 0641998f9b..5fe812cd44 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt
@@ -7,12 +7,12 @@ import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextInput
import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
+import net.mullvad.mullvadvpn.viewmodel.MtuDialogUiState
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -31,13 +31,17 @@ class MtuDialogTest {
@SuppressLint("ComposableNaming")
@Composable
private fun testMtuDialog(
- mtuInitial: Int? = null,
- onSaveMtu: (Int) -> Unit = { _ -> },
+ mtuInput: String = "",
+ isValidInput: Boolean = true,
+ showResetButton: Boolean = true,
+ onInputChanged: (String) -> Unit = { _ -> },
+ onSaveMtu: (String) -> Unit = { _ -> },
onResetMtu: () -> Unit = {},
onDismiss: () -> Unit = {},
) {
MtuDialog(
- mtuInitial = mtuInitial,
+ MtuDialogUiState(mtuInput, isValidInput, showResetButton),
+ onInputChanged = onInputChanged,
onSaveMtu = onSaveMtu,
onResetMtu = onResetMtu,
onDismiss = onDismiss
@@ -60,36 +64,19 @@ class MtuDialogTest {
// Arrange
setContentWithTheme {
testMtuDialog(
- mtuInitial = VALID_DUMMY_MTU_VALUE,
+ mtuInput = VALID_DUMMY_MTU_VALUE,
)
}
// Assert
- onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists()
- }
-
- @Test
- fun testMtuDialogTextInput() =
- composeExtension.use {
- // Arrange
- setContentWithTheme {
- testMtuDialog(
- null,
- )
- }
-
- // Act
- onNodeWithText(EMPTY_STRING).performTextInput(VALID_DUMMY_MTU_VALUE.toString())
-
- // Assert
- onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists()
+ onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists()
}
@Test
fun testMtuDialogSubmitOfValidValue() =
composeExtension.use {
// Arrange
- val mockedSubmitHandler: (Int) -> Unit = mockk(relaxed = true)
+ val mockedSubmitHandler: (String) -> Unit = mockk(relaxed = true)
setContentWithTheme {
testMtuDialog(
VALID_DUMMY_MTU_VALUE,
@@ -108,11 +95,7 @@ class MtuDialogTest {
fun testMtuDialogSubmitButtonDisabledWhenInvalidInput() =
composeExtension.use {
// Arrange
- setContentWithTheme {
- testMtuDialog(
- INVALID_DUMMY_MTU_VALUE,
- )
- }
+ setContentWithTheme { testMtuDialog(INVALID_DUMMY_MTU_VALUE, false) }
// Assert
onNodeWithText("Submit").assertIsNotEnabled()
@@ -125,7 +108,7 @@ class MtuDialogTest {
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
testMtuDialog(
- mtuInitial = VALID_DUMMY_MTU_VALUE,
+ mtuInput = VALID_DUMMY_MTU_VALUE,
onResetMtu = mockedClickHandler,
)
}
@@ -157,7 +140,7 @@ class MtuDialogTest {
companion object {
private const val EMPTY_STRING = ""
- private const val VALID_DUMMY_MTU_VALUE = 1337
- private const val INVALID_DUMMY_MTU_VALUE = 1111
+ private const val VALID_DUMMY_MTU_VALUE = "1337"
+ private const val INVALID_DUMMY_MTU_VALUE = "1111"
}
}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
index 851866818b..98c87114fb 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
@@ -20,16 +20,15 @@ import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.TOP_BAR_ACCOUNT_BUTTON
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.TransportProtocol
+import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.ui.VersionInfo
-import net.mullvad.talpid.net.TransportProtocol
-import net.mullvad.talpid.net.TunnelEndpoint
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.tunnel.ErrorStateCause
import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -78,9 +77,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -112,10 +110,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState =
- TunnelState.Connecting(endpoint = mockTunnelEndpoint, null),
- tunnelRealState =
+ selectedRelayItemTitle = null,
+ tunnelState =
TunnelState.Connecting(endpoint = mockTunnelEndpoint, null),
inAddress = null,
outAddress = "",
@@ -147,9 +143,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connected(mockTunnelEndpoint, null),
- tunnelRealState = TunnelState.Connected(mockTunnelEndpoint, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connected(mockTunnelEndpoint, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -179,9 +174,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connected(mockTunnelEndpoint, null),
- tunnelRealState = TunnelState.Connected(mockTunnelEndpoint, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connected(mockTunnelEndpoint, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -204,19 +198,14 @@ class ConnectScreenTest {
fun testDisconnectingState() {
composeExtension.use {
// Arrange
- val mockSelectedLocation: RelayItem = mockk(relaxed = true)
val mockLocationName = "Home"
- every { mockSelectedLocation.locationName } returns mockLocationName
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = mockSelectedLocation,
- tunnelUiState =
- TunnelState.Disconnecting(ActionAfterDisconnect.Nothing),
- tunnelRealState =
- TunnelState.Disconnecting(ActionAfterDisconnect.Nothing),
+ selectedRelayItemTitle = mockLocationName,
+ tunnelState = TunnelState.Disconnecting(ActionAfterDisconnect.Nothing),
inAddress = null,
outAddress = "",
showLocation = true,
@@ -239,17 +228,14 @@ class ConnectScreenTest {
fun testDisconnectedState() {
composeExtension.use {
// Arrange
- val mockSelectedLocation: RelayItem = mockk(relaxed = true)
val mockLocationName = "Home"
- every { mockSelectedLocation.locationName } returns mockLocationName
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = mockSelectedLocation,
- tunnelUiState = TunnelState.Disconnected(),
- tunnelRealState = TunnelState.Disconnected(),
+ selectedRelayItemTitle = mockLocationName,
+ tunnelState = TunnelState.Disconnected(),
inAddress = null,
outAddress = "",
showLocation = true,
@@ -272,20 +258,14 @@ class ConnectScreenTest {
fun testErrorStateBlocked() {
composeExtension.use {
// Arrange
- val mockSelectedLocation: RelayItem = mockk(relaxed = true)
val mockLocationName = "Home"
- every { mockSelectedLocation.locationName } returns mockLocationName
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = mockSelectedLocation,
- tunnelUiState =
- TunnelState.Error(
- ErrorState(ErrorStateCause.StartTunnelError, true)
- ),
- tunnelRealState =
+ selectedRelayItemTitle = mockLocationName,
+ tunnelState =
TunnelState.Error(
ErrorState(ErrorStateCause.StartTunnelError, true)
),
@@ -315,20 +295,14 @@ class ConnectScreenTest {
fun testErrorStateNotBlocked() {
composeExtension.use {
// Arrange
- val mockSelectedLocation: RelayItem = mockk(relaxed = true)
val mockLocationName = "Home"
- every { mockSelectedLocation.locationName } returns mockLocationName
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = mockSelectedLocation,
- tunnelUiState =
- TunnelState.Error(
- ErrorState(ErrorStateCause.StartTunnelError, false)
- ),
- tunnelRealState =
+ selectedRelayItemTitle = mockLocationName,
+ tunnelState =
TunnelState.Error(
ErrorState(ErrorStateCause.StartTunnelError, false)
),
@@ -364,10 +338,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState =
- TunnelState.Disconnecting(ActionAfterDisconnect.Reconnect),
- tunnelRealState =
+ selectedRelayItemTitle = null,
+ tunnelState =
TunnelState.Disconnecting(ActionAfterDisconnect.Reconnect),
inAddress = null,
outAddress = "",
@@ -393,18 +365,14 @@ class ConnectScreenTest {
fun testDisconnectingBlockState() {
composeExtension.use {
// Arrange
- val mockSelectedLocation: RelayItem = mockk(relaxed = true)
val mockLocationName = "Home"
- every { mockSelectedLocation.locationName } returns mockLocationName
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = mockSelectedLocation,
- tunnelUiState = TunnelState.Disconnecting(ActionAfterDisconnect.Block),
- tunnelRealState =
- TunnelState.Disconnecting(ActionAfterDisconnect.Block),
+ selectedRelayItemTitle = mockLocationName,
+ tunnelState = TunnelState.Disconnecting(ActionAfterDisconnect.Block),
inAddress = null,
outAddress = "",
showLocation = true,
@@ -428,18 +396,15 @@ class ConnectScreenTest {
fun testClickSelectLocationButton() {
composeExtension.use {
// Arrange
- val mockSelectedLocation: RelayItem = mockk(relaxed = true)
val mockLocationName = "Home"
- every { mockSelectedLocation.name } returns mockLocationName
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = mockSelectedLocation,
- tunnelUiState = TunnelState.Disconnected(),
- tunnelRealState = TunnelState.Disconnected(),
+ selectedRelayItemTitle = mockLocationName,
+ tunnelState = TunnelState.Disconnected(),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -471,9 +436,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connected(mockTunnelEndpoint, null),
- tunnelRealState = TunnelState.Connected(mockTunnelEndpoint, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connected(mockTunnelEndpoint, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -505,9 +469,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connected(mockTunnelEndpoint, null),
- tunnelRealState = TunnelState.Connected(mockTunnelEndpoint, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connected(mockTunnelEndpoint, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -538,9 +501,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Disconnected(),
- tunnelRealState = TunnelState.Disconnected(),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Disconnected(),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -571,9 +533,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -612,9 +573,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = mockLocation,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connected(mockTunnelEndpoint, null),
- tunnelRealState = TunnelState.Connected(mockTunnelEndpoint, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connected(mockTunnelEndpoint, null),
inAddress = mockInAddress,
outAddress = mockOutAddress,
showLocation = false,
@@ -644,18 +604,16 @@ class ConnectScreenTest {
val versionInfo =
VersionInfo(
currentVersion = "1.0",
- upgradeVersion = "1.1",
- isOutdated = true,
- isSupported = true
+ isSupported = true,
+ suggestedUpgradeVersion = "1.1"
)
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -680,18 +638,16 @@ class ConnectScreenTest {
val versionInfo =
VersionInfo(
currentVersion = "1.0",
- upgradeVersion = "1.1",
- isOutdated = true,
- isSupported = false
+ isSupported = false,
+ suggestedUpgradeVersion = "1.1"
)
setContentWithTheme {
ConnectScreen(
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -722,9 +678,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -749,10 +704,9 @@ class ConnectScreenTest {
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
val versionInfo =
VersionInfo(
- currentVersion = "1.0",
- upgradeVersion = "1.1",
- isOutdated = true,
- isSupported = false
+ isSupported = false,
+ currentVersion = "",
+ suggestedUpgradeVersion = "1.1"
)
setContentWithTheme {
ConnectScreen(
@@ -760,9 +714,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
@@ -794,9 +747,8 @@ class ConnectScreenTest {
state =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Connecting(null, null),
- tunnelRealState = TunnelState.Connecting(null, null),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Connecting(null, null),
inAddress = null,
outAddress = "",
showLocation = false,
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
index 5951550550..4f4db0a529 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
@@ -14,7 +14,7 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.SAVE_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt
index da9ed60997..bdcb796997 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreenTest.kt
@@ -14,7 +14,7 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.NEW_LIST_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.CustomList
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -56,8 +56,8 @@ class CustomListsScreenTest {
}
// Assert
- onNodeWithText(customLists[0].name).assertExists()
- onNodeWithText(customLists[1].name).assertExists()
+ onNodeWithText(customLists[0].name.value).assertExists()
+ onNodeWithText(customLists[1].name.value).assertExists()
}
@Test
@@ -87,7 +87,7 @@ class CustomListsScreenTest {
// Arrange
val customLists = DUMMY_CUSTOM_LISTS
val clickedList = DUMMY_CUSTOM_LISTS[0]
- val mockedOpenCustomList: (RelayItem.CustomList) -> Unit = mockk(relaxed = true)
+ val mockedOpenCustomList: (CustomList) -> Unit = mockk(relaxed = true)
setContentWithTheme {
CustomListsScreen(
state = CustomListsUiState.Content(customLists = customLists),
@@ -97,7 +97,7 @@ class CustomListsScreenTest {
}
// Act
- onNodeWithText(clickedList.name).performClick()
+ onNodeWithText(clickedList.name.value).performClick()
// Assert
verify { mockedOpenCustomList(clickedList) }
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt
index f44441b536..5e57309777 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt
@@ -14,6 +14,8 @@ import net.mullvad.mullvadvpn.compose.state.EditCustomListState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.DELETE_DROPDOWN_MENU_ITEM_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.TOP_BAR_DROPDOWN_BUTTON_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -64,7 +66,7 @@ class EditCustomListScreenTest {
}
// Assert
- onNodeWithText(customList.name)
+ onNodeWithText(customList.name.value)
}
@Test
@@ -91,7 +93,7 @@ class EditCustomListScreenTest {
fun whenClickingOnDeleteDropdownShouldCallOnDeleteList() =
composeExtension.use {
// Arrange
- val mockedOnDelete: (String) -> Unit = mockk(relaxed = true)
+ val mockedOnDelete: (CustomListName) -> Unit = mockk(relaxed = true)
val customList = DUMMY_CUSTOM_LISTS[0]
setContentWithTheme {
EditCustomListScreen(
@@ -117,7 +119,7 @@ class EditCustomListScreenTest {
fun whenClickingOnNameCellShouldCallOnNameClicked() =
composeExtension.use {
// Arrange
- val mockedOnNameClicked: (String, String) -> Unit = mockk(relaxed = true)
+ val mockedOnNameClicked: (CustomListId, CustomListName) -> Unit = mockk(relaxed = true)
val customList = DUMMY_CUSTOM_LISTS[0]
setContentWithTheme {
EditCustomListScreen(
@@ -132,7 +134,7 @@ class EditCustomListScreenTest {
}
// Act
- onNodeWithText(customList.name).performClick()
+ onNodeWithText(customList.name.value).performClick()
// Assert
verify { mockedOnNameClicked(customList.id, customList.name) }
@@ -142,7 +144,7 @@ class EditCustomListScreenTest {
fun whenClickingOnLocationCellShouldCallOnLocationsClicked() =
composeExtension.use {
// Arrange
- val mockedOnLocationsClicked: (String) -> Unit = mockk(relaxed = true)
+ val mockedOnLocationsClicked: (CustomListId) -> Unit = mockk(relaxed = true)
val customList = DUMMY_CUSTOM_LISTS[0]
setContentWithTheme {
EditCustomListScreen(
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt
index c57c5c3f62..b3cfd7972f 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreenTest.kt
@@ -9,8 +9,9 @@ import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.RelayFilterState
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.Provider
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.ProviderId
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -124,7 +125,8 @@ class FilterScreenTest {
RelayFilterState(
allProviders = listOf(),
selectedOwnership = null,
- selectedProviders = listOf(Provider("31173", true))
+ selectedProviders =
+ listOf(Provider(ProviderId("31173"), Ownership.MullvadOwned))
),
onSelectedProvider = { _, _ -> },
onApplyClick = mockClickListener
@@ -135,47 +137,46 @@ class FilterScreenTest {
}
companion object {
-
private val DUMMY_RELAY_ALL_PROVIDERS =
listOf(
- Provider("31173", true),
- Provider("100TB", false),
- Provider("Blix", true),
- Provider("Creanova", true),
- Provider("DataPacket", false),
- Provider("HostRoyale", false),
- Provider("hostuniversal", false),
- Provider("iRegister", false),
- Provider("M247", false),
- Provider("Makonix", false),
- Provider("PrivateLayer", false),
- Provider("ptisp", false),
- Provider("Qnax", false),
- Provider("Quadranet", false),
- Provider("techfutures", false),
- Provider("Tzulo", false),
- Provider("xtom", false)
+ Provider(ProviderId("31173"), Ownership.MullvadOwned),
+ Provider(ProviderId("100TB"), Ownership.Rented),
+ Provider(ProviderId("Blix"), Ownership.MullvadOwned),
+ Provider(ProviderId("Creanova"), Ownership.MullvadOwned),
+ Provider(ProviderId("DataPacket"), Ownership.Rented),
+ Provider(ProviderId("HostRoyale"), Ownership.Rented),
+ Provider(ProviderId("hostuniversal"), Ownership.Rented),
+ Provider(ProviderId("iRegister"), Ownership.Rented),
+ Provider(ProviderId("M247"), Ownership.Rented),
+ Provider(ProviderId("Makonix"), Ownership.Rented),
+ Provider(ProviderId("PrivateLayer"), Ownership.Rented),
+ Provider(ProviderId("ptisp"), Ownership.Rented),
+ Provider(ProviderId("Qnax"), Ownership.Rented),
+ Provider(ProviderId("Quadranet"), Ownership.Rented),
+ Provider(ProviderId("techfutures"), Ownership.Rented),
+ Provider(ProviderId("Tzulo"), Ownership.Rented),
+ Provider(ProviderId("xtom"), Ownership.Rented)
)
private val DUMMY_SELECTED_PROVIDERS =
listOf(
- Provider("31173", true),
- Provider("100TB", false),
- Provider("Blix", true),
- Provider("Creanova", true),
- Provider("DataPacket", false),
- Provider("HostRoyale", false),
- Provider("hostuniversal", false),
- Provider("iRegister", false),
- Provider("M247", false),
- Provider("Makonix", false),
- Provider("PrivateLayer", false),
- Provider("ptisp", false),
- Provider("Qnax", false),
- Provider("Quadranet", false),
- Provider("techfutures", false),
- Provider("Tzulo", false),
- Provider("xtom", false)
+ Provider(ProviderId("31173"), Ownership.MullvadOwned),
+ Provider(ProviderId("100TB"), Ownership.Rented),
+ Provider(ProviderId("Blix"), Ownership.MullvadOwned),
+ Provider(ProviderId("Creanova"), Ownership.MullvadOwned),
+ Provider(ProviderId("DataPacket"), Ownership.Rented),
+ Provider(ProviderId("HostRoyale"), Ownership.Rented),
+ Provider(ProviderId("hostuniversal"), Ownership.Rented),
+ Provider(ProviderId("iRegister"), Ownership.Rented),
+ Provider(ProviderId("M247"), Ownership.Rented),
+ Provider(ProviderId("Makonix"), Ownership.Rented),
+ Provider(ProviderId("PrivateLayer"), Ownership.Rented),
+ Provider(ProviderId("ptisp"), Ownership.Rented),
+ Provider(ProviderId("Qnax"), Ownership.Rented),
+ Provider(ProviderId("Quadranet"), Ownership.Rented),
+ Provider(ProviderId("techfutures"), Ownership.Rented),
+ Provider(ProviderId("Tzulo"), Ownership.Rented),
+ Provider(ProviderId("xtom"), Ownership.Rented)
)
}
}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt
index f54944356f..7dc378261d 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt
@@ -13,11 +13,11 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.test.PLAY_PAYMENT_INFO_ICON_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.payment.model.ProductPrice
-import net.mullvad.mullvadvpn.model.TunnelState
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt
index b3ac57d95c..d8159dafd0 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/RedeemVoucherDialogTest.kt
@@ -14,6 +14,7 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.compose.test.VOUCHER_INPUT_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -133,7 +134,8 @@ class RedeemVoucherDialogTest {
RedeemVoucherDialog(
state =
VoucherDialogUiState(
- voucherState = VoucherDialogState.Error(ERROR_MESSAGE)
+ voucherState =
+ VoucherDialogState.Error(RedeemVoucherError.InvalidVoucher)
),
onVoucherInputChange = {},
onRedeem = {},
@@ -142,13 +144,13 @@ class RedeemVoucherDialogTest {
}
// Assert
- onNodeWithText(ERROR_MESSAGE).assertExists()
+ onNodeWithText(VOUCHER_CODE_INVALID_ERROR_MESSAGE).assertExists()
}
companion object {
private const val CANCEL_BUTTON_TEXT = "Cancel"
private const val GOT_IT_BUTTON_TEXT = "Got it!"
private const val DUMMY_VOUCHER = "DUMMY____VOUCHER"
- private const val ERROR_MESSAGE = "error_message"
+ private const val VOUCHER_CODE_INVALID_ERROR_MESSAGE = "Voucher code is invalid."
}
}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt
index 28651c3852..4fcee479d6 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreenTest.kt
@@ -9,16 +9,16 @@ import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
-import net.mullvad.mullvadvpn.compose.data.DUMMY_CUSTOM_LISTS
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES
+import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.performLongClick
-import net.mullvad.mullvadvpn.relaylist.RelayItem
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -96,7 +96,7 @@ class SelectLocationScreenTest {
customLists = emptyList(),
filteredCustomLists = emptyList(),
countries = updatedDummyList,
- selectedItem = updatedDummyList[0].cities[0].relays[0],
+ selectedItem = updatedDummyList[0].cities[0].relays[0].id,
selectedOwnership = null,
selectedProvidersCount = 0,
searchTerm = ""
@@ -202,7 +202,7 @@ class SelectLocationScreenTest {
SelectLocationScreen(
state =
SelectLocationUiState.Content(
- customLists = DUMMY_CUSTOM_LISTS,
+ customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
filteredCustomLists = emptyList(),
countries = emptyList(),
selectedItem = null,
@@ -222,14 +222,14 @@ class SelectLocationScreenTest {
fun whenCustomListIsClickedShouldCallOnSelectRelay() =
composeExtension.use {
// Arrange
- val customList = DUMMY_CUSTOM_LISTS[0]
+ val customList = DUMMY_RELAY_ITEM_CUSTOM_LISTS[0]
val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true)
setContentWithTheme {
SelectLocationScreen(
state =
SelectLocationUiState.Content(
- customLists = DUMMY_CUSTOM_LISTS,
- filteredCustomLists = DUMMY_CUSTOM_LISTS,
+ customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ filteredCustomLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
countries = emptyList(),
selectedItem = null,
selectedOwnership = null,
@@ -251,14 +251,14 @@ class SelectLocationScreenTest {
fun whenCustomListIsLongClickedShouldShowBottomSheet() =
composeExtension.use {
// Arrange
- val customList = DUMMY_CUSTOM_LISTS[0]
+ val customList = DUMMY_RELAY_ITEM_CUSTOM_LISTS[0]
val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true)
setContentWithTheme {
SelectLocationScreen(
state =
SelectLocationUiState.Content(
- customLists = DUMMY_CUSTOM_LISTS,
- filteredCustomLists = DUMMY_CUSTOM_LISTS,
+ customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ filteredCustomLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
countries = emptyList(),
selectedItem = null,
selectedOwnership = null,
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt
index 471e39c38f..ca7a01a0a9 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt
@@ -20,10 +20,11 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBER_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Port
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
import net.mullvad.mullvadvpn.onNodeWithTagAndText
import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
import org.junit.jupiter.api.BeforeEach
@@ -49,7 +50,7 @@ class VpnSettingsScreenTest {
)
}
- apply { onNodeWithText("Auto-connect").assertExists() }
+ onNodeWithText("Auto-connect").assertExists()
onNodeWithTag(LAZY_LIST_TEST_TAG)
.performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG))
@@ -67,7 +68,10 @@ class VpnSettingsScreenTest {
// Arrange
setContentWithTheme {
VpnSettingsScreen(
- state = VpnSettingsUiState.createDefault(mtu = VALID_DUMMY_MTU_VALUE),
+ state =
+ VpnSettingsUiState.createDefault(
+ mtu = Mtu.fromString(VALID_DUMMY_MTU_VALUE).getOrNull()!!
+ ),
)
}
@@ -360,7 +364,7 @@ class VpnSettingsScreenTest {
fun testMtuClick() =
composeExtension.use {
// Arrange
- val mockedClickHandler: (Int?) -> Unit = mockk(relaxed = true)
+ val mockedClickHandler: (Mtu?) -> Unit = mockk(relaxed = true)
setContentWithTheme {
VpnSettingsScreen(
state = VpnSettingsUiState.createDefault(),
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt
index d8711b4b61..d60a7b100b 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreenTest.kt
@@ -13,6 +13,7 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.compose.test.PLAY_PAYMENT_INFO_ICON_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.AccountToken
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
@@ -82,7 +83,7 @@ class WelcomeScreenTest {
fun testShowAccountNumber() =
composeExtension.use {
// Arrange
- val rawAccountNumber = "1111222233334444"
+ val rawAccountNumber = AccountToken("1111222233334444")
val expectedAccountNumber = "1111 2222 3333 4444"
setContentWithTheme {
WelcomeScreen(
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0337d1200d..f6f60cdd1f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -19,14 +19,14 @@
android:required="false" />
<uses-feature android:glEsVersion="0x00020000"
android:required="false" />
- <application android:label="@string/app_name"
+ <application android:name=".MullvadApplication"
+ android:allowBackup="false"
+ android:banner="@drawable/banner"
+ android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
- android:extractNativeLibs="true"
- android:allowBackup="false"
- android:banner="@drawable/banner"
- android:name=".MullvadApplication"
tools:ignore="GoogleAppIndexingWarning">
<!--
MainActivity
@@ -37,9 +37,9 @@
since after that it has been patched on a OS level.
-->
<activity android:name="net.mullvad.mullvadvpn.ui.MainActivity"
+ android:configChanges="orientation|screenSize|screenLayout"
android:exported="true"
android:launchMode="singleInstance"
- android:configChanges="orientation|screenSize|screenLayout"
android:screenOrientation="fullUser"
android:windowSoftInputMode="adjustResize"
tools:ignore="DiscouragedApi">
@@ -51,6 +51,9 @@
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
+ <intent-filter>
+ <action android:name="net.mullvad.mullvadvpn.request_vpn_permission" />
+ </intent-filter>
</activity>
<!--
MullvadVpnService
@@ -64,10 +67,9 @@
-->
<service android:name="net.mullvad.mullvadvpn.service.MullvadVpnService"
android:exported="true"
+ android:foregroundServiceType="systemExempted"
android:permission="android.permission.BIND_VPN_SERVICE"
- android:process=":mullvadvpn_daemon"
android:stopWithTask="false"
- android:foregroundServiceType="systemExempted"
tools:ignore="ForegroundServicePermission">
<intent-filter>
<action android:name="android.net.VpnService" />
@@ -89,10 +91,9 @@
-->
<service android:name="net.mullvad.mullvadvpn.tile.MullvadTileService"
android:exported="true"
- android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
- android:label="@string/toggle_vpn"
android:icon="@drawable/small_logo_black"
- android:process=":mullvadvpn_tile">
+ android:label="@string/toggle_vpn"
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/MullvadApplication.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/MullvadApplication.kt
index 4b34886c34..04ccb1cddc 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/MullvadApplication.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/MullvadApplication.kt
@@ -1,7 +1,9 @@
package net.mullvad.mullvadvpn
import android.app.Application
+import net.mullvad.mullvadvpn.di.appModule
import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.loadKoinModules
import org.koin.core.context.startKoin
/**
@@ -13,5 +15,6 @@ class MullvadApplication : Application() {
super.onCreate()
// Used to create/start separate DI graphs for each process. Avoid non-common classes etc.
startKoin { androidContext(this@MullvadApplication) }
+ loadKoinModules(listOf(appModule))
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt
index 3815a3bb46..8ca896cd73 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt
@@ -33,12 +33,12 @@ import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisconnectButton
import net.mullvad.mullvadvpn.lib.theme.color.onVariant
import net.mullvad.mullvadvpn.lib.theme.color.variant
-import net.mullvad.mullvadvpn.model.TunnelState
@Composable
fun ConnectionButton(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
index 65a3399631..529a310919 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
@@ -23,13 +23,13 @@ import net.mullvad.mullvadvpn.lib.theme.Dimens
@Preview
@Composable
private fun PreviewCheckboxCell() {
- AppTheme { CheckboxCell(providerName = "Provider 1", checked = false, onCheckedChange = {}) }
+ AppTheme { CheckboxCell(title = "1337", checked = false, onCheckedChange = {}) }
}
@Composable
internal fun CheckboxCell(
modifier: Modifier = Modifier,
- providerName: String,
+ title: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
background: Color = MaterialTheme.colorScheme.secondaryContainer,
@@ -52,7 +52,7 @@ internal fun CheckboxCell(
Spacer(modifier = Modifier.size(Dimens.mediumPadding))
Text(
- text = providerName,
+ text = title,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground,
modifier =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomListCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomListCell.kt
deleted file mode 100644
index 1029cfada0..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomListCell.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.mullvad.mullvadvpn.compose.cell
-
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.TextStyle
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-
-@Composable
-fun CustomListCell(
- customList: RelayItem.CustomList,
- modifier: Modifier = Modifier,
- onCellClicked: (RelayItem.CustomList) -> Unit = {},
- textStyle: TextStyle = MaterialTheme.typography.titleMedium,
- textColor: Color = MaterialTheme.colorScheme.onPrimary,
- background: Color = MaterialTheme.colorScheme.primary,
-) {
- BaseCell(
- headlineContent = {
- BaseCellTitle(
- title = customList.name,
- style = textStyle,
- color = textColor,
- )
- },
- modifier = modifier,
- background = background,
- onCellClicked = { onCellClicked(customList) }
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomPortCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomPortCell.kt
index cd5a08edbf..cdb4825150 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomPortCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomPortCell.kt
@@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.component.SpacedColumn
+import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
@@ -37,7 +38,7 @@ import net.mullvad.mullvadvpn.lib.theme.color.selected
private fun PreviewCustomPortCell() {
AppTheme {
SpacedColumn(Modifier.background(MaterialTheme.colorScheme.background)) {
- CustomPortCell(title = "Title", isSelected = true, port = 444)
+ CustomPortCell(title = "Title", isSelected = true, port = Port(444))
CustomPortCell(title = "Title", isSelected = false, port = null)
}
}
@@ -47,7 +48,7 @@ private fun PreviewCustomPortCell() {
fun CustomPortCell(
title: String,
isSelected: Boolean,
- port: Int?,
+ port: Port?,
mainTestTag: String = "",
numberTestTag: String = "",
onMainCellClicked: () -> Unit = {},
@@ -100,7 +101,7 @@ fun CustomPortCell(
.testTag(numberTestTag)
) {
Text(
- text = port?.toString() ?: stringResource(id = R.string.port),
+ text = port?.value?.toString() ?: stringResource(id = R.string.port),
color = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.align(Alignment.Center)
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DropdownMenuCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DropdownMenuCell.kt
index 3d52aca80c..9c429757fb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DropdownMenuCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DropdownMenuCell.kt
@@ -1,12 +1,9 @@
package net.mullvad.mullvadvpn.compose.cell
-import androidx.compose.foundation.background
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterCell.kt
index d2dcf1e863..6dfd8f3eb1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/FilterCell.kt
@@ -16,9 +16,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.component.MullvadFilterChip
+import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.model.Ownership
@Preview
@Composable
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt
index d949f2a708..0ea18d0b48 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt
@@ -1,8 +1,6 @@
package net.mullvad.mullvadvpn.compose.cell
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.layout.wrapContentWidth as wrapContentWidth1
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -12,17 +10,18 @@ import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.constant.MTU_MAX_VALUE
import net.mullvad.mullvadvpn.constant.MTU_MIN_VALUE
+import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.theme.AppTheme
@Preview
@Composable
private fun PreviewMtuComposeCell() {
- AppTheme { MtuComposeCell(mtuValue = "1300", onEditMtu = {}) }
+ AppTheme { MtuComposeCell(mtuValue = Mtu(1300), onEditMtu = {}) }
}
@Composable
fun MtuComposeCell(
- mtuValue: String,
+ mtuValue: Mtu?,
onEditMtu: () -> Unit,
) {
val titleModifier = Modifier
@@ -45,10 +44,10 @@ private fun MtuTitle(modifier: Modifier) {
}
@Composable
-private fun MtuBodyView(mtuValue: String, modifier: Modifier) {
- Row(modifier = modifier.wrapContentWidth1().wrapContentHeight()) {
+private fun MtuBodyView(mtuValue: Mtu?, modifier: Modifier) {
+ Row(modifier = modifier) {
Text(
- text = mtuValue.ifEmpty { stringResource(id = R.string.hint_default) },
+ text = mtuValue?.value?.toString() ?: stringResource(id = R.string.hint_default),
color = MaterialTheme.colorScheme.onPrimary
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
index a2d7cc74c1..d1903b75d5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
@@ -32,97 +32,45 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.component.Chevron
import net.mullvad.mullvadvpn.compose.component.MullvadCheckbox
-import net.mullvad.mullvadvpn.compose.util.generateRelayItemCountry
+import net.mullvad.mullvadvpn.compose.preview.RelayItemCheckableCellPreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.preview.RelayItemStatusCellPreviewParameterProvider
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
import net.mullvad.mullvadvpn.lib.theme.color.selected
-import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.relaylist.children
@Composable
@Preview
-private fun PreviewStatusRelayLocationCell() {
+private fun PreviewStatusRelayLocationCell(
+ @PreviewParameter(RelayItemStatusCellPreviewParameterProvider::class)
+ relayItems: List<RelayItem.Location.Country>
+) {
AppTheme {
Column(Modifier.background(color = MaterialTheme.colorScheme.background)) {
- val countryActive =
- generateRelayItemCountry(
- name = "Relay country Active",
- cityNames = listOf("Relay city 1", "Relay city 2"),
- relaysPerCity = 2
- )
- val countryNotActive =
- generateRelayItemCountry(
- name = "Not Enabled Relay country",
- cityNames = listOf("Not Enabled city"),
- relaysPerCity = 1,
- active = false
- )
- val countryExpanded =
- generateRelayItemCountry(
- name = "Relay country Expanded",
- cityNames = listOf("Normal city"),
- relaysPerCity = 2,
- expanded = true
- )
- val countryAndCityExpanded =
- generateRelayItemCountry(
- name = "Country and city Expanded",
- cityNames = listOf("Expanded city A", "Expanded city B"),
- relaysPerCity = 2,
- expanded = true,
- expandChildren = true
- )
- // Active relay list not expanded
- StatusRelayLocationCell(countryActive)
- // Not Active Relay
- StatusRelayLocationCell(countryNotActive)
- // Relay expanded country
- StatusRelayLocationCell(countryExpanded)
- // Relay expanded country and cities
- StatusRelayLocationCell(countryAndCityExpanded)
+ relayItems.map { StatusRelayLocationCell(relay = it) }
}
}
}
@Composable
@Preview
-private fun PreviewCheckableRelayLocationCell() {
+private fun PreviewCheckableRelayLocationCell(
+ @PreviewParameter(RelayItemCheckableCellPreviewParameterProvider::class)
+ relayItems: List<RelayItem.Location.Country>
+) {
AppTheme {
Column(Modifier.background(color = MaterialTheme.colorScheme.background)) {
- val countryActive =
- generateRelayItemCountry(
- name = "Relay country Active",
- cityNames = listOf("Relay city 1", "Relay city 2"),
- relaysPerCity = 2
- )
- val countryExpanded =
- generateRelayItemCountry(
- name = "Relay country Expanded",
- cityNames = listOf("Normal city"),
- relaysPerCity = 2,
- expanded = true
- )
- val countryAndCityExpanded =
- generateRelayItemCountry(
- name = "Country and city Expanded",
- cityNames = listOf("Expanded city A", "Expanded city B"),
- relaysPerCity = 2,
- expanded = true,
- expandChildren = true
- )
- // Active relay list not expanded
- CheckableRelayLocationCell(countryActive)
- // Relay expanded country
- CheckableRelayLocationCell(countryExpanded)
- // Relay expanded country and cities
- CheckableRelayLocationCell(countryAndCityExpanded)
+ relayItems.map { CheckableRelayLocationCell(relay = it) }
}
}
}
@@ -134,14 +82,14 @@ fun StatusRelayLocationCell(
activeColor: Color = MaterialTheme.colorScheme.selected,
inactiveColor: Color = MaterialTheme.colorScheme.error,
disabledColor: Color = MaterialTheme.colorScheme.onSecondary,
- selectedItem: RelayItem? = null,
+ selectedItem: RelayItemId? = null,
onSelectRelay: (item: RelayItem) -> Unit = {},
onLongClick: (item: RelayItem) -> Unit = {},
) {
RelayLocationCell(
relay = relay,
leadingContent = { relayItem ->
- val selected = selectedItem?.code == relayItem.code
+ val selected = selectedItem == relayItem.id
Box(
modifier =
Modifier.align(Alignment.CenterStart)
@@ -175,7 +123,7 @@ fun StatusRelayLocationCell(
modifier = modifier,
specialBackgroundColor = { relayItem ->
when {
- selectedItem?.code == relayItem.code -> MaterialTheme.colorScheme.selected
+ selectedItem == relayItem.id -> MaterialTheme.colorScheme.selected
relayItem is RelayItem.CustomList && !relayItem.active ->
MaterialTheme.colorScheme.surfaceTint
else -> null
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt
index 9ddee73e22..206c90ab7b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt
@@ -2,36 +2,34 @@ package net.mullvad.mullvadvpn.compose.communication
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
-import net.mullvad.mullvadvpn.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
sealed interface CustomListAction : Parcelable {
-
@Parcelize
- data class Rename(
- val customListId: String,
- val name: CustomListName,
- val newName: CustomListName
- ) : CustomListAction {
+ data class Rename(val id: CustomListId, val name: CustomListName, val newName: CustomListName) :
+ CustomListAction {
fun not() = this.copy(name = newName, newName = name)
}
@Parcelize
- data class Delete(val customListId: String) : CustomListAction {
- fun not(name: CustomListName, locations: List<String>) = Create(name, locations)
+ data class Delete(val id: CustomListId) : CustomListAction {
+ fun not(name: CustomListName, locations: List<GeoLocationId>) = Create(name, locations)
}
@Parcelize
- data class Create(val name: CustomListName, val locations: List<String> = emptyList()) :
- CustomListAction, Parcelable {
- fun not(customListId: String) = Delete(customListId)
+ data class Create(val name: CustomListName, val locations: List<GeoLocationId>) :
+ CustomListAction {
+ fun not(customListId: CustomListId) = Delete(customListId)
}
@Parcelize
data class UpdateLocations(
- val customListId: String,
- val locations: List<String> = emptyList()
+ val id: CustomListId,
+ val locations: List<GeoLocationId> = emptyList()
) : CustomListAction {
- fun not(locations: List<String>): UpdateLocations =
- UpdateLocations(customListId = customListId, locations = locations)
+ fun not(locations: List<GeoLocationId>): UpdateLocations =
+ UpdateLocations(id = id, locations = locations)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt
deleted file mode 100644
index 14cba09b44..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package net.mullvad.mullvadvpn.compose.communication
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import net.mullvad.mullvadvpn.model.CustomListName
-
-sealed interface CustomListResult : Parcelable {
- val undo: CustomListAction
-
- @Parcelize
- data class Created(
- val id: String,
- val name: CustomListName,
- val locationName: String?,
- override val undo: CustomListAction.Delete
- ) : CustomListResult
-
- @Parcelize
- data class Deleted(override val undo: CustomListAction.Create) : CustomListResult {
- val name: CustomListName
- get() = undo.name
- }
-
- @Parcelize
- data class Renamed(override val undo: CustomListAction.Rename) : CustomListResult {
- val name: CustomListName
- get() = undo.name
- }
-
- @Parcelize
- data class LocationsChanged(
- val name: CustomListName,
- override val undo: CustomListAction.UpdateLocations
- ) : CustomListResult
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListSuccess.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListSuccess.kt
new file mode 100644
index 0000000000..d83cd4c76d
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListSuccess.kt
@@ -0,0 +1,36 @@
+package net.mullvad.mullvadvpn.compose.communication
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+
+sealed interface CustomListSuccess : Parcelable {
+ val undo: CustomListAction
+}
+
+@Parcelize
+data class Created(
+ val id: CustomListId,
+ val name: CustomListName,
+ val locationNames: List<String>,
+ override val undo: CustomListAction.Delete
+) : CustomListSuccess
+
+@Parcelize
+data class Deleted(override val undo: CustomListAction.Create) : CustomListSuccess {
+ val name: CustomListName
+ get() = undo.name
+}
+
+@Parcelize
+data class Renamed(override val undo: CustomListAction.Rename) : CustomListSuccess {
+ val name: CustomListName
+ get() = undo.name
+}
+
+@Parcelize
+data class LocationsChanged(
+ val name: CustomListName,
+ override val undo: CustomListAction.UpdateLocations
+) : CustomListSuccess
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/DnsDialogResult.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/DnsDialogResult.kt
new file mode 100644
index 0000000000..45eb76cc85
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/DnsDialogResult.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.compose.communication
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+interface DnsDialogResult : Parcelable {
+ @Parcelize data object Success : DnsDialogResult
+
+ @Parcelize data object Error : DnsDialogResult
+
+ @Parcelize data object Cancel : DnsDialogResult
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt
index a081b9f079..9774cc27fb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt
@@ -6,12 +6,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.typeface.connectionStatus
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.tunnel.ErrorStateCause
@Preview
@Composable
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CustomListNameTextField.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CustomListNameTextField.kt
index b3a0ece577..bb4339a1f7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CustomListNameTextField.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CustomListNameTextField.kt
@@ -10,19 +10,16 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
-import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.textfield.CustomTextField
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.lib.model.CustomListName
@Composable
fun CustomListNameTextField(
modifier: Modifier = Modifier,
name: String,
isValidName: Boolean,
- error: CustomListsError?,
+ error: String?,
onValueChanged: (String) -> Unit,
onSubmit: (String) -> Unit
) {
@@ -47,7 +44,7 @@ fun CustomListNameTextField(
error?.let {
{
Text(
- text = it.errorString(),
+ text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
@@ -63,12 +60,3 @@ fun CustomListNameTextField(
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
-
-@Composable
-private fun CustomListsError.errorString() =
- stringResource(
- when (this) {
- CustomListsError.CustomListExists -> R.string.custom_list_error_list_exists
- CustomListsError.OtherError -> R.string.error_occurred
- }
- )
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt
index ed41d25f40..a913368de5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt
@@ -16,13 +16,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.test.LOCATION_INFO_CONNECTION_OUT_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.TransportProtocol
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.talpid.net.TransportProtocol
@Preview
@Composable
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
index 585855cb1d..4e03ebf4ae 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
@@ -270,6 +270,7 @@ fun ScaffoldWithSmallTopBar(
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
+ snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable (modifier: Modifier) -> Unit
) {
Scaffold(
@@ -281,6 +282,12 @@ fun ScaffoldWithSmallTopBar(
actions = actions
)
},
+ snackbarHost = {
+ SnackbarHost(
+ snackbarHostState,
+ snackbar = { snackbarData -> MullvadSnackbar(snackbarData = snackbarData) }
+ )
+ },
content = { content(Modifier.fillMaxSize().padding(it)) }
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Text.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Text.kt
index 18a88fdf79..79fdec7b9d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Text.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Text.kt
@@ -11,59 +11,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextDecoration
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
internal val DEFAULT_TEXT_STEP = 1.sp
@Composable
-fun CapsText(
- text: String,
- modifier: Modifier = Modifier,
- color: Color = Color.Unspecified,
- fontSize: TextUnit = TextUnit.Unspecified,
- fontStyle: androidx.compose.ui.text.font.FontStyle? = null,
- fontWeight: FontWeight? = null,
- fontFamily: FontFamily? = null,
- letterSpacing: TextUnit = TextUnit.Unspecified,
- textDecoration: TextDecoration? = null,
- textAlign: TextAlign? = null,
- lineHeight: TextUnit = TextUnit.Unspecified,
- overflow: TextOverflow = TextOverflow.Clip,
- softWrap: Boolean = true,
- maxLines: Int = Int.MAX_VALUE,
- onTextLayout: (TextLayoutResult) -> Unit = {},
- style: TextStyle = LocalTextStyle.current
-) {
- Text(
- text = text.uppercase(),
- modifier = modifier,
- color = color,
- fontSize = fontSize,
- fontStyle = fontStyle,
- fontWeight = fontWeight,
- fontFamily = fontFamily,
- letterSpacing = letterSpacing,
- textDecoration = textDecoration,
- textAlign = textAlign,
- lineHeight = lineHeight,
- overflow = overflow,
- softWrap = softWrap,
- maxLines = maxLines,
- onTextLayout = onTextLayout,
- style = style,
- )
-}
-
-@Composable
fun AutoResizeText(
text: String,
minTextSize: TextUnit,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
index 94dc40a175..dbc510b009 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
@@ -29,15 +29,14 @@ import net.mullvad.mullvadvpn.compose.component.MullvadTopBar
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION
import net.mullvad.mullvadvpn.compose.util.rememberPrevious
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.ui.VersionInfo
import net.mullvad.mullvadvpn.ui.notification.StatusLevel
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.tunnel.ErrorStateCause
-import net.mullvad.talpid.tunnel.FirewallPolicyError
import org.joda.time.DateTime
@Preview
@@ -52,23 +51,16 @@ private fun PreviewNotificationBanner() {
InAppNotification.UnsupportedVersion(
versionInfo =
VersionInfo(
- currentVersion = null,
- upgradeVersion = null,
- isOutdated = true,
- isSupported = false
+ currentVersion = "1.0",
+ isSupported = false,
+ suggestedUpgradeVersion = null
),
),
InAppNotification.AccountExpiry(expiry = DateTime.now()),
InAppNotification.TunnelStateBlocked,
InAppNotification.NewDevice("Courageous Turtle"),
InAppNotification.TunnelStateError(
- error =
- ErrorState(
- ErrorStateCause.SetFirewallPolicyError(
- FirewallPolicyError.Generic
- ),
- true
- )
+ error = ErrorState(ErrorStateCause.FirewallPolicyError.Generic, true)
)
)
.map { it.toNotificationData(false, {}, {}, {}) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt
index 99501d1f4d..b8ea96fc72 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationData.kt
@@ -13,9 +13,9 @@ import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.extensions.getExpiryQuantityString
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
import net.mullvad.mullvadvpn.lib.common.util.getErrorNotificationResources
+import net.mullvad.mullvadvpn.lib.model.ErrorState
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.ui.notification.StatusLevel
-import net.mullvad.talpid.tunnel.ErrorState
data class NotificationData(
val title: String,
@@ -99,7 +99,7 @@ fun InAppNotification.toNotificationData(
message =
stringResource(
id = R.string.update_available_description,
- versionInfo.upgradeVersion ?: "" // TODO Verify
+ versionInfo.suggestedUpgradeVersion ?: ""
),
statusLevel = StatusLevel.Warning,
action =
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt
index 98f2007bc0..90e82e1fbf 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt
@@ -20,13 +20,15 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Created
import net.mullvad.mullvadvpn.compose.component.CustomListNameTextField
import net.mullvad.mullvadvpn.compose.destinations.CustomListLocationsDestination
import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState
import net.mullvad.mullvadvpn.compose.test.CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError
import net.mullvad.mullvadvpn.viewmodel.CreateCustomListDialogSideEffect
import net.mullvad.mullvadvpn.viewmodel.CreateCustomListDialogViewModel
import org.koin.androidx.compose.koinViewModel
@@ -43,7 +45,10 @@ private fun PreviewCreateCustomListDialog() {
private fun PreviewCreateCustomListDialogError() {
AppTheme {
CreateCustomListDialog(
- state = CreateCustomListUiState(error = CustomListsError.CustomListExists)
+ state =
+ CreateCustomListUiState(
+ error = CreateWithLocationsError.Create(CustomListAlreadyExists)
+ )
)
}
}
@@ -52,8 +57,8 @@ private fun PreviewCreateCustomListDialogError() {
@Destination(style = DestinationStyle.Dialog::class)
fun CreateCustomList(
navigator: DestinationsNavigator,
- backNavigator: ResultBackNavigator<CustomListResult.Created>,
- locationCode: String = ""
+ backNavigator: ResultBackNavigator<Created>,
+ locationCode: GeoLocationId? = null
) {
val vm: CreateCustomListDialogViewModel =
koinViewModel(parameters = { parametersOf(locationCode) })
@@ -106,7 +111,7 @@ fun CreateCustomListDialog(
CustomListNameTextField(
name = name.value,
isValidName = isValidName,
- error = state.error,
+ error = state.error?.errorString(),
onSubmit = createCustomList,
onValueChanged = {
name.value = it
@@ -130,3 +135,13 @@ fun CreateCustomListDialog(
}
)
}
+
+@Composable
+private fun CreateWithLocationsError.errorString() =
+ stringResource(
+ if (this is CreateWithLocationsError.Create && this.error is CustomListAlreadyExists) {
+ R.string.custom_list_error_list_exists
+ } else {
+ R.string.error_occurred
+ }
+ )
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt
index 236dedec6a..e9718d7c24 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt
@@ -1,12 +1,15 @@
package net.mullvad.mullvadvpn.compose.dialog
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@@ -14,14 +17,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.NegativeButton
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Deleted
+import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.viewmodel.DeleteCustomListConfirmationSideEffect
@@ -32,18 +39,24 @@ import org.koin.core.parameter.parametersOf
@Preview
@Composable
private fun PreviewRemoveDeviceConfirmationDialog() {
- AppTheme { DeleteCustomListConfirmationDialog("My Custom List") }
+ AppTheme {
+ DeleteCustomListConfirmationDialog(
+ state = DeleteCustomListUiState(null),
+ name = CustomListName.fromString("My Custom List")
+ )
+ }
}
@Composable
@Destination(style = DestinationStyle.Dialog::class)
fun DeleteCustomList(
- navigator: ResultBackNavigator<CustomListResult.Deleted>,
- customListId: String,
- name: String
+ navigator: ResultBackNavigator<Deleted>,
+ customListId: CustomListId,
+ name: CustomListName
) {
val viewModel: DeleteCustomListConfirmationViewModel =
koinViewModel(parameters = { parametersOf(customListId) })
+ val state = viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffectCollect(viewModel.uiSideEffect) {
when (it) {
@@ -53,6 +66,7 @@ fun DeleteCustomList(
}
DeleteCustomListConfirmationDialog(
+ state = state.value,
name = name,
onDelete = viewModel::deleteCustomList,
onBack = navigator::navigateBack
@@ -61,7 +75,8 @@ fun DeleteCustomList(
@Composable
fun DeleteCustomListConfirmationDialog(
- name: String,
+ state: DeleteCustomListUiState,
+ name: CustomListName,
onDelete: () -> Unit = {},
onBack: () -> Unit = {}
) {
@@ -76,10 +91,23 @@ fun DeleteCustomListConfirmationDialog(
)
},
title = {
- Text(
- text =
- stringResource(id = R.string.delete_custom_list_confirmation_description, name)
- )
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Text(
+ text =
+ stringResource(
+ id = R.string.delete_custom_list_confirmation_description,
+ name.value
+ )
+ )
+ if (state.deleteError != null) {
+ Text(
+ text = stringResource(id = R.string.error_occurred),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.error,
+ modifier = Modifier.padding(top = Dimens.smallPadding)
+ )
+ }
+ }
},
dismissButton = {
PrimaryButton(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt
index 7ac1469f09..5b76023a7e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt
@@ -20,6 +20,7 @@ import com.ramcosta.composedestinations.spec.DestinationStyle
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.NegativeButton
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
+import net.mullvad.mullvadvpn.compose.communication.DnsDialogResult
import net.mullvad.mullvadvpn.compose.textfield.DnsTextField
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.lib.theme.AppTheme
@@ -93,7 +94,7 @@ private fun PreviewDnsDialogEditAllowLanDisabled() {
@Destination(style = DestinationStyle.Dialog::class)
@Composable
fun DnsDialog(
- resultNavigator: ResultBackNavigator<Boolean>,
+ resultNavigator: ResultBackNavigator<DnsDialogResult>,
index: Int?,
initialValue: String?,
) {
@@ -102,7 +103,10 @@ fun DnsDialog(
LaunchedEffectCollect(viewModel.uiSideEffect) {
when (it) {
- DnsDialogSideEffect.Complete -> resultNavigator.navigateBack(result = true)
+ DnsDialogSideEffect.Complete ->
+ resultNavigator.navigateBack(result = DnsDialogResult.Success)
+ DnsDialogSideEffect.Error ->
+ resultNavigator.navigateBack(result = DnsDialogResult.Error)
}
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -112,7 +116,7 @@ fun DnsDialog(
viewModel::onDnsInputChange,
onSaveDnsClick = viewModel::onSaveDnsClick,
onRemoveDnsClick = viewModel::onRemoveDnsClick,
- onDismiss = { resultNavigator.navigateBack(result = false) }
+ onDismiss = { resultNavigator.navigateBack(result = DnsDialogResult.Cancel) }
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt
index 9f46ee1d5a..c01ceab7f8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt
@@ -18,12 +18,18 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Renamed
import net.mullvad.mullvadvpn.compose.component.CustomListNameTextField
-import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState
+import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState
import net.mullvad.mullvadvpn.compose.test.EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GetCustomListError
+import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.UnknownCustomListError
import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.usecase.customlists.RenameError
import net.mullvad.mullvadvpn.viewmodel.EditCustomListNameDialogSideEffect
import net.mullvad.mullvadvpn.viewmodel.EditCustomListNameDialogViewModel
import org.koin.androidx.compose.koinViewModel
@@ -32,15 +38,15 @@ import org.koin.core.parameter.parametersOf
@Preview
@Composable
private fun PreviewEditCustomListNameDialog() {
- AppTheme { EditCustomListNameDialog(UpdateCustomListUiState()) }
+ AppTheme { EditCustomListNameDialog(EditCustomListNameUiState()) }
}
@Composable
@Destination(style = DestinationStyle.Dialog::class)
fun EditCustomListName(
- backNavigator: ResultBackNavigator<CustomListResult.Renamed>,
- customListId: String,
- initialName: String
+ backNavigator: ResultBackNavigator<Renamed>,
+ customListId: CustomListId,
+ initialName: CustomListName
) {
val vm: EditCustomListNameDialogViewModel =
koinViewModel(parameters = { parametersOf(customListId, initialName) })
@@ -63,7 +69,7 @@ fun EditCustomListName(
@Composable
fun EditCustomListNameDialog(
- state: UpdateCustomListUiState,
+ state: EditCustomListNameUiState,
updateName: (String) -> Unit = {},
onInputChanged: () -> Unit = {},
onDismiss: () -> Unit = {}
@@ -81,7 +87,7 @@ fun EditCustomListNameDialog(
CustomListNameTextField(
name = name.value,
isValidName = isValidName,
- error = state.error,
+ error = state.error?.errorString(),
onSubmit = updateName,
onValueChanged = {
name.value = it
@@ -105,3 +111,13 @@ fun EditCustomListNameDialog(
}
)
}
+
+@Composable
+private fun RenameError.errorString() =
+ stringResource(
+ when (error) {
+ is NameAlreadyExists -> R.string.custom_list_error_list_exists
+ is GetCustomListError,
+ is UnknownCustomListError -> R.string.error_occurred
+ }
+ )
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt
index 4644a1aa95..c9276c5c09 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt
@@ -8,14 +8,14 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ramcosta.composedestinations.annotation.Destination
-import com.ramcosta.composedestinations.navigation.DestinationsNavigator
-import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
+import com.ramcosta.composedestinations.result.EmptyResultBackNavigator
+import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.NegativeButton
@@ -24,49 +24,51 @@ import net.mullvad.mullvadvpn.compose.textfield.MtuTextField
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.constant.MTU_MAX_VALUE
import net.mullvad.mullvadvpn.constant.MTU_MIN_VALUE
+import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription
-import net.mullvad.mullvadvpn.util.isValidMtu
import net.mullvad.mullvadvpn.viewmodel.MtuDialogSideEffect
+import net.mullvad.mullvadvpn.viewmodel.MtuDialogUiState
import net.mullvad.mullvadvpn.viewmodel.MtuDialogViewModel
import org.koin.androidx.compose.koinViewModel
+import org.koin.core.parameter.parametersOf
@Preview
@Composable
private fun PreviewMtuDialog() {
- AppTheme { MtuDialog(mtuInitial = 1234, EmptyDestinationsNavigator) }
+ AppTheme { MtuDialog(mtuInitial = Mtu(1234), EmptyResultBackNavigator()) }
}
@Destination(style = DestinationStyle.Dialog::class)
@Composable
-fun MtuDialog(mtuInitial: Int?, navigator: DestinationsNavigator) {
- val viewModel = koinViewModel<MtuDialogViewModel>()
+fun MtuDialog(mtuInitial: Mtu?, navigator: ResultBackNavigator<Boolean>) {
+ val viewModel = koinViewModel<MtuDialogViewModel>(parameters = { parametersOf(mtuInitial) })
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffectCollect(viewModel.uiSideEffect) {
when (it) {
- MtuDialogSideEffect.Complete -> navigator.navigateUp()
+ MtuDialogSideEffect.Complete -> navigator.navigateBack(result = true)
+ MtuDialogSideEffect.Error -> navigator.navigateBack(result = false)
}
}
MtuDialog(
- mtuInitial = mtuInitial,
+ uiState,
+ onInputChanged = viewModel::onInputChanged,
onSaveMtu = viewModel::onSaveClick,
onResetMtu = viewModel::onRestoreClick,
- onDismiss = navigator::navigateUp
+ onDismiss = { navigator.navigateBack(true) }
)
}
@Composable
fun MtuDialog(
- mtuInitial: Int?,
- onSaveMtu: (Int) -> Unit,
+ state: MtuDialogUiState,
+ onInputChanged: (String) -> Unit,
+ onSaveMtu: (String) -> Unit,
onResetMtu: () -> Unit,
onDismiss: () -> Unit,
) {
-
- val mtu = remember { mutableStateOf(mtuInitial?.toString() ?: "") }
- val isValidMtu = mtu.value.toIntOrNull()?.isValidMtu() == true
-
AlertDialog(
onDismissRequest = onDismiss,
title = {
@@ -78,18 +80,13 @@ fun MtuDialog(
text = {
Column {
MtuTextField(
- value = mtu.value,
- onValueChanged = { newMtuValue -> mtu.value = newMtuValue },
- onSubmit = { newMtuValue ->
- val mtuInt = newMtuValue.toIntOrNull()
- if (mtuInt?.isValidMtu() == true) {
- onSaveMtu(mtuInt)
- }
- },
+ value = state.mtuInput,
+ onValueChanged = onInputChanged,
+ onSubmit = onSaveMtu,
isEnabled = true,
placeholderText = stringResource(R.string.enter_value_placeholder),
maxCharLength = 4,
- isValidValue = isValidMtu,
+ isValidValue = state.isValidInput,
modifier = Modifier.fillMaxWidth()
)
@@ -110,17 +107,12 @@ fun MtuDialog(
Column(verticalArrangement = Arrangement.spacedBy(Dimens.buttonSpacing)) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
- isEnabled = isValidMtu,
+ isEnabled = state.isValidInput,
text = stringResource(R.string.submit_button),
- onClick = {
- val mtuInt = mtu.value.toIntOrNull()
- if (mtuInt?.isValidMtu() == true) {
- onSaveMtu(mtuInt)
- }
- }
+ onClick = { onSaveMtu(state.mtuInput) }
)
- if (mtuInitial != null) {
+ if (state.showResetToDefault) {
NegativeButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.reset_to_default_button),
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt
index 88eb682849..7034e67a91 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt
@@ -40,6 +40,7 @@ import net.mullvad.mullvadvpn.compose.textfield.CustomTextField
import net.mullvad.mullvadvpn.compose.util.MAX_VOUCHER_LENGTH
import net.mullvad.mullvadvpn.compose.util.vouchersVisualTransformation
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription
@@ -78,7 +79,11 @@ private fun PreviewRedeemVoucherDialogVerifying() {
private fun PreviewRedeemVoucherDialogError() {
AppTheme {
RedeemVoucherDialog(
- state = VoucherDialogUiState("", VoucherDialogState.Error("An Error message")),
+ state =
+ VoucherDialogUiState(
+ "",
+ VoucherDialogState.Error(RedeemVoucherError.InvalidVoucher)
+ ),
onVoucherInputChange = {},
onRedeem = {},
onDismiss = {}
@@ -263,10 +268,18 @@ private fun EnterVoucherBody(
)
} else if (state.voucherState is VoucherDialogState.Error) {
Text(
- text = state.voucherState.errorMessage,
+ text = stringResource(id = state.voucherState.error.message()),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
}
+
+private fun RedeemVoucherError.message(): Int =
+ when (this) {
+ RedeemVoucherError.InvalidVoucher -> R.string.invalid_voucher
+ RedeemVoucherError.VoucherAlreadyUsed -> R.string.voucher_already_used
+ RedeemVoucherError.RpcError,
+ is RedeemVoucherError.Unknown -> R.string.error_occurred
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RemoveDeviceConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RemoveDeviceConfirmationDialog.kt
index a0270989cf..b8592c1acb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RemoveDeviceConfirmationDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RemoveDeviceConfirmationDialog.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.sp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.result.EmptyResultBackNavigator
@@ -23,24 +24,23 @@ import net.mullvad.mullvadvpn.compose.button.NegativeButton
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
import net.mullvad.mullvadvpn.compose.component.HtmlText
import net.mullvad.mullvadvpn.compose.component.textResource
+import net.mullvad.mullvadvpn.compose.preview.DevicePreviewParameterProvider
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.model.Device
@Preview
@Composable
-private fun PreviewRemoveDeviceConfirmationDialog() {
- AppTheme {
- RemoveDeviceConfirmationDialog(
- EmptyResultBackNavigator(),
- device = Device("test", "test", byteArrayOf(), "test")
- )
- }
+private fun PreviewRemoveDeviceConfirmationDialog(
+ @PreviewParameter(DevicePreviewParameterProvider::class) device: Device
+) {
+ AppTheme { RemoveDeviceConfirmationDialog(EmptyResultBackNavigator(), device = device) }
}
@Destination(style = DestinationStyle.Dialog::class)
@Composable
-fun RemoveDeviceConfirmationDialog(navigator: ResultBackNavigator<String>, device: Device) {
+fun RemoveDeviceConfirmationDialog(navigator: ResultBackNavigator<DeviceId>, device: Device) {
AlertDialog(
onDismissRequest = navigator::navigateBack,
icon = {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt
index c90c22ead4..46111ebf8c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ResetServerIpOverridesConfirmationDialog.kt
@@ -37,6 +37,8 @@ fun ResetServerIpOverridesConfirmation(resultBackNavigator: ResultBackNavigator<
when (it) {
ResetServerIpOverridesConfirmationUiSideEffect.OverridesCleared ->
resultBackNavigator.navigateBack(result = true)
+ is ResetServerIpOverridesConfirmationUiSideEffect.OverridesError ->
+ resultBackNavigator.navigateBack(result = false)
}
}
ResetServerIpOverridesConfirmationDialog(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt
index 44901ce656..6640984a0f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt
@@ -26,12 +26,13 @@ import net.mullvad.mullvadvpn.compose.button.NegativeButton
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG
import net.mullvad.mullvadvpn.compose.textfield.CustomPortTextField
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription
-import net.mullvad.mullvadvpn.model.PortRange
import net.mullvad.mullvadvpn.util.asString
-import net.mullvad.mullvadvpn.util.isPortInValidRanges
+import net.mullvad.mullvadvpn.util.inAnyOf
@Preview
@Composable
@@ -40,7 +41,7 @@ private fun PreviewWireguardCustomPortDialog() {
WireguardCustomPortDialog(
WireguardCustomPortNavArgs(
customPort = null,
- allowedPortRanges = listOf(PortRange(10, 10), PortRange(40, 50)),
+ allowedPortRanges = listOf(PortRange(10..10), PortRange(40..50)),
),
EmptyResultBackNavigator()
)
@@ -49,7 +50,7 @@ private fun PreviewWireguardCustomPortDialog() {
@Parcelize
data class WireguardCustomPortNavArgs(
- val customPort: Int?,
+ val customPort: Port?,
val allowedPortRanges: List<PortRange>,
) : Parcelable
@@ -57,7 +58,7 @@ data class WireguardCustomPortNavArgs(
@Composable
fun WireguardCustomPortDialog(
navArg: WireguardCustomPortNavArgs,
- backNavigator: ResultBackNavigator<Int?>,
+ backNavigator: ResultBackNavigator<Port?>,
) {
WireguardCustomPortDialog(
initialPort = navArg.customPort,
@@ -69,12 +70,14 @@ fun WireguardCustomPortDialog(
@Composable
fun WireguardCustomPortDialog(
- initialPort: Int?,
+ initialPort: Port?,
allowedPortRanges: List<PortRange>,
- onSave: (Int?) -> Unit,
+ onSave: (Port?) -> Unit,
onDismiss: () -> Unit
) {
- val port = remember { mutableStateOf(initialPort?.toString() ?: "") }
+ val port = remember { mutableStateOf(initialPort?.value?.toString() ?: "") }
+
+ val isValidPort = port.value.toPortOrNull()?.inAnyOf(allowedPortRanges) ?: false
AlertDialog(
title = {
@@ -86,10 +89,8 @@ fun WireguardCustomPortDialog(
Column(verticalArrangement = Arrangement.spacedBy(Dimens.buttonSpacing)) {
PrimaryButton(
text = stringResource(id = R.string.custom_port_dialog_submit),
- onClick = { onSave(port.value.toInt()) },
- isEnabled =
- port.value.isNotEmpty() &&
- allowedPortRanges.isPortInValidRanges(port.value.toIntOrNull() ?: 0)
+ onClick = { onSave(port.value.toPortOrNull()) },
+ isEnabled = isValidPort
)
if (initialPort != null) {
NegativeButton(
@@ -105,17 +106,12 @@ fun WireguardCustomPortDialog(
CustomPortTextField(
value = port.value,
onSubmit = { input ->
- if (
- input.isNotEmpty() &&
- allowedPortRanges.isPortInValidRanges(input.toIntOrNull() ?: 0)
- ) {
- onSave(input.toIntOrNull())
+ if (isValidPort) {
+ onSave(input.toPortOrNull())
}
},
onValueChanged = { input -> port.value = input },
- isValidValue =
- port.value.isNotEmpty() &&
- allowedPortRanges.isPortInValidRanges(port.value.toIntOrNull() ?: 0),
+ isValidValue = isValidPort,
maxCharLength = 5,
modifier = Modifier.testTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).fillMaxWidth()
)
@@ -136,3 +132,5 @@ fun WireguardCustomPortDialog(
onDismissRequest = onDismiss
)
}
+
+private fun String.toPortOrNull() = toIntOrNull()?.let { Port(it) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardPortInfoDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardPortInfoDialog.kt
index a3329b1248..7de2e97fbb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardPortInfoDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardPortInfoDialog.kt
@@ -10,8 +10,8 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.parcelize.Parcelize
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.model.PortRange
import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.model.PortRange
import net.mullvad.mullvadvpn.util.asString
@Preview
@@ -20,7 +20,7 @@ private fun PreviewWireguardPortInfoDialog() {
AppTheme {
WireguardPortInfoDialog(
EmptyDestinationsNavigator,
- argument = WireguardPortInfoDialogArgument(listOf(PortRange(1, 2)))
+ argument = WireguardPortInfoDialogArgument(listOf(PortRange(1..2)))
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialog.kt
index 03e9434006..5af5e4305d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialog.kt
@@ -126,10 +126,9 @@ fun Payment(productId: ProductId, resultBackNavigator: ResultBackNavigator<Boole
val vm = koinViewModel<PaymentViewModel>()
val state by vm.uiState.collectAsStateWithLifecycle()
- LaunchedEffectCollect(vm.uiSideEffect) {
- when (it) {
- is PaymentUiSideEffect.PaymentCancelled ->
- resultBackNavigator.navigateBack(result = false)
+ LaunchedEffectCollect(vm.uiSideEffect) { sideEffect ->
+ when (sideEffect) {
+ PaymentUiSideEffect.PaymentCancelled -> resultBackNavigator.navigateBack(result = false)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/SnackbarHostExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/SnackbarHostExtensions.kt
deleted file mode 100644
index 8a418c17aa..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/SnackbarHostExtensions.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package net.mullvad.mullvadvpn.compose.extensions
-
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SnackbarResult
-
-suspend fun SnackbarHostState.showSnackbar(
- message: String,
- actionLabel: String,
- duration: SnackbarDuration = SnackbarDuration.Indefinite,
- onAction: (() -> Unit),
- onDismiss: (() -> Unit) = {}
-) {
- when (showSnackbar(message = message, actionLabel = actionLabel, duration = duration)) {
- SnackbarResult.ActionPerformed -> onAction()
- SnackbarResult.Dismissed -> onDismiss()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt
index e85939c51c..e8a3706b66 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/extensions/UriHandlerExtensions.kt
@@ -4,9 +4,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.common.util.createAccountUri
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
@Composable
-fun UriHandler.createOpenAccountPageHook(): (String) -> Unit {
+fun UriHandler.createOpenAccountPageHook(): (WebsiteAuthToken) -> Unit {
val accountUrl = stringResource(id = R.string.account_url)
- return { token -> this.openUri("$accountUrl?token=$token") }
+ return { token -> this.openUri(createAccountUri(accountUrl, token).toString()) }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DeviceListPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DeviceListPreviewParameterProvider.kt
new file mode 100644
index 0000000000..405c2b1a4d
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DeviceListPreviewParameterProvider.kt
@@ -0,0 +1,16 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.preview.DevicePreviewData.generateDevices
+import net.mullvad.mullvadvpn.compose.state.DeviceItemUiState
+
+class DeviceListPreviewParameterProvider : PreviewParameterProvider<List<DeviceItemUiState>> {
+ override val values =
+ sequenceOf(
+ generateDevices(NUMBER_OF_DEVICES_NORMAL),
+ generateDevices(NUMBER_OF_DEVICES_TOO_MANY)
+ )
+}
+
+private const val NUMBER_OF_DEVICES_NORMAL = 4
+private const val NUMBER_OF_DEVICES_TOO_MANY = 5
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewData.kt
new file mode 100644
index 0000000000..8178431452
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewData.kt
@@ -0,0 +1,29 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import net.mullvad.mullvadvpn.compose.state.DeviceItemUiState
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import org.joda.time.DateTime
+
+internal object DevicePreviewData {
+ fun generateDevices(count: Int) =
+ List(count) { index -> generateDevice(index) }
+ .mapIndexed { index, device ->
+ DeviceItemUiState(device = device, isLoading = index == 0)
+ }
+
+ fun generateDevice(
+ index: Int = 0,
+ id: String = UUID,
+ name: String? = null,
+ ) =
+ Device(
+ id = DeviceId.fromString(id),
+ name = name ?: "Device $index-${id.take(DEVICE_SUFFIX_LENGTH)}",
+ creationDate = DEVICE_CREATION_DATE.plusMonths(index)
+ )
+}
+
+private const val DEVICE_SUFFIX_LENGTH = 4
+private const val UUID = "12345678-1234-5678-1234-567812345678"
+private val DEVICE_CREATION_DATE = DateTime.parse("2024-05-27")
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewParameterProvider.kt
new file mode 100644
index 0000000000..efc0da1fb5
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/DevicePreviewParameterProvider.kt
@@ -0,0 +1,9 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.preview.DevicePreviewData.generateDevice
+import net.mullvad.mullvadvpn.lib.model.Device
+
+class DevicePreviewParameterProvider : PreviewParameterProvider<Device> {
+ override val values: Sequence<Device> = sequenceOf(generateDevice())
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt
new file mode 100644
index 0000000000..c0cae0128f
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt
@@ -0,0 +1,32 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.preview.RelayItemPreviewData.generateRelayItemCountry
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+
+class RelayItemCheckableCellPreviewParameterProvider :
+ PreviewParameterProvider<List<RelayItem.Location.Country>> {
+ override val values =
+ sequenceOf(
+ listOf(
+ generateRelayItemCountry(
+ name = "Relay country Active",
+ cityNames = listOf("Relay city 1", "Relay city 2"),
+ relaysPerCity = 2
+ ),
+ generateRelayItemCountry(
+ name = "Relay country Expanded",
+ cityNames = listOf("Normal city"),
+ relaysPerCity = 2,
+ expanded = true
+ ),
+ generateRelayItemCountry(
+ name = "Country and city Expanded",
+ cityNames = listOf("Expanded city A", "Expanded city B"),
+ relaysPerCity = 2,
+ expanded = true,
+ expandChildren = true
+ )
+ )
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt
new file mode 100644
index 0000000000..afaf81ac55
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt
@@ -0,0 +1,80 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.ProviderId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+
+internal object RelayItemPreviewData {
+ fun generateRelayItemCountry(
+ name: String,
+ cityNames: List<String>,
+ relaysPerCity: Int,
+ active: Boolean = true,
+ expanded: Boolean = false,
+ expandChildren: Boolean = false,
+ ) =
+ RelayItem.Location.Country(
+ name = name,
+ id = name.generateCountryCode(),
+ cities =
+ cityNames.map { cityName ->
+ generateRelayItemCity(
+ cityName,
+ name.generateCountryCode(),
+ relaysPerCity,
+ active,
+ expandChildren
+ )
+ },
+ expanded = expanded,
+ )
+}
+
+private fun generateRelayItemCity(
+ name: String,
+ countryCode: GeoLocationId.Country,
+ numberOfRelays: Int,
+ active: Boolean = true,
+ expanded: Boolean = false,
+) =
+ RelayItem.Location.City(
+ name = name,
+ id = name.generateCityCode(countryCode),
+ relays =
+ List(numberOfRelays) { index ->
+ generateRelayItemRelay(
+ name.generateCityCode(countryCode),
+ generateHostname(name.generateCityCode(countryCode), index),
+ active
+ )
+ },
+ expanded = expanded,
+ )
+
+private fun generateRelayItemRelay(
+ cityCode: GeoLocationId.City,
+ hostName: String,
+ active: Boolean = true,
+) =
+ RelayItem.Location.Relay(
+ id =
+ GeoLocationId.Hostname(
+ city = cityCode,
+ hostname = hostName,
+ ),
+ active = active,
+ provider = Provider(ProviderId("Provider"), Ownership.MullvadOwned),
+ )
+
+private fun String.generateCountryCode() =
+ GeoLocationId.Country((take(1) + takeLast(1)).lowercase())
+
+private fun String.generateCityCode(countryCode: GeoLocationId.Country) =
+ GeoLocationId.City(countryCode, take(CITY_CODE_LENGTH).lowercase())
+
+private fun generateHostname(city: GeoLocationId.City, index: Int) =
+ "${city.countryCode.countryCode}-${city.cityCode}-wg-${index+1}"
+
+private const val CITY_CODE_LENGTH = 3
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt
new file mode 100644
index 0000000000..26ea644185
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt
@@ -0,0 +1,38 @@
+package net.mullvad.mullvadvpn.compose.preview
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.preview.RelayItemPreviewData.generateRelayItemCountry
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+
+class RelayItemStatusCellPreviewParameterProvider :
+ PreviewParameterProvider<List<RelayItem.Location.Country>> {
+ override val values =
+ sequenceOf(
+ listOf(
+ generateRelayItemCountry(
+ name = "Relay country Active",
+ cityNames = listOf("Relay city 1", "Relay city 2"),
+ relaysPerCity = 2
+ ),
+ generateRelayItemCountry(
+ name = "Not Enabled Relay country",
+ cityNames = listOf("Not Enabled city"),
+ relaysPerCity = 1,
+ active = false
+ ),
+ generateRelayItemCountry(
+ name = "Relay country Expanded",
+ cityNames = listOf("Normal city"),
+ relaysPerCity = 2,
+ expanded = true
+ ),
+ generateRelayItemCountry(
+ name = "Country and city Expanded",
+ cityNames = listOf("Expanded city A", "Expanded city B"),
+ relaysPerCity = 2,
+ expanded = true,
+ expandChildren = true
+ )
+ )
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt
index af6f8e992f..4d19095a6e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt
@@ -19,7 +19,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -49,12 +49,12 @@ import net.mullvad.mullvadvpn.compose.destinations.LoginDestination
import net.mullvad.mullvadvpn.compose.destinations.PaymentDestination
import net.mullvad.mullvadvpn.compose.destinations.RedeemVoucherDestination
import net.mullvad.mullvadvpn.compose.destinations.VerificationPendingDialogDestination
+import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromBottomTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.compose.util.SecureScreenWhileInView
import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle
-import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
@@ -165,15 +165,15 @@ fun AccountScreen(
// This will enable SECURE_FLAG while this screen is visible to preview screenshot
SecureScreenWhileInView()
- val context = LocalContext.current
val snackbarHostState = remember { SnackbarHostState() }
val copyTextString = stringResource(id = R.string.copied_mullvad_account_number)
val copyToClipboard = createCopyToClipboardHandle(snackbarHostState = snackbarHostState)
+ val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
LaunchedEffectCollect(uiSideEffect) { sideEffect ->
when (sideEffect) {
AccountViewModel.UiSideEffect.NavigateToLogin -> navigateToLogin()
is AccountViewModel.UiSideEffect.OpenAccountManagementPageInBrowser ->
- context.openAccountPageInBrowser(sideEffect.token)
+ openAccountPage(sideEffect.token)
is AccountViewModel.UiSideEffect.CopyAccountNumber ->
launch { copyToClipboard(sideEffect.accountNumber, copyTextString) }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index fc13e053b8..94b5ef3b5b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -1,7 +1,9 @@
package net.mullvad.mullvadvpn.compose.screen
+import android.content.Context
import android.content.Intent
import android.net.Uri
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
@@ -19,6 +21,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -35,6 +38,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -43,6 +47,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.popUpTo
+import com.ramcosta.composedestinations.result.ResultRecipient
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.NavGraphs
import net.mullvad.mullvadvpn.compose.button.ConnectionButton
@@ -58,6 +64,7 @@ import net.mullvad.mullvadvpn.compose.destinations.DeviceRevokedDestination
import net.mullvad.mullvadvpn.compose.destinations.OutOfTimeDestination
import net.mullvad.mullvadvpn.compose.destinations.SelectLocationDestination
import net.mullvad.mullvadvpn.compose.destinations.SettingsDestination
+import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.CONNECT_BUTTON_TEST_TAG
@@ -67,27 +74,30 @@ import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
+import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
+import net.mullvad.mullvadvpn.compose.util.RequestVpnPermission
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM_ANIMATION_MILLIS
import net.mullvad.mullvadvpn.constant.UNSECURE_ZOOM
import net.mullvad.mullvadvpn.constant.fallbackLatLong
-import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
import net.mullvad.mullvadvpn.lib.map.AnimatedMap
import net.mullvad.mullvadvpn.lib.map.data.GlobeColors
import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors
import net.mullvad.mullvadvpn.lib.map.data.Marker
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.Latitude
+import net.mullvad.mullvadvpn.lib.model.Longitude
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.LatLong
-import net.mullvad.mullvadvpn.model.Latitude
-import net.mullvad.mullvadvpn.model.Longitude
-import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
+import net.mullvad.mullvadvpn.util.removeHtmlTags
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
import org.koin.androidx.compose.koinViewModel
@@ -106,20 +116,31 @@ private fun PreviewConnectScreen() {
@Destination(style = HomeTransition::class)
@Composable
-fun Connect(navigator: DestinationsNavigator) {
+fun Connect(
+ navigator: DestinationsNavigator,
+ selectLocationResultRecipient: ResultRecipient<SelectLocationDestination, Boolean>
+) {
val connectViewModel: ConnectViewModel = koinViewModel()
val state by connectViewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ val launchVpnPermission =
+ rememberLauncherForActivityResult(RequestVpnPermission()) {
+ connectViewModel.requestVpnPermissionResult(it)
+ }
+
+ val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
CollectSideEffectWithLifecycle(
connectViewModel.uiSideEffect,
minActiveState = Lifecycle.State.RESUMED
) { sideEffect ->
when (sideEffect) {
is ConnectViewModel.UiSideEffect.OpenAccountManagementPageInBrowser -> {
- context.openAccountPageInBrowser(sideEffect.token)
+ openAccountPage(sideEffect.token)
}
is ConnectViewModel.UiSideEffect.OutOfTime ->
navigator.navigate(OutOfTimeDestination, true) {
@@ -131,11 +152,25 @@ fun Connect(navigator: DestinationsNavigator) {
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
+ is ConnectViewModel.UiSideEffect.NoVpnPermission -> launchVpnPermission.launch(Unit)
+ is ConnectViewModel.UiSideEffect.ConnectError ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = sideEffect.toMessage(context),
+ )
+ }
+ }
+ }
+
+ selectLocationResultRecipient.OnNavResultValue { result ->
+ if (result) {
+ connectViewModel.onConnectClick()
}
}
ConnectScreen(
state = state,
+ snackbarHostState = snackbarHostState,
onDisconnectClick = connectViewModel::onDisconnectClick,
onReconnectClick = connectViewModel::onReconnectClick,
onConnectClick = connectViewModel::onConnectClick,
@@ -170,6 +205,7 @@ fun Connect(navigator: DestinationsNavigator) {
@Composable
fun ConnectScreen(
state: ConnectUiState,
+ snackbarHostState: SnackbarHostState = SnackbarHostState(),
onDisconnectClick: () -> Unit = {},
onReconnectClick: () -> Unit = {},
onConnectClick: () -> Unit = {},
@@ -185,12 +221,13 @@ fun ConnectScreen(
val scrollState = rememberScrollState()
ScaffoldWithTopBarAndDeviceName(
- topBarColor = state.tunnelUiState.topBarColor(),
- iconTintColor = state.tunnelUiState.iconTintColor(),
+ topBarColor = state.tunnelState.topBarColor(),
+ iconTintColor = state.tunnelState.iconTintColor(),
onSettingsClicked = onSettingsClick,
onAccountClicked = onAccountClick,
deviceName = state.deviceName,
- timeLeft = state.daysLeftUntilExpiry
+ timeLeft = state.daysLeftUntilExpiry,
+ snackbarHostState = snackbarHostState
) {
var progressIndicatorBias by remember { mutableFloatStateOf(0f) }
@@ -264,12 +301,12 @@ private fun MapColumn(
val baseZoom =
animateFloatAsState(
targetValue =
- if (state.tunnelRealState is TunnelState.Connected) SECURE_ZOOM else UNSECURE_ZOOM,
+ if (state.tunnelState is TunnelState.Connected) SECURE_ZOOM else UNSECURE_ZOOM,
animationSpec = tween(SECURE_ZOOM_ANIMATION_MILLIS),
label = "baseZoom"
)
- val markers = state.tunnelRealState.toMarker(state.location)?.let { listOf(it) } ?: emptyList()
+ val markers = state.tunnelState.toMarker(state.location)?.let { listOf(it) } ?: emptyList()
AnimatedMap(
modifier = Modifier.padding(top = it.calculateTopPadding()),
@@ -308,7 +345,7 @@ private fun MapColumn(
@Composable
private fun ConnectionInfo(state: ConnectUiState) {
ConnectionStatusText(
- state = state.tunnelRealState,
+ state = state.tunnelState,
modifier = Modifier.padding(horizontal = Dimens.sideMargin)
)
Text(
@@ -365,15 +402,15 @@ private fun ButtonPanel(
onClick = onSwitchLocationClick,
showChevron = state.showLocation,
text =
- if (state.showLocation && state.selectedRelayItem != null) {
- state.selectedRelayItem.locationName
+ if (state.showLocation && state.selectedRelayItemTitle != null) {
+ state.selectedRelayItemTitle
} else {
stringResource(id = R.string.switch_location)
}
)
Spacer(modifier = Modifier.height(Dimens.buttonSpacing))
ConnectionButton(
- state = state.tunnelUiState,
+ state = state.tunnelState,
modifier =
Modifier.padding(horizontal = Dimens.sideMargin)
.padding(bottom = Dimens.screenVerticalMargin)
@@ -422,3 +459,16 @@ fun TunnelState.iconTintColor(): Color =
fun GeoIpLocation.toLatLong() =
LatLong(Latitude(latitude.toFloat()), Longitude(longitude.toFloat()))
+
+private fun ConnectViewModel.UiSideEffect.ConnectError.toMessage(context: Context): String =
+ when (this) {
+ ConnectViewModel.UiSideEffect.ConnectError.NoVpnPermission ->
+ context.getString(R.string.vpn_permission_denied_error)
+ is ConnectViewModel.UiSideEffect.ConnectError.AlwaysOnVpn ->
+ // Snackbar currently do not support annotated string
+ context
+ .getString(R.string.always_on_vpn_error_notification_content, appName)
+ .removeHtmlTags()
+ ConnectViewModel.UiSideEffect.ConnectError.Generic ->
+ context.getString(R.string.error_occurred)
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
index 3bd924a189..fc5fc62c3d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
@@ -1,7 +1,7 @@
package net.mullvad.mullvadvpn.compose.screen
+import android.content.Context
import androidx.compose.animation.animateContentSize
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -12,12 +12,16 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -27,9 +31,10 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.NavResult
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.cell.CheckableRelayLocationCell
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
@@ -44,10 +49,12 @@ import net.mullvad.mullvadvpn.compose.test.SAVE_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.textfield.SearchTextField
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
-import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsSideEffect
import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsViewModel
import org.koin.androidx.compose.koinViewModel
@@ -63,9 +70,9 @@ private fun PreviewCustomListLocationScreen() {
@Destination(style = SlideInFromRightTransition::class)
fun CustomListLocations(
navigator: DestinationsNavigator,
- backNavigator: ResultBackNavigator<CustomListResult.LocationsChanged>,
+ backNavigator: ResultBackNavigator<LocationsChanged>,
discardChangesResultRecipient: ResultRecipient<DiscardChangesDialogDestination, Boolean>,
- customListId: String,
+ customListId: CustomListId,
newList: Boolean,
) {
val customListsViewModel =
@@ -84,17 +91,27 @@ fun CustomListLocations(
}
}
+ val snackbarHostState = remember { SnackbarHostState() }
+ val context: Context = LocalContext.current
LaunchedEffectCollect(customListsViewModel.uiSideEffect) { sideEffect ->
when (sideEffect) {
is CustomListLocationsSideEffect.ReturnWithResult ->
backNavigator.navigateBack(result = sideEffect.result)
CustomListLocationsSideEffect.CloseScreen -> backNavigator.navigateBack()
+ CustomListLocationsSideEffect.Error ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.error_occurred),
+ duration = SnackbarDuration.Short
+ )
+ }
}
}
val state by customListsViewModel.uiState.collectAsStateWithLifecycle()
CustomListLocationsScreen(
state = state,
+ snackbarHostState = snackbarHostState,
onSearchTermInput = customListsViewModel::onSearchTermInput,
onSaveClick = customListsViewModel::save,
onRelaySelectionClick = customListsViewModel::onRelaySelectionClick,
@@ -108,16 +125,17 @@ fun CustomListLocations(
)
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CustomListLocationsScreen(
state: CustomListLocationsUiState,
+ snackbarHostState: SnackbarHostState = SnackbarHostState(),
onSearchTermInput: (String) -> Unit = {},
onSaveClick: () -> Unit = {},
- onRelaySelectionClick: (RelayItem, selected: Boolean) -> Unit = { _, _ -> },
+ onRelaySelectionClick: (RelayItem.Location, selected: Boolean) -> Unit = { _, _ -> },
onBackClick: () -> Unit = {}
) {
ScaffoldWithSmallTopBar(
+ snackbarHostState = snackbarHostState,
appBarTitle =
stringResource(
if (state.newList) {
@@ -201,7 +219,7 @@ private fun LazyListScope.empty(searchTerm: String) {
private fun LazyListScope.content(
uiState: CustomListLocationsUiState.Content.Data,
- onRelaySelectedChanged: (RelayItem, selected: Boolean) -> Unit,
+ onRelaySelectedChanged: (RelayItem.Location, selected: Boolean) -> Unit,
) {
items(
count = uiState.availableLocations.size,
@@ -212,7 +230,9 @@ private fun LazyListScope.content(
CheckableRelayLocationCell(
relay = country,
modifier = Modifier.animateContentSize(),
- onRelayCheckedChange = onRelaySelectedChanged,
+ onRelayCheckedChange = { item, isChecked ->
+ onRelaySelectedChanged(item as RelayItem.Location, isChecked)
+ },
selectedRelays = uiState.selectedLocations,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt
index 20a92132f1..b039f838a2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt
@@ -30,7 +30,7 @@ import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Deleted
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
@@ -38,15 +38,15 @@ import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.destinations.CreateCustomListDestination
import net.mullvad.mullvadvpn.compose.destinations.EditCustomListDestination
import net.mullvad.mullvadvpn.compose.extensions.itemsWithDivider
-import net.mullvad.mullvadvpn.compose.extensions.showSnackbar
import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.NEW_LIST_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
+import net.mullvad.mullvadvpn.lib.model.CustomList
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.Alpha60
-import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.viewmodel.CustomListsViewModel
import org.koin.androidx.compose.koinViewModel
@@ -60,8 +60,7 @@ private fun PreviewCustomListsScreen() {
@Destination(style = SlideInFromRightTransition::class)
fun CustomLists(
navigator: DestinationsNavigator,
- editCustomListResultRecipient:
- ResultRecipient<EditCustomListDestination, CustomListResult.Deleted>
+ editCustomListResultRecipient: ResultRecipient<EditCustomListDestination, Deleted>
) {
val viewModel = koinViewModel<CustomListsViewModel>()
val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -74,10 +73,9 @@ fun CustomLists(
NavResult.Canceled -> {
/* Do nothing */
}
- is NavResult.Value -> {
+ is NavResult.Value ->
scope.launch {
- snackbarHostState.currentSnackbarData?.dismiss()
- snackbarHostState.showSnackbar(
+ snackbarHostState.showSnackbarImmediately(
message =
context.getString(
R.string.delete_custom_list_message,
@@ -88,7 +86,6 @@ fun CustomLists(
onAction = { viewModel.undoDeleteCustomList(result.value.undo) }
)
}
- }
}
}
@@ -116,7 +113,7 @@ fun CustomListsScreen(
state: CustomListsUiState,
snackbarHostState: SnackbarHostState,
addCustomList: () -> Unit = {},
- openCustomList: (RelayItem.CustomList) -> Unit = {},
+ openCustomList: (CustomList) -> Unit = {},
onBackClick: () -> Unit = {}
) {
ScaffoldWithMediumTopBar(
@@ -169,15 +166,18 @@ private fun LazyListScope.loading() {
}
private fun LazyListScope.content(
- customLists: List<RelayItem.CustomList>,
- openCustomList: (RelayItem.CustomList) -> Unit
+ customLists: List<CustomList>,
+ openCustomList: (CustomList) -> Unit
) {
itemsWithDivider(
items = customLists,
- key = { item: RelayItem.CustomList -> item.id },
+ key = { item: CustomList -> item.id },
contentType = { ContentType.ITEM }
) { customList ->
- NavigationComposeCell(title = customList.name, onClick = { openCustomList(customList) })
+ NavigationComposeCell(
+ title = customList.name.value,
+ onClick = { openCustomList(customList) }
+ )
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
index e6402fc8bd..e781c12de9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
@@ -16,21 +16,27 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.popUpTo
import com.ramcosta.composedestinations.result.NavResult
import com.ramcosta.composedestinations.result.ResultRecipient
+import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
import net.mullvad.mullvadvpn.compose.button.VariantButton
@@ -42,126 +48,56 @@ import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.destinations.LoginDestination
import net.mullvad.mullvadvpn.compose.destinations.RemoveDeviceConfirmationDialogDestination
import net.mullvad.mullvadvpn.compose.destinations.SettingsDestination
-import net.mullvad.mullvadvpn.compose.state.DeviceListItemUiState
+import net.mullvad.mullvadvpn.compose.preview.DeviceListPreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.DeviceItemUiState
import net.mullvad.mullvadvpn.compose.state.DeviceListUiState
import net.mullvad.mullvadvpn.compose.transitions.DefaultTransition
-import net.mullvad.mullvadvpn.lib.common.util.parseAsDateTime
+import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
import net.mullvad.mullvadvpn.lib.theme.typeface.listItemSubText
import net.mullvad.mullvadvpn.lib.theme.typeface.listItemText
-import net.mullvad.mullvadvpn.model.Device
import net.mullvad.mullvadvpn.util.formatDate
+import net.mullvad.mullvadvpn.viewmodel.DeviceListSideEffect
import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
import org.koin.androidx.compose.koinViewModel
+import org.koin.core.parameter.parametersOf
@Composable
@Preview
-private fun PreviewDeviceListScreenTooManyDevices() {
- AppTheme {
- DeviceListScreen(
- state =
- DeviceListUiState(
- deviceUiItems =
- listOf(
- DeviceListItemUiState(
- device =
- Device(
- id = "ID1",
- name = "Name1",
- pubkey = ByteArray(10),
- created = "2012-12-12 12:12:12 UTC"
- ),
- isLoading = false
- ),
- DeviceListItemUiState(
- device =
- Device(
- id = "ID2",
- name = "Name2",
- pubkey = ByteArray(10),
- created = "2012-12-12 12:12:12 UTC"
- ),
- isLoading = false
- ),
- DeviceListItemUiState(
- device =
- Device(
- id = "ID3",
- name = "Name3",
- pubkey = ByteArray(10),
- created = "2012-12-12 12:12:12 UTC"
- ),
- isLoading = false
- ),
- DeviceListItemUiState(
- device =
- Device(
- id = "ID4",
- name = "Name4",
- pubkey = ByteArray(10),
- created = "2012-12-12 12:12:12 UTC"
- ),
- isLoading = false
- ),
- DeviceListItemUiState(
- device =
- Device(
- id = "ID5",
- name = "Name5",
- pubkey = ByteArray(10),
- created = "2012-12-12 12:12:12 UTC"
- ),
- isLoading = true
- )
- ),
- isLoading = false
- )
- )
- }
+private fun PreviewDeviceListScreenContent(
+ @PreviewParameter(DeviceListPreviewParameterProvider::class) devices: List<DeviceItemUiState>
+) {
+ AppTheme { DeviceListScreen(state = DeviceListUiState.Content(devices = devices)) }
}
@Composable
@Preview
-private fun PreviewDeviceListScreenNotTooManyDevices() {
- AppTheme {
- DeviceListScreen(
- state =
- DeviceListUiState(
- deviceUiItems =
- listOf(
- DeviceListItemUiState(
- device =
- Device(
- id = "ID",
- name = "Name",
- pubkey = ByteArray(10),
- created = "2012-12-12 12:12:12 UTC"
- ),
- isLoading = false
- )
- ),
- isLoading = false
- )
- )
- }
+private fun PreviewDeviceListScreenEmpty() {
+ AppTheme { DeviceListScreen(state = DeviceListUiState.Content(devices = emptyList())) }
}
@Composable
@Preview
-private fun PreviewDeviceListScreenEmpty() {
- AppTheme {
- DeviceListScreen(state = DeviceListUiState(deviceUiItems = emptyList(), isLoading = false))
- }
+private fun PreviewDeviceListLoading() {
+ AppTheme { DeviceListScreen(state = DeviceListUiState.Loading) }
}
@Composable
@Preview
-private fun PreviewDeviceListLoading() {
+private fun PreviewDeviceListError() {
AppTheme {
- DeviceListScreen(state = DeviceListUiState(deviceUiItems = emptyList(), isLoading = true))
+ DeviceListScreen(
+ state =
+ DeviceListUiState.Error(GetDeviceListError.Unknown(IllegalStateException("Error")))
+ )
}
}
@@ -170,9 +106,13 @@ private fun PreviewDeviceListLoading() {
fun DeviceList(
navigator: DestinationsNavigator,
accountToken: String,
- confirmRemoveResultRecipient: ResultRecipient<RemoveDeviceConfirmationDialogDestination, String>
+ confirmRemoveResultRecipient:
+ ResultRecipient<RemoveDeviceConfirmationDialogDestination, DeviceId>
) {
- val viewModel = koinViewModel<DeviceListViewModel>()
+ val viewModel =
+ koinViewModel<DeviceListViewModel>(
+ parameters = { parametersOf(AccountToken(accountToken)) }
+ )
val state by viewModel.uiState.collectAsStateWithLifecycle()
confirmRemoveResultRecipient.onNavResult {
@@ -181,13 +121,31 @@ fun DeviceList(
/* Do nothing */
}
is NavResult.Value -> {
- viewModel.removeDevice(accountToken = accountToken, deviceIdToRemove = it.value)
+ viewModel.removeDevice(deviceIdToRemove = it.value)
+ }
+ }
+ }
+
+ val snackbarHostState = remember { SnackbarHostState() }
+ val context = LocalContext.current
+ CollectSideEffectWithLifecycle(
+ viewModel.uiSideEffect,
+ minActiveState = Lifecycle.State.RESUMED
+ ) { sideEffect ->
+ when (sideEffect) {
+ DeviceListSideEffect.FailedToRemoveDevice -> {
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.failed_to_remove_device)
+ )
+ }
}
}
}
DeviceListScreen(
state = state,
+ snackbarHostState = snackbarHostState,
onBackClick = navigator::navigateUp,
onContinueWithLogin = {
navigator.navigate(LoginDestination(accountToken)) {
@@ -196,6 +154,7 @@ fun DeviceList(
}
},
onSettingsClicked = { navigator.navigate(SettingsDestination) { launchSingleTop = true } },
+ onTryAgainClicked = viewModel::fetchDevices,
navigateToRemoveDeviceConfirmationDialog = {
navigator.navigate(RemoveDeviceConfirmationDialogDestination(it)) {
launchSingleTop = true
@@ -207,9 +166,11 @@ fun DeviceList(
@Composable
fun DeviceListScreen(
state: DeviceListUiState,
+ snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
onBackClick: () -> Unit = {},
onContinueWithLogin: () -> Unit = {},
onSettingsClicked: () -> Unit = {},
+ onTryAgainClicked: () -> Unit = {},
navigateToRemoveDeviceConfirmationDialog: (device: Device) -> Unit = {}
) {
@@ -218,6 +179,7 @@ fun DeviceListScreen(
iconTintColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaTopBar),
onSettingsClicked = onSettingsClicked,
onAccountClicked = null,
+ snackbarHostState = snackbarHostState
) {
Column(
modifier = Modifier.fillMaxSize().padding(it),
@@ -232,21 +194,17 @@ fun DeviceListScreen(
.verticalScroll(scrollState)
.weight(1f)
.fillMaxWidth(),
- verticalArrangement =
- if (state.isLoading) {
- Arrangement.Center
- } else {
- Arrangement.Top
- }
) {
- if (state.isLoading) {
- DeviceListLoading()
- } else {
- DeviceListContent(
- state = state,
- navigateToRemoveDeviceConfirmationDialog =
- navigateToRemoveDeviceConfirmationDialog
- )
+ DeviceListHeader(state)
+ when (state) {
+ is DeviceListUiState.Content ->
+ DeviceListContent(
+ state,
+ navigateToRemoveDeviceConfirmationDialog =
+ navigateToRemoveDeviceConfirmationDialog
+ )
+ is DeviceListUiState.Error -> DeviceListError(onTryAgainClicked)
+ DeviceListUiState.Loading -> {}
}
}
DeviceListButtonPanel(state, onContinueWithLogin, onBackClick)
@@ -255,26 +213,38 @@ fun DeviceListScreen(
}
@Composable
-private fun ColumnScope.DeviceListLoading() {
- MullvadCircularProgressIndicatorLarge(
- modifier = Modifier.padding(Dimens.smallPadding).align(Alignment.CenterHorizontally)
- )
+private fun ColumnScope.DeviceListError(tryAgain: () -> Unit) {
+ Column(Modifier.weight(1f), verticalArrangement = Arrangement.Center) {
+ Text(
+ text = stringResource(id = R.string.failed_to_fetch_devices),
+ modifier = Modifier.padding(Dimens.smallPadding).align(Alignment.CenterHorizontally)
+ )
+ PrimaryButton(
+ onClick = tryAgain,
+ text = stringResource(id = R.string.try_again),
+ modifier =
+ Modifier.padding(
+ top = Dimens.buttonSpacing,
+ start = Dimens.sideMargin,
+ end = Dimens.sideMargin
+ )
+ )
+ }
}
@Composable
private fun ColumnScope.DeviceListContent(
- state: DeviceListUiState,
+ state: DeviceListUiState.Content,
navigateToRemoveDeviceConfirmationDialog: (Device) -> Unit
) {
- DeviceListHeader(state = state)
-
- state.deviceUiItems.forEachIndexed { index, deviceUiState ->
+ state.devices.forEachIndexed { index, (device, loading) ->
DeviceListItem(
- deviceUiState = deviceUiState,
+ device = device,
+ isLoading = loading,
) {
- navigateToRemoveDeviceConfirmationDialog(deviceUiState.device)
+ navigateToRemoveDeviceConfirmationDialog(device)
}
- if (state.deviceUiItems.lastIndex != index) {
+ if (state.devices.lastIndex != index) {
HorizontalDivider()
}
}
@@ -282,31 +252,49 @@ private fun ColumnScope.DeviceListContent(
@Composable
private fun ColumnScope.DeviceListHeader(state: DeviceListUiState) {
- Image(
- painter =
- painterResource(
- id =
- if (state.hasTooManyDevices) {
- R.drawable.icon_fail
- } else {
- R.drawable.icon_success
- }
- ),
- contentDescription = null, // No meaningful user info or action.
- modifier =
- Modifier.align(Alignment.CenterHorizontally)
- .padding(top = Dimens.iconFailSuccessTopMargin)
- .size(Dimens.bigIconSize)
- )
+ when (state) {
+ is DeviceListUiState.Content ->
+ Image(
+ painter =
+ painterResource(
+ id =
+ if (state.hasTooManyDevices) {
+ R.drawable.icon_fail
+ } else {
+ R.drawable.icon_success
+ }
+ ),
+ contentDescription = null, // No meaningful user info or action.
+ modifier =
+ Modifier.align(Alignment.CenterHorizontally)
+ .padding(top = Dimens.iconFailSuccessTopMargin)
+ .size(Dimens.bigIconSize)
+ )
+ is DeviceListUiState.Error ->
+ Image(
+ painter = painterResource(id = R.drawable.icon_fail),
+ contentDescription = null, // No meaningful user info or action.
+ modifier =
+ Modifier.align(Alignment.CenterHorizontally)
+ .padding(top = Dimens.iconFailSuccessTopMargin)
+ .size(Dimens.bigIconSize)
+ )
+ DeviceListUiState.Loading ->
+ MullvadCircularProgressIndicatorLarge(
+ modifier =
+ Modifier.align(Alignment.CenterHorizontally)
+ .padding(top = Dimens.iconFailSuccessTopMargin)
+ )
+ }
Text(
text =
stringResource(
id =
- if (state.hasTooManyDevices) {
- R.string.max_devices_warning_title
- } else {
+ if (state is DeviceListUiState.Content && !state.hasTooManyDevices) {
R.string.max_devices_resolved_title
+ } else {
+ R.string.max_devices_warning_title
}
),
style = MaterialTheme.typography.headlineSmall,
@@ -319,51 +307,48 @@ private fun ColumnScope.DeviceListHeader(state: DeviceListUiState) {
),
)
- Text(
- text =
- stringResource(
- id =
- if (state.hasTooManyDevices) {
- R.string.max_devices_warning_description
- } else {
- R.string.max_devices_resolved_description
- }
- ),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onBackground,
- modifier =
- Modifier.wrapContentHeight()
- .animateContentSize()
- .padding(
- top = Dimens.smallPadding,
- start = Dimens.sideMargin,
- end = Dimens.sideMargin,
- bottom = Dimens.spacingAboveButton
- )
- )
+ if (state is DeviceListUiState.Content) {
+ Text(
+ text =
+ stringResource(
+ id =
+ if (state.hasTooManyDevices) {
+ R.string.max_devices_warning_description
+ } else {
+ R.string.max_devices_resolved_description
+ }
+ ),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onBackground,
+ modifier =
+ Modifier.wrapContentHeight()
+ .animateContentSize()
+ .padding(
+ top = Dimens.smallPadding,
+ start = Dimens.sideMargin,
+ end = Dimens.sideMargin,
+ bottom = Dimens.spacingAboveButton
+ )
+ )
+ }
}
@Composable
-private fun DeviceListItem(
- deviceUiState: DeviceListItemUiState,
- onDeviceRemovalClicked: () -> Unit
-) {
+private fun DeviceListItem(device: Device, isLoading: Boolean, onDeviceRemovalClicked: () -> Unit) {
BaseCell(
isRowEnabled = false,
headlineContent = {
Column(modifier = Modifier.weight(1f)) {
Text(
modifier = Modifier.fillMaxWidth(),
- text = deviceUiState.device.displayName(),
+ text = device.displayName(),
style = MaterialTheme.typography.listItemText,
color = MaterialTheme.colorScheme.onPrimary
)
Text(
modifier = Modifier.fillMaxWidth(),
text =
- deviceUiState.device.created.parseAsDateTime()?.let { creationDate ->
- stringResource(id = R.string.created_x, creationDate.formatDate())
- } ?: "",
+ stringResource(id = R.string.created_x, device.creationDate.formatDate()),
style = MaterialTheme.typography.listItemSubText,
color =
MaterialTheme.colorScheme.onPrimary
@@ -373,7 +358,7 @@ private fun DeviceListItem(
}
},
bodyView = {
- if (deviceUiState.isLoading) {
+ if (isLoading) {
MullvadCircularProgressIndicatorMedium(
modifier = Modifier.padding(Dimens.smallPadding)
)
@@ -410,7 +395,7 @@ private fun DeviceListButtonPanel(
VariantButton(
text = stringResource(id = R.string.continue_login),
onClick = onContinueWithLogin,
- isEnabled = state.hasTooManyDevices.not() && state.isLoading.not(),
+ isEnabled = state is DeviceListUiState.Content && !state.hasTooManyDevices,
background = MaterialTheme.colorScheme.secondary
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt
index 9186e639c5..0deadd545c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt
@@ -29,7 +29,7 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.cell.TwoRowCell
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Deleted
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
@@ -42,11 +42,11 @@ import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.DELETE_DROPDOWN_MENU_ITEM_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.TOP_BAR_DROPDOWN_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.viewmodel.EditCustomListViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@@ -58,21 +58,16 @@ private fun PreviewEditCustomListScreen() {
EditCustomListScreen(
state =
EditCustomListState.Content(
- id = "id",
- name = "Custom list",
+ id = CustomListId("id"),
+ name = CustomListName.fromString("Custom list"),
locations =
listOf(
- RelayItem.Relay(
- "Relay",
- "Relay",
- true,
- GeographicLocationConstraint.Hostname(
- "hostname",
- "hostname",
- "hostname"
+ GeoLocationId.Hostname(
+ GeoLocationId.City(
+ GeoLocationId.Country("country"),
+ cityCode = "city"
),
- "Provider",
- Ownership.MullvadOwned
+ "hostname",
)
)
)
@@ -84,10 +79,9 @@ private fun PreviewEditCustomListScreen() {
@Destination(style = SlideInFromRightTransition::class)
fun EditCustomList(
navigator: DestinationsNavigator,
- backNavigator: ResultBackNavigator<CustomListResult.Deleted>,
- customListId: String,
- confirmDeleteListResultRecipient:
- ResultRecipient<DeleteCustomListDestination, CustomListResult.Deleted>
+ backNavigator: ResultBackNavigator<Deleted>,
+ customListId: CustomListId,
+ confirmDeleteListResultRecipient: ResultRecipient<DeleteCustomListDestination, Deleted>
) {
val viewModel =
koinViewModel<EditCustomListViewModel>(parameters = { parametersOf(customListId) })
@@ -130,21 +124,21 @@ fun EditCustomList(
@Composable
fun EditCustomListScreen(
state: EditCustomListState,
- onDeleteList: (name: String) -> Unit = {},
- onNameClicked: (id: String, name: String) -> Unit = { _, _ -> },
- onLocationsClicked: (String) -> Unit = {},
+ onDeleteList: (name: CustomListName) -> Unit = {},
+ onNameClicked: (id: CustomListId, name: CustomListName) -> Unit = { _, _ -> },
+ onLocationsClicked: (CustomListId) -> Unit = {},
onBackClick: () -> Unit = {}
) {
val title =
when (state) {
EditCustomListState.Loading,
- EditCustomListState.NotFound -> ""
+ EditCustomListState.NotFound -> null
is EditCustomListState.Content -> state.name
}
ScaffoldWithMediumTopBar(
appBarTitle = stringResource(id = R.string.edit_list),
navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) },
- actions = { Actions(onDeleteList = { onDeleteList(title) }) },
+ actions = { Actions(enabled = title != null, onDeleteList = { onDeleteList(title!!) }) },
) { modifier: Modifier ->
SpacedColumn(modifier = modifier, alignment = Alignment.Top) {
when (state) {
@@ -165,7 +159,7 @@ fun EditCustomListScreen(
// Name cell
TwoRowCell(
titleText = stringResource(id = R.string.list_name),
- subtitleText = state.name,
+ subtitleText = state.name.value,
onCellClicked = { onNameClicked(state.id, state.name) }
)
// Locations cell
@@ -186,7 +180,7 @@ fun EditCustomListScreen(
}
@Composable
-private fun Actions(onDeleteList: () -> Unit) {
+private fun Actions(enabled: Boolean, onDeleteList: () -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = { showMenu = true },
@@ -217,6 +211,7 @@ private fun Actions(onDeleteList: () -> Unit) {
onDeleteList()
showMenu = false
},
+ enabled = enabled,
modifier = Modifier.testTag(DELETE_DROPDOWN_MENU_ITEM_TEST_TAG)
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt
index bcd42d7c0c..f58b28eaca 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/FilterScreen.kt
@@ -39,10 +39,10 @@ import net.mullvad.mullvadvpn.compose.extensions.itemsWithDivider
import net.mullvad.mullvadvpn.compose.state.RelayFilterState
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.Provider
import net.mullvad.mullvadvpn.viewmodel.FilterScreenSideEffect
import net.mullvad.mullvadvpn.viewmodel.FilterViewModel
import org.koin.androidx.compose.koinViewModel
@@ -151,7 +151,7 @@ fun FilterScreen(
Ownership(ownership, state, onSelectedOwnership)
}
}
- itemWithDivider() { ProvidersHeader(providerExpanded) { providerExpanded = it } }
+ itemWithDivider { ProvidersHeader(providerExpanded) { providerExpanded = it } }
if (providerExpanded) {
itemWithDivider { AllProviders(state, onAllProviderCheckChange) }
itemsWithDivider(state.filteredProvidersByOwnership) { provider ->
@@ -215,7 +215,7 @@ private fun AllProviders(
onAllProviderCheckChange: (isChecked: Boolean) -> Unit
) {
CheckboxCell(
- providerName = stringResource(R.string.all_providers),
+ title = stringResource(R.string.all_providers),
checked = state.isAllProvidersChecked,
onCheckedChange = { isChecked -> onAllProviderCheckChange(isChecked) }
)
@@ -228,7 +228,7 @@ private fun Provider(
onSelectedProvider: (checked: Boolean, provider: Provider) -> Unit
) {
CheckboxCell(
- providerName = provider.name,
+ title = provider.providerId.value,
checked = provider in state.selectedProviders,
onCheckedChange = { checked -> onSelectedProvider(checked, provider) }
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
index 508fcf67f3..e2ee4cc240 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.screen
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -21,9 +22,12 @@ import net.mullvad.mullvadvpn.compose.destinations.ConnectDestination
import net.mullvad.mullvadvpn.compose.destinations.NoDaemonScreenDestination
import net.mullvad.mullvadvpn.compose.destinations.OutOfTimeDestination
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
+import net.mullvad.mullvadvpn.compose.util.RequestVpnPermission
import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import net.mullvad.mullvadvpn.viewmodel.DaemonScreenEvent
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
+import net.mullvad.mullvadvpn.viewmodel.VpnPermissionSideEffect
+import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel
import org.koin.androidx.compose.koinViewModel
private val changeLogDestinations = listOf(ConnectDestination, OutOfTimeDestination)
@@ -35,6 +39,7 @@ fun MullvadApp() {
val navController: NavHostController = engine.rememberNavController()
val serviceVm = koinViewModel<NoDaemonViewModel>()
+ val permissionVm = koinViewModel<VpnPermissionViewModel>()
DisposableEffect(Unit) {
navController.addOnDestinationChangedListener(serviceVm)
@@ -45,7 +50,7 @@ fun MullvadApp() {
modifier = Modifier.semantics { testTagsAsResourceId = true }.fillMaxSize(),
engine = engine,
navController = navController,
- navGraph = NavGraphs.root
+ navGraph = NavGraphs.root,
)
// Globally handle daemon dropped connection with NoDaemonScreen
@@ -68,4 +73,13 @@ fun MullvadApp() {
navController.navigate(ChangelogDestination(it).route)
}
+
+ // Ask for VPN Permission
+ val launchVpnPermission =
+ rememberLauncherForActivityResult(RequestVpnPermission()) { _ -> permissionVm.connect() }
+ LaunchedEffectCollect(permissionVm.uiSideEffect) {
+ if (it is VpnPermissionSideEffect.ShowDialog) {
+ launchVpnPermission.launch(Unit)
+ }
+ }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt
index c5c99c62f5..d557558a60 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt
@@ -47,16 +47,16 @@ import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.test.OUT_OF_TIME_SCREEN_TITLE_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
-import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.viewmodel.OutOfTimeViewModel
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.tunnel.ErrorStateCause
import org.koin.androidx.compose.koinViewModel
@Preview
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt
index 387170e8e0..7ae7a464fc 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/PrivacyDisclaimerScreen.kt
@@ -94,7 +94,7 @@ fun PrivacyDisclaimer(
launch {
try {
withTimeout(DAEMON_READY_TIMEOUT_MS) {
- (context as MainActivity).startServiceSuspend()
+ (context as MainActivity).bindService()
}
viewModel.onServiceStartedSuccessful()
} catch (e: CancellationException) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt
index 4476b8064a..7d861ea717 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt
@@ -49,6 +49,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.NavResult
+import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.spec.DestinationSpec
import kotlinx.coroutines.launch
@@ -59,8 +60,12 @@ import net.mullvad.mullvadvpn.compose.cell.IconCell
import net.mullvad.mullvadvpn.compose.cell.StatusRelayLocationCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
import net.mullvad.mullvadvpn.compose.cell.ThreeDotCell
+import net.mullvad.mullvadvpn.compose.communication.Created
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.CustomListSuccess
+import net.mullvad.mullvadvpn.compose.communication.Deleted
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
+import net.mullvad.mullvadvpn.compose.communication.Renamed
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.MullvadModalBottomSheet
@@ -73,7 +78,6 @@ import net.mullvad.mullvadvpn.compose.destinations.CustomListsDestination
import net.mullvad.mullvadvpn.compose.destinations.DeleteCustomListDestination
import net.mullvad.mullvadvpn.compose.destinations.EditCustomListNameDestination
import net.mullvad.mullvadvpn.compose.destinations.FilterScreenDestination
-import net.mullvad.mullvadvpn.compose.extensions.showSnackbar
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
@@ -83,12 +87,16 @@ import net.mullvad.mullvadvpn.compose.textfield.SearchTextField
import net.mullvad.mullvadvpn.compose.transitions.SelectLocationTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.compose.util.RunOnKeyChange
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.relaylist.RelayItem
import net.mullvad.mullvadvpn.relaylist.canAddLocation
import net.mullvad.mullvadvpn.viewmodel.SelectLocationSideEffect
import net.mullvad.mullvadvpn.viewmodel.SelectLocationViewModel
@@ -102,7 +110,15 @@ private fun PreviewSelectLocationScreen() {
searchTerm = "",
selectedOwnership = null,
selectedProvidersCount = 0,
- countries = listOf(RelayItem.Country("Country 1", "Code 1", false, emptyList())),
+ countries =
+ listOf(
+ RelayItem.Location.Country(
+ GeoLocationId.Country("Country 1"),
+ "Code 1",
+ false,
+ emptyList()
+ )
+ ),
selectedItem = null,
customLists = emptyList(),
filteredCustomLists = emptyList()
@@ -115,17 +131,17 @@ private fun PreviewSelectLocationScreen() {
}
@Destination(style = SelectLocationTransition::class)
+@Suppress("LongMethod")
@Composable
fun SelectLocation(
navigator: DestinationsNavigator,
- createCustomListDialogResultRecipient:
- ResultRecipient<CreateCustomListDestination, CustomListResult.Created>,
+ backNavigator: ResultBackNavigator<Boolean>,
+ createCustomListDialogResultRecipient: ResultRecipient<CreateCustomListDestination, Created>,
editCustomListNameDialogResultRecipient:
- ResultRecipient<EditCustomListNameDestination, CustomListResult.Renamed>,
- deleteCustomListDialogResultRecipient:
- ResultRecipient<DeleteCustomListDestination, CustomListResult.Deleted>,
+ ResultRecipient<EditCustomListNameDestination, Renamed>,
+ deleteCustomListDialogResultRecipient: ResultRecipient<DeleteCustomListDestination, Deleted>,
updateCustomListResultRecipient:
- ResultRecipient<CustomListLocationsDestination, CustomListResult.LocationsChanged>
+ ResultRecipient<CustomListLocationsDestination, LocationsChanged>
) {
val vm = koinViewModel<SelectLocationViewModel>()
val state = vm.uiState.collectAsStateWithLifecycle().value
@@ -135,7 +151,7 @@ fun SelectLocation(
LaunchedEffectCollect(vm.uiSideEffect) {
when (it) {
- SelectLocationSideEffect.CloseScreen -> navigator.navigateUp()
+ SelectLocationSideEffect.CloseScreen -> backNavigator.navigateBack(result = true, true)
is SelectLocationSideEffect.LocationAddedToCustomList ->
launch {
snackbarHostState.showResultSnackbar(
@@ -152,6 +168,13 @@ fun SelectLocation(
onUndo = vm::performAction
)
}
+ SelectLocationSideEffect.GenericError ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.error_occurred),
+ duration = SnackbarDuration.Short
+ )
+ }
}
}
@@ -177,10 +200,10 @@ fun SelectLocation(
snackbarHostState = snackbarHostState,
onSelectRelay = vm::selectRelay,
onSearchTermInput = vm::onSearchTermInput,
- onBackClick = navigator::navigateUp,
+ onBackClick = { backNavigator.navigateBack(true) },
onFilterClick = { navigator.navigate(FilterScreenDestination, true) },
onCreateCustomList = { relayItem ->
- navigator.navigate(CreateCustomListDestination(locationCode = relayItem?.code ?: "")) {
+ navigator.navigate(CreateCustomListDestination(locationCode = relayItem?.id)) {
launchSingleTop = true
}
},
@@ -191,7 +214,7 @@ fun SelectLocation(
onRemoveLocationFromList = vm::removeLocationFromList,
onEditCustomListName = {
navigator.navigate(
- EditCustomListNameDestination(customListId = it.id, initialName = it.name)
+ EditCustomListNameDestination(customListId = it.id, initialName = it.customListName)
)
},
onEditLocationsCustomList = {
@@ -200,7 +223,9 @@ fun SelectLocation(
)
},
onDeleteCustomList = {
- navigator.navigate(DeleteCustomListDestination(customListId = it.id, name = it.name))
+ navigator.navigate(
+ DeleteCustomListDestination(customListId = it.id, name = it.customListName)
+ )
}
)
}
@@ -215,13 +240,15 @@ fun SelectLocationScreen(
onSearchTermInput: (searchTerm: String) -> Unit = {},
onBackClick: () -> Unit = {},
onFilterClick: () -> Unit = {},
- onCreateCustomList: (location: RelayItem?) -> Unit = {},
+ onCreateCustomList: (location: RelayItem.Location?) -> Unit = {},
onEditCustomLists: () -> Unit = {},
removeOwnershipFilter: () -> Unit = {},
removeProviderFilter: () -> Unit = {},
- onAddLocationToList: (location: RelayItem, customList: RelayItem.CustomList) -> Unit = { _, _ ->
- },
- onRemoveLocationFromList: (location: RelayItem, customList: RelayItem.CustomList) -> Unit =
+ onAddLocationToList: (location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit =
+ { _, _ ->
+ },
+ onRemoveLocationFromList:
+ (location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit =
{ _, _ ->
},
onEditCustomListName: (RelayItem.CustomList) -> Unit = {},
@@ -252,30 +279,7 @@ fun SelectLocationScreen(
)
Column(modifier = Modifier.padding(it).background(backgroundColor).fillMaxSize()) {
- Row(modifier = Modifier.fillMaxWidth()) {
- IconButton(onClick = onBackClick) {
- Icon(
- modifier = Modifier.rotate(270f),
- painter = painterResource(id = R.drawable.icon_back),
- tint = Color.Unspecified,
- contentDescription = null,
- )
- }
- Text(
- text = stringResource(id = R.string.select_location),
- modifier = Modifier.align(Alignment.CenterVertically).weight(weight = 1f),
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.titleLarge,
- color = MaterialTheme.colorScheme.onPrimary,
- )
- IconButton(onClick = onFilterClick) {
- Icon(
- painter = painterResource(id = R.drawable.icons_more_circle),
- contentDescription = null,
- tint = Color.Unspecified,
- )
- }
- }
+ SelectLocationTopBar(onBackClick = onBackClick, onFilterClick = onFilterClick)
when (state) {
SelectLocationUiState.Loading -> {}
@@ -303,8 +307,7 @@ fun SelectLocationScreen(
}
Spacer(modifier = Modifier.height(height = Dimens.verticalSpace))
val lazyListState = rememberLazyListState()
- val selectedItemCode =
- (state as? SelectLocationUiState.Content)?.selectedItem?.code ?: ""
+ val selectedItemCode = (state as? SelectLocationUiState.Content)?.selectedItem ?: ""
RunOnKeyChange(key = selectedItemCode) {
val index = state.indexOfSelectedRelayItem()
@@ -345,7 +348,7 @@ fun SelectLocationScreen(
BottomSheetState.ShowEditCustomListBottomSheet(customList)
},
onShowEditCustomListEntryBottomSheet = {
- item: RelayItem,
+ item: RelayItem.Location,
customList: RelayItem.CustomList ->
bottomSheetState =
BottomSheetState.ShowCustomListsEntryBottomSheet(
@@ -387,6 +390,34 @@ fun SelectLocationScreen(
}
}
+@Composable
+private fun SelectLocationTopBar(onBackClick: () -> Unit, onFilterClick: () -> Unit) {
+ Row(modifier = Modifier.fillMaxWidth()) {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ modifier = Modifier.rotate(270f),
+ painter = painterResource(id = R.drawable.icon_back),
+ tint = Color.Unspecified,
+ contentDescription = null,
+ )
+ }
+ Text(
+ text = stringResource(id = R.string.select_location),
+ modifier = Modifier.align(Alignment.CenterVertically).weight(weight = 1f),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onPrimary,
+ )
+ IconButton(onClick = onFilterClick) {
+ Icon(
+ painter = painterResource(id = R.drawable.icons_more_circle),
+ contentDescription = null,
+ tint = Color.Unspecified,
+ )
+ }
+ }
+}
+
private fun LazyListScope.loading() {
item(contentType = ContentType.PROGRESS) {
MullvadCircularProgressIndicatorLarge(Modifier.testTag(CIRCULAR_PROGRESS_INDICATOR))
@@ -396,12 +427,12 @@ private fun LazyListScope.loading() {
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.customLists(
customLists: List<RelayItem.CustomList>,
- selectedItem: RelayItem?,
+ selectedItem: RelayItemId?,
backgroundColor: Color,
onSelectRelay: (item: RelayItem) -> Unit,
onShowCustomListBottomSheet: () -> Unit,
onShowEditBottomSheet: (RelayItem.CustomList) -> Unit,
- onShowEditCustomListEntryBottomSheet: (item: RelayItem, RelayItem.CustomList) -> Unit
+ onShowEditCustomListEntryBottomSheet: (item: RelayItem.Location, RelayItem.CustomList) -> Unit
) {
item(
contentType = { ContentType.HEADER },
@@ -418,18 +449,18 @@ private fun LazyListScope.customLists(
if (customLists.isNotEmpty()) {
items(
items = customLists,
- key = { item -> item.code },
+ key = { item -> item.id },
contentType = { ContentType.ITEM },
) { customList ->
StatusRelayLocationCell(
relay = customList,
// Do not show selection for locations in custom lists
- selectedItem = selectedItem as? RelayItem.CustomList,
+ selectedItem = selectedItem as? CustomListId,
onSelectRelay = onSelectRelay,
onLongClick = {
if (it is RelayItem.CustomList) {
onShowEditBottomSheet(it)
- } else if (it in customList.locations) {
+ } else if (it is RelayItem.Location && it in customList.locations) {
onShowEditCustomListEntryBottomSheet(it, customList)
}
},
@@ -456,10 +487,10 @@ private fun LazyListScope.customLists(
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.relayList(
- countries: List<RelayItem.Country>,
- selectedItem: RelayItem?,
+ countries: List<RelayItem.Location.Country>,
+ selectedItem: RelayItemId?,
onSelectRelay: (item: RelayItem) -> Unit,
- onShowLocationBottomSheet: (item: RelayItem) -> Unit,
+ onShowLocationBottomSheet: (item: RelayItem.Location) -> Unit,
) {
item(
contentType = ContentType.HEADER,
@@ -471,14 +502,14 @@ private fun LazyListScope.relayList(
}
items(
items = countries,
- key = { item -> item.code },
+ key = { item -> item.id },
contentType = { ContentType.ITEM },
) { country ->
StatusRelayLocationCell(
relay = country,
selectedItem = selectedItem,
onSelectRelay = onSelectRelay,
- onLongClick = onShowLocationBottomSheet,
+ onLongClick = { onShowLocationBottomSheet(it as RelayItem.Location) },
modifier = Modifier.animateContentSize().animateItemPlacement(),
)
}
@@ -488,10 +519,10 @@ private fun LazyListScope.relayList(
@Composable
private fun BottomSheets(
bottomSheetState: BottomSheetState?,
- onCreateCustomList: (RelayItem?) -> Unit,
+ onCreateCustomList: (RelayItem.Location?) -> Unit,
onEditCustomLists: () -> Unit,
- onAddLocationToList: (RelayItem, RelayItem.CustomList) -> Unit,
- onRemoveLocationFromList: (RelayItem, RelayItem.CustomList) -> Unit,
+ onAddLocationToList: (RelayItem.Location, RelayItem.CustomList) -> Unit,
+ onRemoveLocationFromList: (RelayItem.Location, RelayItem.CustomList) -> Unit,
onEditCustomListName: (RelayItem.CustomList) -> Unit,
onEditLocationsCustomList: (RelayItem.CustomList) -> Unit,
onDeleteCustomList: (RelayItem.CustomList) -> Unit,
@@ -566,30 +597,18 @@ private fun BottomSheets(
private fun SelectLocationUiState.indexOfSelectedRelayItem(): Int =
if (this is SelectLocationUiState.Content) {
when (selectedItem) {
- is RelayItem.Country,
- is RelayItem.City,
- is RelayItem.Relay ->
- countries.indexOfFirst { it.code == selectedItem.countryCode() } +
+ is CustomListId ->
+ filteredCustomLists.indexOfFirst { it.id == selectedItem } + EXTRA_ITEM_CUSTOM_LIST
+ is GeoLocationId ->
+ countries.indexOfFirst { it.id == selectedItem.country } +
customLists.size +
EXTRA_ITEMS_LOCATION
- is RelayItem.CustomList ->
- filteredCustomLists.indexOfFirst { it.id == selectedItem.id } +
- EXTRA_ITEM_CUSTOM_LIST
else -> -1
}
} else {
-1
}
-private fun RelayItem.countryCode(): String =
- when (this) {
- is RelayItem.Country -> this.code
- is RelayItem.City -> this.location.countryCode
- is RelayItem.Relay -> this.location.countryCode
- is RelayItem.CustomList ->
- throw IllegalArgumentException("Custom list does not have a country code")
- }
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CustomListsBottomSheet(
@@ -604,7 +623,7 @@ private fun CustomListsBottomSheet(
sheetState = sheetState,
onDismissRequest = { closeBottomSheet(false) },
modifier = Modifier.testTag(SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG)
- ) { ->
+ ) {
HeaderCell(
text = stringResource(id = R.string.edit_custom_lists),
background = Color.Unspecified
@@ -648,9 +667,9 @@ private fun LocationBottomSheet(
onBackgroundColor: Color,
sheetState: SheetState,
customLists: List<RelayItem.CustomList>,
- item: RelayItem,
- onCreateCustomList: (relayItem: RelayItem) -> Unit,
- onAddLocationToList: (location: RelayItem, customList: RelayItem.CustomList) -> Unit,
+ item: RelayItem.Location,
+ onCreateCustomList: (relayItem: RelayItem.Location) -> Unit,
+ onAddLocationToList: (location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit,
closeBottomSheet: (animate: Boolean) -> Unit
) {
MullvadModalBottomSheet(
@@ -756,15 +775,16 @@ private fun CustomListEntryBottomSheet(
onBackgroundColor: Color,
sheetState: SheetState,
customList: RelayItem.CustomList,
- item: RelayItem,
- onRemoveLocationFromList: (location: RelayItem, customList: RelayItem.CustomList) -> Unit,
+ item: RelayItem.Location,
+ onRemoveLocationFromList:
+ (location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit,
closeBottomSheet: (animate: Boolean) -> Unit
) {
MullvadModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { closeBottomSheet(false) },
modifier = Modifier.testTag(SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG)
- ) { ->
+ ) {
HeaderCell(
text = stringResource(id = R.string.remove_location_from_list, item.name),
background = Color.Unspecified
@@ -797,11 +817,10 @@ private suspend fun LazyListState.animateScrollAndCentralizeItem(index: Int) {
private suspend fun SnackbarHostState.showResultSnackbar(
context: Context,
- result: CustomListResult,
+ result: CustomListSuccess,
onUndo: (CustomListAction) -> Unit
) {
- currentSnackbarData?.dismiss()
- showSnackbar(
+ showSnackbarImmediately(
message = result.message(context),
actionLabel = context.getString(R.string.undo),
duration = SnackbarDuration.Long,
@@ -809,18 +828,19 @@ private suspend fun SnackbarHostState.showResultSnackbar(
)
}
-private fun CustomListResult.message(context: Context): String =
+private fun CustomListSuccess.message(context: Context): String =
when (this) {
- is CustomListResult.Created ->
- context.getString(R.string.location_was_added_to_list, locationName, name)
- is CustomListResult.Deleted -> context.getString(R.string.delete_custom_list_message, name)
- is CustomListResult.Renamed -> context.getString(R.string.name_was_changed_to, name)
- is CustomListResult.LocationsChanged ->
- context.getString(R.string.locations_were_changed_for, name)
+ is Created ->
+ locationNames.firstOrNull()?.let { locationName ->
+ context.getString(R.string.location_was_added_to_list, locationName, name)
+ } ?: context.getString(R.string.locations_were_changed_for, name)
+ is Deleted -> context.getString(R.string.delete_custom_list_message, name)
+ is Renamed -> context.getString(R.string.name_was_changed_to, name)
+ is LocationsChanged -> context.getString(R.string.locations_were_changed_for, name)
}
@Composable
-private fun <D : DestinationSpec<*>, R : CustomListResult> ResultRecipient<D, R>
+private fun <D : DestinationSpec<*>, R : CustomListSuccess> ResultRecipient<D, R>
.OnCustomListNavResult(
snackbarHostState: SnackbarHostState,
performAction: (action: CustomListAction) -> Unit
@@ -856,12 +876,12 @@ sealed interface BottomSheetState {
data class ShowCustomListsEntryBottomSheet(
val customList: RelayItem.CustomList,
- val item: RelayItem
+ val item: RelayItem.Location
) : BottomSheetState
data class ShowLocationBottomSheet(
val customLists: List<RelayItem.CustomList>,
- val item: RelayItem
+ val item: RelayItem.Location
) : BottomSheetState
data class ShowEditCustomListBottomSheet(val customList: RelayItem.CustomList) :
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt
index 33b8419b9c..7f9542f22a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ServerIpOverridesScreen.kt
@@ -67,10 +67,10 @@ import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightLeafTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
+import net.mullvad.mullvadvpn.lib.model.SettingsPatchError
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled
-import net.mullvad.mullvadvpn.model.SettingsPatchError
import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesUiSideEffect
import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewModel
import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewState
@@ -107,11 +107,12 @@ fun ServerIpOverrides(
LaunchedEffectCollect(vm.uiSideEffect) { sideEffect ->
when (sideEffect) {
is ServerIpOverridesUiSideEffect.ImportResult ->
- snackbarHostState.showSnackbarImmediately(
- this,
- message = sideEffect.error.toString(context),
- actionLabel = null
- )
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = sideEffect.error.toString(context),
+ actionLabel = null
+ )
+ }
}
}
@@ -119,11 +120,15 @@ fun ServerIpOverrides(
// On successful clear of overrides, show snackbar
val scope = rememberCoroutineScope()
- clearOverridesResult.OnNavResultValue {
+ clearOverridesResult.OnNavResultValue { clearSuccessful ->
scope.launch {
snackbarHostState.showSnackbarImmediately(
- this,
- message = context.getString(R.string.overrides_cleared),
+ message =
+ if (clearSuccessful) {
+ context.getString(R.string.overrides_cleared)
+ } else {
+ context.getString(R.string.error_occurred)
+ },
actionLabel = null
)
}
@@ -233,7 +238,7 @@ private fun ImportOverridesByBottomSheet(
MullvadModalBottomSheet(
sheetState = sheetState,
onDismissRequest = { showBottomSheet(false) },
- ) { ->
+ ) {
HeaderCell(
text = stringResource(id = R.string.server_ip_overrides_import_by),
background = Color.Unspecified
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
index c355eb6405..a9b7873a2f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
@@ -62,24 +62,24 @@ private fun PreviewSplitTunnelingScreen() {
AppData(
packageName = "my.package.a",
name = "TitleA",
- iconRes = R.drawable.icon_alert
+ iconRes = R.drawable.icon_alert,
),
AppData(
packageName = "my.package.b",
name = "TitleB",
- iconRes = R.drawable.icon_chevron
- )
+ iconRes = R.drawable.icon_chevron,
+ ),
),
includedApps =
listOf(
AppData(
packageName = "my.package.c",
name = "TitleC",
- iconRes = R.drawable.icon_alert
- )
+ iconRes = R.drawable.icon_alert,
+ ),
),
- showSystemApps = true
- )
+ showSystemApps = true,
+ ),
)
}
}
@@ -91,6 +91,7 @@ fun SplitTunneling(navigator: DestinationsNavigator) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val packageManager = remember(context) { context.packageManager }
+
SplitTunnelingScreen(
state = state,
onEnableSplitTunneling = viewModel::onEnableSplitTunneling,
@@ -100,7 +101,7 @@ fun SplitTunneling(navigator: DestinationsNavigator) {
onBackClick = navigator::navigateUp,
onResolveIcon = { packageName ->
packageManager.getApplicationIconBitmapOrNull(packageName)
- }
+ },
)
}
@@ -119,12 +120,12 @@ fun SplitTunnelingScreen(
ScaffoldWithMediumTopBar(
modifier = Modifier.fillMaxSize(),
appBarTitle = stringResource(id = R.string.split_tunneling),
- navigationIcon = { NavigateBackIconButton(onBackClick) }
+ navigationIcon = { NavigateBackIconButton(onBackClick) },
) { modifier, lazyListState ->
LazyColumn(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
- state = lazyListState
+ state = lazyListState,
) {
description()
enabledToggle(enabled = state.enabled, onEnableSplitTunneling = onEnableSplitTunneling)
@@ -140,7 +141,7 @@ fun SplitTunnelingScreen(
onShowSystemAppsClick = onShowSystemAppsClick,
onExcludeAppClick = onExcludeAppClick,
onIncludeAppClick = onIncludeAppClick,
- onResolveIcon = onResolveIcon
+ onResolveIcon = onResolveIcon,
)
}
}
@@ -156,7 +157,7 @@ private fun LazyListScope.enabledToggle(
HeaderSwitchComposeCell(
title = textResource(id = R.string.enable),
isToggled = enabled,
- onCellClicked = onEnableSplitTunneling
+ onCellClicked = onEnableSplitTunneling,
)
}
}
@@ -168,7 +169,7 @@ private fun LazyListScope.description() {
buildString {
appendLine(stringResource(id = R.string.split_tunneling_description))
append(stringResource(id = R.string.split_tunneling_description_warning))
- }
+ },
)
}
}
@@ -191,7 +192,7 @@ private fun LazyListScope.appList(
headerItem(
key = SplitTunnelingContentKey.EXCLUDED_APPLICATIONS,
textId = R.string.exclude_applications,
- enabled = state.enabled
+ enabled = state.enabled,
)
appItems(
apps = state.excludedApps,
@@ -199,19 +200,19 @@ private fun LazyListScope.appList(
onAppClick = onIncludeAppClick,
onResolveIcon = onResolveIcon,
enabled = state.enabled,
- excluded = true
+ excluded = true,
)
spacer()
}
systemAppsToggle(
showSystemApps = state.showSystemApps,
onShowSystemAppsClick = onShowSystemAppsClick,
- enabled = state.enabled
+ enabled = state.enabled,
)
headerItem(
key = SplitTunnelingContentKey.INCLUDED_APPLICATIONS,
textId = R.string.all_applications,
- enabled = state.enabled
+ enabled = state.enabled,
)
appItems(
apps = state.includedApps,
@@ -219,7 +220,7 @@ private fun LazyListScope.appList(
onAppClick = onExcludeAppClick,
onResolveIcon = onResolveIcon,
enabled = state.enabled,
- excluded = false
+ excluded = false,
)
}
@@ -235,7 +236,7 @@ private fun LazyListScope.appItems(
itemsIndexedWithDivider(
items = apps,
key = { _, listItem -> listItem.packageName },
- contentType = { _, _ -> ContentType.ITEM }
+ contentType = { _, _ -> ContentType.ITEM },
) { index, listItem ->
SplitTunnelingCell(
title = listItem.name,
@@ -250,9 +251,9 @@ private fun LazyListScope.appItems(
AlphaVisible
} else {
AlphaDisabled
- }
+ },
),
- onResolveIcon = onResolveIcon
+ onResolveIcon = onResolveIcon,
) {
// Move focus down unless the clicked item was the last in this
// section.
@@ -278,10 +279,10 @@ private fun LazyListScope.headerItem(key: String, textId: Int, enabled: Boolean)
AlphaVisible
} else {
AlphaDisabled
- }
+ },
),
text = stringResource(id = textId),
- background = MaterialTheme.colorScheme.primary
+ background = MaterialTheme.colorScheme.primary,
)
}
}
@@ -294,7 +295,7 @@ private fun LazyListScope.systemAppsToggle(
) {
itemWithDivider(
key = SplitTunnelingContentKey.SHOW_SYSTEM_APPLICATIONS,
- contentType = ContentType.OTHER_ITEM
+ contentType = ContentType.OTHER_ITEM,
) {
HeaderSwitchComposeCell(
title = stringResource(id = R.string.show_system_apps),
@@ -308,8 +309,8 @@ private fun LazyListScope.systemAppsToggle(
AlphaVisible
} else {
AlphaDisabled
- }
- )
+ },
+ ),
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt
index 3e5a973d93..05eb72c142 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt
@@ -141,6 +141,6 @@ fun ViewLogsScreen(
}
private fun shareText(context: Context, logContent: String) {
- val shareIntent = context.getLogsShareIntent("Share logs", logContent)
+ val shareIntent = context.getLogsShareIntent(logContent)
context.startActivity(shareIntent)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
index 5b4c907103..7eee7c1398 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.screen
+import android.content.Context
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@@ -19,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@@ -31,7 +33,6 @@ import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
-import com.ramcosta.composedestinations.result.NavResult
import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
@@ -49,6 +50,7 @@ import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell
import net.mullvad.mullvadvpn.compose.cell.NormalSwitchComposeCell
import net.mullvad.mullvadvpn.compose.cell.SelectableCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
+import net.mullvad.mullvadvpn.compose.communication.DnsDialogResult
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
import net.mullvad.mullvadvpn.compose.component.textResource
@@ -81,17 +83,19 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect
import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
+import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Port
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.SelectedObfuscation
import net.mullvad.mullvadvpn.util.hasValue
import net.mullvad.mullvadvpn.util.isCustom
-import net.mullvad.mullvadvpn.util.toValueOrNull
+import net.mullvad.mullvadvpn.util.toPortOrNull
import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsSideEffect
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
@@ -105,7 +109,7 @@ private fun PreviewVpnSettings() {
state =
VpnSettingsUiState.createDefault(
isAutoConnectEnabled = true,
- mtu = "1337",
+ mtu = Mtu(1337),
isCustomDnsEnabled = true,
customDnsItems = listOf(CustomDnsItem("0.0.0.0", false)),
),
@@ -131,45 +135,47 @@ private fun PreviewVpnSettings() {
@Destination(style = SlideInFromRightTransition::class)
@Composable
+@Suppress("LongMethod")
fun VpnSettings(
navigator: DestinationsNavigator,
- dnsDialogResult: ResultRecipient<DnsDialogDestination, Boolean>,
- customWgPortResult: ResultRecipient<WireguardCustomPortDialogDestination, Int?>
+ dnsDialogResult: ResultRecipient<DnsDialogDestination, DnsDialogResult>,
+ customWgPortResult: ResultRecipient<WireguardCustomPortDialogDestination, Port?>,
+ mtuDialogResult: ResultRecipient<MtuDialogDestination, Boolean>,
) {
val vm = koinViewModel<VpnSettingsViewModel>()
val state by vm.uiState.collectAsStateWithLifecycle()
dnsDialogResult.OnNavResultValue { result ->
- if (result) {
- vm.showApplySettingChangesWarningToast()
- } else {
- vm.onDnsDialogDismissed()
+ when (result) {
+ DnsDialogResult.Success -> vm.showApplySettingChangesWarningToast()
+ DnsDialogResult.Cancel -> vm.onDnsDialogDismissed()
+ DnsDialogResult.Error -> {
+ vm.showGenericErrorToast()
+ vm.onDnsDialogDismissed()
+ }
}
}
- customWgPortResult.onNavResult {
- when (it) {
- NavResult.Canceled -> {}
- is NavResult.Value -> {
- val port = it.value
+ customWgPortResult.OnNavResultValue { port ->
+ if (port != null) {
+ vm.onWireguardPortSelected(Constraint.Only(port))
+ } else {
+ vm.resetCustomPort()
+ }
+ }
- if (port != null) {
- vm.onWireguardPortSelected(Constraint.Only(Port(port)))
- } else {
- vm.resetCustomPort()
- }
- }
+ mtuDialogResult.OnNavResultValue { result ->
+ if (!result) {
+ vm.showGenericErrorToast()
}
}
val snackbarHostState = remember { SnackbarHostState() }
+ val context = LocalContext.current
LaunchedEffectCollect(vm.uiSideEffect) {
when (it) {
is VpnSettingsSideEffect.ShowToast ->
- launch {
- snackbarHostState.currentSnackbarData?.dismiss()
- snackbarHostState.showSnackbar(message = it.message)
- }
+ launch { snackbarHostState.showSnackbarImmediately(message = it.message(context)) }
VpnSettingsSideEffect.NavigateToDnsDialog ->
navigator.navigate(DnsDialogDestination(null, null)) { launchSingleTop = true }
}
@@ -240,7 +246,7 @@ fun VpnSettings(
navigateToWireguardPortDialog = {
val args =
WireguardCustomPortNavArgs(
- state.customWireguardPort?.toValueOrNull(),
+ state.customWireguardPort?.toPortOrNull(),
state.availablePortRanges
)
navigator.navigate(WireguardCustomPortDialogDestination(args)) {
@@ -280,7 +286,7 @@ fun VpnSettingsScreen(
onToggleBlockAdultContent: (Boolean) -> Unit = {},
onToggleBlockGambling: (Boolean) -> Unit = {},
onToggleBlockSocialMedia: (Boolean) -> Unit = {},
- navigateToMtuDialog: (mtu: Int?) -> Unit = {},
+ navigateToMtuDialog: (mtu: Mtu?) -> Unit = {},
navigateToDns: (index: Int?, address: String?) -> Unit = { _, _ -> },
onToggleDnsClick: (Boolean) -> Unit = {},
onBackClick: () -> Unit = {},
@@ -512,8 +518,8 @@ fun VpnSettingsScreen(
itemWithDivider {
SelectableCell(
title = stringResource(id = R.string.automatic),
- isSelected = state.selectedWireguardPort is Constraint.Any,
- onCellClicked = { onWireguardPortSelected(Constraint.Any()) }
+ isSelected = state.selectedWireguardPort == Constraint.Any,
+ onCellClicked = { onWireguardPortSelected(Constraint.Any) }
)
}
@@ -532,7 +538,7 @@ fun VpnSettingsScreen(
CustomPortCell(
title = stringResource(id = R.string.wireguard_custon_port_title),
isSelected = state.selectedWireguardPort.isCustom(),
- port = state.customWireguardPort?.toValueOrNull(),
+ port = state.customWireguardPort?.toPortOrNull(),
onMainCellClicked = {
if (state.customWireguardPort != null) {
onWireguardPortSelected(state.customWireguardPort)
@@ -610,10 +616,7 @@ fun VpnSettingsScreen(
}
item {
- MtuComposeCell(
- mtuValue = state.mtu,
- onEditMtu = { navigateToMtuDialog(state.mtu.toIntOrNull()) }
- )
+ MtuComposeCell(mtuValue = state.mtu, onEditMtu = { navigateToMtuDialog(state.mtu) })
}
item {
MtuSubtitle(modifier = Modifier.testTag(LAZY_LIST_LAST_ITEM_TEST_TAG))
@@ -632,3 +635,10 @@ private fun ServerIpOverrides(onServerIpOverridesClick: () -> Unit) {
onClick = onServerIpOverridesClick
)
}
+
+private fun VpnSettingsSideEffect.ShowToast.message(context: Context) =
+ when (this) {
+ VpnSettingsSideEffect.ShowToast.ApplySettingsWarning ->
+ context.getString(R.string.settings_changes_effect_warning_short)
+ VpnSettingsSideEffect.ShowToast.GenericError -> context.getString(R.string.error_occurred)
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
index 8dcbea0350..29bc0c3306 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
@@ -20,7 +20,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -47,13 +47,14 @@ import net.mullvad.mullvadvpn.compose.destinations.PaymentDestination
import net.mullvad.mullvadvpn.compose.destinations.RedeemVoucherDestination
import net.mullvad.mullvadvpn.compose.destinations.SettingsDestination
import net.mullvad.mullvadvpn.compose.destinations.VerificationPendingDialogDestination
+import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle
import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces
-import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
+import net.mullvad.mullvadvpn.lib.model.AccountToken
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.payment.model.ProductPrice
@@ -71,7 +72,7 @@ private fun PreviewWelcomeScreen() {
WelcomeScreen(
state =
WelcomeUiState(
- accountNumber = "4444555566667777",
+ accountNumber = AccountToken("4444555566667777"),
deviceName = "Happy Mole",
billingPaymentState =
PaymentState.PaymentAvailable(
@@ -126,13 +127,11 @@ fun Welcome(
}
}
- val context = LocalContext.current
-
+ val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
CollectSideEffectWithLifecycle(sideEffect = vm.uiSideEffect, Lifecycle.State.RESUMED) {
uiSideEffect ->
when (uiSideEffect) {
- is WelcomeViewModel.UiSideEffect.OpenAccountView ->
- context.openAccountPageInBrowser(uiSideEffect.token)
+ is WelcomeViewModel.UiSideEffect.OpenAccountView -> openAccountPage(uiSideEffect.token)
WelcomeViewModel.UiSideEffect.OpenConnectScreen ->
navigator.navigate(ConnectDestination) {
launchSingleTop = true
@@ -274,7 +273,7 @@ private fun AccountNumberRow(snackbarHostState: SnackbarHostState, state: Welcom
val copiedAccountNumberMessage = stringResource(id = R.string.copied_mullvad_account_number)
val copyToClipboard = createCopyToClipboardHandle(snackbarHostState = snackbarHostState)
val onCopyToClipboard = {
- copyToClipboard(state.accountNumber ?: "", copiedAccountNumberMessage)
+ copyToClipboard(state.accountNumber?.value ?: "", copiedAccountNumberMessage)
}
Row(
@@ -286,7 +285,7 @@ private fun AccountNumberRow(snackbarHostState: SnackbarHostState, state: Welcom
.padding(horizontal = Dimens.sideMargin)
) {
Text(
- text = state.accountNumber?.groupWithSpaces() ?: "",
+ text = state.accountNumber?.value?.groupWithSpaces() ?: "",
modifier = Modifier.weight(1f).padding(vertical = Dimens.smallPadding),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onPrimary
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt
index 4a1c41e562..910bdaa17f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt
@@ -1,16 +1,14 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.TransportProtocol
+import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.repository.InAppNotification
-import net.mullvad.talpid.net.TransportProtocol
data class ConnectUiState(
val location: GeoIpLocation?,
- val selectedRelayItem: RelayItem?,
- val tunnelUiState: TunnelState,
- val tunnelRealState: TunnelState,
+ val selectedRelayItemTitle: String?,
+ val tunnelState: TunnelState,
val inAddress: Triple<String, Int, TransportProtocol>?,
val outAddress: String,
val showLocation: Boolean,
@@ -21,17 +19,16 @@ data class ConnectUiState(
) {
val showLocationInfo: Boolean =
- tunnelRealState !is TunnelState.Disconnected && location?.hostname != null
+ tunnelState !is TunnelState.Disconnected && location?.hostname != null
val showLoading =
- tunnelRealState is TunnelState.Connecting || tunnelRealState is TunnelState.Disconnecting
+ tunnelState is TunnelState.Connecting || tunnelState is TunnelState.Disconnecting
companion object {
val INITIAL =
ConnectUiState(
location = null,
- selectedRelayItem = null,
- tunnelUiState = TunnelState.Disconnected(),
- tunnelRealState = TunnelState.Disconnected(),
+ selectedRelayItemTitle = null,
+ tunnelState = TunnelState.Disconnected(),
inAddress = null,
outAddress = "",
showLocation = false,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CreateCustomListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CreateCustomListUiState.kt
index 43052702bd..255e0bf561 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CreateCustomListUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CreateCustomListUiState.kt
@@ -1,5 +1,5 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError
-data class CreateCustomListUiState(val error: CustomListsError? = null)
+data class CreateCustomListUiState(val error: CreateWithLocationsError? = null)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
index 7c9c5aedec..f207d85359 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
@@ -1,6 +1,6 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItem
sealed interface CustomListLocationsUiState {
val newList: Boolean
@@ -22,7 +22,7 @@ sealed interface CustomListLocationsUiState {
data class Data(
override val newList: Boolean = false,
- val availableLocations: List<RelayItem.Country> = emptyList(),
+ val availableLocations: List<RelayItem.Location.Country> = emptyList(),
val selectedLocations: Set<RelayItem> = emptySet(),
override val searchTerm: String = "",
override val saveEnabled: Boolean = false,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt
index f055bf95d2..63e3167881 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListsUiState.kt
@@ -1,10 +1,9 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.CustomList
interface CustomListsUiState {
object Loading : CustomListsUiState
- data class Content(val customLists: List<RelayItem.CustomList> = emptyList()) :
- CustomListsUiState
+ data class Content(val customLists: List<CustomList> = emptyList()) : CustomListsUiState
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt
new file mode 100644
index 0000000000..000fc13f4a
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.usecase.customlists.DeleteWithUndoError
+
+data class DeleteCustomListUiState(val deleteError: DeleteWithUndoError?)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt
index e539dbafc6..c5c2d5fab0 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceListUiState.kt
@@ -1,16 +1,24 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.Device
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
-data class DeviceListUiState(
- val deviceUiItems: List<DeviceListItemUiState>,
- val isLoading: Boolean,
-) {
- val hasTooManyDevices = deviceUiItems.count() >= 5
+sealed interface DeviceListUiState {
+ data object Loading : DeviceListUiState
+
+ data class Error(val error: GetDeviceListError) : DeviceListUiState
+
+ data class Content(
+ val devices: List<DeviceItemUiState>,
+ ) : DeviceListUiState {
+ val hasTooManyDevices = devices.size >= MAXIMUM_DEVICES
+ }
companion object {
- val INITIAL = DeviceListUiState(deviceUiItems = emptyList(), isLoading = true)
+ val INITIAL: DeviceListUiState = Loading
}
}
-data class DeviceListItemUiState(val device: Device, val isLoading: Boolean)
+data class DeviceItemUiState(val device: Device, val isLoading: Boolean)
+
+private const val MAXIMUM_DEVICES = 5
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt
new file mode 100644
index 0000000000..9e6bcdecf8
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.usecase.customlists.RenameError
+
+data class EditCustomListNameUiState(val name: String = "", val error: RenameError? = null)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListState.kt
index 9b564bb407..fa583e6fb9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListState.kt
@@ -1,12 +1,17 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
sealed interface EditCustomListState {
data object Loading : EditCustomListState
data object NotFound : EditCustomListState
- data class Content(val id: String, val name: String, val locations: List<RelayItem>) :
- EditCustomListState
+ data class Content(
+ val id: CustomListId,
+ val name: CustomListName,
+ val locations: List<GeoLocationId>
+ ) : EditCustomListState
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt
index ad301877c4..52ef7445b0 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt
@@ -1,34 +1,34 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Providers
-import net.mullvad.mullvadvpn.relaylist.Provider
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.Providers
fun Constraint<Ownership>.toNullableOwnership(): Ownership? =
when (this) {
- is Constraint.Any -> null
+ Constraint.Any -> null
is Constraint.Only -> this.value
}
fun Ownership?.toOwnershipConstraint(): Constraint<Ownership> =
when (this) {
- null -> Constraint.Any()
+ null -> Constraint.Any
else -> Constraint.Only(this)
}
fun Constraint<Providers>.toSelectedProviders(allProviders: List<Provider>): List<Provider> =
when (this) {
- is Constraint.Any -> allProviders
+ Constraint.Any -> allProviders
is Constraint.Only ->
- value.providers.toList().mapNotNull { providerName ->
- allProviders.firstOrNull { it.name == providerName }
+ value.providers.toList().mapNotNull { provider ->
+ allProviders.firstOrNull { it.providerId == provider }
}
}
fun List<Provider>.toConstraintProviders(allProviders: List<Provider>): Constraint<Providers> =
if (size == allProviders.size) {
- Constraint.Any()
+ Constraint.Any
} else {
- Constraint.Only(Providers(map { provider -> provider.name }.toHashSet()))
+ Constraint.Only(Providers(map { provider -> provider.providerId }.toHashSet()))
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt
index 82f69e5380..0babd243da 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt
@@ -1,6 +1,6 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.AccountToken
const val MIN_ACCOUNT_LOGIN_LENGTH = 8
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt
index d72e015194..6e195d40d8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt
@@ -1,6 +1,6 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.TunnelState
data class OutOfTimeUiState(
val tunnelState: TunnelState = TunnelState.Disconnected(),
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterState.kt
index 664f03ce40..0ef8dfb9c1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayFilterState.kt
@@ -1,7 +1,7 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.Provider
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
data class RelayFilterState(
val selectedOwnership: Ownership? = null,
@@ -15,21 +15,12 @@ data class RelayFilterState(
Ownership.entries
} else {
Ownership.entries.filter { ownership ->
- selectedProviders.any { provider ->
- if (provider.mullvadOwned) {
- ownership == Ownership.MullvadOwned
- } else {
- ownership == Ownership.Rented
- }
- }
+ selectedProviders.any { provider -> provider.ownership == ownership }
}
}
val filteredProvidersByOwnership =
- when (selectedOwnership) {
- Ownership.MullvadOwned -> allProviders.filter { it.mullvadOwned }
- Ownership.Rented -> allProviders.filterNot { it.mullvadOwned }
- else -> allProviders
- }
+ if (selectedOwnership == null) allProviders
+ else allProviders.filter { provider -> provider.ownership == selectedOwnership }
val isAllProvidersChecked = allProviders.size == selectedProviders.size
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt
index 747e21d91c..79f434aad1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt
@@ -1,8 +1,9 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.relaylist.MIN_SEARCH_LENGTH
-import net.mullvad.mullvadvpn.relaylist.RelayItem
sealed interface SelectLocationUiState {
@@ -14,8 +15,8 @@ sealed interface SelectLocationUiState {
val selectedProvidersCount: Int?,
val filteredCustomLists: List<RelayItem.CustomList>,
val customLists: List<RelayItem.CustomList>,
- val countries: List<RelayItem.Country>,
- val selectedItem: RelayItem?
+ val countries: List<RelayItem.Location.Country>,
+ val selectedItem: RelayItemId?
) : SelectLocationUiState {
val hasFilter: Boolean = (selectedProvidersCount != null || selectedOwnership != null)
val inSearch = searchTerm.length >= MIN_SEARCH_LENGTH
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt
deleted file mode 100644
index 7eac74a40a..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/UpdateCustomListUiState.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package net.mullvad.mullvadvpn.compose.state
-
-import net.mullvad.mullvadvpn.model.CustomListsError
-
-data class UpdateCustomListUiState(val name: String = "", val error: CustomListsError? = null)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt
index c143dda0e8..925dc33aa9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VoucherDialogUiState.kt
@@ -1,5 +1,7 @@
package net.mullvad.mullvadvpn.compose.state
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
+
data class VoucherDialogUiState(
val voucherInput: String = "",
val voucherState: VoucherDialogState = VoucherDialogState.Default
@@ -17,5 +19,5 @@ sealed interface VoucherDialogState {
data class Success(val addedTime: Long) : VoucherDialogState
- data class Error(val errorMessage: String) : VoucherDialogState
+ data class Error(val error: RedeemVoucherError) : VoucherDialogState
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
index 75abbc7cef..dd9802db2c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
@@ -1,15 +1,16 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.DefaultDnsOptions
-import net.mullvad.mullvadvpn.model.Port
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.SelectedObfuscation
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
data class VpnSettingsUiState(
- val mtu: String,
+ val mtu: Mtu?,
val isAutoConnectEnabled: Boolean,
val isLocalNetworkSharingEnabled: Boolean,
val isCustomDnsEnabled: Boolean,
@@ -25,7 +26,7 @@ data class VpnSettingsUiState(
companion object {
fun createDefault(
- mtu: String = "",
+ mtu: Mtu? = null,
isAutoConnectEnabled: Boolean = false,
isLocalNetworkSharingEnabled: Boolean = false,
isCustomDnsEnabled: Boolean = false,
@@ -33,7 +34,7 @@ data class VpnSettingsUiState(
contentBlockersOptions: DefaultDnsOptions = DefaultDnsOptions(),
selectedObfuscation: SelectedObfuscation = SelectedObfuscation.Off,
quantumResistant: QuantumResistantState = QuantumResistantState.Off,
- selectedWireguardPort: Constraint<Port> = Constraint.Any(),
+ selectedWireguardPort: Constraint<Port> = Constraint.Any,
customWireguardPort: Constraint.Only<Port>? = null,
availablePortRanges: List<PortRange> = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt
index e43cf6bb98..02e8217172 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt
@@ -1,10 +1,11 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.TunnelState
data class WelcomeUiState(
val tunnelState: TunnelState = TunnelState.Disconnected(),
- val accountNumber: String? = null,
+ val accountNumber: AccountToken? = null,
val deviceName: String? = null,
val showSitePayment: Boolean = false,
val billingPaymentState: PaymentState? = null,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt
index 6c5e80d6ed..ce8f9989bb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Clipboard.kt
@@ -22,9 +22,7 @@ fun createCopyToClipboardHandle(
return { textToCopy: String, toastMessage: String? ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && toastMessage != null) {
scope.launch {
- // Dismiss to prevent queueing up of snackbar data.
- snackbarHostState.currentSnackbarData?.dismiss()
- snackbarHostState.showSnackbar(
+ snackbarHostState.showSnackbarImmediately(
message = toastMessage,
duration = SnackbarDuration.Short
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/PreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/PreviewData.kt
deleted file mode 100644
index 3581d1d0b4..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/PreviewData.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package net.mullvad.mullvadvpn.compose.util
-
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-
-fun generateRelayItemCountry(
- name: String,
- cityNames: List<String>,
- relaysPerCity: Int,
- active: Boolean = true,
- expanded: Boolean = false,
- expandChildren: Boolean = false,
-) =
- RelayItem.Country(
- name = name,
- code = name.generateCountryCode(),
- cities =
- cityNames.map { cityName ->
- generateRelayItemCity(
- cityName,
- name.generateCountryCode(),
- relaysPerCity,
- active,
- expandChildren
- )
- },
- expanded = expanded,
- )
-
-fun generateRelayItemCity(
- name: String,
- countryCode: String,
- numberOfRelays: Int,
- active: Boolean = true,
- expanded: Boolean = false,
-) =
- RelayItem.City(
- name = name,
- code = name.generateCityCode(),
- relays =
- List(numberOfRelays) { index ->
- generateRelayItemRelay(
- countryCode,
- name.generateCityCode(),
- generateHostname(countryCode, name.generateCityCode(), index),
- active
- )
- },
- expanded = expanded,
- location = GeographicLocationConstraint.City(countryCode, name.generateCityCode()),
- )
-
-fun generateRelayItemRelay(
- countryCode: String,
- cityCode: String,
- hostName: String,
- active: Boolean = true,
-) =
- RelayItem.Relay(
- name = hostName,
- location =
- GeographicLocationConstraint.Hostname(
- countryCode = countryCode,
- cityCode = cityCode,
- hostname = hostName,
- ),
- locationName = "$cityCode $hostName",
- active = active,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned,
- )
-
-private fun String.generateCountryCode() = (take(1) + takeLast(1)).lowercase()
-
-private fun String.generateCityCode() = take(CITY_CODE_LENGTH).lowercase()
-
-private fun generateHostname(countryCode: String, cityCode: String, index: Int) =
- "$countryCode-$cityCode-wg-${index+1}"
-
-private const val CITY_CODE_LENGTH = 3
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt
new file mode 100644
index 0000000000..13817db4bc
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/RequestVpnPermission.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.compose.util
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.VpnService
+import androidx.activity.result.contract.ActivityResultContract
+
+class RequestVpnPermission : ActivityResultContract<Unit, Boolean>() {
+ override fun createIntent(context: Context, input: Unit): Intent {
+ // We expect this permission to only be requested when the permission is missing, however,
+ // if it for some reason is called incorrectly we should return an empty intent so we avoid
+ // a crash.
+ return VpnService.prepare(context) ?: Intent()
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
+ return resultCode == Activity.RESULT_OK
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt
index 3e5b7e1618..1dcbc302ef 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Snackbar.kt
@@ -2,18 +2,21 @@ package net.mullvad.mullvadvpn.compose.util
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
+import androidx.compose.material3.SnackbarResult
+@Suppress("LongParameterList")
suspend fun SnackbarHostState.showSnackbarImmediately(
- coroutineScope: CoroutineScope,
message: String,
actionLabel: String? = null,
+ onAction: (() -> Unit) = {},
withDismissAction: Boolean = false,
+ onDismiss: (() -> Unit) = {},
duration: SnackbarDuration =
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
-) =
- coroutineScope.launch {
- currentSnackbarData?.dismiss()
- showSnackbar(message, actionLabel, withDismissAction, duration)
+) {
+ currentSnackbarData?.dismiss()
+ when (showSnackbar(message, actionLabel, withDismissAction, duration)) {
+ SnackbarResult.ActionPerformed -> onAction()
+ SnackbarResult.Dismissed -> onDismiss()
}
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt
index d58107c713..8efe66085f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt
@@ -1,9 +1,9 @@
package net.mullvad.mullvadvpn.constant
import androidx.compose.animation.core.Spring
-import net.mullvad.mullvadvpn.model.LatLong
-import net.mullvad.mullvadvpn.model.Latitude
-import net.mullvad.mullvadvpn.model.Longitude
+import net.mullvad.mullvadvpn.lib.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.Latitude
+import net.mullvad.mullvadvpn.lib.model.Longitude
const val MINIMUM_LOADING_TIME_MILLIS = 500L
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PathConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PathConstant.kt
new file mode 100644
index 0000000000..755e076721
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/PathConstant.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.constant
+
+const val GRPC_SOCKET_FILE_NAME = "rpc-socket"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt
new file mode 100644
index 0000000000..d116d929b4
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt
@@ -0,0 +1,32 @@
+package net.mullvad.mullvadvpn.di
+
+import kotlinx.coroutines.MainScope
+import net.mullvad.mullvadvpn.BuildConfig
+import net.mullvad.mullvadvpn.constant.GRPC_SOCKET_FILE_NAME
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.intent.IntentProvider
+import net.mullvad.mullvadvpn.lib.model.BuildVersion
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+
+val appModule = module {
+ single(named(RPC_SOCKET_PATH)) { "${androidContext().dataDir.path}/$GRPC_SOCKET_FILE_NAME" }
+ single {
+ ManagementService(
+ rpcSocketPath = get(named(RPC_SOCKET_PATH)),
+ extensiveLogging = BuildConfig.DEBUG,
+ scope = MainScope(),
+ )
+ }
+ single { BuildVersion(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) }
+ single { IntentProvider() }
+ single { AccountRepository(get(), get(), MainScope()) }
+ single { VpnPermissionRepository(androidContext()) }
+ single { ConnectionProxy(get(), get()) }
+}
+
+const val RPC_SOCKET_PATH = "RPC_SOCKET"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index fe02cf5b7a..b6dba8f74e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -3,42 +3,45 @@ package net.mullvad.mullvadvpn.di
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
-import android.os.Messenger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
-import net.mullvad.mullvadvpn.repository.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
import net.mullvad.mullvadvpn.repository.ChangelogRepository
import net.mullvad.mullvadvpn.repository.CustomListsRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
import net.mullvad.mullvadvpn.repository.InAppNotificationController
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.repository.ProblemReportRepository
+import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
+import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository
+import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
import net.mullvad.mullvadvpn.usecase.AccountExpiryNotificationUseCase
+import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase
import net.mullvad.mullvadvpn.usecase.ConnectivityUseCase
import net.mullvad.mullvadvpn.usecase.EmptyPaymentUseCase
+import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
+import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.usecase.PlayPaymentUseCase
-import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase
import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
import net.mullvad.mullvadvpn.util.ChangelogDataProvider
import net.mullvad.mullvadvpn.util.IChangelogDataProvider
import net.mullvad.mullvadvpn.viewmodel.AccountViewModel
@@ -69,6 +72,7 @@ import net.mullvad.mullvadvpn.viewmodel.SplashViewModel
import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
import net.mullvad.mullvadvpn.viewmodel.ViewLogsViewModel
import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel
+import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel
import org.apache.commons.validator.routines.InetAddressValidator
@@ -76,7 +80,6 @@ import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
-import org.koin.dsl.bind
import org.koin.dsl.module
val uiModule = module {
@@ -90,48 +93,47 @@ val uiModule = module {
viewModel { SplitTunnelingViewModel(get(), get(), Dispatchers.Default) }
single { ApplicationsProvider(get(), get(named(SELF_PACKAGE_NAME))) }
- single { (messenger: Messenger, dispatcher: EventDispatcher) ->
- SplitTunneling(messenger, dispatcher)
- }
-
- single { ServiceConnectionManager(androidContext()) } bind MessageHandler::class
+ single { ServiceConnectionManager(androidContext()) }
single { InetAddressValidator.getInstance() }
single { androidContext().resources }
single { androidContext().assets }
single { androidContext().contentResolver }
single { ChangelogRepository(get(named(APP_PREFERENCES_NAME)), get()) }
-
- single { AccountRepository(get()) }
single { DeviceRepository(get()) }
single {
PrivacyDisclaimerRepository(
- androidContext().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ androidContext().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE),
)
}
- single { SettingsRepository(get(), get()) }
+ single { SettingsRepository(get()) }
single { MullvadProblemReport(get()) }
- single { RelayOverridesRepository(get(), get()) }
- single { CustomListsRepository(get(), get(), get()) }
+ single { RelayOverridesRepository(get()) }
+ single { CustomListsRepository(get()) }
+ single { RelayListRepository(get()) }
+ single { RelayListFilterRepository(get()) }
+ single { VoucherRepository(get(), get()) }
+ single { SplitTunnelingRepository(get()) }
single { AccountExpiryNotificationUseCase(get()) }
single { TunnelStateNotificationUseCase(get()) }
single { VersionNotificationUseCase(get(), BuildConfig.ENABLE_IN_APP_VERSION_NOTIFICATIONS) }
single { NewDeviceNotificationUseCase(get()) }
- single { PortRangeUseCase(get()) }
- single { RelayListUseCase(get(), get()) }
single { OutOfTimeUseCase(get(), get(), MainScope()) }
single { ConnectivityUseCase(get()) }
single { SystemVpnSettingsUseCase(androidContext()) }
single { CustomListActionUseCase(get(), get()) }
+ single { SelectedLocationTitleUseCase(get(), get()) }
+ single { AvailableProvidersUseCase(get()) }
+ single { CustomListsRelayItemUseCase(get(), get()) }
+ single { CustomListRelayItemsUseCase(get(), get()) }
+ single { FilteredRelayListUseCase(get(), get()) }
+ single { LastKnownLocationUseCase(get()) }
single { InAppNotificationController(get(), get(), get(), get(), MainScope()) }
single<IChangelogDataProvider> { ChangelogDataProvider(get()) }
- single { RelayListFilterUseCase(get(), get()) }
- single { RelayListListener(get()) }
-
// Will be resolved using from either of the two PaymentModule.kt classes.
single { PaymentProvider(get()) }
@@ -146,36 +148,48 @@ val uiModule = module {
single { ProblemReportRepository() }
+ single { AppVersionInfoRepository(get(), get()) }
+
// View models
- viewModel { AccountViewModel(get(), get(), get(), get(), IS_PLAY_BUILD) }
- viewModel {
- ChangelogViewModel(get(), BuildConfig.VERSION_CODE, BuildConfig.ALWAYS_SHOW_CHANGELOG)
- }
+ viewModel { AccountViewModel(get(), get(), get(), IS_PLAY_BUILD) }
+ viewModel { ChangelogViewModel(get(), get(), BuildConfig.ALWAYS_SHOW_CHANGELOG) }
viewModel {
- ConnectViewModel(get(), get(), get(), get(), get(), get(), get(), get(), IS_PLAY_BUILD)
+ ConnectViewModel(
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ get(),
+ IS_PLAY_BUILD
+ )
}
- viewModel { DeviceListViewModel(get(), get()) }
+ viewModel { parameters -> DeviceListViewModel(get(), parameters.get()) }
viewModel { DeviceRevokedViewModel(get(), get()) }
- viewModel { MtuDialogViewModel(get()) }
+ viewModel { parameters -> MtuDialogViewModel(get(), parameters.getOrNull()) }
viewModel { parameters ->
DnsDialogViewModel(get(), get(), parameters.getOrNull(), parameters.getOrNull())
}
- viewModel { LoginViewModel(get(), get(), get(), get()) }
+ viewModel { LoginViewModel(get(), get(), get()) }
viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) }
- viewModel { SelectLocationViewModel(get(), get(), get(), get()) }
+ viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get()) }
viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) }
viewModel { SplashViewModel(get(), get(), get()) }
- viewModel { VoucherDialogViewModel(get(), get()) }
- viewModel { VpnSettingsViewModel(get(), get(), get(), get(), get()) }
+ viewModel { VoucherDialogViewModel(get()) }
+ viewModel { VpnSettingsViewModel(get(), get(), get()) }
viewModel { WelcomeViewModel(get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
viewModel { ReportProblemViewModel(get(), get()) }
viewModel { ViewLogsViewModel(get()) }
viewModel { OutOfTimeViewModel(get(), get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
viewModel { PaymentViewModel(get()) }
- viewModel { FilterViewModel(get()) }
- viewModel { parameters -> CreateCustomListDialogViewModel(parameters.get(), get()) }
+ viewModel { FilterViewModel(get(), get()) }
+ viewModel { (location: GeoLocationId?) -> CreateCustomListDialogViewModel(location, get()) }
viewModel { parameters ->
- CustomListLocationsViewModel(parameters.get(), parameters.get(), get(), get())
+ CustomListLocationsViewModel(parameters.get(), parameters.get(), get(), get(), get())
}
viewModel { parameters -> EditCustomListViewModel(parameters.get(), get()) }
viewModel { parameters ->
@@ -183,8 +197,9 @@ val uiModule = module {
}
viewModel { CustomListsViewModel(get(), get()) }
viewModel { parameters -> DeleteCustomListConfirmationViewModel(parameters.get(), get()) }
- viewModel { ServerIpOverridesViewModel(get(), get(), get(), get()) }
+ viewModel { ServerIpOverridesViewModel(get(), get()) }
viewModel { ResetServerIpOverridesConfirmationViewModel(get()) }
+ viewModel { VpnPermissionViewModel(get(), get()) }
// This view model must be single so we correctly attach lifecycle and share it with activity
single { NoDaemonViewModel(get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/provider/MullvadFileProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/provider/MullvadFileProvider.kt
index 0410117366..d15f83da0c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/provider/MullvadFileProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/provider/MullvadFileProvider.kt
@@ -24,7 +24,7 @@ enum class ProviderCacheDirectory(val directoryName: String) {
LOGS("logs")
}
-fun Context.getLogsShareIntent(shareTitle: String, logContent: String): Intent {
+fun Context.getLogsShareIntent(logContent: String): Intent {
val fileName = createShareLogFileName()
val cacheFile = createCacheFile(ProviderCacheDirectory.LOGS, fileName)
cacheFile.writeText(logContent)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt
index ad668ed9e8..2a7eeddb69 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt
@@ -1,25 +1,19 @@
package net.mullvad.mullvadvpn.relaylist
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
-private fun CustomList.toRelayItemCustomList(
- relayCountries: List<RelayItem.Country>
+fun CustomList.toRelayItemCustomList(
+ relayCountries: List<RelayItem.Location.Country>
): RelayItem.CustomList =
RelayItem.CustomList(
- id = this.id,
- customListName = CustomListName.fromString(name),
+ id = id,
+ customListName = name,
expanded = false,
- locations =
- this.locations.mapNotNull {
- relayCountries.findItemForGeographicLocationConstraint(it)
- },
+ locations = locations.mapNotNull { relayCountries.findByGeoLocationId(it) },
)
-fun List<CustomList>.toRelayItemLists(
- relayCountries: List<RelayItem.Country>
-): List<RelayItem.CustomList> = this.map { it.toRelayItemCustomList(relayCountries) }
-
fun List<RelayItem.CustomList>.filterOnSearchTerm(searchTerm: String) =
if (searchTerm.length >= MIN_SEARCH_LENGTH) {
this.filter { it.name.contains(searchTerm, ignoreCase = true) }
@@ -28,7 +22,9 @@ fun List<RelayItem.CustomList>.filterOnSearchTerm(searchTerm: String) =
}
fun RelayItem.CustomList.canAddLocation(location: RelayItem) =
- this.locations.none { it.code == location.code } &&
- this.locations.flatMap { it.descendants() }.none { it.code == location.code }
+ this.locations.none { it.id == location.id } &&
+ this.locations.flatMap { it.descendants() }.none { it.id == location.id }
+
+fun List<RelayItem.CustomList>.getById(id: CustomListId) = this.find { it.id == id }
-fun List<RelayItem.CustomList>.getById(id: String) = this.find { it.id == id }
+fun List<CustomList>.getById(id: CustomListId) = this.find { it.id == id }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Provider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Provider.kt
deleted file mode 100644
index c103976700..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Provider.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package net.mullvad.mullvadvpn.relaylist
-
-data class Provider(val name: String, val mullvadOwned: Boolean)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt
deleted file mode 100644
index af4a0084d2..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.mullvad.mullvadvpn.relaylist
-
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-
-sealed interface RelayItem {
- val name: String
- val code: String
- val active: Boolean
- val hasChildren: Boolean
-
- val locationName: String
- get() = name
-
- val expanded: Boolean
-
- data class CustomList(
- val customListName: CustomListName,
- override val expanded: Boolean,
- val id: String,
- val locations: List<RelayItem>,
- ) : RelayItem {
- override val name: String = customListName.value
- override val active
- get() = locations.any { location -> location.active }
-
- override val hasChildren
- get() = locations.isNotEmpty()
-
- override val code = id
- }
-
- data class Country(
- override val name: String,
- override val code: String,
- override val expanded: Boolean,
- val cities: List<City>
- ) : RelayItem {
- val location = GeographicLocationConstraint.Country(code)
- val relays = cities.flatMap { city -> city.relays }
- override val active
- get() = cities.any { city -> city.active }
-
- override val hasChildren
- get() = cities.isNotEmpty()
- }
-
- data class City(
- override val name: String,
- override val code: String,
- override val expanded: Boolean,
- val location: GeographicLocationConstraint.City,
- val relays: List<Relay>
- ) : RelayItem {
-
- override val active
- get() = relays.any { relay -> relay.active }
-
- override val hasChildren
- get() = relays.isNotEmpty()
- }
-
- data class Relay(
- override val name: String,
- override val locationName: String,
- override val active: Boolean,
- val location: GeographicLocationConstraint.Hostname,
- val providerName: String,
- val ownership: Ownership,
- ) : RelayItem {
- override val code = name
- override val hasChildren = false
- override val expanded = false
- }
-
- fun location(): GeoIpLocation? {
- return when (this) {
- is CustomList -> null
- is Country -> location.location
- is City -> location.location
- is Relay -> location.location
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
index 3f138dee29..a3758b25fe 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
@@ -1,69 +1,58 @@
package net.mullvad.mullvadvpn.relaylist
-import java.lang.IllegalArgumentException
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.LocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Providers
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.RelayItem
-fun RelayItem.toLocationConstraint(): LocationConstraint {
+fun RelayItem.children(): List<RelayItem> {
return when (this) {
- is RelayItem.Country -> LocationConstraint.Location(location)
- is RelayItem.City -> LocationConstraint.Location(location)
- is RelayItem.Relay -> LocationConstraint.Location(location)
- is RelayItem.CustomList -> LocationConstraint.CustomList(id)
+ is RelayItem.Location.Country -> cities
+ is RelayItem.Location.City -> relays
+ is RelayItem.CustomList -> locations
+ else -> emptyList()
}
}
-fun RelayItem.children(): List<RelayItem> {
+fun RelayItem.Location.children(): List<RelayItem.Location> {
return when (this) {
- is RelayItem.Country -> cities
- is RelayItem.City -> relays
- is RelayItem.CustomList -> locations
+ is RelayItem.Location.Country -> cities
+ is RelayItem.Location.City -> relays
else -> emptyList()
}
}
-fun RelayItem.descendants(): List<RelayItem> {
+fun RelayItem.Location.descendants(): List<RelayItem.Location> {
val children = children()
return children + children.flatMap { it.descendants() }
}
-private fun RelayItem.hasOwnership(ownershipConstraint: Constraint<Ownership>): Boolean =
+fun List<RelayItem.Location>.withDescendants(): List<RelayItem.Location> =
+ this + flatMap { it.descendants() }
+
+private fun RelayItem.Location.hasOwnership(ownershipConstraint: Constraint<Ownership>): Boolean =
if (ownershipConstraint is Constraint.Only) {
when (this) {
- is RelayItem.Country -> cities.any { it.hasOwnership(ownershipConstraint) }
- is RelayItem.City -> relays.any { it.hasOwnership(ownershipConstraint) }
- is RelayItem.Relay -> this.ownership == ownershipConstraint.value
- is RelayItem.CustomList -> locations.any { it.hasOwnership(ownershipConstraint) }
+ is RelayItem.Location.Country -> cities.any { it.hasOwnership(ownershipConstraint) }
+ is RelayItem.Location.City -> relays.any { it.hasOwnership(ownershipConstraint) }
+ is RelayItem.Location.Relay -> this.provider.ownership == ownershipConstraint.value
}
} else {
true
}
-private fun RelayItem.hasProvider(providersConstraint: Constraint<Providers>): Boolean =
+private fun RelayItem.Location.hasProvider(providersConstraint: Constraint<Providers>): Boolean =
if (providersConstraint is Constraint.Only) {
when (this) {
- is RelayItem.Country -> cities.any { it.hasProvider(providersConstraint) }
- is RelayItem.City -> relays.any { it.hasProvider(providersConstraint) }
- is RelayItem.Relay -> providersConstraint.value.providers.contains(providerName)
- is RelayItem.CustomList -> locations.any { it.hasProvider(providersConstraint) }
+ is RelayItem.Location.Country -> cities.any { it.hasProvider(providersConstraint) }
+ is RelayItem.Location.City -> relays.any { it.hasProvider(providersConstraint) }
+ is RelayItem.Location.Relay ->
+ providersConstraint.value.providers.contains(provider.providerId)
}
} else {
true
}
-fun RelayItem.filterOnOwnershipAndProvider(
- ownership: Constraint<Ownership>,
- providers: Constraint<Providers>
-): RelayItem? =
- when (this) {
- is RelayItem.City -> filterOnOwnershipAndProvider(ownership, providers)
- is RelayItem.Country -> filterOnOwnershipAndProvider(ownership, providers)
- is RelayItem.CustomList -> filterOnOwnershipAndProvider(ownership, providers)
- is RelayItem.Relay -> filterOnOwnershipAndProvider(ownership, providers)
- }
-
fun RelayItem.CustomList.filterOnOwnershipAndProvider(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>
@@ -71,20 +60,19 @@ fun RelayItem.CustomList.filterOnOwnershipAndProvider(
val newLocations =
locations.mapNotNull {
when (it) {
- is RelayItem.City -> it.filterOnOwnershipAndProvider(ownership, providers)
- is RelayItem.Country -> it.filterOnOwnershipAndProvider(ownership, providers)
- is RelayItem.CustomList ->
- throw IllegalArgumentException("CustomList can't contain CustomList")
- is RelayItem.Relay -> it.filterOnOwnershipAndProvider(ownership, providers)
+ is RelayItem.Location.Country ->
+ it.filterOnOwnershipAndProvider(ownership, providers)
+ is RelayItem.Location.City -> it.filterOnOwnershipAndProvider(ownership, providers)
+ is RelayItem.Location.Relay -> it.filterOnOwnershipAndProvider(ownership, providers)
}
}
return copy(locations = newLocations)
}
-fun RelayItem.Country.filterOnOwnershipAndProvider(
+fun RelayItem.Location.Country.filterOnOwnershipAndProvider(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>
-): RelayItem.Country? {
+): RelayItem.Location.Country? {
val cities = cities.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) }
return if (cities.isNotEmpty()) {
this.copy(cities = cities)
@@ -93,10 +81,10 @@ fun RelayItem.Country.filterOnOwnershipAndProvider(
}
}
-private fun RelayItem.City.filterOnOwnershipAndProvider(
+private fun RelayItem.Location.City.filterOnOwnershipAndProvider(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>
-): RelayItem.City? {
+): RelayItem.Location.City? {
val relays = relays.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) }
return if (relays.isNotEmpty()) {
this.copy(relays = relays)
@@ -105,10 +93,10 @@ private fun RelayItem.City.filterOnOwnershipAndProvider(
}
}
-private fun RelayItem.Relay.filterOnOwnershipAndProvider(
+private fun RelayItem.Location.Relay.filterOnOwnershipAndProvider(
ownership: Constraint<Ownership>,
providers: Constraint<Providers>
-): RelayItem.Relay? {
+): RelayItem.Location.Relay? {
return if (hasOwnership(ownership) && hasProvider(providers)) {
this
} else {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt
deleted file mode 100644
index e469aec118..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.mullvadvpn.relaylist
-
-data class RelayList(
- val customLists: List<RelayItem.CustomList>,
- val allCountries: List<RelayItem.Country>,
- val filteredCountries: List<RelayItem.Country>,
- val selectedItem: RelayItem?,
-)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt
index 78b3732734..069f0e1a08 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt
@@ -1,91 +1,15 @@
package net.mullvad.mullvadvpn.relaylist
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Relay as DaemonRelay
-import net.mullvad.mullvadvpn.model.RelayList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
-/**
- * Convert from a model.RelayList to list of relaylist.RelayCountry Non-wiregaurd relays are
- * filtered out and also relays that do not fit the ownership and provider list So are also cities
- * that only contains non-wireguard relays Countries, cities and relays are ordered by name
- */
-fun RelayList.toRelayCountries(): List<RelayItem.Country> {
- val relayCountries =
- this.countries
- .map { country ->
- val cities = mutableListOf<RelayItem.City>()
- val relayCountry = RelayItem.Country(country.name, country.code, false, cities)
-
- for (city in country.cities) {
- val relays = mutableListOf<RelayItem.Relay>()
- val relayCity =
- RelayItem.City(
- name = city.name,
- code = city.code,
- location = GeographicLocationConstraint.City(country.code, city.code),
- expanded = false,
- relays = relays
- )
-
- val validCityRelays = city.relays.filterValidRelays()
-
- for (relay in validCityRelays) {
- relays.add(
- RelayItem.Relay(
- name = relay.hostname,
- location =
- GeographicLocationConstraint.Hostname(
- country.code,
- city.code,
- relay.hostname
- ),
- locationName = "${city.name} (${relay.hostname})",
- active = relay.active,
- providerName = relay.provider,
- ownership =
- if (relay.owned) Ownership.MullvadOwned else Ownership.Rented
- )
- )
- }
- relays.sortWith(RelayNameComparator)
+fun List<RelayItem.Location.Country>.findByGeoLocationId(geoLocationId: GeoLocationId) =
+ withDescendants().firstOrNull { it.id == geoLocationId }
- if (relays.isNotEmpty()) {
- cities.add(relayCity)
- }
- }
-
- cities.sortBy { it.name }
- relayCountry
- }
- .filter { country -> country.cities.isNotEmpty() }
- .toMutableList()
-
- relayCountries.sortBy { it.name }
-
- return relayCountries.toList()
-}
-
-fun List<RelayItem.Country>.findItemForGeographicLocationConstraint(
- constraint: GeographicLocationConstraint
-) =
- when (constraint) {
- is GeographicLocationConstraint.Country -> {
- this.find { country -> country.code == constraint.countryCode }
- }
- is GeographicLocationConstraint.City -> {
- val country = this.find { country -> country.code == constraint.countryCode }
-
- country?.cities?.find { city -> city.code == constraint.cityCode }
- }
- is GeographicLocationConstraint.Hostname -> {
- val country = this.find { country -> country.code == constraint.countryCode }
-
- val city = country?.cities?.find { city -> city.code == constraint.cityCode }
-
- city?.relays?.find { relay -> relay.name == constraint.hostname }
- }
- }
+fun List<RelayItem.Location.Country>.findByGeoLocationId(geoLocationId: GeoLocationId.City) =
+ flatMap { it.cities }.firstOrNull { it.id == geoLocationId }
/**
* Filter and expand the list based on search terms If a country is matched, that country and all
@@ -94,41 +18,41 @@ fun List<RelayItem.Country>.findItemForGeographicLocationConstraint(
* expanded If a relay is matched, its parents are added and expanded and itself is also added.
*/
@Suppress("NestedBlockDepth")
-fun List<RelayItem.Country>.filterOnSearchTerm(
+fun List<RelayItem.Location.Country>.filterOnSearchTerm(
searchTerm: String,
- selectedItem: RelayItem?
-): List<RelayItem.Country> {
+ selectedItem: RelayItemId?
+): List<RelayItem.Location.Country> {
return if (searchTerm.length >= MIN_SEARCH_LENGTH) {
- val filteredCountries = mutableMapOf<String, RelayItem.Country>()
+ val filteredCountries = mutableMapOf<GeoLocationId.Country, RelayItem.Location.Country>()
this.forEach { relayCountry ->
- val cities = mutableListOf<RelayItem.City>()
+ val cities = mutableListOf<RelayItem.Location.City>()
// Try to match the search term with a country
// If we match a country, add that country and all cities and relays in that country
// Do not currently expand the country or any city
if (relayCountry.name.contains(other = searchTerm, ignoreCase = true)) {
cities.addAll(relayCountry.cities.map { city -> city.copy(expanded = false) })
- filteredCountries[relayCountry.code] =
+ filteredCountries[relayCountry.id] =
relayCountry.copy(expanded = false, cities = cities)
}
// Go through and try to match the search term with every city
relayCountry.cities.forEach { relayCity ->
- val relays = mutableListOf<RelayItem.Relay>()
+ val relays = mutableListOf<RelayItem.Location.Relay>()
// If we match and we already added the country to the filtered list just expand the
// country.
// If the country is not currently in the filtered list, add it and expand it.
// Finally if the city has not already been added to the filtered list, add it, but
// do not expand it yet.
if (relayCity.name.contains(other = searchTerm, ignoreCase = true)) {
- val value = filteredCountries[relayCountry.code]
+ val value = filteredCountries[relayCountry.id]
if (value != null) {
- filteredCountries[relayCountry.code] = value.copy(expanded = true)
+ filteredCountries[relayCountry.id] = value.copy(expanded = true)
} else {
- filteredCountries[relayCountry.code] =
+ filteredCountries[relayCountry.id] =
relayCountry.copy(expanded = true, cities = cities)
}
- if (cities.none { city -> city.code == relayCity.code }) {
+ if (cities.none { city -> city.id == relayCity.id }) {
cities.add(relayCity.copy(expanded = false))
}
}
@@ -141,14 +65,14 @@ fun List<RelayItem.Country>.filterOnSearchTerm(
// if so expand it, if not add it to the filtered list and expand it.
// Finally add the relay to the list.
if (relay.name.contains(other = searchTerm, ignoreCase = true)) {
- val value = filteredCountries[relayCountry.code]
+ val value = filteredCountries[relayCountry.id]
if (value != null) {
- filteredCountries[relayCountry.code] = value.copy(expanded = true)
+ filteredCountries[relayCountry.id] = value.copy(expanded = true)
} else {
- filteredCountries[relayCountry.code] =
+ filteredCountries[relayCountry.id] =
relayCountry.copy(expanded = true, cities = cities)
}
- val cityIndex = cities.indexOfFirst { it.code == relayCity.code }
+ val cityIndex = cities.indexOfFirst { it.id == relayCity.id }
// No city found
if (cityIndex < 0) {
@@ -169,78 +93,40 @@ fun List<RelayItem.Country>.filterOnSearchTerm(
}
}
-private fun List<DaemonRelay>.filterValidRelays(): List<DaemonRelay> = filter {
- it.isWireguardRelay
-}
-
/** Expand the parent(s), if any, for the current selected item */
-private fun List<RelayItem.Country>.expandItemForSelection(
- selectedItem: RelayItem?
-): List<RelayItem.Country> {
- return selectedItem?.let {
- when (selectedItem) {
- is RelayItem.Country -> {
- this
- }
- is RelayItem.City -> {
- this.map { country ->
- if (country.code == selectedItem.location.countryCode) {
- country.copy(expanded = true)
- } else {
- country
- }
- }
- }
- is RelayItem.Relay -> {
- this.map { country ->
- if (country.code == selectedItem.location.countryCode) {
- country.copy(
- expanded = true,
- cities =
- country.cities.map { city ->
- if (city.code == selectedItem.location.cityCode) {
- city.copy(expanded = true)
- } else {
- city
- }
+private fun List<RelayItem.Location.Country>.expandItemForSelection(
+ selectedItem: RelayItemId?
+): List<RelayItem.Location.Country> {
+ selectedItem ?: return this
+ return when (selectedItem) {
+ is CustomListId,
+ is GeoLocationId.Country -> this
+ is GeoLocationId.City ->
+ map { if (it.id == selectedItem.country) it.copy(expanded = true) else it }
+ is GeoLocationId.Hostname -> {
+ map { country ->
+ if (country.id == selectedItem.country) {
+ country.copy(
+ expanded = true,
+ cities =
+ country.cities.map { city ->
+ if (city.id == selectedItem.city) {
+ city.copy(expanded = true)
+ } else {
+ city
}
- )
- } else {
- country
- }
- }
- }
- is RelayItem.CustomList -> this
- }
- } ?: this
-}
-
-@Suppress("NestedBlockDepth", "ReturnCount")
-fun RelayList.getGeographicLocationConstraintByCode(code: String): GeographicLocationConstraint? {
- countries.forEach { country ->
- val countryCode = country.code
- if (country.code == code) {
- return GeographicLocationConstraint.Country(countryCode)
- }
- country.cities.forEach { city ->
- val cityCode = city.code
- if (city.code == code) {
- return GeographicLocationConstraint.City(countryCode, city.code)
- }
- city.relays.forEach { relay ->
- if (relay.hostname == code) {
- return GeographicLocationConstraint.Hostname(
- countryCode,
- cityCode,
- relay.hostname
+ },
)
+ } else {
+ country
}
}
}
}
- return null
}
-fun List<RelayItem.Country>.getRelayItemsByCodes(codes: List<String>): List<RelayItem> =
- this.filter { codes.contains(it.code) } +
- this.flatMap { it.descendants() }.filter { codes.contains(it.code) }
+fun List<RelayItem.Location.Country>.getRelayItemsByCodes(
+ codes: List<GeoLocationId>
+): List<RelayItem.Location> =
+ this.filter { codes.contains(it.id) } +
+ this.flatMap { it.descendants() }.filter { codes.contains(it.id) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt
deleted file mode 100644
index c062fd1466..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.mullvad.mullvadvpn.relaylist
-
-internal object RelayNameComparator : Comparator<RelayItem.Relay> {
- override fun compare(o1: RelayItem.Relay, o2: RelayItem.Relay): Int {
- val partitions1 = o1.name.split(regex)
- val partitions2 = o2.name.split(regex)
- return if (partitions1.size > partitions2.size) partitions1 compareWith partitions2
- else -(partitions2 compareWith partitions1)
- }
-
- private infix fun List<String>.compareWith(other: List<String>): Int {
- this.forEachIndexed { index, s ->
- if (other.size <= index) return 1
- val partsCompareResult = compareStringOrInt(other[index], s)
- if (partsCompareResult != 0) return partsCompareResult
- }
- return 0
- }
-
- private fun compareStringOrInt(s1: String, s2: String): Int {
- val int1 = s1.toIntOrNull()
- val int2 = s2.toIntOrNull()
- return if (int1 == null || int2 == null || int1 == int2) {
- s2.compareTo(s1)
- } else {
- int2.compareTo(int1)
- }
- }
-
- private val regex = "(?<=\\d)(?=\\D)|(?<=\\D)(?=\\d)".toRegex()
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AccountRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AccountRepository.kt
deleted file mode 100644
index 369f3e8fee..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AccountRepository.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package net.mullvad.mullvadvpn.repository
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.AccountCreationResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.AccountHistory
-import net.mullvad.mullvadvpn.model.LoginResult
-
-class AccountRepository(
- private val messageHandler: MessageHandler,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val accountCreationEvents: SharedFlow<AccountCreationResult> =
- messageHandler
- .events<Event.AccountCreationEvent>()
- .map { it.result }
- .shareIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed())
-
- val accountExpiryState: StateFlow<AccountExpiry> =
- messageHandler
- .events<Event.AccountExpiryEvent>()
- .map { it.expiry }
- .stateIn(CoroutineScope(dispatcher), SharingStarted.Eagerly, AccountExpiry.Missing)
-
- val accountHistory: StateFlow<AccountHistory> =
- messageHandler
- .events<Event.AccountHistoryEvent>()
- .map { it.history }
- .onStart { fetchAccountHistory() }
- .stateIn(CoroutineScope(dispatcher), SharingStarted.Lazily, AccountHistory.Missing)
-
- private val loginEvents: SharedFlow<LoginResult> =
- messageHandler
- .events<Event.LoginEvent>()
- .map { it.result }
- .shareIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed())
-
- suspend fun createAccount(): AccountCreationResult =
- withContext(dispatcher) {
- val deferred = async { accountCreationEvents.first() }
- messageHandler.trySendRequest(Request.CreateAccount)
- deferred.await().also { fetchAccountHistory() }
- }
-
- suspend fun login(accountToken: String): LoginResult =
- withContext(Dispatchers.IO) {
- val deferred = async { loginEvents.first() }
- messageHandler.trySendRequest(Request.Login(accountToken))
- deferred.await().also { fetchAccountHistory() }
- }
-
- fun logout() {
- messageHandler.trySendRequest(Request.Logout)
- }
-
- fun fetchAccountExpiry() {
- messageHandler.trySendRequest(Request.FetchAccountExpiry)
- }
-
- fun fetchAccountHistory() {
- messageHandler.trySendRequest(Request.FetchAccountHistory)
- }
-
- fun clearAccountHistory() {
- messageHandler.trySendRequest(Request.ClearAccountHistory)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt
index 0832f434a5..fd67a6c17a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt
@@ -1,79 +1,68 @@
package net.mullvad.mullvadvpn.repository
-import kotlinx.coroutines.flow.first
+import arrow.core.Either
+import arrow.core.raise.either
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapNotNull
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.CreateCustomListResult
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.CustomListsError
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.UpdateCustomListResult
-import net.mullvad.mullvadvpn.relaylist.getGeographicLocationConstraintByCode
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
-import net.mullvad.mullvadvpn.util.firstOrNullWithTimeout
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.common.util.firstOrNullWithTimeout
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.GetCustomListError
+import net.mullvad.mullvadvpn.lib.model.UpdateCustomListLocationsError
+import net.mullvad.mullvadvpn.lib.model.UpdateCustomListNameError
class CustomListsRepository(
- private val messageHandler: MessageHandler,
- private val settingsRepository: SettingsRepository,
- private val relayListListener: RelayListListener
+ private val managementService: ManagementService,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
- suspend fun createCustomList(name: CustomListName): CreateCustomListResult {
- val result = messageHandler.trySendRequest(Request.CreateCustomList(name.value))
+ val customLists: StateFlow<List<CustomList>?> =
+ managementService.settings
+ .mapNotNull { it.customLists }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.Eagerly, null)
- return if (result) {
- messageHandler.events<Event.CreateCustomListResultEvent>().first().result
- } else {
- CreateCustomListResult.Error(CustomListsError.OtherError)
- }
- }
+ suspend fun createCustomList(name: CustomListName) = managementService.createCustomList(name)
- fun deleteCustomList(id: String) = messageHandler.trySendRequest(Request.DeleteCustomList(id))
+ suspend fun deleteCustomList(id: CustomListId) = managementService.deleteCustomList(id)
- private suspend fun updateCustomList(customList: CustomList): UpdateCustomListResult {
- val result = messageHandler.trySendRequest(Request.UpdateCustomList(customList))
+ private suspend fun updateCustomList(customList: CustomList) =
+ managementService.updateCustomList(customList)
- return if (result) {
- messageHandler.events<Event.UpdateCustomListResultEvent>().first().result
- } else {
- UpdateCustomListResult.Error(CustomListsError.OtherError)
- }
+ suspend fun updateCustomListName(
+ id: CustomListId,
+ name: CustomListName
+ ): Either<UpdateCustomListNameError, Unit> = either {
+ val customList = getCustomListById(id).bind()
+ updateCustomList(customList.copy(name = name))
+ .mapLeft(UpdateCustomListNameError::from)
+ .bind()
}
- suspend fun updateCustomListLocationsFromCodes(
- id: String,
- locationCodes: List<String>
- ): UpdateCustomListResult =
- updateCustomListLocations(
- id = id,
- locations =
- ArrayList(locationCodes.mapNotNull { getGeographicLocationConstraintByCode(it) })
- )
-
- suspend fun updateCustomListName(id: String, name: CustomListName): UpdateCustomListResult =
- getCustomListById(id)?.let { updateCustomList(it.copy(name = name.value)) }
- ?: UpdateCustomListResult.Error(CustomListsError.OtherError)
-
- private suspend fun updateCustomListLocations(
- id: String,
- locations: ArrayList<GeographicLocationConstraint>
- ): UpdateCustomListResult =
- awaitCustomListById(id)?.let { updateCustomList(it.copy(locations = locations)) }
- ?: UpdateCustomListResult.Error(CustomListsError.OtherError)
-
- private suspend fun awaitCustomListById(id: String): CustomList? =
- settingsRepository.settingsUpdates
- .mapNotNull { settings -> settings?.customLists?.customLists?.find { it.id == id } }
- .firstOrNullWithTimeout(GET_CUSTOM_LIST_TIMEOUT_MS)
-
- fun getCustomListById(id: String): CustomList? =
- settingsRepository.settingsUpdates.value?.customLists?.customLists?.find { it.id == id }
+ suspend fun updateCustomListLocations(
+ id: CustomListId,
+ locations: List<GeoLocationId>
+ ): Either<UpdateCustomListLocationsError, Unit> = either {
+ val customList = getCustomListById(id).bind()
+ updateCustomList(customList.copy(locations = locations))
+ .mapLeft(UpdateCustomListLocationsError::from)
+ .bind()
+ }
- private fun getGeographicLocationConstraintByCode(code: String): GeographicLocationConstraint? =
- relayListListener.relayListEvents.value.getGeographicLocationConstraintByCode(code)
+ suspend fun getCustomListById(id: CustomListId): Either<GetCustomListError, CustomList> =
+ either {
+ customLists
+ .mapNotNull { it?.find { customList -> customList.id == id } }
+ .firstOrNullWithTimeout(GET_CUSTOM_LIST_TIMEOUT_MS)
+ ?: raise(GetCustomListError(id))
+ }
+ .mapLeft { GetCustomListError(id) }
companion object {
private const val GET_CUSTOM_LIST_TIMEOUT_MS = 5000L
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/DeviceRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/DeviceRepository.kt
deleted file mode 100644
index 4fa211c874..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/DeviceRepository.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-package net.mullvad.mullvadvpn.repository
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withTimeoutOrNull
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.model.DeviceList
-import net.mullvad.mullvadvpn.model.DeviceListEvent
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.deviceDataSource
-
-class DeviceRepository(
- private val serviceConnectionManager: ServiceConnectionManager,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val cachedDeviceList = MutableStateFlow<DeviceList>(DeviceList.Unavailable)
-
- val deviceState =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- state.container.deviceDataSource.deviceStateUpdates
- } else {
- flowOf(DeviceState.Unknown)
- }
- }
- .stateIn(CoroutineScope(dispatcher), SharingStarted.Eagerly, DeviceState.Initial)
-
- private val deviceListEvents =
- serviceConnectionManager.connectionState.flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- state.container.deviceDataSource.deviceListUpdates
- } else {
- emptyFlow()
- }
- }
-
- val deviceList =
- deviceListEvents
- .map {
- if (it is DeviceListEvent.Available) {
- cachedDeviceList.value = DeviceList.Available(it.devices)
- DeviceList.Available(it.devices)
- } else {
- DeviceList.Error
- }
- }
- .onStart {
- if (cachedDeviceList.value is DeviceList.Available) {
- emit(cachedDeviceList.value)
- }
- }
- .shareIn(CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed())
-
- val deviceRemovalEvent: SharedFlow<Event.DeviceRemovalEvent> =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- state.container.deviceDataSource.deviceRemovalResult
- } else {
- emptyFlow()
- }
- }
- .shareIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed())
-
- fun refreshDeviceState() {
- serviceConnectionManager.deviceDataSource()?.refreshDevice()
- }
-
- fun removeDevice(accountToken: String, deviceId: String) {
- serviceConnectionManager.deviceDataSource()?.removeDevice(accountToken, deviceId)
- }
-
- fun refreshDeviceList(accountToken: String) {
- serviceConnectionManager.deviceDataSource()?.refreshDeviceList(accountToken)
- }
-
- fun clearCache() {
- cachedDeviceList.value = DeviceList.Unavailable
- }
-
- private fun updateCache(event: DeviceListEvent, accountToken: String) {
- cachedDeviceList.value =
- if (event is DeviceListEvent.Available && event.accountToken == accountToken) {
- DeviceList.Available(event.devices)
- } else if (event is DeviceListEvent.Error) {
- DeviceList.Error
- } else {
- DeviceList.Unavailable
- }
- }
-
- suspend fun refreshAndAwaitDeviceListWithTimeout(
- accountToken: String,
- shouldClearCache: Boolean,
- shouldOverrideCache: Boolean,
- timeoutMillis: Long,
- ): DeviceListEvent {
- if (shouldClearCache) {
- clearCache()
- }
-
- val result =
- withTimeoutOrNull(timeoutMillis) {
- deviceListEvents.onStart { refreshDeviceList(accountToken) }.firstOrNull()
- ?: DeviceListEvent.Error
- } ?: DeviceListEvent.Error
-
- if (shouldOverrideCache) {
- updateCache(result, accountToken)
- }
-
- return result
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt
index 0751d0b1f7..decff575f8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/InAppNotificationController.kt
@@ -1,17 +1,16 @@
package net.mullvad.mullvadvpn.repository
-import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.model.ErrorState
import net.mullvad.mullvadvpn.ui.VersionInfo
import net.mullvad.mullvadvpn.usecase.AccountExpiryNotificationUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase
import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase
-import net.mullvad.talpid.tunnel.ErrorState
import org.joda.time.DateTime
enum class StatusLevel {
@@ -21,7 +20,6 @@ enum class StatusLevel {
}
sealed class InAppNotification {
- val uuid: UUID = UUID.randomUUID()
abstract val statusLevel: StatusLevel
abstract val priority: Long
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepository.kt
new file mode 100644
index 0000000000..9251cac65c
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepository.kt
@@ -0,0 +1,39 @@
+package net.mullvad.mullvadvpn.repository
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Providers
+
+class RelayListFilterRepository(
+ private val managementService: ManagementService,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO
+) {
+ val selectedOwnership: StateFlow<Constraint<Ownership>> =
+ managementService.settings
+ .map { settings -> settings.relaySettings.relayConstraints.ownership }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), Constraint.Any)
+
+ val selectedProviders: StateFlow<Constraint<Providers>> =
+ managementService.settings
+ .map { settings -> settings.relaySettings.relayConstraints.providers }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), Constraint.Any)
+
+ suspend fun updateSelectedOwnershipAndProviderFilter(
+ ownership: Constraint<Ownership>,
+ providers: Constraint<Providers>
+ ) = managementService.setOwnershipAndProviders(ownership, providers)
+
+ suspend fun updateSelectedOwnership(value: Constraint<Ownership>) =
+ managementService.setOwnership(value)
+
+ suspend fun updateSelectedProviders(value: Constraint<Providers>) =
+ managementService.setProviders(value)
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt
new file mode 100644
index 0000000000..7d9846c31b
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt
@@ -0,0 +1,53 @@
+package net.mullvad.mullvadvpn.repository
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData
+
+class RelayListRepository(
+ private val managementService: ManagementService,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO
+) {
+ val relayList: StateFlow<List<RelayItem.Location.Country>> =
+ managementService.relayCountries.stateIn(
+ CoroutineScope(dispatcher),
+ SharingStarted.WhileSubscribed(),
+ emptyList()
+ )
+
+ val wireguardEndpointData: StateFlow<WireguardEndpointData> =
+ managementService.wireguardEndpointData.stateIn(
+ CoroutineScope(dispatcher),
+ SharingStarted.WhileSubscribed(),
+ defaultWireguardEndpointData()
+ )
+
+ val selectedLocation: StateFlow<Constraint<RelayItemId>> =
+ managementService.settings
+ .map { it.relaySettings.relayConstraints.location }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), Constraint.Any)
+
+ val portRanges: Flow<List<PortRange>> =
+ wireguardEndpointData.map { it.portRanges }.distinctUntilChanged()
+
+ suspend fun updateSelectedRelayLocation(value: RelayItemId) =
+ managementService.setRelayLocation(value)
+
+ suspend fun updateSelectedWireguardConstraints(value: WireguardConstraints) =
+ managementService.setWireguardConstraints(value)
+
+ private fun defaultWireguardEndpointData() = WireguardEndpointData(emptyList())
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt
index 835cab4710..ddc6a6f529 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt
@@ -5,40 +5,21 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.RelayOverride
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
-import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.RelayOverride
class RelayOverridesRepository(
- private val serviceConnectionManager: ServiceConnectionManager,
- private val messageHandler: MessageHandler,
+ private val managementService: ManagementService,
dispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
- fun clearAllOverrides() {
- messageHandler.trySendRequest(Request.ClearAllRelayOverrides)
- }
+ suspend fun clearAllOverrides() = managementService.clearAllRelayOverrides()
+
+ suspend fun applySettingsPatch(json: String) = managementService.applySettingsPatch(json)
val relayOverrides: StateFlow<List<RelayOverride>?> =
- serviceConnectionManager.connectionState
- .flatMapReadyConnectionOrDefault(flowOf()) { state ->
- callbackFlowFromNotifier(state.container.settingsListener.settingsNotifier)
- }
- .mapNotNull { it?.relayOverrides?.toList() }
- .onStart {
- serviceConnectionManager
- .settingsListener()
- ?.settingsNotifier
- ?.latestEvent
- ?.relayOverrides
- ?.toList()
- }
+ managementService.settings
+ .mapNotNull { it.relayOverrides }
.stateIn(CoroutineScope(dispatcher), SharingStarted.Eagerly, null)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
index 7d61feaf0c..e2469f626f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
@@ -4,107 +4,66 @@ import java.net.InetAddress
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
-import net.mullvad.mullvadvpn.lib.ipc.Event.ApplyJsonSettingsResult
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.CustomDnsOptions
-import net.mullvad.mullvadvpn.model.DefaultDnsOptions
-import net.mullvad.mullvadvpn.model.DnsOptions
-import net.mullvad.mullvadvpn.model.DnsState
-import net.mullvad.mullvadvpn.model.ObfuscationSettings
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.customDns
-import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
-import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions
+import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
+import net.mullvad.mullvadvpn.lib.model.DnsOptions
+import net.mullvad.mullvadvpn.lib.model.DnsState
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.Settings
class SettingsRepository(
- private val serviceConnectionManager: ServiceConnectionManager,
- private val messageHandler: MessageHandler,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ private val managementService: ManagementService,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val settingsUpdates: StateFlow<Settings?> =
- serviceConnectionManager.connectionState
- .flatMapReadyConnectionOrDefault(flowOf()) { state ->
- callbackFlowFromNotifier(state.container.settingsListener.settingsNotifier)
- }
- .onStart { serviceConnectionManager.settingsListener()?.settingsNotifier?.latestEvent }
- .stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), null)
+ managementService.settings.stateIn(
+ CoroutineScope(dispatcher),
+ SharingStarted.WhileSubscribed(),
+ null
+ )
- fun setDnsOptions(
+ suspend fun setDnsOptions(
isCustomDnsEnabled: Boolean,
dnsList: List<InetAddress>,
contentBlockersOptions: DefaultDnsOptions
- ) {
- updateDnsSettings {
+ ) =
+ managementService.setDnsOptions(
DnsOptions(
state = if (isCustomDnsEnabled) DnsState.Custom else DnsState.Default,
customOptions = CustomDnsOptions(ArrayList(dnsList)),
defaultOptions = contentBlockersOptions
)
- }
- }
+ )
- fun setDnsState(
+ suspend fun setDnsState(
state: DnsState,
- ) {
- updateDnsSettings { it.copy(state = state) }
- }
+ ) = managementService.setDnsState(state)
- fun updateCustomDnsList(update: (List<InetAddress>) -> List<InetAddress>) {
- updateDnsSettings { dnsOptions ->
- val newDnsList = ArrayList(update(dnsOptions.customOptions.addresses.map { it }))
- dnsOptions.copy(
- state = if (newDnsList.isEmpty()) DnsState.Default else DnsState.Custom,
- customOptions =
- CustomDnsOptions(
- addresses = newDnsList,
- )
- )
- }
- }
+ suspend fun deleteCustomDns(address: InetAddress) = managementService.deleteCustomDns(address)
+
+ suspend fun setCustomDns(index: Int, address: InetAddress) =
+ managementService.setCustomDns(index, address)
- private fun updateDnsSettings(lambda: (DnsOptions) -> DnsOptions) {
- settingsUpdates.value?.tunnelOptions?.dnsOptions?.let {
- serviceConnectionManager.customDns()?.setDnsOptions(lambda(it))
- }
- }
+ suspend fun addCustomDns(address: InetAddress) = managementService.addCustomDns(address)
- fun setWireguardMtu(value: Int?) {
- serviceConnectionManager.settingsListener()?.wireguardMtu = value
- }
+ suspend fun setWireguardMtu(mtu: Mtu) = managementService.setWireguardMtu(mtu.value)
- fun setWireguardQuantumResistant(value: QuantumResistantState) {
- serviceConnectionManager.settingsListener()?.wireguardQuantumResistant = value
- }
+ suspend fun resetWireguardMtu() = managementService.resetWireguardMtu()
- fun setObfuscationOptions(value: ObfuscationSettings) {
- serviceConnectionManager.settingsListener()?.obfuscationSettings = value
- }
+ suspend fun setWireguardQuantumResistant(value: QuantumResistantState) =
+ managementService.setWireguardQuantumResistant(value)
- fun setAutoConnect(isEnabled: Boolean) {
- serviceConnectionManager.settingsListener()?.autoConnect = isEnabled
- }
+ suspend fun setObfuscationOptions(value: ObfuscationSettings) =
+ managementService.setObfuscationOptions(value)
- fun setLocalNetworkSharing(isEnabled: Boolean) {
- serviceConnectionManager.settingsListener()?.allowLan = isEnabled
- }
+ suspend fun setAutoConnect(isEnabled: Boolean) = managementService.setAutoConnect(isEnabled)
- suspend fun applySettingsPatch(json: String) =
- withContext(dispatcher) {
- val deferred = async { messageHandler.events<ApplyJsonSettingsResult>().first() }
- messageHandler.trySendRequest(Request.ApplyJsonSettings(json))
- deferred.await()
- }
+ suspend fun setLocalNetworkSharing(isEnabled: Boolean) =
+ managementService.setAllowLan(isEnabled)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplitTunnelingRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplitTunnelingRepository.kt
new file mode 100644
index 0000000000..383d52b6ce
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SplitTunnelingRepository.kt
@@ -0,0 +1,32 @@
+package net.mullvad.mullvadvpn.repository
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.AppId
+
+class SplitTunnelingRepository(
+ private val managementService: ManagementService,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO
+) {
+ val splitTunnelingEnabled =
+ managementService.settings
+ .map { it.splitTunnelSettings.enabled }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), false)
+
+ val excludedApps =
+ managementService.settings
+ .map { it.splitTunnelSettings.excludedApps }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), emptySet())
+
+ suspend fun enableSplitTunneling(enabled: Boolean) =
+ managementService.setSplitTunnelingState(enabled)
+
+ suspend fun excludeApp(app: AppId) = managementService.addSplitTunnelingApp(app)
+
+ suspend fun includeApp(app: AppId) = managementService.removeSplitTunnelingApp(app)
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
index 2bfe5d5d9d..5649442050 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
@@ -1,10 +1,7 @@
package net.mullvad.mullvadvpn.ui
-import android.app.Activity
import android.content.Intent
-import android.net.VpnService
import android.os.Bundle
-import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
@@ -12,21 +9,15 @@ import androidx.core.view.WindowCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.screen.MullvadApp
import net.mullvad.mullvadvpn.di.paymentModule
import net.mullvad.mullvadvpn.di.uiModule
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.requestNotificationPermissionIfMissing
-import net.mullvad.mullvadvpn.lib.endpoint.getApiEndpointConfigurationExtras
+import net.mullvad.mullvadvpn.lib.intent.IntentProvider
import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
import org.koin.android.ext.android.getKoin
import org.koin.core.context.loadKoinModules
@@ -38,12 +29,10 @@ class MainActivity : ComponentActivity() {
// handling the callback value.
}
- private lateinit var accountRepository: AccountRepository
- private lateinit var deviceRepository: DeviceRepository
private lateinit var privacyDisclaimerRepository: PrivacyDisclaimerRepository
private lateinit var serviceConnectionManager: ServiceConnectionManager
- private lateinit var changelogViewModel: ChangelogViewModel
- private lateinit var serviceConnectionViewModel: NoDaemonViewModel
+ private lateinit var noDaemonViewModel: NoDaemonViewModel
+ private lateinit var intentProvider: IntentProvider
override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(uiModule, paymentModule))
@@ -51,18 +40,21 @@ class MainActivity : ComponentActivity() {
// Tell the system that we will draw behind the status bar and navigation bar
WindowCompat.setDecorFitsSystemWindows(window, false)
- getKoin().apply {
- accountRepository = get()
- deviceRepository = get()
+ with(getKoin()) {
privacyDisclaimerRepository = get()
serviceConnectionManager = get()
- changelogViewModel = get()
- serviceConnectionViewModel = get()
+ noDaemonViewModel = get()
+ intentProvider = get()
}
- lifecycle.addObserver(serviceConnectionViewModel)
+ lifecycle.addObserver(noDaemonViewModel)
super.onCreate(savedInstanceState)
+ // Needs to be before set content since we want to access the intent in compose
+ if (savedInstanceState == null) {
+ intentProvider.setStartIntent(intent)
+ }
+
setContent { AppTheme { MullvadApp() } }
// This is to protect against tapjacking attacks
@@ -74,56 +66,29 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
if (privacyDisclaimerRepository.hasAcceptedPrivacyDisclosure()) {
- startServiceSuspend(waitForConnectedReady = false)
+ bindService()
}
}
}
}
- suspend fun startServiceSuspend(waitForConnectedReady: Boolean = true) {
- requestNotificationPermissionIfMissing(requestNotificationPermissionLauncher)
- serviceConnectionManager.bind(
- vpnPermissionRequestHandler = ::requestVpnPermission,
- apiEndpointConfiguration = intent?.getApiEndpointConfigurationExtras()
- )
- if (waitForConnectedReady) {
- // Ensure we wait until the service is ready
- serviceConnectionManager.connectionState
- .filterIsInstance<ServiceConnectionState.ConnectedReady>()
- .first()
- }
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ intentProvider.setStartIntent(intent)
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
- // super call is needed for return value when opening file.
- super.onActivityResult(requestCode, resultCode, resultData)
-
- // Ensure we are responding to the correct request
- if (requestCode == REQUEST_VPN_PERMISSION_RESULT_CODE) {
- serviceConnectionManager.onVpnPermissionResult(resultCode == Activity.RESULT_OK)
- }
+ fun bindService() {
+ requestNotificationPermissionIfMissing(requestNotificationPermissionLauncher)
+ serviceConnectionManager.bind()
}
override fun onStop() {
- Log.d("mullvad", "Stopping main activity")
super.onStop()
serviceConnectionManager.unbind()
}
override fun onDestroy() {
- serviceConnectionManager.onDestroy()
- lifecycle.removeObserver(serviceConnectionViewModel)
+ lifecycle.removeObserver(noDaemonViewModel)
super.onDestroy()
}
-
- @Suppress("DEPRECATION")
- private fun requestVpnPermission() {
- val intent = VpnService.prepare(this)
-
- startActivityForResult(intent, REQUEST_VPN_PERMISSION_RESULT_CODE)
- }
-
- companion object {
- private const val REQUEST_VPN_PERMISSION_RESULT_CODE = 0
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt
index ca5fe50aed..c0ab7dd0ed 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/VersionInfo.kt
@@ -1,8 +1,9 @@
package net.mullvad.mullvadvpn.ui
data class VersionInfo(
- @Deprecated(message = "Use BuildConfig.VERSION_NAME") val currentVersion: String?,
- val upgradeVersion: String?,
- val isOutdated: Boolean,
- val isSupported: Boolean
-)
+ val currentVersion: String,
+ val isSupported: Boolean,
+ val suggestedUpgradeVersion: String?
+) {
+ val isUpdateAvailable: Boolean = suggestedUpgradeVersion != null
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoCache.kt
deleted file mode 100644
index 9210e5809b..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoCache.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.model.AppVersionInfo
-
-class AppVersionInfoCache(
- eventDispatcher: EventDispatcher,
- private val settingsListener: SettingsListener
-) {
- private var appVersionInfo by
- observable<AppVersionInfo?>(null) { _, _, _ -> onUpdate?.invoke() }
-
- val isSupported
- get() = appVersionInfo?.supported ?: true
-
- val isOutdated
- get() = appVersionInfo?.suggestedUpgrade != null
-
- val upgradeVersion
- get() = appVersionInfo?.suggestedUpgrade
-
- var onUpdate by observable<(() -> Unit)?>(null) { _, _, callback -> callback?.invoke() }
-
- var showBetaReleases by
- observable(false) { _, wasShowing, shouldShow ->
- if (shouldShow != wasShowing) {
- onUpdate?.invoke()
- }
- }
- private set
-
- var version: String? = null
- private set
-
- init {
- eventDispatcher.apply {
- registerHandler(Event.CurrentVersion::class) { event -> version = event.version }
-
- registerHandler(Event.AppVersionInfo::class) { event ->
- appVersionInfo = event.versionInfo
- }
- }
-
- settingsListener.settingsNotifier.subscribe(this) { maybeSettings ->
- maybeSettings?.let { settings -> showBetaReleases = settings.showBetaReleases }
- }
- }
-
- fun onDestroy() {
- settingsListener.settingsNotifier.unsubscribe(this)
- onUpdate = null
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt
new file mode 100644
index 0000000000..74b67348b3
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AppVersionInfoRepository.kt
@@ -0,0 +1,21 @@
+package net.mullvad.mullvadvpn.ui.serviceconnection
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.BuildVersion
+import net.mullvad.mullvadvpn.ui.VersionInfo
+
+class AppVersionInfoRepository(
+ private val buildVersion: BuildVersion,
+ private val managementService: ManagementService
+) {
+ fun versionInfo(): Flow<VersionInfo> =
+ managementService.versionInfo.map { appVersionInfo ->
+ VersionInfo(
+ currentVersion = buildVersion.name,
+ isSupported = appVersionInfo.supported,
+ suggestedUpgradeVersion = appVersionInfo.suggestedUpgrade,
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AuthTokenCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AuthTokenCache.kt
deleted file mode 100644
index 2c7ea5385c..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/AuthTokenCache.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import java.util.LinkedList
-import kotlinx.coroutines.CompletableDeferred
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class AuthTokenCache(private val connection: Messenger, eventDispatcher: EventDispatcher) {
- private val fetchQueue = LinkedList<CompletableDeferred<String>>()
-
- init {
- eventDispatcher.registerHandler(Event.AuthToken::class) { event ->
- synchronized(this@AuthTokenCache) { fetchQueue.poll()?.complete(event.token ?: "") }
- }
- }
-
- suspend fun fetchAuthToken(): String {
- val authToken = CompletableDeferred<String>()
-
- synchronized(this) { fetchQueue.offer(authToken) }
-
- connection.send(Request.FetchAuthToken.message)
-
- return authToken.await()
- }
-
- fun onDestroy() {
- synchronized(this) {
- for (pendingFetch in fetchQueue) {
- pendingFetch.cancel()
- }
-
- fetchQueue.clear()
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt
deleted file mode 100644
index bbc267b2fa..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxy.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.extensions.trySendRequest
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.util.EventNotifier
-
-const val ANTICIPATED_STATE_TIMEOUT_MS = 1500L
-
-class ConnectionProxy(private val connection: Messenger, eventDispatcher: EventDispatcher) {
- private var resetAnticipatedStateJob: Job? = null
-
- val onStateChange = EventNotifier<TunnelState>(TunnelState.Disconnected())
- val onUiStateChange = EventNotifier<TunnelState>(TunnelState.Disconnected())
-
- var state by onStateChange.notifiable()
- private set
-
- var uiState by onUiStateChange.notifiable()
- private set
-
- init {
- eventDispatcher.registerHandler(Event.TunnelStateChange::class) { event ->
- handleNewState(event.tunnelState)
- }
- }
-
- fun connect() {
- if (anticipateConnectingState()) {
- connection.trySendRequest(Request.Connect, true)
- }
- }
-
- fun disconnect() {
- if (anticipateReconnectingState()) {
- connection.trySendRequest(Request.Disconnect, true)
- }
- }
-
- fun reconnect() {
- if (anticipateDisconnectingState()) {
- connection.trySendRequest(Request.Reconnect, true)
- }
- }
-
- fun onDestroy() {
- onStateChange.unsubscribeAll()
- onUiStateChange.unsubscribeAll()
- }
-
- private fun handleNewState(newState: TunnelState) {
- synchronized(this) {
- resetAnticipatedStateJob?.cancel()
- state = newState
- uiState = newState
- }
- }
-
- private fun anticipateConnectingState(): Boolean {
- synchronized(this) {
- val currentState = uiState
-
- if (currentState is TunnelState.Connecting || currentState is TunnelState.Connected) {
- return false
- } else {
- scheduleToResetAnticipatedState()
- uiState = TunnelState.Connecting(null, null)
- return true
- }
- }
- }
-
- private fun anticipateReconnectingState(): Boolean {
- synchronized(this) {
- val currentState = uiState
-
- val willReconnect =
- when (currentState) {
- is TunnelState.Disconnected -> false
- is TunnelState.Disconnecting -> {
- when (currentState.actionAfterDisconnect) {
- ActionAfterDisconnect.Nothing -> false
- ActionAfterDisconnect.Reconnect -> true
- ActionAfterDisconnect.Block -> true
- }
- }
- is TunnelState.Connecting -> true
- is TunnelState.Connected -> true
- is TunnelState.Error -> true
- }
-
- if (willReconnect) {
- scheduleToResetAnticipatedState()
- uiState = TunnelState.Disconnecting(ActionAfterDisconnect.Reconnect)
- }
-
- return willReconnect
- }
- }
-
- private fun anticipateDisconnectingState(): Boolean {
- synchronized(this) {
- val currentState = uiState
-
- if (currentState is TunnelState.Disconnected) {
- return false
- } else {
- scheduleToResetAnticipatedState()
- uiState = TunnelState.Disconnecting(ActionAfterDisconnect.Nothing)
- return true
- }
- }
- }
-
- private fun scheduleToResetAnticipatedState() {
- resetAnticipatedStateJob?.cancel()
-
- var currentJob: Job? = null
-
- val newJob =
- GlobalScope.launch(Dispatchers.Default) {
- delay(ANTICIPATED_STATE_TIMEOUT_MS)
-
- synchronized(this@ConnectionProxy) {
- if (!currentJob!!.isCancelled) {
- uiState = state
- }
- }
- }
-
- currentJob = newJob
- resetAnticipatedStateJob = newJob
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/CustomDns.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/CustomDns.kt
deleted file mode 100644
index bfad798e08..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/CustomDns.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.extensions.trySendRequest
-import net.mullvad.mullvadvpn.model.DnsOptions
-
-class CustomDns(private val connection: Messenger) {
-
- fun setDnsOptions(dnsOptions: DnsOptions) {
- connection.trySendRequest(Request.SetDnsOptions(dnsOptions), false)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/EmptyServiceConnection.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/EmptyServiceConnection.kt
new file mode 100644
index 0000000000..28819f7aa0
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/EmptyServiceConnection.kt
@@ -0,0 +1,16 @@
+package net.mullvad.mullvadvpn.ui.serviceconnection
+
+import android.content.ComponentName
+import android.content.ServiceConnection
+import android.os.IBinder
+
+class EmptyServiceConnection : ServiceConnection {
+ @Suppress("EmptyFunctionBlock")
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {}
+
+ @Suppress("EmptyFunctionBlock") override fun onServiceDisconnected(name: ComponentName?) {}
+
+ override fun onNullBinding(name: ComponentName?) {
+ error("Received onNullBinding")
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt
deleted file mode 100644
index 841c9e0c59..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.LocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Providers
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.WireguardConstraints
-import net.mullvad.mullvadvpn.model.WireguardEndpointData
-
-class RelayListListener(
- private val messageHandler: MessageHandler,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- val relayListEvents: StateFlow<RelayList> =
- messageHandler
- .events<Event.NewRelayList>()
- .map { it.relayList ?: defaultRelayList() }
- // This is added so that we always have a relay list. Otherwise sometimes there would
- // not be a relay list since the fetching of a relay list would be done before the
- // event stream is available.
- .onStart { messageHandler.trySendRequest(Request.FetchRelayList) }
- .stateIn(
- CoroutineScope(dispatcher),
- SharingStarted.WhileSubscribed(),
- defaultRelayList()
- )
-
- fun updateSelectedRelayLocation(value: LocationConstraint) {
- messageHandler.trySendRequest(Request.SetRelayLocation(value))
- }
-
- fun updateSelectedWireguardConstraints(value: WireguardConstraints) {
- messageHandler.trySendRequest(Request.SetWireguardConstraints(value))
- }
-
- fun updateSelectedOwnershipAndProviderFilter(
- ownership: Constraint<Ownership>,
- providers: Constraint<Providers>
- ) {
- messageHandler.trySendRequest(Request.SetOwnershipAndProviders(ownership, providers))
- }
-
- fun fetchRelayList() {
- messageHandler.trySendRequest(Request.FetchRelayList)
- }
-
- private fun defaultRelayList() = RelayList(ArrayList(), WireguardEndpointData(ArrayList()))
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt
deleted file mode 100644
index 8aabe6c9f5..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionContainer.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Looper
-import android.os.Messenger
-import android.os.RemoteException
-import android.util.Log
-import kotlinx.coroutines.flow.filterIsInstance
-import net.mullvad.mullvadvpn.lib.ipc.DispatchingHandler
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.extensions.trySendRequest
-import org.koin.core.component.KoinComponent
-
-// Container of classes that communicate with the service through an active connection
-//
-// The properties of this class can be used to send events to the service, to listen for events from
-// the service and to get values received from events.
-class ServiceConnectionContainer(
- val connection: Messenger,
- onServiceReady: (ServiceConnectionContainer) -> Unit,
- onVpnPermissionRequest: () -> Unit
-) : KoinComponent {
- private val dispatcher =
- DispatchingHandler(Looper.getMainLooper()) { message -> Event.fromMessage(message) }
-
- val events = dispatcher.parsedMessages.filterIsInstance<Event>()
-
- val authTokenCache = AuthTokenCache(connection, dispatcher)
- val connectionProxy = ConnectionProxy(connection, dispatcher)
- val deviceDataSource = ServiceConnectionDeviceDataSource(connection, dispatcher)
- val settingsListener = SettingsListener(connection, dispatcher)
-
- val splitTunneling = SplitTunneling(connection, dispatcher)
- val voucherRedeemer = VoucherRedeemer(connection, dispatcher)
- val vpnPermission = VpnPermission(connection, dispatcher)
-
- val appVersionInfoCache = AppVersionInfoCache(dispatcher, settingsListener)
- val customDns = CustomDns(connection)
-
- private var listenerId: Int? = null
-
- init {
- vpnPermission.onRequest = onVpnPermissionRequest
-
- dispatcher.registerHandler(Event.ListenerReady::class) { event ->
- listenerId = event.listenerId
- onServiceReady.invoke(this@ServiceConnectionContainer)
- }
-
- registerListener(connection)
- }
-
- fun trySendRequest(request: Request, logErrors: Boolean): Boolean {
- return connection.trySendRequest(request, logErrors = logErrors)
- }
-
- fun onDestroy() {
- unregisterListener()
-
- dispatcher.onDestroy()
-
- authTokenCache.onDestroy()
- connectionProxy.onDestroy()
- settingsListener.onDestroy()
- voucherRedeemer.onDestroy()
-
- appVersionInfoCache.onDestroy()
- }
-
- private fun registerListener(connection: Messenger) {
- val listener = Messenger(dispatcher)
- val request = Request.RegisterListener(listener)
-
- try {
- connection.send(request.message)
- } catch (exception: RemoteException) {
- Log.e("mullvad", "Failed to register listener for service events", exception)
- }
- }
-
- private fun unregisterListener() {
- listenerId?.let { id ->
- try {
- connection.send(Request.UnregisterListener(id).message)
- } catch (exception: RemoteException) {
- Log.e("mullvad", "Failed to unregister listener for service events", exception)
- }
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSource.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSource.kt
deleted file mode 100644
index a9094ed011..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSource.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.callbackFlow
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.extensions.trySendRequest
-
-class ServiceConnectionDeviceDataSource(
- private val connection: Messenger,
- private val dispatcher: EventDispatcher
-) {
- val deviceStateUpdates = callbackFlow {
- val handler: (Event.DeviceStateEvent) -> Unit = { event -> trySend(event.newState) }
- dispatcher.registerHandler(Event.DeviceStateEvent::class, handler)
- connection.trySendRequest(Request.GetDevice, false)
- awaitClose {
- // The current dispatcher doesn't support unregistration of handlers.
- }
- }
-
- val deviceListUpdates = callbackFlow {
- val handler: (Event.DeviceListUpdate) -> Unit = { event -> trySend(event.event) }
- dispatcher.registerHandler(Event.DeviceListUpdate::class, handler)
- awaitClose {
- // The current dispatcher doesn't support unregistration of handlers.
- }
- }
-
- val deviceRemovalResult = callbackFlow {
- val handler: (Event.DeviceRemovalEvent) -> Unit = { event -> trySend(event) }
- dispatcher.registerHandler(Event.DeviceRemovalEvent::class, handler)
- awaitClose {
- // The current dispatcher doesn't support unregistration of handlers.
- }
- }
-
- // Async result: Event.DeviceChanged
- fun refreshDevice() {
- connection.trySendRequest(Request.RefreshDeviceState, true)
- }
-
- fun getDevice() {
- connection.trySendRequest(Request.GetDevice, true)
- }
-
- fun removeDevice(accountToken: String, deviceId: String) {
- connection.trySendRequest(Request.RemoveDevice(accountToken, deviceId), true)
- }
-
- fun refreshDeviceList(accountToken: String) {
- connection.trySendRequest(Request.GetDeviceList(accountToken), true)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt
index 4e1d773f1e..315243a77c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManager.kt
@@ -1,149 +1,51 @@
package net.mullvad.mullvadvpn.ui.serviceconnection
-import android.content.ComponentName
import android.content.Context
+import android.content.Context.BIND_AUTO_CREATE
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
-import android.os.IBinder
-import android.os.Messenger
-import android.util.Log
-import kotlin.reflect.KClass
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.map
-import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
-import net.mullvad.mullvadvpn.lib.endpoint.BuildConfig
-import net.mullvad.mullvadvpn.lib.endpoint.putApiEndpointConfigurationExtra
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
import net.mullvad.mullvadvpn.service.MullvadVpnService
-import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault
-import net.mullvad.talpid.util.EventNotifier
-class ServiceConnectionManager(private val context: Context) : MessageHandler {
+class ServiceConnectionManager(private val context: Context) {
private val _connectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
+ MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Unbound)
val connectionState = _connectionState.asStateFlow()
- // TODO: Remove after refactoring fragments to support flow.
- @Deprecated(message = "Use connectionState")
- val serviceNotifier = EventNotifier<ServiceConnectionContainer?>(null)
+ // Dummy service connection to be able to bind, all communication goes over gRPC.
+ private val serviceConnection = EmptyServiceConnection()
- var isBound = false
- private var vpnPermissionRequestHandler: (() -> Unit)? = null
+ @Synchronized
+ fun bind() {
+ if (_connectionState.value is ServiceConnectionState.Unbound) {
+ val intent = Intent(context, MullvadVpnService::class.java)
- private val events =
- connectionState.flatMapReadyConnectionOrDefault(emptyFlow()) { it.container.events }
-
- private val serviceConnection =
- object : android.content.ServiceConnection {
- override fun onServiceConnected(className: ComponentName, binder: IBinder) {
- Log.d("mullvad", "UI successfully connected to the service")
-
- notify(
- ServiceConnectionState.ConnectedNotReady(
- ServiceConnectionContainer(
- Messenger(binder),
- ::handleNewServiceConnection,
- ::handleVpnPermissionRequest
- )
- )
+ // We set BIND_AUTO_CREATE so that the service is started if it is not already running
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ context.bindService(
+ intent,
+ serviceConnection,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED or BIND_AUTO_CREATE
)
+ } else {
+ context.bindService(intent, serviceConnection, BIND_AUTO_CREATE)
}
-
- override fun onServiceDisconnected(className: ComponentName) {
- Log.d("mullvad", "UI lost the connection to the service")
- _connectionState.value.readyContainer()?.onDestroy()
- notify(ServiceConnectionState.Disconnected)
- }
- }
-
- fun bind(
- vpnPermissionRequestHandler: () -> Unit,
- apiEndpointConfiguration: ApiEndpointConfiguration?
- ) {
- synchronized(this) {
- if (isBound.not()) {
- this.vpnPermissionRequestHandler = vpnPermissionRequestHandler
- val intent = Intent(context, MullvadVpnService::class.java)
-
- if (BuildConfig.DEBUG && apiEndpointConfiguration != null) {
- intent.putApiEndpointConfigurationExtra(apiEndpointConfiguration)
- }
-
- context.startService(intent)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- context.bindService(
- intent,
- serviceConnection,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
- )
- } else {
- context.bindService(intent, serviceConnection, 0)
- }
- isBound = true
- }
+ _connectionState.value = ServiceConnectionState.Bound
+ } else {
+ error("Service is already bound")
}
}
+ @Synchronized
fun unbind() {
- synchronized(this) {
- if (isBound) {
- _connectionState.value.readyContainer()?.onDestroy()
- context.unbindService(serviceConnection)
- notify(ServiceConnectionState.Disconnected)
- vpnPermissionRequestHandler = null
- isBound = false
- }
- }
- }
-
- override fun <E : Event> events(klass: KClass<E>): Flow<E> {
- return events.map { it }.filterIsInstance(klass)
- }
-
- override fun trySendRequest(request: Request): Boolean {
- return connectionState.value.readyContainer()?.trySendRequest(request, logErrors = false)
- ?: false
- }
-
- fun onDestroy() {
- _connectionState.value.readyContainer()?.onDestroy()
- serviceNotifier.unsubscribeAll()
- notify(ServiceConnectionState.Disconnected)
- vpnPermissionRequestHandler = null
- }
-
- fun onVpnPermissionResult(isGranted: Boolean) {
- _connectionState.value.let { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- state.container.vpnPermission.grant(isGranted)
- }
- }
- }
-
- private fun notify(state: ServiceConnectionState) {
- _connectionState.value = state
-
- // TODO: Remove once `serviceNotifier` is no longer used.
- if (state is ServiceConnectionState.ConnectedReady) {
- serviceNotifier.notify(state.container)
- } else if (state is ServiceConnectionState.Disconnected) {
- serviceNotifier.notify(null)
+ if (_connectionState.value is ServiceConnectionState.Bound) {
+ context.unbindService(serviceConnection)
+ _connectionState.value = ServiceConnectionState.Unbound
+ } else {
+ error("Service is not bound")
}
}
-
- private fun handleVpnPermissionRequest() {
- vpnPermissionRequestHandler?.invoke()
- }
-
- private fun handleNewServiceConnection(serviceConnectionContainer: ServiceConnectionContainer) {
- notify(ServiceConnectionState.ConnectedReady(serviceConnectionContainer))
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt
deleted file mode 100644
index 31ac1befdc..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionManagerExtensions.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-fun ServiceConnectionManager.appVersionInfoCache() =
- this.connectionState.value.readyContainer()?.appVersionInfoCache
-
-fun ServiceConnectionManager.authTokenCache() =
- this.connectionState.value.readyContainer()?.authTokenCache
-
-fun ServiceConnectionManager.connectionProxy() =
- this.connectionState.value.readyContainer()?.connectionProxy
-
-fun ServiceConnectionManager.deviceDataSource() =
- this.connectionState.value.readyContainer()?.deviceDataSource
-
-fun ServiceConnectionManager.customDns() = this.connectionState.value.readyContainer()?.customDns
-
-fun ServiceConnectionManager.settingsListener() =
- this.connectionState.value.readyContainer()?.settingsListener
-
-fun ServiceConnectionManager.splitTunneling() =
- this.connectionState.value.readyContainer()?.splitTunneling
-
-fun ServiceConnectionManager.voucherRedeemer() =
- this.connectionState.value.readyContainer()?.voucherRedeemer
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt
index ca868e5cfa..7747844673 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionState.kt
@@ -1,14 +1,7 @@
package net.mullvad.mullvadvpn.ui.serviceconnection
sealed class ServiceConnectionState {
- data class ConnectedReady(val container: ServiceConnectionContainer) : ServiceConnectionState()
+ data object Bound : ServiceConnectionState()
- data class ConnectedNotReady(val container: ServiceConnectionContainer) :
- ServiceConnectionState()
-
- object Disconnected : ServiceConnectionState()
-
- fun readyContainer(): ServiceConnectionContainer? {
- return (this as? ConnectedReady)?.container
- }
+ data object Unbound : ServiceConnectionState()
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt
deleted file mode 100644
index e2ccc2e470..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.ObfuscationSettings
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.talpid.util.EventNotifier
-
-class SettingsListener(private val connection: Messenger, eventDispatcher: EventDispatcher) {
- val relaySettingsNotifier = EventNotifier<RelaySettings?>(null)
- val settingsNotifier = EventNotifier<Settings?>(null)
-
- private var settings by settingsNotifier.notifiable()
-
- var allowLan: Boolean
- get() = settingsNotifier.latestEvent?.allowLan ?: false
- set(value) {
- connection.send(Request.SetAllowLan(value).message)
- }
-
- var autoConnect: Boolean
- get() = settingsNotifier.latestEvent?.autoConnect ?: false
- set(value) {
- connection.send(Request.SetAutoConnect(value).message)
- }
-
- var wireguardMtu: Int?
- get() = settingsNotifier.latestEvent?.tunnelOptions?.wireguard?.mtu
- set(value) {
- connection.send(Request.SetWireGuardMtu(value).message)
- }
-
- var wireguardQuantumResistant: QuantumResistantState
- get() =
- settingsNotifier.latestEvent?.tunnelOptions?.wireguard?.quantumResistant
- ?: QuantumResistantState.Off
- set(value) {
- connection.send(Request.SetWireGuardQuantumResistant(value).message)
- }
-
- var obfuscationSettings: ObfuscationSettings?
- get() = settingsNotifier.latestEvent?.obfuscationSettings
- set(value) {
- connection.send(Request.SetObfuscationSettings(value).message)
- }
-
- init {
- eventDispatcher.registerHandler(Event.SettingsUpdate::class, ::handleNewEvent)
- }
-
- fun onDestroy() {
- relaySettingsNotifier.unsubscribeAll()
- settingsNotifier.unsubscribeAll()
- }
-
- private fun handleNewEvent(event: Event.SettingsUpdate) {
- event.settings?.let { settings -> handleNewSettings(settings) }
- }
-
- private fun handleNewSettings(newSettings: Settings) {
- if (settings?.relaySettings != newSettings.relaySettings) {
- relaySettingsNotifier.notify(newSettings.relaySettings)
- }
-
- settings = newSettings
- }
-
- fun applySettingsPatch(json: String) {
- connection.send(Request.ApplyJsonSettings(json).message)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt
deleted file mode 100644
index 666d772184..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SplitTunneling.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class SplitTunneling(private val connection: Messenger, eventDispatcher: EventDispatcher) {
- private var _excludedApps by
- observable(emptySet<String>()) { _, _, apps -> excludedAppsChange.invoke(apps) }
-
- var enabled by observable(false) { _, _, isEnabled -> enabledChange.invoke(isEnabled) }
-
- var enabledChange: (enabled: Boolean) -> Unit = {}
- set(value) {
- field = value
- synchronized(this) { value.invoke(enabled) }
- }
-
- var excludedAppsChange: (apps: Set<String>) -> Unit = {}
- set(value) {
- field = value
- synchronized(this) { value.invoke(_excludedApps) }
- }
-
- init {
- eventDispatcher.registerHandler(Event.SplitTunnelingUpdate::class) { event ->
- if (event.excludedApps != null) {
- enabled = true
- _excludedApps = event.excludedApps!!.toSet()
- } else {
- enabled = false
- }
- }
- }
-
- fun excludeApp(appPackageName: String) =
- connection.send(Request.ExcludeApp(appPackageName).message)
-
- fun includeApp(appPackageName: String) =
- connection.send(Request.IncludeApp(appPackageName).message)
-
- fun persist() = connection.send(Request.PersistExcludedApps.message)
-
- fun enableSplitTunneling(isEnabled: Boolean) =
- connection.send(Request.SetEnableSplitTunneling(isEnabled).message)
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VoucherRedeemer.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VoucherRedeemer.kt
deleted file mode 100644
index fbf082ba3c..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VoucherRedeemer.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import kotlinx.coroutines.CompletableDeferred
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
-
-class VoucherRedeemer(val connection: Messenger, eventDispatcher: MessageDispatcher<Event>) {
- private val activeSubmissions =
- mutableMapOf<String, CompletableDeferred<VoucherSubmissionResult>>()
-
- init {
- eventDispatcher.registerHandler(Event.VoucherSubmissionResult::class) { event ->
- synchronized(this@VoucherRedeemer) {
- activeSubmissions.remove(event.voucher)?.complete(event.result)
- }
- }
- }
-
- suspend fun submit(voucher: String): VoucherSubmissionResult {
- val result = CompletableDeferred<VoucherSubmissionResult>()
-
- synchronized(this) { activeSubmissions.put(voucher, result) }
-
- connection.send(Request.SubmitVoucher(voucher).message)
-
- return result.await()
- }
-
- fun onDestroy() {
- synchronized(this) {
- for ((_, submission) in activeSubmissions) {
- submission.cancel()
- }
-
- activeSubmissions.clear()
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VpnPermission.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VpnPermission.kt
deleted file mode 100644
index 143a01d719..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/VpnPermission.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.Messenger
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class VpnPermission(private val connection: Messenger, eventDispatcher: MessageDispatcher<Event>) {
- var onRequest: (() -> Unit)? = null
-
- init {
- eventDispatcher.registerHandler(Event.VpnPermissionRequest::class) { _ ->
- onRequest?.invoke()
- }
- }
-
- fun grant(isGranted: Boolean) {
- connection.send(Request.VpnPermissionResponse(isGranted).message)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCase.kt
index a4961bafe7..65822788cb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCase.kt
@@ -4,8 +4,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD_DAYS
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.repository.AccountRepository
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
import org.joda.time.DateTime
@@ -13,19 +13,19 @@ class AccountExpiryNotificationUseCase(
private val accountRepository: AccountRepository,
) {
fun notifications(): Flow<List<InAppNotification>> =
- accountRepository.accountExpiryState
+ accountRepository.accountData
.map(::accountExpiryNotification)
.map(::listOfNotNull)
.distinctUntilChanged()
- private fun accountExpiryNotification(accountExpiry: AccountExpiry) =
- if (accountExpiry.isCloseToExpiring()) {
- InAppNotification.AccountExpiry(accountExpiry.date() ?: DateTime.now())
+ private fun accountExpiryNotification(accountData: AccountData?) =
+ if (accountData != null && accountData.expiryDate.isCloseToExpiring()) {
+ InAppNotification.AccountExpiry(accountData.expiryDate)
} else null
- private fun AccountExpiry.isCloseToExpiring(): Boolean {
+ private fun DateTime.isCloseToExpiring(): Boolean {
val threeDaysFromNow =
DateTime.now().plusDays(ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD_DAYS)
- return this.date()?.isBefore(threeDaysFromNow) == true
+ return isBefore(threeDaysFromNow)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AvailableProvidersUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AvailableProvidersUseCase.kt
new file mode 100644
index 0000000000..f79c0421f6
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/AvailableProvidersUseCase.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.usecase
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+
+class AvailableProvidersUseCase(private val relayListRepository: RelayListRepository) {
+
+ fun availableProviders(): Flow<List<Provider>> =
+ relayListRepository.relayList.map { relayList ->
+ relayList
+ .flatMap(RelayItem.Location.Country::cities)
+ .flatMap(RelayItem.Location.City::relays)
+ .map(RelayItem.Location.Relay::provider)
+ .distinct()
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
new file mode 100644
index 0000000000..265c127227
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
@@ -0,0 +1,32 @@
+package net.mullvad.mullvadvpn.usecase
+
+import kotlinx.coroutines.flow.combine
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.relaylist.filterOnOwnershipAndProvider
+import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+
+class FilteredRelayListUseCase(
+ private val relayListRepository: RelayListRepository,
+ private val relayListFilterRepository: RelayListFilterRepository
+) {
+ fun filteredRelayList() =
+ combine(
+ relayListRepository.relayList,
+ relayListFilterRepository.selectedOwnership,
+ relayListFilterRepository.selectedProviders,
+ ) { relayList, selectedOwnership, selectedProviders ->
+ relayList.filterOnOwnershipAndProvider(
+ selectedOwnership,
+ selectedProviders,
+ )
+ }
+
+ private fun List<RelayItem.Location.Country>.filterOnOwnershipAndProvider(
+ ownership: Constraint<Ownership>,
+ providers: Constraint<Providers>
+ ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/LastKnownLocationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/LastKnownLocationUseCase.kt
new file mode 100644
index 0000000000..67bc12cc92
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/LastKnownLocationUseCase.kt
@@ -0,0 +1,24 @@
+package net.mullvad.mullvadvpn.usecase
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+
+class LastKnownLocationUseCase(
+ connectionProxy: ConnectionProxy,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO,
+) {
+ val lastKnownDisconnectedLocation: Flow<GeoIpLocation?> =
+ connectionProxy.tunnelState
+ .filterIsInstance<TunnelState.Disconnected>()
+ .mapNotNull { it.location }
+ .stateIn(CoroutineScope(dispatcher), SharingStarted.Lazily, null)
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceNotificationUseCase.kt
index 628cc555ec..06d26a76e8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceNotificationUseCase.kt
@@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
class NewDeviceNotificationUseCase(private val deviceRepository: DeviceRepository) {
@@ -12,7 +12,7 @@ class NewDeviceNotificationUseCase(private val deviceRepository: DeviceRepositor
fun notifications() =
combine(
- deviceRepository.deviceState.map { it.deviceName() }.distinctUntilChanged(),
+ deviceRepository.deviceState.map { it?.displayName() },
_mutableShowNewDeviceNotification
) { deviceName, newDeviceCreated ->
if (newDeviceCreated && deviceName != null) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt
index 88ec42f986..a86124c8a9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCase.kt
@@ -13,18 +13,15 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.talpid.tunnel.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import org.joda.time.DateTime
class OutOfTimeUseCase(
+ private val connectionProxy: ConnectionProxy,
private val repository: AccountRepository,
- private val messageHandler: MessageHandler,
scope: CoroutineScope
) {
@@ -47,9 +44,8 @@ class OutOfTimeUseCase(
}
private fun isTunnelBlockedBecauseOutOfTime(): Flow<Boolean> =
- messageHandler
- .events<Event.TunnelStateChange>()
- .map { it.tunnelState.isTunnelErrorStateDueToExpiredAccount() }
+ connectionProxy.tunnelState
+ .map { it.isTunnelErrorStateDueToExpiredAccount() }
.onStart { emit(false) }
private fun TunnelState.isTunnelErrorStateDueToExpiredAccount(): Boolean {
@@ -58,11 +54,11 @@ class OutOfTimeUseCase(
}
private fun pastAccountExpiry(): Flow<Boolean?> =
- repository.accountExpiryState
+ repository.accountData
.flatMapLatest {
- if (it is AccountExpiry.Available) {
+ if (it != null) {
flow {
- val millisUntilExpiry = it.expiryDateTime.millis - DateTime.now().millis
+ val millisUntilExpiry = it.expiryDate.millis - DateTime.now().millis
if (millisUntilExpiry > 0) {
emit(false)
delay(millisUntilExpiry)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PortRangeUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PortRangeUseCase.kt
deleted file mode 100644
index 2b104cda39..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/PortRangeUseCase.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package net.mullvad.mullvadvpn.usecase
-
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
-
-class PortRangeUseCase(private val relayListListener: RelayListListener) {
- fun portRanges(): Flow<List<PortRange>> =
- relayListListener.relayListEvents
- .map { it?.wireguardEndpointData?.portRanges ?: emptyList() }
- .distinctUntilChanged()
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListFilterUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListFilterUseCase.kt
deleted file mode 100644
index f480e6a23a..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListFilterUseCase.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.mullvad.mullvadvpn.usecase
-
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Providers
-import net.mullvad.mullvadvpn.model.RelayListCity
-import net.mullvad.mullvadvpn.model.RelayListCountry
-import net.mullvad.mullvadvpn.relaylist.Provider
-import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
-
-class RelayListFilterUseCase(
- private val relayListListener: RelayListListener,
- private val settingsRepository: SettingsRepository
-) {
- fun updateOwnershipAndProviderFilter(
- ownership: Constraint<Ownership>,
- providers: Constraint<Providers>
- ) {
- relayListListener.updateSelectedOwnershipAndProviderFilter(ownership, providers)
- }
-
- fun selectedOwnership(): Flow<Constraint<Ownership>> =
- settingsRepository.settingsUpdates.map { settings ->
- settings?.relaySettings?.relayConstraints()?.ownership ?: Constraint.Any()
- }
-
- fun selectedProviders(): Flow<Constraint<Providers>> =
- settingsRepository.settingsUpdates.map { settings ->
- settings?.relaySettings?.relayConstraints()?.providers ?: Constraint.Any()
- }
-
- fun availableProviders(): Flow<List<Provider>> =
- relayListListener.relayListEvents.map { relayList ->
- relayList.countries
- .flatMap(RelayListCountry::cities)
- .flatMap(RelayListCity::relays)
- .filter { relay -> relay.isWireguardRelay }
- .map { relay -> Provider(relay.provider, relay.owned) }
- .distinct()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListUseCase.kt
deleted file mode 100644
index b4197fe7b7..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RelayListUseCase.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package net.mullvad.mullvadvpn.usecase
-
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.LocationConstraint
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.WireguardConstraints
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.relaylist.RelayList
-import net.mullvad.mullvadvpn.relaylist.filterOnOwnershipAndProvider
-import net.mullvad.mullvadvpn.relaylist.findItemForGeographicLocationConstraint
-import net.mullvad.mullvadvpn.relaylist.toRelayCountries
-import net.mullvad.mullvadvpn.relaylist.toRelayItemLists
-import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
-
-class RelayListUseCase(
- private val relayListListener: RelayListListener,
- private val settingsRepository: SettingsRepository
-) {
-
- fun updateSelectedRelayLocation(value: LocationConstraint) {
- relayListListener.updateSelectedRelayLocation(value)
- }
-
- fun updateSelectedWireguardConstraints(value: WireguardConstraints) {
- relayListListener.updateSelectedWireguardConstraints(value)
- }
-
- fun relayListWithSelection(): Flow<RelayList> =
- combine(relayListListener.relayListEvents, settingsRepository.settingsUpdates) {
- relayList,
- settings ->
- val ownership =
- settings?.relaySettings?.relayConstraints()?.ownership ?: Constraint.Any()
- val providers =
- settings?.relaySettings?.relayConstraints()?.providers ?: Constraint.Any()
- val relayCountries = relayList.toRelayCountries()
- val customLists =
- settings?.customLists?.customLists?.toRelayItemLists(relayCountries) ?: emptyList()
- val relayCountriesFiltered =
- relayCountries.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) }
- val selectedItem =
- findSelectedRelayItem(
- relaySettings = settings?.relaySettings,
- relayCountries = relayCountries,
- customLists = customLists,
- )
- RelayList(
- customLists = customLists,
- allCountries = relayCountries,
- filteredCountries = relayCountriesFiltered,
- selectedItem = selectedItem
- )
- }
-
- fun selectedRelayItem(): Flow<RelayItem?> = relayListWithSelection().map { it.selectedItem }
-
- fun fullRelayList(): Flow<List<RelayItem.Country>> =
- relayListWithSelection().map { it.allCountries }
-
- fun customLists(): Flow<List<RelayItem.CustomList>> =
- relayListWithSelection().map { it.customLists }
-
- fun fetchRelayList() {
- relayListListener.fetchRelayList()
- }
-
- private fun findSelectedRelayItem(
- relaySettings: RelaySettings?,
- relayCountries: List<RelayItem.Country>,
- customLists: List<RelayItem.CustomList>
- ): RelayItem? {
- val locationConstraint = relaySettings?.relayConstraints()?.location
- return if (locationConstraint is Constraint.Only) {
- when (val location = locationConstraint.value) {
- is LocationConstraint.CustomList -> {
- customLists.firstOrNull { it.id == location.listId }
- }
- is LocationConstraint.Location -> {
- relayCountries.findItemForGeographicLocationConstraint(location.location)
- }
- }
- } else {
- null
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationTitleUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationTitleUseCase.kt
new file mode 100644
index 0000000000..a37e33492d
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationTitleUseCase.kt
@@ -0,0 +1,56 @@
+package net.mullvad.mullvadvpn.usecase
+
+import arrow.core.raise.nullable
+import kotlinx.coroutines.flow.combine
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.relaylist.findByGeoLocationId
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+
+class SelectedLocationTitleUseCase(
+ private val customListsRepository: CustomListsRepository,
+ private val relayListRepository: RelayListRepository,
+) {
+ fun selectedLocationTitle() =
+ combine(
+ customListsRepository.customLists,
+ relayListRepository.relayList,
+ relayListRepository.selectedLocation
+ ) { customLists, relayList, selectedLocation ->
+ if (selectedLocation is Constraint.Only) {
+ createRelayItemTitle(selectedLocation.value, relayList, customLists ?: emptyList())
+ } else {
+ null
+ }
+ }
+
+ private fun createRelayItemTitle(
+ relayItemId: RelayItemId,
+ relayCountries: List<RelayItem.Location.Country>,
+ customLists: List<CustomList>
+ ): String? =
+ when (relayItemId) {
+ is CustomListId -> customLists.firstOrNull { it.id == relayItemId }?.name?.value
+ is GeoLocationId.Hostname -> createRelayTitle(relayCountries, relayItemId)
+ is GeoLocationId.City -> relayCountries.findByGeoLocationId(relayItemId)?.name
+ is GeoLocationId.Country -> relayCountries.firstOrNull { it.id == relayItemId }?.name
+ }
+
+ private fun createRelayTitle(
+ relayCountries: List<RelayItem.Location.Country>,
+ relayItemId: GeoLocationId.Hostname
+ ): String? = nullable {
+ val city = relayCountries.findByGeoLocationId(relayItemId.city).bind()
+ val relay = city.relays.firstOrNull { it.id == relayItemId }.bind()
+
+ relay.formatTitle(city)
+ }
+
+ private fun RelayItem.Location.Relay.formatTitle(city: RelayItem.Location.City) =
+ "${city.name} (${name})"
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
index dec794c86c..ce0878c517 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
@@ -2,28 +2,18 @@ package net.mullvad.mullvadvpn.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.repository.InAppNotification
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
-import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-class TunnelStateNotificationUseCase(
- private val serviceConnectionManager: ServiceConnectionManager,
-) {
+class TunnelStateNotificationUseCase(private val connectionProxy: ConnectionProxy) {
fun notifications(): Flow<List<InAppNotification>> =
- serviceConnectionManager.connectionState
- .flatMapReadyConnectionOrDefault(flowOf(emptyList())) {
- it.container.connectionProxy
- .tunnelUiStateFlow()
- .distinctUntilChanged()
- .map(::tunnelStateNotification)
- .map(::listOfNotNull)
- }
+ connectionProxy.tunnelState
+ .distinctUntilChanged()
+ .map(::tunnelStateNotification)
+ .map(::listOfNotNull)
.distinctUntilChanged()
private fun tunnelStateNotification(tunnelUiState: TunnelState): InAppNotification? =
@@ -41,7 +31,4 @@ class TunnelStateNotificationUseCase(
is TunnelState.Connected,
is TunnelState.Disconnected -> null
}
-
- private fun ConnectionProxy.tunnelUiStateFlow(): Flow<TunnelState> =
- callbackFlowFromNotifier(this.onUiStateChange)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt
index 28496c4639..b7dc50a241 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCase.kt
@@ -1,28 +1,24 @@
package net.mullvad.mullvadvpn.usecase
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.ui.VersionInfo
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.util.appVersionCallbackFlow
-import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault
+import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
class VersionNotificationUseCase(
- private val serviceConnectionManager: ServiceConnectionManager,
+ private val appVersionInfoRepository: AppVersionInfoRepository,
private val isVersionInfoNotificationEnabled: Boolean,
) {
fun notifications() =
- serviceConnectionManager.connectionState
- .flatMapReadyConnectionOrDefault(flowOf(emptyList())) {
- it.container.appVersionInfoCache.appVersionCallbackFlow().map { versionInfo ->
- listOfNotNull(
- unsupportedVersionNotification(versionInfo),
- updateAvailableNotification(versionInfo)
- )
- }
+ appVersionInfoRepository
+ .versionInfo()
+ .map { versionInfo ->
+ listOfNotNull(
+ unsupportedVersionNotification(versionInfo),
+ updateAvailableNotification(versionInfo)
+ )
}
.distinctUntilChanged()
@@ -31,7 +27,7 @@ class VersionNotificationUseCase(
return null
}
- return if (versionInfo.isOutdated) {
+ return if (versionInfo.isUpdateAvailable) {
InAppNotification.UpdateAvailable(versionInfo)
} else null
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt
index 16c86c0d59..180381f771 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt
@@ -1,22 +1,30 @@
package net.mullvad.mullvadvpn.usecase.customlists
+import arrow.core.Either
+import arrow.core.raise.either
import kotlinx.coroutines.flow.firstOrNull
+import net.mullvad.mullvadvpn.compose.communication.Created
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
-import net.mullvad.mullvadvpn.model.CreateCustomListResult
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.UpdateCustomListResult
+import net.mullvad.mullvadvpn.compose.communication.CustomListSuccess
+import net.mullvad.mullvadvpn.compose.communication.Deleted
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
+import net.mullvad.mullvadvpn.compose.communication.Renamed
+import net.mullvad.mullvadvpn.lib.model.CreateCustomListError
+import net.mullvad.mullvadvpn.lib.model.DeleteCustomListError
+import net.mullvad.mullvadvpn.lib.model.GetCustomListError
+import net.mullvad.mullvadvpn.lib.model.UpdateCustomListLocationsError
+import net.mullvad.mullvadvpn.lib.model.UpdateCustomListNameError
import net.mullvad.mullvadvpn.relaylist.getRelayItemsByCodes
import net.mullvad.mullvadvpn.repository.CustomListsRepository
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.repository.RelayListRepository
class CustomListActionUseCase(
private val customListsRepository: CustomListsRepository,
- private val relayListUseCase: RelayListUseCase
+ private val relayListRepository: RelayListRepository
) {
- suspend fun performAction(action: CustomListAction): Result<CustomListResult> {
+ suspend fun performAction(
+ action: CustomListAction
+ ): Either<CustomListActionError, CustomListSuccess> {
return when (action) {
is CustomListAction.Create -> {
performAction(action)
@@ -33,86 +41,101 @@ class CustomListActionUseCase(
}
}
- suspend fun performAction(action: CustomListAction.Rename): Result<CustomListResult.Renamed> =
- when (
- val result =
- customListsRepository.updateCustomListName(action.customListId, action.newName)
- ) {
- is UpdateCustomListResult.Ok ->
- Result.success(CustomListResult.Renamed(undo = action.not()))
- is UpdateCustomListResult.Error -> Result.failure(CustomListsException(result.error))
- }
+ suspend fun performAction(action: CustomListAction.Rename): Either<RenameError, Renamed> =
+ customListsRepository
+ .updateCustomListName(action.id, action.newName)
+ .map { Renamed(undo = action.not()) }
+ .mapLeft(::RenameError)
+
+ suspend fun performAction(
+ action: CustomListAction.Create
+ ): Either<CreateWithLocationsError, Created> = either {
+ val customListId =
+ customListsRepository
+ .createCustomList(action.name)
+ .mapLeft(CreateWithLocationsError::Create)
+ .bind()
+
+ val locationNames =
+ if (action.locations.isNotEmpty()) {
+ customListsRepository
+ .updateCustomListLocations(customListId, action.locations)
+ .mapLeft(CreateWithLocationsError::UpdateLocations)
+ .bind()
- suspend fun performAction(action: CustomListAction.Create): Result<CustomListResult.Created> =
- when (val result = customListsRepository.createCustomList(action.name)) {
- is CreateCustomListResult.Ok -> {
- if (action.locations.isNotEmpty()) {
- customListsRepository.updateCustomListLocationsFromCodes(
- result.id,
- action.locations
- )
- val locationNames =
- relayListUseCase
- .fullRelayList()
- .firstOrNull()
- ?.getRelayItemsByCodes(action.locations)
- ?.map { it.name }
- Result.success(
- CustomListResult.Created(
- id = result.id,
- name = action.name,
- locationName = locationNames?.firstOrNull(),
- undo = action.not(result.id)
- )
- )
- } else {
- Result.success(
- CustomListResult.Created(
- id = result.id,
- name = action.name,
- locationName = null,
- undo = action.not(result.id)
- )
- )
- }
+ relayListRepository.relayList
+ .firstOrNull()
+ ?.getRelayItemsByCodes(action.locations)
+ ?.map { it.name } ?: raise(CreateWithLocationsError.UnableToFetchRelayList)
+ } else {
+ emptyList()
}
- is CreateCustomListResult.Error -> Result.failure(CustomListsException(result.error))
- }
- fun performAction(action: CustomListAction.Delete): Result<CustomListResult.Deleted> {
- val customList: CustomList = customListsRepository.getCustomListById(action.customListId)!!
- val oldLocations = customList.locations()
- val name = CustomListName.fromString(customList.name)
- customListsRepository.deleteCustomList(action.customListId)
- return Result.success(
- CustomListResult.Deleted(undo = action.not(locations = oldLocations, name = name))
+ Created(
+ id = customListId,
+ name = action.name,
+ locationNames = locationNames,
+ undo = action.not(customListId)
)
}
suspend fun performAction(
+ action: CustomListAction.Delete
+ ): Either<DeleteWithUndoError, Deleted> = either {
+ val customList =
+ customListsRepository
+ .getCustomListById(action.id)
+ .mapLeft(DeleteWithUndoError::Fetch)
+ .bind()
+ customListsRepository
+ .deleteCustomList(action.id)
+ .mapLeft(DeleteWithUndoError::Delete)
+ .bind()
+ Deleted(undo = action.not(locations = customList.locations, name = customList.name))
+ }
+
+ suspend fun performAction(
action: CustomListAction.UpdateLocations
- ): Result<CustomListResult.LocationsChanged> {
- val customList = customListsRepository.getCustomListById(action.customListId)!!
- val oldLocations = customList.locations()
- val name = CustomListName.fromString(customList.name)
- customListsRepository.updateCustomListLocationsFromCodes(
- action.customListId,
- action.locations
- )
- return Result.success(
- CustomListResult.LocationsChanged(
- name = name,
- undo = action.not(locations = oldLocations)
- )
+ ): Either<UpdateLocationsError, LocationsChanged> = either {
+ val customList =
+ customListsRepository
+ .getCustomListById(action.id)
+ .mapLeft(UpdateLocationsError::Fetch)
+ .bind()
+ customListsRepository
+ .updateCustomListLocations(action.id, action.locations)
+ .mapLeft(UpdateLocationsError::UpdateLocations)
+ .bind()
+ LocationsChanged(
+ name = customList.name,
+ undo = action.not(locations = customList.locations)
)
}
+}
- private fun CustomList?.locations(): List<String> =
- this?.locations?.map {
- when (it) {
- is GeographicLocationConstraint.City -> it.cityCode
- is GeographicLocationConstraint.Country -> it.countryCode
- is GeographicLocationConstraint.Hostname -> it.hostname
- }
- } ?: emptyList()
+sealed interface CustomListActionError
+
+sealed interface CreateWithLocationsError : CustomListActionError {
+
+ data class Create(val error: CreateCustomListError) : CreateWithLocationsError
+
+ data class UpdateLocations(val error: UpdateCustomListLocationsError) :
+ CreateWithLocationsError
+
+ data object UnableToFetchRelayList : CreateWithLocationsError
+}
+
+sealed interface DeleteWithUndoError : CustomListActionError {
+ data class Fetch(val error: GetCustomListError) : DeleteWithUndoError
+
+ data class Delete(val error: DeleteCustomListError) : DeleteWithUndoError
+}
+
+data class RenameError(val error: UpdateCustomListNameError) : CustomListActionError
+
+sealed interface UpdateLocationsError : CustomListActionError {
+
+ data class Fetch(val error: GetCustomListError) : UpdateLocationsError
+
+ data class UpdateLocations(val error: UpdateCustomListLocationsError) : UpdateLocationsError
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListRelayItemsUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListRelayItemsUseCase.kt
new file mode 100644
index 0000000000..d28bfe1d55
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListRelayItemsUseCase.kt
@@ -0,0 +1,26 @@
+package net.mullvad.mullvadvpn.usecase.customlists
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapNotNull
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.relaylist.getById
+import net.mullvad.mullvadvpn.relaylist.getRelayItemsByCodes
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+
+class CustomListRelayItemsUseCase(
+ private val customListsRepository: CustomListsRepository,
+ private val relayListRepository: RelayListRepository
+) {
+ fun getRelayItemLocationsForCustomList(
+ customListId: CustomListId
+ ): Flow<List<RelayItem.Location>> =
+ combine(
+ customListsRepository.customLists.mapNotNull { it?.getById(customListId) },
+ relayListRepository.relayList
+ ) { customList, countries ->
+ countries.getRelayItemsByCodes(customList.locations)
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt
deleted file mode 100644
index 07c37f7333..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package net.mullvad.mullvadvpn.usecase.customlists
-
-import net.mullvad.mullvadvpn.model.CustomListsError
-
-class CustomListsException(val error: CustomListsError) : Throwable()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsRelayItemUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsRelayItemUseCase.kt
new file mode 100644
index 0000000000..015aa8ab4f
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsRelayItemUseCase.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.usecase.customlists
+
+import kotlinx.coroutines.flow.combine
+import net.mullvad.mullvadvpn.relaylist.toRelayItemCustomList
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+
+class CustomListsRelayItemUseCase(
+ private val customListsRepository: CustomListsRepository,
+ private val relayListRepository: RelayListRepository,
+) {
+
+ fun relayItemCustomLists() =
+ combine(customListsRepository.customLists, relayListRepository.relayList) {
+ customLists,
+ relayList ->
+ customLists?.map { it.toRelayItemCustomList(relayList) } ?: emptyList()
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt
deleted file mode 100644
index ae79c1364f..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/CacheExtensions.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.mullvad.mullvadvpn.util
-
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.callbackFlow
-import net.mullvad.mullvadvpn.ui.VersionInfo
-import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
-
-fun AppVersionInfoCache.appVersionCallbackFlow() = callbackFlow {
- this@appVersionCallbackFlow.onUpdate = {
- trySend(
- VersionInfo(
- currentVersion = this@appVersionCallbackFlow.version,
- upgradeVersion = this@appVersionCallbackFlow.upgradeVersion,
- isOutdated = this@appVersionCallbackFlow.isOutdated,
- isSupported = this@appVersionCallbackFlow.isSupported,
- )
- )
- }
- awaitClose { onUpdate = null }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt
deleted file mode 100644
index 13b8f84599..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DeviceStateExtensions.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.mullvad.mullvadvpn.util
-
-import kotlin.reflect.KClass
-import net.mullvad.mullvadvpn.model.DeviceState
-
-const val UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS = 2000L
-private const val ZERO_DEBOUNCE_DELAY_MILLISECONDS = 0L
-
-fun DeviceState.addDebounceForUnknownState(delay: Long): Long {
- return addDebounceForStates(delay, DeviceState.Unknown::class)
-}
-
-fun <T> DeviceState.addDebounceForStates(delay: Long, vararg states: KClass<T>): Long where
-T : DeviceState {
- val result = states.any { this::class == it }
- return if (result) {
- delay
- } else {
- ZERO_DEBOUNCE_DELAY_MILLISECONDS
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
index b3a8727df9..fbe44a5fea 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt
@@ -3,37 +3,11 @@
package net.mullvad.mullvadvpn.util
import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retryWhen
-import kotlinx.coroutines.withTimeoutOrNull
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.talpid.util.EventNotifier
-
-fun <R> Flow<ServiceConnectionState>.flatMapReadyConnectionOrDefault(
- default: Flow<R>,
- transform: (value: ServiceConnectionState.ConnectedReady) -> Flow<R>
-): Flow<R> {
- return flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- transform.invoke(state)
- } else {
- default
- }
- }
-}
-
-fun <T> callbackFlowFromNotifier(notifier: EventNotifier<T>) = callbackFlow {
- val handler: (T) -> Unit = { value -> trySend(value) }
- notifier.subscribe(this, handler)
- awaitClose { notifier.unsubscribe(this) }
-}
inline fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
@@ -110,9 +84,6 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
}
}
-suspend inline fun <T> Deferred<T>.awaitWithTimeoutOrNull(timeout: Long) =
- withTimeoutOrNull(timeout) { await() }
-
fun <T> Deferred<T>.getOrDefault(default: T) =
try {
getCompleted()
@@ -150,7 +121,3 @@ suspend inline fun <T> Flow<T>.retryWithExponentialBackOff(
}
class ExceptionWrapper(val item: Any) : Throwable()
-
-suspend fun <T> Flow<T>.firstOrNullWithTimeout(timeMillis: Long): T? {
- return withTimeoutOrNull(timeMillis) { firstOrNull() }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt
index b978caad53..d908f44158 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt
@@ -1,6 +1,6 @@
package net.mullvad.mullvadvpn.util
-import net.mullvad.mullvadvpn.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
fun GeoIpLocation.toOutAddress(): String =
when {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt
deleted file mode 100644
index a1a1d54b36..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package net.mullvad.mullvadvpn.util
-
-fun Int.isValidMtu(): Boolean {
- return this in 1280..1420
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortConstraintExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt
index 0f0708707e..ac93b60d00 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortConstraintExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortExtensions.kt
@@ -1,8 +1,9 @@
package net.mullvad.mullvadvpn.util
import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Port
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
fun Constraint<Port>.hasValue(value: Int) =
when (this) {
@@ -16,8 +17,13 @@ fun Constraint<Port>.isCustom() =
is Constraint.Only -> !WIREGUARD_PRESET_PORTS.contains(this.value.value)
}
-fun Constraint<Port>.toValueOrNull() =
+fun Constraint<Port>.toPortOrNull() =
when (this) {
is Constraint.Any -> null
- is Constraint.Only -> this.value.value
+ is Constraint.Only -> this.value
}
+
+fun Port.inAnyOf(portRanges: List<PortRange>): Boolean =
+ portRanges.any { portRange -> this in portRange }
+
+fun List<PortRange>.asString() = joinToString(", ", transform = PortRange::toFormattedString)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortRangeExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortRangeExtensions.kt
deleted file mode 100644
index 7b7fa8b104..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PortRangeExtensions.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.mullvad.mullvadvpn.util
-
-import net.mullvad.mullvadvpn.model.PortRange
-
-fun List<PortRange>.isPortInValidRanges(port: Int) =
- this.any { portRange -> portRange.from <= port && portRange.to >= port }
-
-fun List<PortRange>.asString() = buildString {
- this@asString.forEachIndexed { index, range ->
- if (index != 0) {
- append(", ")
- }
- if (range.from == range.to) {
- append(range.from)
- } else {
- append("${range.from}-${range.to}")
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/StringExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/StringExtensions.kt
index 41e83465a1..f0cf46978b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/StringExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/StringExtensions.kt
@@ -1,8 +1,14 @@
package net.mullvad.mullvadvpn.util
+import android.text.Html
+import androidx.core.text.HtmlCompat
+
fun String.appendHideNavOnPlayBuild(isPlayBuild: Boolean): String =
if (isPlayBuild) {
"$this?hide_nav"
} else {
this
}
+
+fun String.removeHtmlTags(): String =
+ Html.fromHtml(this, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt
index d39104e67a..d8c310b029 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt
@@ -1,7 +1,7 @@
package net.mullvad.mullvadvpn.util
-import net.mullvad.talpid.net.TransportProtocol
-import net.mullvad.talpid.net.TunnelEndpoint
+import net.mullvad.mullvadvpn.lib.model.TransportProtocol
+import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
fun TunnelEndpoint.toInAddress(): Triple<String, Int, TransportProtocol> {
val relayEndpoint = this.obfuscation?.endpoint ?: this.endpoint
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
index e3c0b226dd..0a497c22f6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt
@@ -11,22 +11,18 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.PaymentState
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.util.toPaymentState
import org.joda.time.DateTime
class AccountViewModel(
private val accountRepository: AccountRepository,
- private val serviceConnectionManager: ServiceConnectionManager,
- private val paymentUseCase: PaymentUseCase,
deviceRepository: DeviceRepository,
+ private val paymentUseCase: PaymentUseCase,
private val isPlayBuild: Boolean,
) : ViewModel() {
private val _uiSideEffect = Channel<UiSideEffect>()
@@ -35,13 +31,13 @@ class AccountViewModel(
val uiState: StateFlow<AccountUiState> =
combine(
deviceRepository.deviceState,
- accountRepository.accountExpiryState,
+ accountRepository.accountData,
paymentUseCase.paymentAvailability
- ) { deviceState, accountExpiry, paymentAvailability ->
+ ) { deviceState, accountData, paymentAvailability ->
AccountUiState(
- deviceName = deviceState.deviceName() ?: "",
- accountNumber = deviceState.token() ?: "",
- accountExpiry = accountExpiry.date(),
+ deviceName = deviceState?.displayName() ?: "",
+ accountNumber = deviceState?.token()?.value ?: "",
+ accountExpiry = accountData?.expiryDate,
showSitePayment = !isPlayBuild,
billingPaymentState = paymentAvailability?.toPaymentState()
)
@@ -56,17 +52,17 @@ class AccountViewModel(
fun onManageAccountClick() {
viewModelScope.launch {
- _uiSideEffect.send(
- UiSideEffect.OpenAccountManagementPageInBrowser(
- serviceConnectionManager.authTokenCache()?.fetchAuthToken() ?: ""
- )
- )
+ accountRepository.getWebsiteAuthToken()?.let { wwwAuthToken ->
+ _uiSideEffect.send(UiSideEffect.OpenAccountManagementPageInBrowser(wwwAuthToken))
+ }
}
}
fun onLogoutClick() {
- accountRepository.logout()
- viewModelScope.launch { _uiSideEffect.send(UiSideEffect.NavigateToLogin) }
+ viewModelScope.launch {
+ accountRepository.logout()
+ _uiSideEffect.send(UiSideEffect.NavigateToLogin)
+ }
}
fun onCopyAccountNumber(accountNumber: String) {
@@ -105,13 +101,13 @@ class AccountViewModel(
}
private fun updateAccountExpiry() {
- accountRepository.fetchAccountExpiry()
+ viewModelScope.launch { accountRepository.getAccountData() }
}
sealed class UiSideEffect {
data object NavigateToLogin : UiSideEffect()
- data class OpenAccountManagementPageInBrowser(val token: String) : UiSideEffect()
+ data class OpenAccountManagementPageInBrowser(val token: WebsiteAuthToken) : UiSideEffect()
data class CopyAccountNumber(val accountNumber: String) : UiSideEffect()
}
@@ -127,9 +123,9 @@ data class AccountUiState(
companion object {
fun default() =
AccountUiState(
- deviceName = DeviceState.Unknown.deviceName(),
- accountNumber = DeviceState.Unknown.token(),
- accountExpiry = AccountExpiry.Missing.date(),
+ deviceName = null,
+ accountNumber = null,
+ accountExpiry = null,
showSitePayment = false,
billingPaymentState = PaymentState.Loading,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt
index 6b17592b8e..7b15e74a0e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModel.kt
@@ -7,12 +7,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
-import net.mullvad.mullvadvpn.BuildConfig
+import net.mullvad.mullvadvpn.lib.model.BuildVersion
import net.mullvad.mullvadvpn.repository.ChangelogRepository
class ChangelogViewModel(
private val changelogRepository: ChangelogRepository,
- private val buildVersionCode: Int,
+ private val buildVersion: BuildVersion,
private val alwaysShowChangelog: Boolean
) : ViewModel() {
@@ -22,18 +22,18 @@ class ChangelogViewModel(
init {
if (shouldShowChangelog()) {
val changelog =
- Changelog(BuildConfig.VERSION_NAME, changelogRepository.getLastVersionChanges())
+ Changelog(buildVersion.name, changelogRepository.getLastVersionChanges())
viewModelScope.launch { _uiSideEffect.emit(changelog) }
}
}
fun markChangelogAsRead() {
- changelogRepository.setVersionCodeOfMostRecentChangelogShowed(buildVersionCode)
+ changelogRepository.setVersionCodeOfMostRecentChangelogShowed(buildVersion.code)
}
private fun shouldShowChangelog(): Boolean =
alwaysShowChangelog ||
- (changelogRepository.getVersionCodeOfMostRecentChangelogShowed() < buildVersionCode &&
+ (changelogRepository.getVersionCodeOfMostRecentChangelogShowed() < buildVersion.code &&
changelogRepository.getLastVersionChanges().isNotEmpty())
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
index bebb0d6e42..c98ce4fa59 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
@@ -4,57 +4,49 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.ConnectError
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
import net.mullvad.mullvadvpn.repository.InAppNotificationController
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
+import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
+import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
import net.mullvad.mullvadvpn.util.combine
import net.mullvad.mullvadvpn.util.daysFromNow
import net.mullvad.mullvadvpn.util.toInAddress
import net.mullvad.mullvadvpn.util.toOutAddress
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-@OptIn(FlowPreview::class)
+@Suppress("LongParameterList")
class ConnectViewModel(
- private val serviceConnectionManager: ServiceConnectionManager,
- accountRepository: AccountRepository,
+ private val accountRepository: AccountRepository,
private val deviceRepository: DeviceRepository,
- private val inAppNotificationController: InAppNotificationController,
+ inAppNotificationController: InAppNotificationController,
private val newDeviceNotificationUseCase: NewDeviceNotificationUseCase,
- private val relayListUseCase: RelayListUseCase,
+ selectedLocationTitleUseCase: SelectedLocationTitleUseCase,
private val outOfTimeUseCase: OutOfTimeUseCase,
private val paymentUseCase: PaymentUseCase,
+ private val connectionProxy: ConnectionProxy,
+ lastKnownLocationUseCase: LastKnownLocationUseCase,
+ private val vpnPermissionRepository: VpnPermissionRepository,
private val isPlayBuild: Boolean
) : ViewModel() {
private val _uiSideEffect = Channel<UiSideEffect>()
@@ -62,124 +54,114 @@ class ConnectViewModel(
val uiSideEffect =
merge(_uiSideEffect.receiveAsFlow(), outOfTimeEffect(), revokedDeviceEffect())
- private val _shared: SharedFlow<ServiceConnectionContainer> =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .shareIn(viewModelScope, SharingStarted.WhileSubscribed())
-
+ @OptIn(FlowPreview::class)
val uiState: StateFlow<ConnectUiState> =
- _shared
- .flatMapLatest { serviceConnection ->
- combine(
- relayListUseCase.selectedRelayItem(),
- inAppNotificationController.notifications,
- serviceConnection.connectionProxy.tunnelUiStateFlow(),
- serviceConnection.connectionProxy.tunnelRealStateFlow(),
- serviceConnection.connectionProxy.lastKnownDisconnectedLocation(),
- accountRepository.accountExpiryState,
- deviceRepository.deviceState.map { it.deviceName() }
- ) {
- selectedRelayItem,
- notifications,
- tunnelUiState,
- tunnelRealState,
- lastKnownDisconnectedLocation,
- accountExpiry,
- deviceName ->
- ConnectUiState(
- location =
- when (tunnelRealState) {
- is TunnelState.Disconnected ->
- tunnelRealState.location() ?: lastKnownDisconnectedLocation
- is TunnelState.Connecting ->
- tunnelRealState.location ?: selectedRelayItem?.location()
- is TunnelState.Connected -> tunnelRealState.location
- is TunnelState.Disconnecting -> lastKnownDisconnectedLocation
- is TunnelState.Error -> null
- },
- selectedRelayItem = selectedRelayItem,
- tunnelUiState = tunnelUiState,
- tunnelRealState = tunnelRealState,
- inAddress =
- when (tunnelRealState) {
- is TunnelState.Connected -> tunnelRealState.endpoint.toInAddress()
- is TunnelState.Connecting -> tunnelRealState.endpoint?.toInAddress()
- else -> null
- },
- outAddress = tunnelRealState.location()?.toOutAddress() ?: "",
- showLocation =
- when (tunnelUiState) {
- is TunnelState.Disconnected -> true
- is TunnelState.Disconnecting -> {
- when (tunnelUiState.actionAfterDisconnect) {
- ActionAfterDisconnect.Nothing -> false
- ActionAfterDisconnect.Block -> true
- ActionAfterDisconnect.Reconnect -> false
- }
+ combine(
+ selectedLocationTitleUseCase.selectedLocationTitle(),
+ inAppNotificationController.notifications,
+ connectionProxy.tunnelState,
+ lastKnownLocationUseCase.lastKnownDisconnectedLocation,
+ accountRepository.accountData,
+ deviceRepository.deviceState.map { it?.displayName() }
+ ) {
+ selectedRelayItemTitle,
+ notifications,
+ tunnelState,
+ lastKnownDisconnectedLocation,
+ accountData,
+ deviceName ->
+ ConnectUiState(
+ location =
+ when (tunnelState) {
+ is TunnelState.Disconnected ->
+ tunnelState.location() ?: lastKnownDisconnectedLocation
+ is TunnelState.Connecting -> tunnelState.location
+ is TunnelState.Connected -> tunnelState.location
+ is TunnelState.Disconnecting -> lastKnownDisconnectedLocation
+ is TunnelState.Error -> null
+ },
+ selectedRelayItemTitle = selectedRelayItemTitle,
+ tunnelState = tunnelState,
+ inAddress =
+ when (tunnelState) {
+ is TunnelState.Connected -> tunnelState.endpoint.toInAddress()
+ is TunnelState.Connecting -> tunnelState.endpoint?.toInAddress()
+ else -> null
+ },
+ outAddress = tunnelState.location()?.toOutAddress() ?: "",
+ showLocation =
+ when (tunnelState) {
+ is TunnelState.Disconnected -> true
+ is TunnelState.Disconnecting -> {
+ when (tunnelState.actionAfterDisconnect) {
+ ActionAfterDisconnect.Nothing -> false
+ ActionAfterDisconnect.Block -> true
+ ActionAfterDisconnect.Reconnect -> false
}
- is TunnelState.Connecting -> false
- is TunnelState.Connected -> false
- is TunnelState.Error -> true
- },
- inAppNotification = notifications.firstOrNull(),
- deviceName = deviceName,
- daysLeftUntilExpiry = accountExpiry.date()?.daysFromNow(),
- isPlayBuild = isPlayBuild,
- )
- }
+ }
+ is TunnelState.Connecting -> false
+ is TunnelState.Connected -> false
+ is TunnelState.Error -> true
+ },
+ inAppNotification = notifications.firstOrNull(),
+ deviceName = deviceName,
+ daysLeftUntilExpiry = accountData?.expiryDate?.daysFromNow(),
+ isPlayBuild = isPlayBuild,
+ )
}
.debounce(UI_STATE_DEBOUNCE_DURATION_MILLIS)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ConnectUiState.INITIAL)
init {
-
viewModelScope.launch {
- paymentUseCase.verifyPurchases { accountRepository.fetchAccountExpiry() }
+ paymentUseCase.verifyPurchases {
+ viewModelScope.launch { accountRepository.getAccountData() }
+ }
}
}
- private fun ConnectionProxy.tunnelUiStateFlow(): Flow<TunnelState> =
- callbackFlowFromNotifier(this.onUiStateChange)
-
- private fun ConnectionProxy.tunnelRealStateFlow(): Flow<TunnelState> =
- callbackFlowFromNotifier(this.onStateChange)
-
- private fun ConnectionProxy.lastKnownDisconnectedLocation(): Flow<GeoIpLocation?> =
- tunnelRealStateFlow()
- .filterIsInstance<TunnelState.Disconnected>()
- .filter { it.location != null }
- .map { it.location }
- .onStart { emit(null) }
-
fun onDisconnectClick() {
- serviceConnectionManager.connectionProxy()?.disconnect()
+ viewModelScope.launch { connectionProxy.disconnect() }
}
fun onReconnectClick() {
- serviceConnectionManager.connectionProxy()?.reconnect()
+ viewModelScope.launch { connectionProxy.reconnect() }
}
fun onConnectClick() {
- serviceConnectionManager.connectionProxy()?.connect()
+ viewModelScope.launch {
+ connectionProxy.connect().onLeft { connectError ->
+ when (connectError) {
+ ConnectError.NoVpnPermission -> _uiSideEffect.send(UiSideEffect.NoVpnPermission)
+ is ConnectError.Unknown -> {
+ _uiSideEffect.send(UiSideEffect.ConnectError.Generic)
+ }
+ }
+ }
+ }
+ }
+
+ fun requestVpnPermissionResult(hasVpnPermission: Boolean) {
+ viewModelScope.launch {
+ if (hasVpnPermission) {
+ connectionProxy.connect()
+ } else {
+ vpnPermissionRepository.getAlwaysOnVpnAppName()?.let {
+ _uiSideEffect.send(UiSideEffect.ConnectError.AlwaysOnVpn(it))
+ } ?: _uiSideEffect.send(UiSideEffect.ConnectError.NoVpnPermission)
+ }
+ }
}
fun onCancelClick() {
- serviceConnectionManager.connectionProxy()?.disconnect()
+ viewModelScope.launch { connectionProxy.disconnect() }
}
fun onManageAccountClick() {
viewModelScope.launch {
- _uiSideEffect.trySend(
- UiSideEffect.OpenAccountManagementPageInBrowser(
- serviceConnectionManager.authTokenCache()?.fetchAuthToken() ?: ""
- )
- )
+ accountRepository.getWebsiteAuthToken()?.let { wwwAuthToken ->
+ _uiSideEffect.send(UiSideEffect.OpenAccountManagementPageInBrowser(wwwAuthToken))
+ }
}
}
@@ -196,11 +178,21 @@ class ConnectViewModel(
}
sealed interface UiSideEffect {
- data class OpenAccountManagementPageInBrowser(val token: String) : UiSideEffect
+ data class OpenAccountManagementPageInBrowser(val token: WebsiteAuthToken) : UiSideEffect
data object OutOfTime : UiSideEffect
data object RevokedDevice : UiSideEffect
+
+ data object NoVpnPermission : UiSideEffect
+
+ sealed interface ConnectError : UiSideEffect {
+ data object Generic : ConnectError
+
+ data object NoVpnPermission : ConnectError
+
+ data class AlwaysOnVpn(val appName: String) : ConnectError
+ }
}
companion object {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt
index f58916cd66..043f989598 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt
@@ -10,16 +10,17 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.compose.communication.Created
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
-import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException
class CreateCustomListDialogViewModel(
- private val locationCode: String,
+ private val locationCode: GeoLocationId?,
private val customListActionUseCase: CustomListActionUseCase,
) : ViewModel() {
@@ -27,7 +28,7 @@ class CreateCustomListDialogViewModel(
Channel<CreateCustomListDialogSideEffect>(1, BufferOverflow.DROP_OLDEST)
val uiSideEffect = _uiSideEffect.receiveAsFlow()
- private val _error = MutableStateFlow<CustomListsError?>(null)
+ private val _error = MutableStateFlow<CreateWithLocationsError?>(null)
val uiState =
_error
@@ -40,32 +41,22 @@ class CreateCustomListDialogViewModel(
.performAction(
CustomListAction.Create(
CustomListName.fromString(name),
- if (locationCode.isNotEmpty()) {
- listOf(locationCode)
- } else {
- emptyList()
- }
+ listOfNotNull(locationCode)
)
)
.fold(
- onSuccess = { result ->
- if (result.locationName != null) {
+ { _error.emit(it) },
+ {
+ if (it.locationNames.isEmpty()) {
_uiSideEffect.send(
- CreateCustomListDialogSideEffect.ReturnWithResult(result)
+ CreateCustomListDialogSideEffect
+ .NavigateToCustomListLocationsScreen(it.id)
)
} else {
_uiSideEffect.send(
- CreateCustomListDialogSideEffect
- .NavigateToCustomListLocationsScreen(result.id)
+ CreateCustomListDialogSideEffect.ReturnWithResult(it)
)
}
- },
- onFailure = { error ->
- if (error is CustomListsException) {
- _error.emit(error.error)
- } else {
- _error.emit(CustomListsError.OtherError)
- }
}
)
}
@@ -78,9 +69,8 @@ class CreateCustomListDialogViewModel(
sealed interface CreateCustomListDialogSideEffect {
- data class NavigateToCustomListLocationsScreen(val customListId: String) :
+ data class NavigateToCustomListLocationsScreen(val customListId: CustomListId) :
CreateCustomListDialogSideEffect
- data class ReturnWithResult(val result: CustomListResult.Created) :
- CreateCustomListDialogSideEffect
+ data class ReturnWithResult(val result: Created) : CreateCustomListDialogSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
index cdbcebbb83..581c11c397 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
@@ -7,37 +7,39 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm
-import net.mullvad.mullvadvpn.relaylist.getById
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.relaylist.withDescendants
+import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
-import net.mullvad.mullvadvpn.util.firstOrNullWithTimeout
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase
class CustomListLocationsViewModel(
- private val customListId: String,
+ private val customListId: CustomListId,
private val newList: Boolean,
- private val relayListUseCase: RelayListUseCase,
+ relayListRepository: RelayListRepository,
+ private val customListRelayItemsUseCase: CustomListRelayItemsUseCase,
private val customListActionUseCase: CustomListActionUseCase
) : ViewModel() {
private val _uiSideEffect =
MutableSharedFlow<CustomListLocationsSideEffect>(replay = 1, extraBufferCapacity = 1)
val uiSideEffect: SharedFlow<CustomListLocationsSideEffect> = _uiSideEffect
- private val _initialLocations = MutableStateFlow<Set<RelayItem>>(emptySet())
- private val _selectedLocations = MutableStateFlow<Set<RelayItem>?>(null)
+ private val _initialLocations = MutableStateFlow<Set<RelayItem.Location>>(emptySet())
+ private val _selectedLocations = MutableStateFlow<Set<RelayItem.Location>?>(null)
private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM)
val uiState =
- combine(relayListUseCase.fullRelayList(), _searchTerm, _selectedLocations) {
+ combine(relayListRepository.relayList, _searchTerm, _selectedLocations) {
relayCountries,
searchTerm,
selectedLocations ->
@@ -77,27 +79,32 @@ class CustomListLocationsViewModel(
fun save() {
viewModelScope.launch {
_selectedLocations.value?.let { selectedLocations ->
- val result =
- customListActionUseCase.performAction(
+ customListActionUseCase
+ .performAction(
CustomListAction.UpdateLocations(
customListId,
- selectedLocations.calculateLocationsToSave().map { it.code }
+ selectedLocations.calculateLocationsToSave().map { it.id }
)
)
- _uiSideEffect.tryEmit(
- // This is so that we don't show a snackbar after returning to the select
- // location screen
- if (newList) {
- CustomListLocationsSideEffect.CloseScreen
- } else {
- CustomListLocationsSideEffect.ReturnWithResult(result.getOrThrow())
- }
- )
+ .fold(
+ { _uiSideEffect.tryEmit(CustomListLocationsSideEffect.Error) },
+ {
+ _uiSideEffect.tryEmit(
+ // This is so that we don't show a snackbar after returning to the
+ // select location screen
+ if (newList) {
+ CustomListLocationsSideEffect.CloseScreen
+ } else {
+ CustomListLocationsSideEffect.ReturnWithResult(it)
+ }
+ )
+ }
+ )
}
}
}
- fun onRelaySelectionClick(relayItem: RelayItem, selected: Boolean) {
+ fun onRelaySelectionClick(relayItem: RelayItem.Location, selected: Boolean) {
if (selected) {
selectLocation(relayItem)
} else {
@@ -109,13 +116,7 @@ class CustomListLocationsViewModel(
viewModelScope.launch { _searchTerm.emit(searchTerm) }
}
- private suspend fun awaitCustomListById(id: String): RelayItem.CustomList? =
- relayListUseCase
- .customLists()
- .mapNotNull { customList -> customList.getById(id) }
- .firstOrNullWithTimeout(GET_CUSTOM_LIST_TIMEOUT_MS)
-
- private fun selectLocation(relayItem: RelayItem) {
+ private fun selectLocation(relayItem: RelayItem.Location) {
viewModelScope.launch {
_selectedLocations.update {
it?.plus(relayItem)?.plus(relayItem.descendants()) ?: setOf(relayItem)
@@ -123,7 +124,7 @@ class CustomListLocationsViewModel(
}
}
- private fun deselectLocation(relayItem: RelayItem) {
+ private fun deselectLocation(relayItem: RelayItem.Location) {
viewModelScope.launch {
_selectedLocations.update {
val newSelectedLocations = it?.toMutableSet() ?: mutableSetOf()
@@ -136,30 +137,31 @@ class CustomListLocationsViewModel(
}
}
- private fun availableLocations(): List<RelayItem.Country> =
+ private fun availableLocations(): List<RelayItem.Location.Country> =
(uiState.value as? CustomListLocationsUiState.Content.Data)?.availableLocations
?: emptyList()
- private fun Set<RelayItem>.deselectParents(relayItem: RelayItem): Set<RelayItem> {
+ private fun Set<RelayItem.Location>.deselectParents(
+ relayItem: RelayItem.Location
+ ): Set<RelayItem.Location> {
val availableLocations = availableLocations()
val updateSelectionList = this.toMutableSet()
when (relayItem) {
- is RelayItem.City -> {
+ is RelayItem.Location.City -> {
availableLocations
- .find { it.code == relayItem.location.countryCode }
+ .find { it.id == relayItem.id.country }
?.let { updateSelectionList.remove(it) }
}
- is RelayItem.Relay -> {
+ is RelayItem.Location.Relay -> {
availableLocations
.flatMap { country -> country.cities }
- .find { it.code == relayItem.location.cityCode }
+ .find { it.id == relayItem.id.city }
?.let { updateSelectionList.remove(it) }
availableLocations
- .find { it.code == relayItem.location.countryCode }
+ .find { it.id == relayItem.id.country }
?.let { updateSelectionList.remove(it) }
}
- is RelayItem.Country,
- is RelayItem.CustomList -> {
+ is RelayItem.Location.Country -> {
/* Do nothing */
}
}
@@ -167,20 +169,19 @@ class CustomListLocationsViewModel(
return updateSelectionList
}
- private fun Set<RelayItem>.calculateLocationsToSave(): List<RelayItem> {
+ private fun Set<RelayItem.Location>.calculateLocationsToSave(): List<RelayItem.Location> {
// We don't want to save children for a selected parent
val saveSelectionList = this.toMutableList()
this.forEach { relayItem ->
when (relayItem) {
- is RelayItem.Country -> {
+ is RelayItem.Location.Country -> {
saveSelectionList.removeAll(relayItem.cities)
saveSelectionList.removeAll(relayItem.relays)
}
- is RelayItem.City -> {
+ is RelayItem.Location.City -> {
saveSelectionList.removeAll(relayItem.relays)
}
- is RelayItem.Relay,
- is RelayItem.CustomList -> {
+ is RelayItem.Location.Relay -> {
/* Do nothing */
}
}
@@ -188,25 +189,27 @@ class CustomListLocationsViewModel(
return saveSelectionList
}
- private fun List<RelayItem>.selectChildren(): Set<RelayItem> =
- (this + flatMap { it.descendants() }).toSet()
-
private suspend fun fetchInitialSelectedLocations() {
- _selectedLocations.value =
- awaitCustomListById(customListId)?.locations?.selectChildren().apply {
- _initialLocations.value = this ?: emptySet()
- }
+ val selectedLocations =
+ customListRelayItemsUseCase
+ .getRelayItemLocationsForCustomList(customListId)
+ .first()
+ .withDescendants()
+ .toSet()
+
+ _initialLocations.value = selectedLocations
+ _selectedLocations.value = selectedLocations
}
companion object {
private const val EMPTY_SEARCH_TERM = ""
- private const val GET_CUSTOM_LIST_TIMEOUT_MS = 5000L
}
}
sealed interface CustomListLocationsSideEffect {
data object CloseScreen : CustomListLocationsSideEffect
- data class ReturnWithResult(val result: CustomListResult.LocationsChanged) :
- CustomListLocationsSideEffect
+ data class ReturnWithResult(val result: LocationsChanged) : CustomListLocationsSideEffect
+
+ data object Error : CustomListLocationsSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
index 79a2ba61c6..3689ad7fc8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModel.kt
@@ -3,23 +3,24 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
class CustomListsViewModel(
- relayListUseCase: RelayListUseCase,
+ customListsRepository: CustomListsRepository,
private val customListActionUseCase: CustomListActionUseCase
) : ViewModel() {
val uiState =
- relayListUseCase
- .customLists()
- .map { CustomListsUiState.Content(it) }
+ customListsRepository.customLists
+ .filterNotNull()
+ .map(CustomListsUiState::Content)
.stateIn(
viewModelScope,
started = SharingStarted.WhileSubscribed(),
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt
index e3c7f45664..79c2a133c2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt
@@ -3,31 +3,54 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Deleted
+import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState
+import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.DeleteWithUndoError
class DeleteCustomListConfirmationViewModel(
- private val customListId: String,
+ private val customListId: CustomListId,
private val customListActionUseCase: CustomListActionUseCase
) : ViewModel() {
private val _uiSideEffect = Channel<DeleteCustomListConfirmationSideEffect>(Channel.BUFFERED)
val uiSideEffect = _uiSideEffect.receiveAsFlow()
+ private val _error = MutableStateFlow<DeleteWithUndoError?>(null)
+
+ val uiState =
+ _error
+ .map { DeleteCustomListUiState(it) }
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ DeleteCustomListUiState(null)
+ )
+
fun deleteCustomList() {
viewModelScope.launch {
- val result =
- customListActionUseCase
- .performAction(CustomListAction.Delete(customListId))
- .getOrThrow()
- _uiSideEffect.send(DeleteCustomListConfirmationSideEffect.ReturnWithResult(result))
+ _error.emit(null)
+ customListActionUseCase
+ .performAction(CustomListAction.Delete(customListId))
+ .fold(
+ { _error.tryEmit(it) },
+ {
+ _uiSideEffect.send(
+ DeleteCustomListConfirmationSideEffect.ReturnWithResult(it)
+ )
+ }
+ )
}
}
}
-sealed class DeleteCustomListConfirmationSideEffect {
- data class ReturnWithResult(val result: CustomListResult.Deleted) :
- DeleteCustomListConfirmationSideEffect()
+sealed interface DeleteCustomListConfirmationSideEffect {
+ data class ReturnWithResult(val result: Deleted) : DeleteCustomListConfirmationSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
index 7b6c092ded..d2c8780606 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt
@@ -1,6 +1,5 @@
package net.mullvad.mullvadvpn.viewmodel
-import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
@@ -8,111 +7,87 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.onSubscription
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeoutOrNull
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.state.DeviceListItemUiState
+import net.mullvad.mullvadvpn.compose.state.DeviceItemUiState
import net.mullvad.mullvadvpn.compose.state.DeviceListUiState
-import net.mullvad.mullvadvpn.lib.common.util.parseAsDateTime
-import net.mullvad.mullvadvpn.model.Device
-import net.mullvad.mullvadvpn.model.DeviceList
-import net.mullvad.mullvadvpn.model.RemoveDeviceResult
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-
-typealias DeviceId = String
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
class DeviceListViewModel(
private val deviceRepository: DeviceRepository,
- private val resources: Resources,
- private val dispatcher: CoroutineDispatcher = Dispatchers.Default
+ private val token: AccountToken,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.Default,
) : ViewModel() {
- private val _loadingDevices = MutableStateFlow<List<DeviceId>>(emptyList())
+ private val loadingDevices = MutableStateFlow<Set<DeviceId>>(emptySet())
+ private val deviceList = MutableStateFlow<List<Device>>(emptyList())
+ private val loading = MutableStateFlow(true)
+ private val error = MutableStateFlow<GetDeviceListError?>(null)
private val _uiSideEffect = Channel<DeviceListSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
- private var cachedDeviceList: List<Device>? = null
-
- val uiState =
- combine(deviceRepository.deviceList, _loadingDevices) { deviceList, loadingDevices ->
- val devices =
- if (deviceList is DeviceList.Available) {
- deviceList.devices.also { cachedDeviceList = it }
- } else {
- cachedDeviceList
- }
- val deviceUiItems =
- devices
- ?.sortedBy { it.created.parseAsDateTime() }
- ?.map { device ->
- DeviceListItemUiState(
- device,
- loadingDevices.any { loadingDevice -> device.id == loadingDevice }
- )
- }
- val isLoading = devices == null
- DeviceListUiState(
- deviceUiItems = deviceUiItems ?: emptyList(),
- isLoading = isLoading,
- )
- }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DeviceListUiState.INITIAL)
-
- fun removeDevice(accountToken: String, deviceIdToRemove: DeviceId) {
-
- viewModelScope.launch {
- withContext(dispatcher) {
- val result =
- withTimeoutOrNull(DEVICE_REMOVAL_TIMEOUT_MILLIS) {
- deviceRepository.deviceRemovalEvent
- .onSubscription {
- setLoadingDevice(deviceIdToRemove)
- deviceRepository.removeDevice(accountToken, deviceIdToRemove)
- }
- .filter { (deviceId, result) ->
- deviceId == deviceIdToRemove && result == RemoveDeviceResult.Ok
- }
- .first()
- }
-
- clearLoadingDevice(deviceIdToRemove)
-
- if (result == null) {
- _uiSideEffect.send(
- DeviceListSideEffect.ShowToast(
- resources.getString(R.string.failed_to_remove_device)
+ val uiState: StateFlow<DeviceListUiState> =
+ combine(
+ loadingDevices,
+ deviceList.map { it.sortedBy { it.creationDate } },
+ loading,
+ error
+ ) { loadingDevices, devices, loading, error ->
+ when {
+ loading -> DeviceListUiState.Loading
+ error != null -> DeviceListUiState.Error(error)
+ else ->
+ DeviceListUiState.Content(
+ devices.map { DeviceItemUiState(it, loadingDevices.contains(it.id)) }
)
- )
- refreshDeviceList(accountToken)
}
}
- }
- }
+ .onStart { fetchDevices() }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), DeviceListUiState.Loading)
- fun refreshDeviceState() = deviceRepository.refreshDeviceState()
-
- fun refreshDeviceList(accountToken: String) = deviceRepository.refreshDeviceList(accountToken)
+ fun fetchDevices() =
+ viewModelScope.launch {
+ error.value = null
+ loading.value = true
+ deviceRepository.deviceList(token).fold({ error.value = it }, { deviceList.value = it })
+ loading.value = false
+ }
- private fun setLoadingDevice(deviceId: DeviceId) {
- _loadingDevices.value = _loadingDevices.value.toMutableList().apply { add(deviceId) }
- }
+ fun removeDevice(deviceIdToRemove: DeviceId) =
+ viewModelScope.launch(dispatcher) {
+ setLoadingState(deviceIdToRemove, true)
+ deviceRepository
+ .removeDevice(token, deviceIdToRemove)
+ .fold(
+ {
+ _uiSideEffect.send(DeviceListSideEffect.FailedToRemoveDevice)
+ setLoadingState(deviceIdToRemove, false)
+ deviceRepository.deviceList(token).onRight { deviceList.value = it }
+ },
+ { removeDeviceFromState(deviceIdToRemove) }
+ )
+ }
- private fun clearLoadingDevice(deviceId: DeviceId) {
- _loadingDevices.value = _loadingDevices.value.toMutableList().apply { remove(deviceId) }
+ private fun setLoadingState(deviceId: DeviceId, isLoading: Boolean) {
+ loadingDevices.update { if (isLoading) it + deviceId else it - deviceId }
}
- companion object {
- private const val DEVICE_REMOVAL_TIMEOUT_MILLIS = 5000L
+ private fun removeDeviceFromState(deviceId: DeviceId) {
+ deviceList.update { devices -> devices.filter { item -> item.id != deviceId } }
+ loadingDevices.update { it - deviceId }
}
}
sealed interface DeviceListSideEffect {
- data class ShowToast(val text: String) : DeviceListSideEffect
+ data object FailedToRemoveDevice : DeviceListSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
index 4cb02c748f..8d526ba9b2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt
@@ -7,37 +7,28 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.DeviceRevokedUiState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
-import net.mullvad.talpid.util.callbackFlowFromSubscription
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
-// TODO: Refactor ConnectionProxy to be easily injectable rather than injecting
-// ServiceConnectionManager here.
class DeviceRevokedViewModel(
- private val serviceConnectionManager: ServiceConnectionManager,
private val accountRepository: AccountRepository,
+ private val connectionProxy: ConnectionProxy,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
val uiState =
- serviceConnectionManager.connectionState
- .map { connectionState -> connectionState.readyContainer()?.connectionProxy }
- .flatMapLatest { proxy ->
- proxy?.onUiStateChange?.callbackFlowFromSubscription(this)?.map {
- if (it.isSecured()) {
- DeviceRevokedUiState.SECURED
- } else {
- DeviceRevokedUiState.UNSECURED
- }
- } ?: flowOf(DeviceRevokedUiState.UNKNOWN)
+ connectionProxy.tunnelState
+ .map {
+ if (it.isSecured()) {
+ DeviceRevokedUiState.SECURED
+ } else {
+ DeviceRevokedUiState.UNSECURED
+ }
}
.stateIn(
scope = CoroutineScope(dispatcher),
@@ -49,12 +40,10 @@ class DeviceRevokedViewModel(
val uiSideEffect = _uiSideEffect.receiveAsFlow()
fun onGoToLoginClicked() {
- serviceConnectionManager.connectionProxy()?.let { proxy ->
- if (proxy.state.isSecured()) {
- proxy.disconnect()
- }
+ viewModelScope.launch {
+ connectionProxy.disconnect()
+ accountRepository.logout()
}
- accountRepository.logout()
viewModelScope.launch { _uiSideEffect.send(DeviceRevokedSideEffect.NavigateToLogin) }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
index 4703e1cbf9..cc377b0bab 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt
@@ -15,13 +15,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.constant.EMPTY_STRING
-import net.mullvad.mullvadvpn.model.Settings
+import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.SettingsRepository
import org.apache.commons.validator.routines.InetAddressValidator
sealed interface DnsDialogSideEffect {
data object Complete : DnsDialogSideEffect
+
+ data object Error : DnsDialogSideEffect
}
data class DnsDialogViewModelState(
@@ -116,25 +117,25 @@ class DnsDialogViewModel(
val address = InetAddress.getByName(uiState.value.ipAddress)
- repository.updateCustomDnsList {
- it.toMutableList().apply {
- if (index != null) {
- set(index, address)
- } else {
- add(address)
- }
+ if (index != null) {
+ repository.setCustomDns(index = index, address = address)
+ } else {
+ repository.addCustomDns(address = address)
}
- }
-
- _uiSideEffect.send(DnsDialogSideEffect.Complete)
+ .fold(
+ { _uiSideEffect.send(DnsDialogSideEffect.Error) },
+ { _uiSideEffect.send(DnsDialogSideEffect.Complete) }
+ )
}
fun onRemoveDnsClick() =
viewModelScope.launch(dispatcher) {
- repository.updateCustomDnsList {
- it.filter { it.hostAddress != uiState.value.ipAddress }
- }
- _uiSideEffect.send(DnsDialogSideEffect.Complete)
+ repository
+ .deleteCustomDns(InetAddress.getByName(uiState.value.ipAddress))
+ .fold(
+ { _uiSideEffect.send(DnsDialogSideEffect.Error) },
+ { _uiSideEffect.send(DnsDialogSideEffect.Complete) }
+ )
}
private fun String.isValidIp(): Boolean {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt
index 9a8d3d2f62..7c45bed0d7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt
@@ -11,16 +11,16 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
-import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.compose.communication.Renamed
+import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
-import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException
+import net.mullvad.mullvadvpn.usecase.customlists.RenameError
class EditCustomListNameDialogViewModel(
- private val customListId: String,
- private val initialName: String,
+ private val customListId: CustomListId,
+ private val initialName: CustomListName,
private val customListActionUseCase: CustomListActionUseCase
) : ViewModel() {
@@ -28,15 +28,15 @@ class EditCustomListNameDialogViewModel(
Channel<EditCustomListNameDialogSideEffect>(1, BufferOverflow.DROP_OLDEST)
val uiSideEffect = _uiSideEffect.receiveAsFlow()
- private val _error = MutableStateFlow<CustomListsError?>(null)
+ private val _error = MutableStateFlow<RenameError?>(null)
val uiState =
_error
- .map { UpdateCustomListUiState(name = initialName, error = it) }
+ .map { EditCustomListNameUiState(name = initialName.value, error = it) }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
- UpdateCustomListUiState(name = initialName)
+ EditCustomListNameUiState(name = initialName.value)
)
fun updateCustomListName(name: String) {
@@ -44,24 +44,14 @@ class EditCustomListNameDialogViewModel(
customListActionUseCase
.performAction(
CustomListAction.Rename(
- customListId = customListId,
- name = CustomListName.fromString(initialName),
+ id = customListId,
+ name = initialName,
newName = CustomListName.fromString(name)
)
)
.fold(
- onSuccess = { result ->
- _uiSideEffect.send(
- EditCustomListNameDialogSideEffect.ReturnWithResult(result)
- )
- },
- onFailure = { exception ->
- if (exception is CustomListsException) {
- _error.emit(exception.error)
- } else {
- _error.emit(CustomListsError.OtherError)
- }
- }
+ { _error.emit(it) },
+ { _uiSideEffect.send(EditCustomListNameDialogSideEffect.ReturnWithResult(it)) }
)
}
}
@@ -72,6 +62,5 @@ class EditCustomListNameDialogViewModel(
}
sealed interface EditCustomListNameDialogSideEffect {
- data class ReturnWithResult(val result: CustomListResult.Renamed) :
- EditCustomListNameDialogSideEffect
+ data class ReturnWithResult(val result: Renamed) : EditCustomListNameDialogSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt
index 81232e63d5..adfacceb4e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt
@@ -6,18 +6,18 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.EditCustomListState
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
class EditCustomListViewModel(
- private val customListId: String,
- relayListUseCase: RelayListUseCase
+ private val customListId: CustomListId,
+ customListsRepository: CustomListsRepository
) : ViewModel() {
val uiState =
- relayListUseCase
- .customLists()
+ customListsRepository.customLists
.map { customLists ->
customLists
- .find { it.id == customListId }
+ ?.find { it.id == customListId }
?.let {
EditCustomListState.Content(
id = it.id,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt
index 0d39ffa625..6e139f4d7f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt
@@ -16,12 +16,14 @@ import net.mullvad.mullvadvpn.compose.state.toConstraintProviders
import net.mullvad.mullvadvpn.compose.state.toNullableOwnership
import net.mullvad.mullvadvpn.compose.state.toOwnershipConstraint
import net.mullvad.mullvadvpn.compose.state.toSelectedProviders
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.Provider
-import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
+import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase
class FilterViewModel(
- private val relayListFilterUseCase: RelayListFilterUseCase,
+ private val availableProvidersUseCase: AvailableProvidersUseCase,
+ private val relayListFilterRepository: RelayListFilterRepository
) : ViewModel() {
private val _uiSideEffect = Channel<FilterScreenSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
@@ -33,14 +35,14 @@ class FilterViewModel(
viewModelScope.launch {
selectedProviders.value =
combine(
- relayListFilterUseCase.availableProviders(),
- relayListFilterUseCase.selectedProviders(),
+ availableProvidersUseCase.availableProviders(),
+ relayListFilterRepository.selectedProviders,
) { allProviders, selectedConstraintProviders ->
selectedConstraintProviders.toSelectedProviders(allProviders)
}
.first()
- val ownershipConstraint = relayListFilterUseCase.selectedOwnership().first()
+ val ownershipConstraint = relayListFilterRepository.selectedOwnership.first()
selectedOwnership.value = ownershipConstraint.toNullableOwnership()
}
}
@@ -48,7 +50,7 @@ class FilterViewModel(
val uiState: StateFlow<RelayFilterState> =
combine(
selectedOwnership,
- relayListFilterUseCase.availableProviders(),
+ availableProvidersUseCase.availableProviders(),
selectedProviders,
) { selectedOwnership, allProviders, selectedProviders ->
RelayFilterState(
@@ -84,7 +86,7 @@ class FilterViewModel(
viewModelScope.launch {
selectedProviders.value =
if (isChecked) {
- relayListFilterUseCase.availableProviders().first()
+ availableProvidersUseCase.availableProviders().first()
} else {
emptyList()
}
@@ -97,7 +99,7 @@ class FilterViewModel(
selectedProviders.value.toConstraintProviders(uiState.value.allProviders)
viewModelScope.launch {
- relayListFilterUseCase.updateOwnershipAndProviderFilter(
+ relayListFilterRepository.updateSelectedOwnershipAndProviderFilter(
newSelectedOwnership,
newSelectedProviders
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
index 9af9d700ce..e568021177 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt
@@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -24,16 +24,11 @@ import net.mullvad.mullvadvpn.compose.state.LoginState.Idle
import net.mullvad.mullvadvpn.compose.state.LoginState.Loading
import net.mullvad.mullvadvpn.compose.state.LoginState.Success
import net.mullvad.mullvadvpn.compose.state.LoginUiState
-import net.mullvad.mullvadvpn.constant.LOGIN_TIMEOUT_MILLIS
-import net.mullvad.mullvadvpn.model.AccountCreationResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.AccountToken
-import net.mullvad.mullvadvpn.model.LoginResult
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.LoginAccountError
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.usecase.ConnectivityUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
-import net.mullvad.mullvadvpn.util.awaitWithTimeoutOrNull
import net.mullvad.mullvadvpn.util.getOrDefault
private const val MINIMUM_LOADING_SPINNER_TIME_MILLIS = 500L
@@ -50,7 +45,6 @@ sealed interface LoginUiSideEffect {
class LoginViewModel(
private val accountRepository: AccountRepository,
- private val deviceRepository: DeviceRepository,
private val newDeviceNotificationUseCase: NewDeviceNotificationUseCase,
private val connectivityUseCase: ConnectivityUseCase,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
@@ -61,27 +55,42 @@ class LoginViewModel(
private val _uiSideEffect = Channel<LoginUiSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
+ private val _mutableAccountHistory: MutableStateFlow<AccountToken?> = MutableStateFlow(null)
+
private val _uiState =
combine(
_loginInput,
- accountRepository.accountHistory,
+ _mutableAccountHistory,
_loginState,
- ) { loginInput, accountHistoryState, loginState ->
- LoginUiState(
- loginInput,
- accountHistoryState.accountToken()?.let(::AccountToken),
- loginState
- )
+ ) { loginInput, historyAccountToken, loginState ->
+ LoginUiState(loginInput, historyAccountToken, loginState)
}
+
val uiState: StateFlow<LoginUiState> =
- _uiState.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), LoginUiState.INITIAL)
+ _uiState
+ .onStart {
+ viewModelScope.launch {
+ _mutableAccountHistory.update { accountRepository.fetchAccountHistory() }
+ }
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), LoginUiState.INITIAL)
- fun clearAccountHistory() = accountRepository.clearAccountHistory()
+ fun clearAccountHistory() =
+ viewModelScope.launch {
+ accountRepository.clearAccountHistory()
+ _mutableAccountHistory.update { null }
+ _mutableAccountHistory.update { accountRepository.fetchAccountHistory() }
+ }
fun createAccount() {
_loginState.value = Loading.CreatingAccount
viewModelScope.launch(dispatcher) {
- accountRepository.createAccount().mapToUiState()?.let { _loginState.value = it }
+ accountRepository
+ .createAccount()
+ .fold(
+ { _loginState.value = Idle(LoginError.UnableToCreateAccount) },
+ { _uiSideEffect.send(LoginUiSideEffect.NavigateToWelcome) }
+ )
}
}
@@ -94,76 +103,68 @@ class LoginViewModel(
viewModelScope.launch(dispatcher) {
// Ensure we always take at least MINIMUM_LOADING_SPINNER_TIME_MILLIS to show the
// loading indicator
- val loginDeferred = async { accountRepository.login(accountToken) }
+ val result = async { accountRepository.login(AccountToken(accountToken)) }
+
delay(MINIMUM_LOADING_SPINNER_TIME_MILLIS)
val uiState =
- // If timed out will go to the else branch
- when (val result = loginDeferred.awaitWithTimeoutOrNull(LOGIN_TIMEOUT_MILLIS)) {
- LoginResult.Ok -> {
- newDeviceNotificationUseCase.newDeviceCreated()
- launch {
- val isOutOfTimeDeferred = async {
- accountRepository.accountExpiryState
- .filterIsInstance<AccountExpiry.Available>()
- .map { it.expiryDateTime.isBeforeNow }
- .first()
- }
- delay(1000)
- val isOutOfTime = isOutOfTimeDeferred.getOrDefault(false)
- if (isOutOfTime) {
- _uiSideEffect.send(LoginUiSideEffect.NavigateToOutOfTime)
- } else {
- _uiSideEffect.send(LoginUiSideEffect.NavigateToConnect)
- }
+ result
+ .await()
+ .fold(
+ { it.toUiState() },
+ {
+ onSuccessfulLogin()
+ Success
}
- Success
- }
- LoginResult.InvalidAccount -> Idle(LoginError.InvalidCredentials)
- LoginResult.MaxDevicesReached -> {
- // TODO this refresh process should be handled by DeviceListScreen.
- val refreshResult =
- deviceRepository.refreshAndAwaitDeviceListWithTimeout(
- accountToken = accountToken,
- shouldClearCache = true,
- shouldOverrideCache = true,
- timeoutMillis = 5000L
- )
+ )
- if (refreshResult.isAvailable()) {
- // Navigate to device list
-
- _uiSideEffect.send(
- LoginUiSideEffect.TooManyDevices(AccountToken(accountToken))
- )
- Idle()
- } else {
- // Failed to fetch devices list
- Idle(LoginError.Unknown(result.toString()))
- }
- }
- else -> Idle(LoginError.Unknown(result.toString()))
- }
_loginState.update { uiState }
}
}
+ private suspend fun onSuccessfulLogin() {
+ newDeviceNotificationUseCase.newDeviceCreated()
+
+ viewModelScope.launch(dispatcher) {
+ // Find if user is out of time
+ val isOutOfTimeDeferred = async {
+ accountRepository.accountData.mapNotNull { it?.expiryDate?.isBeforeNow }.first()
+ }
+
+ // Always show successful login for some time.
+ delay(SHOW_SUCCESSFUL_LOGIN_MILLIS)
+
+ // Get the result of isOutOfTime or assume not out of time
+ val isOutOfTime = isOutOfTimeDeferred.getOrDefault(false)
+
+ if (isOutOfTime) {
+ _uiSideEffect.send(LoginUiSideEffect.NavigateToOutOfTime)
+ } else {
+ _uiSideEffect.send(LoginUiSideEffect.NavigateToConnect)
+ }
+ }
+ }
+
fun onAccountNumberChange(accountNumber: String) {
_loginInput.value = accountNumber.filter { it.isDigit() }
// If there is an error, clear it
_loginState.update { if (it is Idle) Idle() else it }
}
- private suspend fun AccountCreationResult.mapToUiState(): LoginState? {
- return if (this is AccountCreationResult.Success) {
- _uiSideEffect.send(LoginUiSideEffect.NavigateToWelcome)
- null
- } else {
- Idle(LoginError.UnableToCreateAccount)
+ private suspend fun LoginAccountError.toUiState(): LoginState =
+ when (this) {
+ LoginAccountError.InvalidAccount -> Idle(LoginError.InvalidCredentials)
+ is LoginAccountError.MaxDevicesReached ->
+ Idle().also { _uiSideEffect.send(LoginUiSideEffect.TooManyDevices(accountToken)) }
+ LoginAccountError.RpcError,
+ is LoginAccountError.Unknown -> Idle(LoginError.Unknown(this.toString()))
}
- }
private fun isInternetAvailable(): Boolean {
return connectivityUseCase.isInternetAvailable()
}
+
+ companion object {
+ private const val SHOW_SUCCESSFUL_LOGIN_MILLIS = 1000L
+ }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt
index 4b6e8ed767..9d1a17207c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt
@@ -5,34 +5,77 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.util.isValidMtu
class MtuDialogViewModel(
private val repository: SettingsRepository,
+ private val initialMtu: Mtu?,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
+ private val _mtuInput = MutableStateFlow(initialMtu?.value?.toString() ?: "")
+ private val _isValidMtu = MutableStateFlow(true)
+ val uiState: StateFlow<MtuDialogUiState> =
+ combine(_mtuInput, _isValidMtu, ::createState)
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ createState(_mtuInput.value, _isValidMtu.value)
+ )
+
private val _uiSideEffect = Channel<MtuDialogSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
- fun onSaveClick(mtuValue: Int) =
+ private fun createState(mtuInput: String, isValidMtuInput: Boolean) =
+ MtuDialogUiState(
+ mtuInput = mtuInput,
+ isValidInput = isValidMtuInput,
+ showResetToDefault = initialMtu != null
+ )
+
+ fun onInputChanged(value: String) {
+ _mtuInput.value = value
+ _isValidMtu.value = Mtu.fromString(value).isRight()
+ }
+
+ fun onSaveClick(mtuValue: String) =
viewModelScope.launch(dispatcher) {
- if (mtuValue.isValidMtu()) {
- repository.setWireguardMtu(mtuValue)
- }
- _uiSideEffect.send(MtuDialogSideEffect.Complete)
+ val mtu = Mtu.fromString(mtuValue).getOrNull() ?: return@launch
+ repository
+ .setWireguardMtu(mtu)
+ .fold(
+ { _uiSideEffect.send(MtuDialogSideEffect.Error) },
+ { _uiSideEffect.send(MtuDialogSideEffect.Complete) }
+ )
}
fun onRestoreClick() =
viewModelScope.launch(dispatcher) {
- repository.setWireguardMtu(null)
- _uiSideEffect.send(MtuDialogSideEffect.Complete)
+ repository
+ .resetWireguardMtu()
+ .fold(
+ { _uiSideEffect.send(MtuDialogSideEffect.Error) },
+ { _uiSideEffect.send(MtuDialogSideEffect.Complete) }
+ )
}
}
sealed interface MtuDialogSideEffect {
data object Complete : MtuDialogSideEffect
+
+ data object Error : MtuDialogSideEffect
}
+
+data class MtuDialogUiState(
+ val mtuInput: String,
+ val isValidInput: Boolean,
+ val showResetToDefault: Boolean
+)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/NoDaemonViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/NoDaemonViewModel.kt
index eff31be0ee..f8863f2433 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/NoDaemonViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/NoDaemonViewModel.kt
@@ -22,12 +22,12 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.destinations.PrivacyDisclaimerDestination
import net.mullvad.mullvadvpn.compose.destinations.SplashDestination
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
+import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
private val noServiceDestinations = listOf(SplashDestination, PrivacyDisclaimerDestination)
-class NoDaemonViewModel(serviceConnectionManager: ServiceConnectionManager) :
+class NoDaemonViewModel(managementService: ManagementService) :
ViewModel(), LifecycleEventObserver, NavController.OnDestinationChangedListener {
private val lifecycleFlow: MutableSharedFlow<Lifecycle.Event> = MutableSharedFlow()
@@ -35,7 +35,7 @@ class NoDaemonViewModel(serviceConnectionManager: ServiceConnectionManager) :
@OptIn(FlowPreview::class)
val uiSideEffect =
- combine(lifecycleFlow, serviceConnectionManager.connectionState, destinationFlow) {
+ combine(lifecycleFlow, managementService.connectionState, destinationFlow) {
event,
connEvent,
destination ->
@@ -66,7 +66,7 @@ class NoDaemonViewModel(serviceConnectionManager: ServiceConnectionManager) :
private fun toDaemonState(
lifecycleEvent: Lifecycle.Event,
- serviceState: ServiceConnectionState,
+ serviceState: GrpcConnectivityState,
currentDestination: DestinationSpec<*>
): DaemonState {
// In these destinations we don't care about showing the NoDaemonScreen
@@ -77,9 +77,11 @@ class NoDaemonViewModel(serviceConnectionManager: ServiceConnectionManager) :
return if (lifecycleEvent.targetState.isAtLeast(Lifecycle.State.STARTED)) {
// If we are started we want to show the overlay if we are not connected to daemon
when (serviceState) {
- is ServiceConnectionState.ConnectedNotReady,
- ServiceConnectionState.Disconnected -> DaemonState.Show
- is ServiceConnectionState.ConnectedReady -> DaemonState.Hidden.Connected
+ GrpcConnectivityState.Connecting,
+ GrpcConnectivityState.Shutdown,
+ GrpcConnectivityState.TransientFailure,
+ GrpcConnectivityState.Idle -> DaemonState.Show
+ GrpcConnectivityState.Ready -> DaemonState.Hidden.Connected
}
} else {
// If we are stopped we intentionally stop service and don't care about showing overlay.
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
index 3c70717e47..66e9a719eb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt
@@ -4,13 +4,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.receiveAsFlow
@@ -18,25 +14,20 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
import net.mullvad.mullvadvpn.util.toPaymentState
class OutOfTimeViewModel(
private val accountRepository: AccountRepository,
- private val serviceConnectionManager: ServiceConnectionManager,
- private val deviceRepository: DeviceRepository,
+ deviceRepository: DeviceRepository,
private val paymentUseCase: PaymentUseCase,
private val outOfTimeUseCase: OutOfTimeUseCase,
+ private val connectionProxy: ConnectionProxy,
private val pollAccountExpiry: Boolean = true,
private val isPlayBuild: Boolean
) : ViewModel() {
@@ -45,27 +36,17 @@ class OutOfTimeViewModel(
val uiSideEffect = merge(_uiSideEffect.receiveAsFlow(), notOutOfTimeEffect())
val uiState =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .flatMapLatest { serviceConnection ->
- combine(
- serviceConnection.connectionProxy.tunnelStateFlow(),
- deviceRepository.deviceState,
- paymentUseCase.paymentAvailability,
- ) { tunnelState, deviceState, paymentAvailability ->
- OutOfTimeUiState(
- tunnelState = tunnelState,
- deviceName = deviceState.deviceName() ?: "",
- showSitePayment = !isPlayBuild,
- billingPaymentState = paymentAvailability?.toPaymentState(),
- )
- }
+ combine(
+ connectionProxy.tunnelState,
+ deviceRepository.deviceState,
+ paymentUseCase.paymentAvailability,
+ ) { tunnelState, deviceState, paymentAvailability ->
+ OutOfTimeUiState(
+ tunnelState = tunnelState,
+ deviceName = deviceState?.displayName() ?: "",
+ showSitePayment = !isPlayBuild,
+ billingPaymentState = paymentAvailability?.toPaymentState(),
+ )
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState())
@@ -80,21 +61,16 @@ class OutOfTimeViewModel(
fetchPaymentAvailability()
}
- private fun ConnectionProxy.tunnelStateFlow(): Flow<TunnelState> =
- callbackFlowFromNotifier(this.onStateChange)
-
fun onSitePaymentClick() {
viewModelScope.launch {
- _uiSideEffect.send(
- UiSideEffect.OpenAccountView(
- serviceConnectionManager.authTokenCache()?.fetchAuthToken() ?: ""
- )
- )
+ accountRepository.getWebsiteAuthToken()?.let { wwwAuthToken ->
+ _uiSideEffect.send(UiSideEffect.OpenAccountView(wwwAuthToken))
+ }
}
}
fun onDisconnectClick() {
- viewModelScope.launch { serviceConnectionManager.connectionProxy()?.disconnect() }
+ viewModelScope.launch { connectionProxy.disconnect() }
}
private fun verifyPurchases() {
@@ -114,8 +90,7 @@ class OutOfTimeViewModel(
// If the payment was successful we want to update the account expiry. If not successful we
// should check payment availability and verify any purchases to handle potential errors.
if (success) {
- updateAccountExpiry()
- // _uiSideEffect.tryEmit(UiSideEffect.OpenConnectScreen)
+ viewModelScope.launch { updateAccountExpiry() }
} else {
fetchPaymentAvailability()
verifyPurchases() // Attempt to verify again
@@ -125,8 +100,8 @@ class OutOfTimeViewModel(
}
}
- private fun updateAccountExpiry() {
- accountRepository.fetchAccountExpiry()
+ private suspend fun updateAccountExpiry() {
+ accountRepository.getAccountData()
}
private fun notOutOfTimeEffect() =
@@ -138,7 +113,7 @@ class OutOfTimeViewModel(
}
sealed interface UiSideEffect {
- data class OpenAccountView(val token: String) : UiSideEffect
+ data class OpenAccountView(val token: WebsiteAuthToken) : UiSideEffect
data object OpenConnectScreen : UiSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModel.kt
index 4afa12219a..f7bbd73907 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.lib.model.ClearAllOverridesError
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
class ResetServerIpOverridesConfirmationViewModel(
@@ -15,11 +16,26 @@ class ResetServerIpOverridesConfirmationViewModel(
fun clearAllOverrides() =
viewModelScope.launch {
- relayOverridesRepository.clearAllOverrides()
- _uiSideEffect.send(ResetServerIpOverridesConfirmationUiSideEffect.OverridesCleared)
+ relayOverridesRepository
+ .clearAllOverrides()
+ .fold(
+ {
+ _uiSideEffect.send(
+ ResetServerIpOverridesConfirmationUiSideEffect.OverridesError(it)
+ )
+ },
+ {
+ _uiSideEffect.send(
+ ResetServerIpOverridesConfirmationUiSideEffect.OverridesCleared
+ )
+ }
+ )
}
}
sealed class ResetServerIpOverridesConfirmationUiSideEffect {
data object OverridesCleared : ResetServerIpOverridesConfirmationUiSideEffect()
+
+ data class OverridesError(val error: ClearAllOverridesError) :
+ ResetServerIpOverridesConfirmationUiSideEffect()
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt
index 27edb95457..2ab757bd78 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt
@@ -5,52 +5,59 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.compose.state.toNullableOwnership
import net.mullvad.mullvadvpn.compose.state.toSelectedProviders
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.Provider
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.filterOnOwnershipAndProvider
import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm
-import net.mullvad.mullvadvpn.relaylist.toLocationConstraint
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
-import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase
+import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
+import net.mullvad.mullvadvpn.util.combine
class SelectLocationViewModel(
- private val serviceConnectionManager: ServiceConnectionManager,
- private val relayListUseCase: RelayListUseCase,
- private val relayListFilterUseCase: RelayListFilterUseCase,
- private val customListActionUseCase: CustomListActionUseCase
+ private val relayListFilterRepository: RelayListFilterRepository,
+ availableProvidersUseCase: AvailableProvidersUseCase,
+ customListsRelayItemUseCase: CustomListsRelayItemUseCase,
+ private val customListActionUseCase: CustomListActionUseCase,
+ filteredRelayListUseCase: FilteredRelayListUseCase,
+ private val relayListRepository: RelayListRepository
) : ViewModel() {
private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM)
@Suppress("DestructuringDeclarationWithTooManyEntries")
val uiState =
combine(
- relayListUseCase.relayListWithSelection(),
+ filteredRelayListUseCase.filteredRelayList(),
+ customListsRelayItemUseCase.relayItemCustomLists(),
+ relayListRepository.selectedLocation,
_searchTerm,
- relayListFilterUseCase.selectedOwnership(),
- relayListFilterUseCase.availableProviders(),
- relayListFilterUseCase.selectedProviders(),
+ relayListFilterRepository.selectedOwnership,
+ availableProvidersUseCase.availableProviders(),
+ relayListFilterRepository.selectedProviders,
) {
- (customLists, _, relayCountries, selectedItem),
+ relayCountries,
+ customLists,
+ selectedItem,
searchTerm,
selectedOwnership,
allProviders,
selectedConstraintProviders ->
+ val selectRelayItemId = selectedItem.getOrNull()
val selectedOwnershipItem = selectedOwnership.toNullableOwnership()
val selectedProvidersCount =
when (selectedConstraintProviders) {
@@ -58,21 +65,21 @@ class SelectLocationViewModel(
is Constraint.Only ->
filterSelectedProvidersByOwnership(
selectedConstraintProviders.toSelectedProviders(allProviders),
- selectedOwnershipItem
+ selectedOwnershipItem,
)
.size
}
val filteredRelayCountries =
- relayCountries.filterOnSearchTerm(searchTerm, selectedItem)
+ relayCountries.filterOnSearchTerm(searchTerm, selectRelayItemId)
val filteredCustomLists =
- customLists.filterOnSearchTerm(searchTerm).map { customList ->
- customList.filterOnOwnershipAndProvider(
- selectedOwnership,
- selectedConstraintProviders
+ customLists
+ .filterOnSearchTerm(searchTerm)
+ .filterOnOwnershipAndProvider(
+ ownership = selectedOwnership,
+ providers = selectedConstraintProviders,
)
- }
SelectLocationUiState.Content(
searchTerm = searchTerm,
@@ -81,7 +88,7 @@ class SelectLocationViewModel(
filteredCustomLists = filteredCustomLists,
customLists = customLists,
countries = filteredRelayCountries,
- selectedItem = selectedItem,
+ selectedItem = selectRelayItemId,
)
}
.stateIn(
@@ -93,15 +100,16 @@ class SelectLocationViewModel(
private val _uiSideEffect = Channel<SelectLocationSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
- init {
- viewModelScope.launch { relayListUseCase.fetchRelayList() }
- }
-
fun selectRelay(relayItem: RelayItem) {
- val locationConstraint = relayItem.toLocationConstraint()
- relayListUseCase.updateSelectedRelayLocation(locationConstraint)
- serviceConnectionManager.connectionProxy()?.connect()
- _uiSideEffect.trySend(SelectLocationSideEffect.CloseScreen)
+ viewModelScope.launch {
+ val locationConstraint = relayItem.id
+ relayListRepository
+ .updateSelectedRelayLocation(locationConstraint)
+ .fold(
+ { _uiSideEffect.trySend(SelectLocationSideEffect.GenericError) },
+ { _uiSideEffect.trySend(SelectLocationSideEffect.CloseScreen) },
+ )
+ }
}
fun onSearchTermInput(searchTerm: String) {
@@ -112,41 +120,27 @@ class SelectLocationViewModel(
selectedProviders: List<Provider>,
selectedOwnership: Ownership?
): List<Provider> =
- when (selectedOwnership) {
- Ownership.MullvadOwned -> selectedProviders.filter { it.mullvadOwned }
- Ownership.Rented -> selectedProviders.filterNot { it.mullvadOwned }
- else -> selectedProviders
- }
+ if (selectedOwnership == null) selectedProviders
+ else selectedProviders.filter { it.ownership == selectedOwnership }
fun removeOwnerFilter() {
- viewModelScope.launch {
- relayListFilterUseCase.updateOwnershipAndProviderFilter(
- Constraint.Any(),
- relayListFilterUseCase.selectedProviders().first(),
- )
- }
+ viewModelScope.launch { relayListFilterRepository.updateSelectedOwnership(Constraint.Any) }
}
fun removeProviderFilter() {
- viewModelScope.launch {
- relayListFilterUseCase.updateOwnershipAndProviderFilter(
- relayListFilterUseCase.selectedOwnership().first(),
- Constraint.Any(),
- )
- }
+ viewModelScope.launch { relayListFilterRepository.updateSelectedProviders(Constraint.Any) }
}
- fun addLocationToList(item: RelayItem, customList: RelayItem.CustomList) {
+ fun addLocationToList(item: RelayItem.Location, customList: RelayItem.CustomList) {
viewModelScope.launch {
val newLocations =
- (customList.locations + item).filter { it !in item.descendants() }.map { it.code }
- val result =
- customListActionUseCase.performAction(
- CustomListAction.UpdateLocations(customList.id, newLocations)
+ (customList.locations + item).filter { it !in item.descendants() }.map { it.id }
+ customListActionUseCase
+ .performAction(CustomListAction.UpdateLocations(customList.id, newLocations))
+ .fold(
+ { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
+ { _uiSideEffect.send(SelectLocationSideEffect.LocationAddedToCustomList(it)) },
)
- _uiSideEffect.send(
- SelectLocationSideEffect.LocationAddedToCustomList(result.getOrThrow())
- )
}
}
@@ -154,19 +148,29 @@ class SelectLocationViewModel(
viewModelScope.launch { customListActionUseCase.performAction(action) }
}
- fun removeLocationFromList(item: RelayItem, customList: RelayItem.CustomList) {
+ fun removeLocationFromList(item: RelayItem.Location, customList: RelayItem.CustomList) {
viewModelScope.launch {
- val newLocations = (customList.locations - item).map { it.code }
- val result =
- customListActionUseCase.performAction(
- CustomListAction.UpdateLocations(customList.id, newLocations)
+ val newLocations = (customList.locations - item).map { it.id }
+ customListActionUseCase
+ .performAction(CustomListAction.UpdateLocations(customList.id, newLocations))
+ .fold(
+ { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
+ {
+ _uiSideEffect.send(
+ SelectLocationSideEffect.LocationRemovedFromCustomList(it)
+ )
+ }
)
- _uiSideEffect.send(
- SelectLocationSideEffect.LocationRemovedFromCustomList(result.getOrThrow())
- )
}
}
+ private fun List<RelayItem.CustomList>.filterOnOwnershipAndProvider(
+ ownership: Constraint<Ownership>,
+ providers: Constraint<Providers>
+ ): List<RelayItem.CustomList> = map { item ->
+ item.filterOnOwnershipAndProvider(ownership, providers)
+ }
+
companion object {
private const val EMPTY_SEARCH_TERM = ""
}
@@ -175,9 +179,9 @@ class SelectLocationViewModel(
sealed interface SelectLocationSideEffect {
data object CloseScreen : SelectLocationSideEffect
- data class LocationAddedToCustomList(val result: CustomListResult.LocationsChanged) :
- SelectLocationSideEffect
+ data class LocationAddedToCustomList(val result: LocationsChanged) : SelectLocationSideEffect
+
+ class LocationRemovedFromCustomList(val result: LocationsChanged) : SelectLocationSideEffect
- class LocationRemovedFromCustomList(val result: CustomListResult.LocationsChanged) :
- SelectLocationSideEffect
+ data object GenericError : SelectLocationSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt
index 5a77727b18..069eda8dc8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModel.kt
@@ -5,29 +5,20 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.io.InputStreamReader
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
-import net.mullvad.mullvadvpn.model.SettingsPatchError
+import net.mullvad.mullvadvpn.lib.model.SettingsPatchError
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
-import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
class ServerIpOverridesViewModel(
- private val serviceConnectionManager: ServiceConnectionManager,
- relayOverridesRepository: RelayOverridesRepository,
- private val settingsRepository: SettingsRepository,
+ private val relayOverridesRepository: RelayOverridesRepository,
private val contentResolver: ContentResolver,
) : ViewModel() {
@@ -56,21 +47,17 @@ class ServerIpOverridesViewModel(
fun importText(json: String) = viewModelScope.launch { applySettingsPatch(json) }
private suspend fun applySettingsPatch(json: String) {
- // Wait for daemon to come online since we might be disconnected (due to File picker being
- // open
- // and we disconnect from daemon in paused state)
- val connResult =
- withTimeoutOrNull(5.seconds) {
- serviceConnectionManager.connectionState
- .filterIsInstance(ServiceConnectionState.ConnectedReady::class)
- .first()
- }
- if (connResult != null) {
- // Apply patch
- val result = settingsRepository.applySettingsPatch(json)
- _uiSideEffect.send(ServerIpOverridesUiSideEffect.ImportResult(result.error))
- } else {
- // Service never came online, at this point we should already display daemon overlay
+ // Since we are currently using waitForReady this will just wait to apply until gRPC is
+ // ready
+ viewModelScope.launch {
+ relayOverridesRepository
+ .applySettingsPatch(json)
+ .fold(
+ { error ->
+ _uiSideEffect.send(ServerIpOverridesUiSideEffect.ImportResult(error))
+ },
+ { _uiSideEffect.send(ServerIpOverridesUiSideEffect.ImportResult(null)) }
+ )
}
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt
index b836894cb7..5150af2747 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt
@@ -7,26 +7,25 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.compose.state.SettingsUiState
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
class SettingsViewModel(
deviceRepository: DeviceRepository,
- serviceConnectionManager: ServiceConnectionManager,
+ appVersionInfoRepository: AppVersionInfoRepository,
isPlayBuild: Boolean
) : ViewModel() {
private val vmState: StateFlow<SettingsUiState> =
- combine(deviceRepository.deviceState, serviceConnectionManager.connectionState) {
+ combine(deviceRepository.deviceState, appVersionInfoRepository.versionInfo()) {
deviceState,
versionInfo ->
- val cachedVersionInfo = versionInfo.readyContainer()?.appVersionInfoCache
SettingsUiState(
isLoggedIn = deviceState is DeviceState.LoggedIn,
- appVersion = cachedVersionInfo?.version ?: "",
+ appVersion = versionInfo.currentVersion,
isUpdateAvailable =
- cachedVersionInfo?.let { it.isSupported.not() || it.isOutdated } ?: false,
+ versionInfo.let { it.isSupported.not() || it.isUpdateAvailable },
isPlayBuild = isPlayBuild
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
index 83442059da..bd34161e2c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplashViewModel.kt
@@ -10,17 +10,15 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.selects.onTimeout
import kotlinx.coroutines.selects.select
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_TIMEOUT_MS
-import net.mullvad.mullvadvpn.model.AccountAndDevice
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
class SplashViewModel(
private val privacyDisclaimerRepository: PrivacyDisclaimerRepository,
- private val deviceRepository: DeviceRepository,
private val accountRepository: AccountRepository,
+ private val deviceRepository: DeviceRepository,
) : ViewModel() {
val uiSideEffect = flow { emit(getStartDestination()) }
@@ -34,12 +32,10 @@ class SplashViewModel(
deviceRepository.deviceState
.map {
when (it) {
- DeviceState.Initial -> null
- is DeviceState.LoggedIn ->
- ValidStartDeviceState.LoggedIn(it.accountAndDevice)
+ is DeviceState.LoggedIn -> ValidStartDeviceState.LoggedIn
DeviceState.LoggedOut -> ValidStartDeviceState.LoggedOut
DeviceState.Revoked -> ValidStartDeviceState.Revoked
- DeviceState.Unknown -> null
+ null -> null
}
}
.filterNotNull()
@@ -48,38 +44,30 @@ class SplashViewModel(
return when (deviceState) {
ValidStartDeviceState.LoggedOut -> SplashUiSideEffect.NavigateToLogin
ValidStartDeviceState.Revoked -> SplashUiSideEffect.NavigateToRevoked
- is ValidStartDeviceState.LoggedIn -> getLoggedInStartDestination()
+ ValidStartDeviceState.LoggedIn -> getLoggedInStartDestination()
}
}
// We know the user is logged in, but we need to find out if their account has expired
private suspend fun getLoggedInStartDestination(): SplashUiSideEffect {
- val expiry =
- viewModelScope.async {
- accountRepository.accountExpiryState.first { it !is AccountExpiry.Missing }
- }
+ val expiry = viewModelScope.async { accountRepository.accountData.filterNotNull().first() }
- val accountExpiry = select {
+ val accountData = select {
expiry.onAwait { it }
// If we don't get a response within 1 second, assume the account expiry is Missing
- onTimeout(ACCOUNT_EXPIRY_TIMEOUT_MS) { AccountExpiry.Missing }
+ onTimeout(ACCOUNT_EXPIRY_TIMEOUT_MS) { null }
}
- return when (accountExpiry) {
- is AccountExpiry.Available -> {
- if (accountExpiry.expiryDateTime.isBeforeNow) {
- SplashUiSideEffect.NavigateToOutOfTime
- } else {
- SplashUiSideEffect.NavigateToConnect
- }
- }
- AccountExpiry.Missing -> SplashUiSideEffect.NavigateToConnect
+ return if (accountData != null && accountData.expiryDate.isBeforeNow) {
+ SplashUiSideEffect.NavigateToOutOfTime
+ } else {
+ SplashUiSideEffect.NavigateToConnect
}
}
}
private sealed interface ValidStartDeviceState {
- data class LoggedIn(val accountAndDevice: AccountAndDevice) : ValidStartDeviceState
+ data object LoggedIn : ValidStartDeviceState
data object Revoked : ValidStartDeviceState
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
index 833117c046..b43e046e57 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
@@ -3,69 +3,46 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.applist.AppData
import net.mullvad.mullvadvpn.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
-import net.mullvad.mullvadvpn.ui.serviceconnection.splitTunneling
+import net.mullvad.mullvadvpn.lib.model.AppId
+import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository
class SplitTunnelingViewModel(
private val appsProvider: ApplicationsProvider,
- private val serviceConnectionManager: ServiceConnectionManager,
+ private val splitTunnelingRepository: SplitTunnelingRepository,
private val dispatcher: CoroutineDispatcher
) : ViewModel() {
private val allApps = MutableStateFlow<List<AppData>?>(null)
private val showSystemApps = MutableStateFlow(false)
- private val _shared: SharedFlow<ServiceConnectionContainer> =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .shareIn(viewModelScope, SharingStarted.WhileSubscribed())
-
- private val vmState =
- _shared
- .flatMapLatest { serviceConnection ->
- combine(
- serviceConnection.splitTunneling.excludedAppsCallbackFlow(),
- serviceConnection.splitTunneling.enabledCallbackFlow(),
- allApps,
- showSystemApps,
- ) { excludedApps, enabled, allApps, showSystemApps ->
- SplitTunnelingViewModelState(
- excludedApps = excludedApps,
- enabled = enabled,
- allApps = allApps,
- showSystemApps = showSystemApps
- )
- }
+ private val vmState: StateFlow<SplitTunnelingViewModelState> =
+ combine(
+ splitTunnelingRepository.excludedApps,
+ splitTunnelingRepository.splitTunnelingEnabled,
+ allApps,
+ showSystemApps,
+ ) { excludedApps, enabled, allApps, showSystemApps ->
+ SplitTunnelingViewModelState(
+ excludedApps = excludedApps,
+ enabled = enabled,
+ allApps = allApps,
+ showSystemApps = showSystemApps,
+ )
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
- SplitTunnelingViewModelState()
+ SplitTunnelingViewModelState(),
)
val uiState =
@@ -74,33 +51,28 @@ class SplitTunnelingViewModel(
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
- SplitTunnelingUiState.Loading(enabled = false)
+ SplitTunnelingUiState.Loading(enabled = false),
)
init {
viewModelScope.launch(dispatcher) { fetchApps() }
}
- override fun onCleared() {
- serviceConnectionManager.splitTunneling()?.persist()
- super.onCleared()
- }
-
fun onEnableSplitTunneling(isEnabled: Boolean) {
viewModelScope.launch(dispatcher) {
- serviceConnectionManager.splitTunneling()?.enableSplitTunneling(isEnabled)
+ splitTunnelingRepository.enableSplitTunneling(isEnabled)
}
}
fun onIncludeAppClick(packageName: String) {
viewModelScope.launch(dispatcher) {
- serviceConnectionManager.splitTunneling()?.includeApp(packageName)
+ splitTunnelingRepository.includeApp(AppId(packageName))
}
}
fun onExcludeAppClick(packageName: String) {
viewModelScope.launch(dispatcher) {
- serviceConnectionManager.splitTunneling()?.excludeApp(packageName)
+ splitTunnelingRepository.excludeApp(AppId(packageName))
}
}
@@ -111,14 +83,4 @@ class SplitTunnelingViewModel(
private suspend fun fetchApps() {
appsProvider.getAppsList().let { appsList -> allApps.emit(appsList) }
}
-
- private fun SplitTunneling.excludedAppsCallbackFlow() = callbackFlow {
- excludedAppsChange = { apps -> trySend(apps) }
- awaitClose { emptySet<String>() }
- }
-
- private fun SplitTunneling.enabledCallbackFlow() = callbackFlow {
- enabledChange = { isEnabled -> trySend(isEnabled) }
- awaitClose()
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
index bc16662f00..89dde0decb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
@@ -2,10 +2,11 @@ package net.mullvad.mullvadvpn.viewmodel
import net.mullvad.mullvadvpn.applist.AppData
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
+import net.mullvad.mullvadvpn.lib.model.AppId
data class SplitTunnelingViewModelState(
val enabled: Boolean = false,
- val excludedApps: Set<String> = emptySet(),
+ val excludedApps: Set<AppId> = emptySet(),
val allApps: List<AppData>? = null,
val showSystemApps: Boolean = false
) {
@@ -13,7 +14,7 @@ data class SplitTunnelingViewModelState(
return allApps
?.partition { appData ->
if (enabled) {
- excludedApps.contains(appData.packageName)
+ excludedApps.contains(AppId(appData.packageName))
} else {
false
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt
index 8022332650..3d67b42bd1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModel.kt
@@ -1,67 +1,40 @@
package net.mullvad.mullvadvpn.viewmodel
-import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.compose.state.VoucherDialogUiState
import net.mullvad.mullvadvpn.constant.VOUCHER_LENGTH
-import net.mullvad.mullvadvpn.model.VoucherSubmissionError
-import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.voucherRedeemer
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
+import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
import net.mullvad.mullvadvpn.util.VoucherRegexHelper
-class VoucherDialogViewModel(
- private val serviceConnectionManager: ServiceConnectionManager,
- private val resources: Resources
-) : ViewModel() {
+class VoucherDialogViewModel(private val voucherRepository: VoucherRepository) : ViewModel() {
private val vmState = MutableStateFlow<VoucherDialogState>(VoucherDialogState.Default)
private val voucherInput = MutableStateFlow("")
- private val _shared: SharedFlow<ServiceConnectionContainer> =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .shareIn(viewModelScope, SharingStarted.WhileSubscribed())
-
val uiState =
- _shared
- .flatMapLatest {
- combine(vmState, voucherInput) { state, input ->
- VoucherDialogUiState(voucherInput = input, voucherState = state)
- }
+ combine(vmState, voucherInput) { state, input ->
+ VoucherDialogUiState(voucherInput = input, voucherState = state)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), VoucherDialogUiState.INITIAL)
fun onRedeem(voucherCode: String) {
vmState.update { VoucherDialogState.Verifying }
viewModelScope.launch {
- when (val result = serviceConnectionManager.voucherRedeemer()?.submit(voucherCode)) {
- is VoucherSubmissionResult.Ok -> handleAddedTime(result.submission.timeAdded)
- is VoucherSubmissionResult.Error -> setError(result.error)
- null -> vmState.update { VoucherDialogState.Default }
- }
+ voucherRepository
+ .submitVoucher(voucherCode)
+ .fold(
+ { error -> setError(error) },
+ { success -> handleAddedTime(success.timeAdded) }
+ )
}
}
@@ -81,18 +54,7 @@ class VoucherDialogViewModel(
viewModelScope.launch { vmState.update { VoucherDialogState.Success(timeAdded) } }
}
- private fun setError(error: VoucherSubmissionError) {
- viewModelScope.launch {
- val message =
- resources.getString(
- when (error) {
- VoucherSubmissionError.InvalidVoucher -> R.string.invalid_voucher
- VoucherSubmissionError.VoucherAlreadyUsed -> R.string.voucher_already_used
- VoucherSubmissionError.RpcError,
- VoucherSubmissionError.OtherError -> R.string.error_occurred
- }
- )
- vmState.update { VoucherDialogState.Error(message) }
- }
+ private fun setError(error: RedeemVoucherError) {
+ viewModelScope.launch { vmState.update { VoucherDialogState.Error(error) } }
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt
new file mode 100644
index 0000000000..cd9a52efa1
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnPermissionViewModel.kt
@@ -0,0 +1,34 @@
+package net.mullvad.mullvadvpn.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.lib.common.constant.KEY_REQUEST_VPN_PERMISSION
+import net.mullvad.mullvadvpn.lib.intent.IntentProvider
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+
+class VpnPermissionViewModel(
+ intentProvider: IntentProvider,
+ private val connectionProxy: ConnectionProxy
+) : ViewModel() {
+ val uiSideEffect: Flow<VpnPermissionSideEffect> =
+ intentProvider.intents
+ .filter { it?.action == KEY_REQUEST_VPN_PERMISSION }
+ .distinctUntilChanged()
+ .map { VpnPermissionSideEffect.ShowDialog }
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed())
+
+ fun connect() {
+ viewModelScope.launch { connectionProxy.connectWithoutPermissionCheck() }
+ }
+}
+
+sealed interface VpnPermissionSideEffect {
+ data object ShowDialog : VpnPermissionSideEffect
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
index ba487c5a40..864d402fb3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
@@ -1,6 +1,5 @@
package net.mullvad.mullvadvpn.viewmodel
-import android.content.res.Resources
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -19,36 +18,35 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.DefaultDnsOptions
-import net.mullvad.mullvadvpn.model.DnsState
-import net.mullvad.mullvadvpn.model.ObfuscationSettings
-import net.mullvad.mullvadvpn.model.Port
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.SelectedObfuscation
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.Udp2TcpObfuscationSettings
-import net.mullvad.mullvadvpn.model.WireguardConstraints
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
+import net.mullvad.mullvadvpn.lib.model.DnsState
+import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
+import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
import net.mullvad.mullvadvpn.util.isCustom
sealed interface VpnSettingsSideEffect {
- data class ShowToast(val message: String) : VpnSettingsSideEffect
+ sealed interface ShowToast : VpnSettingsSideEffect {
+ data object ApplySettingsWarning : ShowToast
+
+ data object GenericError : ShowToast
+ }
data object NavigateToDnsDialog : VpnSettingsSideEffect
}
class VpnSettingsViewModel(
private val repository: SettingsRepository,
- private val resources: Resources,
- portRangeUseCase: PortRangeUseCase,
- private val relayListUseCase: RelayListUseCase,
+ private val relayListRepository: RelayListRepository,
private val systemVpnSettingsUseCase: SystemVpnSettingsUseCase,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
@@ -59,12 +57,12 @@ class VpnSettingsViewModel(
private val customPort = MutableStateFlow<Constraint<Port>?>(null)
private val vmState =
- combine(repository.settingsUpdates, portRangeUseCase.portRanges(), customPort) {
+ combine(repository.settingsUpdates, relayListRepository.portRanges, customPort) {
settings,
portRanges,
customWgPort ->
VpnSettingsViewModelState(
- mtuValue = settings?.mtuString() ?: "",
+ mtuValue = settings?.tunnelOptions?.wireguard?.mtu,
isAutoConnectEnabled = settings?.autoConnect ?: false,
isLocalNetworkSharingEnabled = settings?.allowLan ?: false,
isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false,
@@ -74,7 +72,7 @@ class VpnSettingsViewModel(
selectedObfuscation =
settings?.selectedObfuscationSettings() ?: SelectedObfuscation.Off,
quantumResistant = settings?.quantumResistant() ?: QuantumResistantState.Off,
- selectedWireguardPort = settings?.getWireguardPort() ?: Constraint.Any(),
+ selectedWireguardPort = settings?.getWireguardPort() ?: Constraint.Any,
customWireguardPort = customWgPort,
availablePortRanges = portRanges,
systemVpnSettingsAvailable =
@@ -111,11 +109,19 @@ class VpnSettingsViewModel(
}
fun onToggleAutoConnect(isEnabled: Boolean) {
- viewModelScope.launch(dispatcher) { repository.setAutoConnect(isEnabled) }
+ viewModelScope.launch(dispatcher) {
+ repository.setAutoConnect(isEnabled).onLeft {
+ _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
+ }
+ }
}
fun onToggleLocalNetworkSharing(isEnabled: Boolean) {
- viewModelScope.launch(dispatcher) { repository.setLocalNetworkSharing(isEnabled) }
+ viewModelScope.launch(dispatcher) {
+ repository.setLocalNetworkSharing(isEnabled).onLeft {
+ _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
+ }
+ }
}
fun onDnsDialogDismissed() {
@@ -125,11 +131,21 @@ class VpnSettingsViewModel(
}
fun onToggleCustomDns(enable: Boolean) {
- repository.setDnsState(if (enable) DnsState.Custom else DnsState.Default)
- if (enable && vmState.value.customDnsList.isEmpty()) {
- viewModelScope.launch { _uiSideEffect.send(VpnSettingsSideEffect.NavigateToDnsDialog) }
- } else if (vmState.value.customDnsList.isNotEmpty()) {
- showApplySettingChangesWarningToast()
+ viewModelScope.launch {
+ repository
+ .setDnsState(if (enable) DnsState.Custom else DnsState.Default)
+ .fold(
+ { _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) },
+ {
+ if (enable && vmState.value.customDnsList.isEmpty()) {
+ viewModelScope.launch {
+ _uiSideEffect.send(VpnSettingsSideEffect.NavigateToDnsDialog)
+ }
+ } else if (vmState.value.customDnsList.isNotEmpty()) {
+ showApplySettingChangesWarningToast()
+ }
+ }
+ )
}
}
@@ -176,25 +192,33 @@ class VpnSettingsViewModel(
}
fun onStopEvent() {
- if (vmState.value.customDnsList.isEmpty()) {
- repository.setDnsState(DnsState.Default)
+ viewModelScope.launch {
+ if (vmState.value.customDnsList.isEmpty()) {
+ repository.setDnsState(DnsState.Default).onLeft {
+ _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
+ }
+ }
}
}
fun onSelectObfuscationSetting(selectedObfuscation: SelectedObfuscation) {
viewModelScope.launch(dispatcher) {
- repository.setObfuscationOptions(
- ObfuscationSettings(
- selectedObfuscation = selectedObfuscation,
- udp2tcp = Udp2TcpObfuscationSettings(Constraint.Any())
+ repository
+ .setObfuscationOptions(
+ ObfuscationSettings(
+ selectedObfuscation = selectedObfuscation,
+ udp2tcp = Udp2TcpObfuscationSettings(Constraint.Any)
+ )
)
- )
+ .onLeft { _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) }
}
}
fun onSelectQuantumResistanceSetting(quantumResistant: QuantumResistantState) {
viewModelScope.launch(dispatcher) {
- repository.setWireguardQuantumResistant(quantumResistant)
+ repository.setWireguardQuantumResistant(quantumResistant).onLeft {
+ _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
+ }
}
}
@@ -202,26 +226,34 @@ class VpnSettingsViewModel(
if (port.isCustom()) {
customPort.update { port }
}
- relayListUseCase.updateSelectedWireguardConstraints(WireguardConstraints(port = port))
+ viewModelScope.launch {
+ relayListRepository.updateSelectedWireguardConstraints(
+ WireguardConstraints(port = port)
+ )
+ }
}
fun resetCustomPort() {
customPort.update { null }
// If custom port was selected, update selection to be any.
if (vmState.value.selectedWireguardPort.isCustom()) {
- relayListUseCase.updateSelectedWireguardConstraints(
- WireguardConstraints(port = Constraint.Any())
- )
+ viewModelScope.launch {
+ relayListRepository.updateSelectedWireguardConstraints(
+ WireguardConstraints(port = Constraint.Any)
+ )
+ }
}
}
private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) =
viewModelScope.launch(dispatcher) {
- repository.setDnsOptions(
- isCustomDnsEnabled = vmState.value.isCustomDnsEnabled,
- dnsList = vmState.value.customDnsList.map { it.address }.asInetAddressList(),
- contentBlockersOptions = contentBlockersOption
- )
+ repository
+ .setDnsOptions(
+ isCustomDnsEnabled = vmState.value.isCustomDnsEnabled,
+ dnsList = vmState.value.customDnsList.map { it.address }.asInetAddressList(),
+ contentBlockersOptions = contentBlockersOption
+ )
+ .onLeft { _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) }
}
private fun List<String>.asInetAddressList(): List<InetAddress> {
@@ -239,8 +271,6 @@ class VpnSettingsViewModel(
}
}
- private fun Settings.mtuString() = tunnelOptions.wireguard.mtu?.toString() ?: EMPTY_STRING
-
private fun Settings.quantumResistant() = tunnelOptions.wireguard.quantumResistant
private fun Settings.isCustomDnsEnabled() = tunnelOptions.dnsOptions.state == DnsState.Custom
@@ -252,11 +282,7 @@ class VpnSettingsViewModel(
private fun Settings.selectedObfuscationSettings() = obfuscationSettings.selectedObfuscation
private fun Settings.getWireguardPort() =
- when (relaySettings) {
- RelaySettings.CustomTunnelEndpoint -> Constraint.Any()
- is RelaySettings.Normal ->
- (relaySettings as RelaySettings.Normal).relayConstraints.wireguardConstraints.port
- }
+ relaySettings.relayConstraints.wireguardConstraints.port
private fun InetAddress.isLocalAddress(): Boolean {
return isLinkLocalAddress || isSiteLocalAddress
@@ -264,14 +290,14 @@ class VpnSettingsViewModel(
fun showApplySettingChangesWarningToast() {
viewModelScope.launch {
- _uiSideEffect.send(
- VpnSettingsSideEffect.ShowToast(
- resources.getString(R.string.settings_changes_effect_warning_short)
- )
- )
+ _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.ApplySettingsWarning)
}
}
+ fun showGenericErrorToast() {
+ viewModelScope.launch { _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) }
+ }
+
companion object {
private const val EMPTY_STRING = ""
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
index 91866d5cc2..f8e4f0b799 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
@@ -1,15 +1,16 @@
package net.mullvad.mullvadvpn.viewmodel
import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.DefaultDnsOptions
-import net.mullvad.mullvadvpn.model.Port
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.SelectedObfuscation
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
data class VpnSettingsViewModelState(
- val mtuValue: String,
+ val mtuValue: Mtu?,
val isAutoConnectEnabled: Boolean,
val isLocalNetworkSharingEnabled: Boolean,
val isCustomDnsEnabled: Boolean,
@@ -39,11 +40,9 @@ data class VpnSettingsViewModelState(
)
companion object {
- private const val EMPTY_STRING = ""
-
fun default() =
VpnSettingsViewModelState(
- mtuValue = EMPTY_STRING,
+ mtuValue = null,
isAutoConnectEnabled = false,
isLocalNetworkSharingEnabled = false,
isCustomDnsEnabled = false,
@@ -51,7 +50,7 @@ data class VpnSettingsViewModelState(
contentBlockersOptions = DefaultDnsOptions(),
selectedObfuscation = SelectedObfuscation.Auto,
quantumResistant = QuantumResistantState.Off,
- selectedWireguardPort = Constraint.Any(),
+ selectedWireguardPort = Constraint.Any,
customWireguardPort = null,
availablePortRanges = emptyList(),
systemVpnSettingsAvailable = false
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
index 0f6b23a306..208c9d871b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
@@ -2,19 +2,13 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
@@ -22,25 +16,18 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
-import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
-import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
import net.mullvad.mullvadvpn.util.toPaymentState
-@OptIn(FlowPreview::class)
class WelcomeViewModel(
private val accountRepository: AccountRepository,
- private val deviceRepository: DeviceRepository,
- private val serviceConnectionManager: ServiceConnectionManager,
+ deviceRepository: DeviceRepository,
private val paymentUseCase: PaymentUseCase,
+ connectionProxy: ConnectionProxy,
private val pollAccountExpiry: Boolean = true,
private val isPlayBuild: Boolean
) : ViewModel() {
@@ -48,30 +35,18 @@ class WelcomeViewModel(
val uiSideEffect = merge(_uiSideEffect.receiveAsFlow(), hasAddedTimeEffect())
val uiState =
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .flatMapLatest { serviceConnection ->
- combine(
- serviceConnection.connectionProxy.tunnelUiStateFlow(),
- deviceRepository.deviceState.debounce {
- it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS)
- },
- paymentUseCase.paymentAvailability,
- ) { tunnelState, deviceState, paymentAvailability ->
- WelcomeUiState(
- tunnelState = tunnelState,
- accountNumber = deviceState.token(),
- deviceName = deviceState.deviceName(),
- showSitePayment = !isPlayBuild,
- billingPaymentState = paymentAvailability?.toPaymentState(),
- )
- }
+ combine(
+ connectionProxy.tunnelState,
+ deviceRepository.deviceState.filterNotNull(),
+ paymentUseCase.paymentAvailability,
+ ) { tunnelState, accountState, paymentAvailability ->
+ WelcomeUiState(
+ tunnelState = tunnelState,
+ accountNumber = accountState.token(),
+ deviceName = accountState.displayName(),
+ showSitePayment = !isPlayBuild,
+ billingPaymentState = paymentAvailability?.toPaymentState(),
+ )
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), WelcomeUiState())
@@ -87,22 +62,17 @@ class WelcomeViewModel(
}
private fun hasAddedTimeEffect() =
- accountRepository.accountExpiryState
- .mapNotNull { it.date() }
- .filter { it.minusHours(MIN_HOURS_PAST_ACCOUNT_EXPIRY).isAfterNow }
+ accountRepository.accountData
+ .filterNotNull()
+ .filter { it.expiryDate.minusHours(MIN_HOURS_PAST_ACCOUNT_EXPIRY).isAfterNow }
.onEach { paymentUseCase.resetPurchaseResult() }
.map { UiSideEffect.OpenConnectScreen }
- private fun ConnectionProxy.tunnelUiStateFlow(): Flow<TunnelState> =
- callbackFlowFromNotifier(this.onUiStateChange)
-
fun onSitePaymentClick() {
viewModelScope.launch {
- _uiSideEffect.send(
- UiSideEffect.OpenAccountView(
- serviceConnectionManager.authTokenCache()?.fetchAuthToken() ?: ""
- )
- )
+ accountRepository.getWebsiteAuthToken()?.let { token ->
+ _uiSideEffect.send(UiSideEffect.OpenAccountView(token))
+ }
}
}
@@ -123,7 +93,7 @@ class WelcomeViewModel(
// If the payment was successful we want to update the account expiry. If not successful we
// should check payment availability and verify any purchases to handle potential errors.
if (success) {
- updateAccountExpiry()
+ viewModelScope.launch { updateAccountExpiry() }
// Emission of out of time navigation is handled by launch in onStart
} else {
fetchPaymentAvailability()
@@ -134,12 +104,12 @@ class WelcomeViewModel(
}
}
- private fun updateAccountExpiry() {
- accountRepository.fetchAccountExpiry()
+ private suspend fun updateAccountExpiry() {
+ accountRepository.getAccountData()
}
sealed interface UiSideEffect {
- data class OpenAccountView(val token: String) : UiSideEffect
+ data class OpenAccountView(val token: WebsiteAuthToken) : UiSideEffect
data object OpenConnectScreen : UiSideEffect
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt
index 9767d3930a..c9cfb0e75c 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/InAppNotificationControllerTest.kt
@@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.model.ErrorState
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.repository.InAppNotificationController
import net.mullvad.mullvadvpn.usecase.AccountExpiryNotificationUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase
import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase
-import net.mullvad.talpid.tunnel.ErrorState
import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt
deleted file mode 100644
index eb66c2d4f9..0000000000
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparatorTest.kt
+++ /dev/null
@@ -1,256 +0,0 @@
-package net.mullvad.mullvadvpn.relaylist
-
-import io.mockk.mockk
-import io.mockk.unmockkAll
-import net.mullvad.mullvadvpn.model.Ownership
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.Assertions.assertTrue
-import org.junit.jupiter.api.Test
-
-class RelayNameComparatorTest {
-
- @AfterEach
- fun tearDown() {
- unmockkAll()
- }
-
- @Test
- fun `given two relays with same prefix but different numbers comparator should return lowest number first`() {
- val relay9 =
- RelayItem.Relay(
- name = "se9-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay10 =
- RelayItem.Relay(
- name = "se10-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- relay9 assertOrderBothDirection relay10
- }
-
- @Test
- fun `given two relays with same name with number in name comparator should return 0`() {
- val relay9a =
- RelayItem.Relay(
- name = "se9-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay9b =
- RelayItem.Relay(
- name = "se9-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0)
- assertTrue(RelayNameComparator.compare(relay9b, relay9a) == 0)
- }
-
- @Test
- fun `comparator should be able to handle name of only numbers`() {
- val relay001 =
- RelayItem.Relay(
- name = "001",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay1 =
- RelayItem.Relay(
- name = "1",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay3 =
- RelayItem.Relay(
- name = "3",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay100 =
- RelayItem.Relay(
- name = "100",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- relay001 assertOrderBothDirection relay1
- relay001 assertOrderBothDirection relay3
- relay1 assertOrderBothDirection relay3
- relay3 assertOrderBothDirection relay100
- }
-
- @Test
- fun `given two relays with same name and without number comparator should return 0`() {
- val relay9a =
- RelayItem.Relay(
- name = "se-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay9b =
- RelayItem.Relay(
- name = "se-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0)
- assertTrue(RelayNameComparator.compare(relay9b, relay9a) == 0)
- }
-
- @Test
- fun `given two relays with leading zeroes comparator should return lowest number first`() {
- val relay001 =
- RelayItem.Relay(
- name = "se001-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay005 =
- RelayItem.Relay(
- name = "se005-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- relay001 assertOrderBothDirection relay005
- }
-
- @Test
- fun `given 4 relays comparator should sort by prefix then number`() {
- val relayAr2 =
- RelayItem.Relay(
- name = "ar2-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relayAr8 =
- RelayItem.Relay(
- name = "ar8-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relaySe5 =
- RelayItem.Relay(
- name = "se5-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relaySe10 =
- RelayItem.Relay(
- name = "se10-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- relayAr2 assertOrderBothDirection relayAr8
- relayAr8 assertOrderBothDirection relaySe5
- relaySe5 assertOrderBothDirection relaySe10
- }
-
- @Test
- fun `given two relays with same prefix and number comparator should sort by suffix`() {
- val relay2c =
- RelayItem.Relay(
- name = "se2-cloud",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay2w =
- RelayItem.Relay(
- name = "se2-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- relay2c assertOrderBothDirection relay2w
- }
-
- @Test
- fun `given two relays with same prefix, but one with no suffix, the one with no suffix should come first`() {
- val relay22a =
- RelayItem.Relay(
- name = "se22",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
- val relay22b =
- RelayItem.Relay(
- name = "se22-wireguard",
- location = mockk(),
- locationName = "mock",
- active = false,
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
- )
-
- relay22a assertOrderBothDirection relay22b
- }
-
- private infix fun RelayItem.Relay.assertOrderBothDirection(other: RelayItem.Relay) {
- assertTrue(RelayNameComparator.compare(this, other) < 0)
- assertTrue(RelayNameComparator.compare(other, this) > 0)
- }
-}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepositoryTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepositoryTest.kt
index 9c2ac615c3..4b8a524e5c 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepositoryTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepositoryTest.kt
@@ -1,271 +1,236 @@
package net.mullvad.mullvadvpn.repository
+import arrow.core.left
+import arrow.core.right
+import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
-import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.CreateCustomListResult
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.CustomListsError
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.UpdateCustomListResult
-import net.mullvad.mullvadvpn.relaylist.getGeographicLocationConstraintByCode
-import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.GetCustomListError
+import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.Settings
import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class CustomListsRepositoryTest {
- private val mockMessageHandler: MessageHandler = mockk()
- private val mockSettingsRepository: SettingsRepository = mockk()
- private val mockRelayListListener: RelayListListener = mockk()
- private val customListsRepository =
- CustomListsRepository(
- messageHandler = mockMessageHandler,
- settingsRepository = mockSettingsRepository,
- relayListListener = mockRelayListListener
- )
+ private val mockManagementService: ManagementService = mockk()
+ private lateinit var customListsRepository: CustomListsRepository
- private val settingsFlow: MutableStateFlow<Settings?> = MutableStateFlow(null)
- private val relayListFlow: MutableStateFlow<RelayList> = MutableStateFlow(mockk())
+ private val settingsFlow: MutableStateFlow<Settings> = MutableStateFlow(mockk(relaxed = true))
@BeforeEach
fun setup() {
mockkStatic(RELAY_LIST_EXTENSIONS)
- every { mockSettingsRepository.settingsUpdates } returns settingsFlow
- every { mockRelayListListener.relayListEvents } returns relayListFlow
+ every { mockManagementService.settings } returns settingsFlow
+ customListsRepository =
+ CustomListsRepository(
+ managementService = mockManagementService,
+ dispatcher = UnconfinedTestDispatcher()
+ )
}
@Test
- fun `get custom list by id should return custom list when id matches custom list in settings`() {
- // Arrange
- val mockCustomList: CustomList = mockk()
- val mockSettings: Settings = mockk()
- val customListId = "1"
- settingsFlow.value = mockSettings
- every { mockSettings.customLists.customLists } returns arrayListOf(mockCustomList)
- every { mockCustomList.id } returns customListId
+ fun `get custom list by id should return custom list when id matches custom list in settings`() =
+ runTest {
+ // Arrange
+ val customListId = CustomListId("1")
+ val mockCustomList =
+ CustomList(
+ id = customListId,
+ name = mockk(relaxed = true),
+ locations = mockk(relaxed = true)
+ )
+ val mockSettings: Settings = mockk()
+ every { mockSettings.customLists } returns listOf(mockCustomList)
+ settingsFlow.value = mockSettings
- // Act
- val result = customListsRepository.getCustomListById(customListId)
+ // Act
+ val result = customListsRepository.getCustomListById(customListId)
- // Assert
- assertEquals(mockCustomList, result)
- }
+ // Assert
+ assertEquals(mockCustomList, result.getOrNull())
+ }
+
+ @Test
+ fun `get custom list by id should return get custom list error when id does not matches custom list in settings`() =
+ runTest {
+ // Arrange
+ val customListId = CustomListId("1")
+ val mockCustomList =
+ CustomList(
+ id = customListId,
+ name = mockk(relaxed = true),
+ locations = mockk(relaxed = true)
+ )
+ val mockSettings: Settings = mockk()
+ val otherCustomListId = CustomListId("2")
+ every { mockSettings.customLists } returns listOf(mockCustomList)
+ settingsFlow.value = mockSettings
+
+ // Act
+ val result = customListsRepository.getCustomListById(otherCustomListId)
+
+ // Assert
+ assertEquals(GetCustomListError(otherCustomListId), result.leftOrNull())
+ }
@Test
- fun `get custom list by id should return null when id does not matches custom list in settings`() {
+ fun `create custom list should return id when creation is successful`() = runTest {
// Arrange
- val mockCustomList: CustomList = mockk()
- val mockSettings: Settings = mockk()
- val customListId = "1"
- val otherCustomListId = "2"
- settingsFlow.value = mockSettings
- every { mockSettings.customLists.customLists } returns arrayListOf(mockCustomList)
- every { mockCustomList.id } returns customListId
+ val customListId = CustomListId("1")
+ val expectedResult = customListId.right()
+ val customListName = CustomListName.fromString("CUSTOM")
+ coEvery { mockManagementService.createCustomList(customListName) } returns expectedResult
// Act
- val result = customListsRepository.getCustomListById(otherCustomListId)
+ val result = customListsRepository.createCustomList(customListName)
// Assert
- assertNull(result)
+ assertEquals(expectedResult, result)
}
@Test
- fun `create custom list should return Ok when creation is successful`() = runTest {
+ fun `create custom list should return lists exists error from management service`() = runTest {
// Arrange
- val customListId = "1"
- val expectedResult = CreateCustomListResult.Ok(customListId)
- val customListName = "CUSTOM"
- every {
- mockMessageHandler.trySendRequest(Request.CreateCustomList(customListName))
- } returns true
- every { mockMessageHandler.events<Event.CreateCustomListResultEvent>() } returns
- flowOf(Event.CreateCustomListResultEvent(expectedResult))
+ val expectedResult = CustomListAlreadyExists.left()
+ val customListName = CustomListName.fromString("CUSTOM")
+ coEvery { mockManagementService.createCustomList(customListName) } returns expectedResult
// Act
- val result =
- customListsRepository.createCustomList(CustomListName.fromString(customListName))
+ val result = customListsRepository.createCustomList(customListName)
// Assert
assertEquals(expectedResult, result)
}
@Test
- fun `create custom list should return lists exists when lists exists error event is received`() =
+ fun `update custom list name should return success when call ManagementService is successful`() =
runTest {
// Arrange
- val expectedResult = CreateCustomListResult.Error(CustomListsError.CustomListExists)
- val customListName = "CUSTOM"
- every {
- mockMessageHandler.trySendRequest(Request.CreateCustomList(customListName))
- } returns true
- every { mockMessageHandler.events<Event.CreateCustomListResultEvent>() } returns
- flowOf(Event.CreateCustomListResultEvent(expectedResult))
+ val customListId = CustomListId("1")
+ val expectedResult = Unit.right()
+ val customListName = CustomListName.fromString("CUSTOM")
+ val mockSettings: Settings = mockk()
+ val mockCustomList =
+ CustomList(
+ id = customListId,
+ name = mockk(relaxed = true),
+ locations = mockk(relaxed = true)
+ )
+ every { mockSettings.customLists } returns listOf(mockCustomList)
+ settingsFlow.value = mockSettings
+ coEvery { mockManagementService.updateCustomList(any<CustomList>()) } returns
+ expectedResult
// Act
- val result =
- customListsRepository.createCustomList(CustomListName.fromString(customListName))
+ val result = customListsRepository.updateCustomListName(customListId, customListName)
// Assert
assertEquals(expectedResult, result)
}
@Test
- fun `update custom list name should return ok when list updated event is received`() = runTest {
- // Arrange
- val customListId = "1"
- val expectedResult = UpdateCustomListResult.Ok
- val customListName = "CUSTOM"
- val mockSettings: Settings = mockk()
- val mockCustomList: CustomList = mockk()
- val updatedCustomList: CustomList = mockk()
- settingsFlow.value = mockSettings
- every { mockCustomList.id } returns customListId
- every { mockCustomList.copy(customListId, customListName, any()) } returns updatedCustomList
- every {
- mockMessageHandler.trySendRequest(Request.UpdateCustomList(updatedCustomList))
- } returns true
- every { mockMessageHandler.events<Event.UpdateCustomListResultEvent>() } returns
- flowOf(Event.UpdateCustomListResultEvent(expectedResult))
- every { mockSettings.customLists.customLists } returns arrayListOf(mockCustomList)
-
- // Act
- val result =
- customListsRepository.updateCustomListName(
- customListId,
- CustomListName.fromString(customListName)
- )
-
- // Assert
- assertEquals(expectedResult, result)
- }
-
- @Test
fun `update custom list name should return list exists error when list exists error is received`() =
runTest {
// Arrange
- val customListId = "1"
- val expectedResult = UpdateCustomListResult.Error(CustomListsError.CustomListExists)
- val customListName = "CUSTOM"
+ val customListId = CustomListId("1")
+ val customListName = CustomListName.fromString("CUSTOM")
+ val expectedResult = NameAlreadyExists(customListName.value).left()
val mockSettings: Settings = mockk()
- val mockCustomList: CustomList = mockk()
- val updatedCustomList: CustomList = mockk()
+ val mockCustomList =
+ CustomList(
+ id = customListId,
+ name = CustomListName.fromString("OLD CUSTOM"),
+ locations = emptyList()
+ )
+ val updatedCustomList =
+ CustomList(id = customListId, name = customListName, locations = emptyList())
+ every { mockSettings.customLists } returns listOf(mockCustomList)
settingsFlow.value = mockSettings
- every { mockCustomList.id } returns customListId
- every { mockCustomList.copy(customListId, customListName, any()) } returns
- updatedCustomList
- every {
- mockMessageHandler.trySendRequest(Request.UpdateCustomList(updatedCustomList))
- } returns true
- every { mockMessageHandler.events<Event.UpdateCustomListResultEvent>() } returns
- flowOf(Event.UpdateCustomListResultEvent(expectedResult))
- every { mockSettings.customLists.customLists } returns arrayListOf(mockCustomList)
+ coEvery { mockManagementService.updateCustomList(updatedCustomList) } returns
+ expectedResult
// Act
- val result =
- customListsRepository.updateCustomListName(
- customListId,
- CustomListName.fromString(customListName)
- )
+ val result = customListsRepository.updateCustomListName(customListId, customListName)
// Assert
assertEquals(expectedResult, result)
}
@Test
- fun `when delete custom lists is called a delete custom event should be sent`() = runTest {
- // Arrange
- val customListId = "1"
- every { mockMessageHandler.trySendRequest(Request.DeleteCustomList(customListId)) } returns
- true
+ fun `when delete custom lists is called Managementservice delete custom list should be called`() =
+ runTest {
+ // Arrange
+ val customListId = CustomListId("1")
+ coEvery { mockManagementService.deleteCustomList(customListId) } returns Unit.right()
- // Act
- customListsRepository.deleteCustomList(customListId)
+ // Act
+ customListsRepository.deleteCustomList(customListId)
- // Assert
- verify { mockMessageHandler.trySendRequest(Request.DeleteCustomList(customListId)) }
- }
+ // Assert
+ coVerify { mockManagementService.deleteCustomList(customListId) }
+ }
@Test
- fun `update custom list locations should return ok when list exists and ok updated list event is received`() =
+ fun `update custom list locations should return successful when list exists and update is successful`() =
runTest {
// Arrange
- val expectedResult = UpdateCustomListResult.Ok
- val customListId = "1"
- val customListName = "CUSTOM"
- val locationCode = "AB"
+ val expectedResult = Unit.right()
+ val customListId = CustomListId("1")
+ val customListName = CustomListName.fromString("CUSTOM")
+ val location = GeoLocationId.Country("se")
val mockSettings: Settings = mockk()
- val mockRelayList: RelayList = mockk()
- val mockCustomList: CustomList = mockk()
- val updatedCustomList: CustomList = mockk()
- val mockLocationConstraint: GeographicLocationConstraint = mockk()
+ val mockCustomList =
+ CustomList(id = customListId, name = customListName, locations = emptyList())
+ val updatedCustomList =
+ CustomList(id = customListId, name = customListName, locations = listOf(location))
+ every { mockSettings.customLists } returns listOf(mockCustomList)
settingsFlow.value = mockSettings
- relayListFlow.value = mockRelayList
- every { mockCustomList.id } returns customListId
- every { mockCustomList.name } returns customListName
- every {
- mockCustomList.copy(
- customListId,
- customListName,
- arrayListOf(mockLocationConstraint)
- )
- } returns updatedCustomList
- every {
- mockMessageHandler.trySendRequest(Request.UpdateCustomList(updatedCustomList))
- } returns true
- every { mockMessageHandler.events<Event.UpdateCustomListResultEvent>() } returns
- flowOf(Event.UpdateCustomListResultEvent(expectedResult))
- every { mockSettings.customLists.customLists } returns arrayListOf(mockCustomList)
- every { mockRelayList.getGeographicLocationConstraintByCode(locationCode) } returns
- mockLocationConstraint
+ coEvery { mockManagementService.updateCustomList(updatedCustomList) } returns
+ Unit.right()
// Act
val result =
- customListsRepository.updateCustomListLocationsFromCodes(
- customListId,
- listOf(locationCode)
- )
+ customListsRepository.updateCustomListLocations(customListId, listOf(location))
// Assert
assertEquals(expectedResult, result)
}
@Test
- fun `update custom list locations should return other error when list does not exist`() =
+ fun `update custom list locations should return get custom list error when list does not exist`() =
runTest {
// Arrange
- val expectedResult = UpdateCustomListResult.Error(CustomListsError.OtherError)
- val mockCustomList: CustomList = mockk()
val mockSettings: Settings = mockk()
- val customListId = "1"
- val otherCustomListId = "2"
- val locationCode = "AB"
- val mockRelayList: RelayList = mockk()
- val mockLocationConstraint: GeographicLocationConstraint = mockk()
+ val customListId = CustomListId("1")
+ val otherCustomListId = CustomListId("2")
+ val expectedResult = GetCustomListError(otherCustomListId).left()
+ val mockCustomList =
+ CustomList(
+ id = customListId,
+ name = CustomListName.fromString("name"),
+ locations = emptyList()
+ )
+ val locationId = GeoLocationId.Country("se")
+ every { mockSettings.customLists } returns listOf(mockCustomList)
settingsFlow.value = mockSettings
- relayListFlow.value = mockRelayList
- every { mockSettings.customLists.customLists } returns arrayListOf(mockCustomList)
- every { mockCustomList.id } returns customListId
- every { mockRelayList.getGeographicLocationConstraintByCode(locationCode) } returns
- mockLocationConstraint
// Act
val result =
- customListsRepository.updateCustomListLocationsFromCodes(
+ customListsRepository.updateCustomListLocations(
otherCustomListId,
- listOf(locationCode)
+ listOf(locationId)
)
// Assert
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepositoryTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepositoryTest.kt
new file mode 100644
index 0000000000..c8027240a2
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/RelayListFilterRepositoryTest.kt
@@ -0,0 +1,174 @@
+package net.mullvad.mullvadvpn.repository
+
+import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.ProviderId
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.SetWireguardConstraintsError
+import net.mullvad.mullvadvpn.lib.model.Settings
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class RelayListFilterRepositoryTest {
+ private val mockManagementService: ManagementService = mockk()
+
+ private lateinit var relayListFilterRepository: RelayListFilterRepository
+
+ private val settingsFlow = MutableStateFlow(mockk<Settings>(relaxed = true))
+
+ @BeforeEach
+ fun setUp() {
+ every { mockManagementService.settings } returns settingsFlow
+ relayListFilterRepository =
+ RelayListFilterRepository(
+ managementService = mockManagementService,
+ dispatcher = UnconfinedTestDispatcher()
+ )
+ }
+
+ @Test
+ fun `when settings is updated selected ownership should update`() = runTest {
+ // Arrange
+ val mockSettings: Settings = mockk()
+ val selectedOwnership: Constraint<Ownership> = Constraint.Only(Ownership.MullvadOwned)
+ every { mockSettings.relaySettings.relayConstraints.ownership } returns selectedOwnership
+
+ // Act, Assert
+ relayListFilterRepository.selectedOwnership.test {
+ assertEquals(Constraint.Any, awaitItem())
+ settingsFlow.emit(mockSettings)
+ assertEquals(selectedOwnership, awaitItem())
+ }
+ }
+
+ @Test
+ fun `when settings is updated selected providers should update`() = runTest {
+ // Arrange
+ val mockSettings: Settings = mockk()
+ val selectedProviders: Constraint<Providers> =
+ Constraint.Only(Providers(setOf(ProviderId("Prove"))))
+ every { mockSettings.relaySettings.relayConstraints.providers } returns selectedProviders
+
+ // Act, Assert
+ relayListFilterRepository.selectedProviders.test {
+ assertEquals(Constraint.Any, awaitItem())
+ settingsFlow.emit(mockSettings)
+ assertEquals(selectedProviders, awaitItem())
+ }
+ }
+
+ @Test
+ fun `when successfully updating selected ownership and filter should return successful`() =
+ runTest {
+ // Arrange
+ val ownership = Constraint.Any
+ val providers = Constraint.Any
+ coEvery { mockManagementService.setOwnershipAndProviders(ownership, providers) } returns
+ Unit.right()
+
+ // Act
+ val result =
+ relayListFilterRepository.updateSelectedOwnershipAndProviderFilter(
+ ownership,
+ providers
+ )
+
+ // Assert
+ coVerify { mockManagementService.setOwnershipAndProviders(ownership, providers) }
+ assertEquals(Unit.right(), result)
+ }
+
+ @Test
+ fun `when failing to update selected ownership and filter should return SetWireguardConstraintsError`() =
+ runTest {
+ // Arrange
+ val ownership = Constraint.Any
+ val providers = Constraint.Any
+ val error = SetWireguardConstraintsError.Unknown(mockk())
+ coEvery { mockManagementService.setOwnershipAndProviders(ownership, providers) } returns
+ error.left()
+
+ // Act
+ val result =
+ relayListFilterRepository.updateSelectedOwnershipAndProviderFilter(
+ ownership,
+ providers
+ )
+
+ // Assert
+ coVerify { mockManagementService.setOwnershipAndProviders(ownership, providers) }
+ assertEquals(error.left(), result)
+ }
+
+ @Test
+ fun `when successfully updating selected ownership should return successful`() = runTest {
+ // Arrange
+ val ownership = Constraint.Only(Ownership.Rented)
+ coEvery { mockManagementService.setOwnership(ownership) } returns Unit.right()
+
+ // Act
+ val result = relayListFilterRepository.updateSelectedOwnership(ownership)
+
+ // Assert
+ coVerify { mockManagementService.setOwnership(ownership) }
+ assertEquals(Unit.right(), result)
+ }
+
+ @Test
+ fun `when failing to update selected ownership should return SetWireguardConstraintsError`() =
+ runTest {
+ // Arrange
+ val ownership = Constraint.Only(Ownership.Rented)
+ val error = SetWireguardConstraintsError.Unknown(mockk())
+ coEvery { mockManagementService.setOwnership(ownership) } returns error.left()
+
+ // Act
+ val result = relayListFilterRepository.updateSelectedOwnership(ownership)
+
+ // Assert
+ coVerify { mockManagementService.setOwnership(ownership) }
+ assertEquals(error.left(), result)
+ }
+
+ @Test
+ fun `when successfully updating selected providers should return successful`() = runTest {
+ // Arrange
+ val providers = Constraint.Only(Providers(setOf(ProviderId("Mopp"))))
+ coEvery { mockManagementService.setProviders(providers) } returns Unit.right()
+
+ // Act
+ val result = relayListFilterRepository.updateSelectedProviders(providers)
+
+ // Assert
+ coVerify { mockManagementService.setProviders(providers) }
+ assertEquals(Unit.right(), result)
+ }
+
+ @Test
+ fun `when failing to update selected providers should return SetWireguardConstraintsError`() =
+ runTest {
+ // Arrange
+ val providers = Constraint.Only(Providers(setOf(ProviderId("Mopp"))))
+ val error = SetWireguardConstraintsError.Unknown(mockk())
+ coEvery { mockManagementService.setProviders(providers) } returns error.left()
+
+ // Act
+ val result = relayListFilterRepository.updateSelectedProviders(providers)
+
+ // Assert
+ coVerify { mockManagementService.setProviders(providers) }
+ assertEquals(error.left(), result)
+ }
+}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt
deleted file mode 100644
index 8fd21c5533..0000000000
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ConnectionProxyTest.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.DeadObjectException
-import android.os.Looper
-import android.os.Messenger
-import android.util.Log
-import io.mockk.MockKAnnotations
-import io.mockk.Runs
-import io.mockk.every
-import io.mockk.impl.annotations.MockK
-import io.mockk.just
-import io.mockk.mockk
-import io.mockk.mockkObject
-import io.mockk.mockkStatic
-import io.mockk.slot
-import io.mockk.unmockkAll
-import kotlin.reflect.KClass
-import kotlin.test.assertEquals
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-
-class ConnectionProxyTest {
-
- @MockK private lateinit var mockedMainLooper: Looper
-
- @MockK private lateinit var connection: Messenger
-
- @MockK private lateinit var mockedDispatchingHandler: EventDispatcher
- lateinit var connectionProxy: ConnectionProxy
-
- @BeforeEach
- fun setup() {
- mockkStatic(Looper::class)
- mockkStatic(Log::class)
- MockKAnnotations.init(this)
- mockkObject(Request.Connect, Request.Disconnect)
- every { Request.Connect.message } returns mockk()
- every { Request.Disconnect.message } returns mockk()
- every { Looper.getMainLooper() } returns mockedMainLooper
- every { Log.e(any(), any()) } returns mockk(relaxed = true)
- }
-
- @AfterEach
- fun tearDown() {
- unmockkAll()
- }
-
- @Test
- fun `initialize connection proxy should work`() {
- // Arrange
- val eventType = slot<KClass<Event.TunnelStateChange>>()
- every { mockedDispatchingHandler.registerHandler(capture(eventType), any()) } just Runs
- // Create ConnectionProxy instance and assert initial Event type
- connectionProxy = ConnectionProxy(connection, mockedDispatchingHandler)
- assertEquals(Event.TunnelStateChange::class, eventType.captured.java.kotlin)
- }
-
- @Test
- fun `normal connect and disconnect should not crash`() {
- // Arrange
- every { connection.send(any()) } just Runs
- every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs
- // Act and Assert no crashes
- connectionProxy = ConnectionProxy(connection, mockedDispatchingHandler)
- connectionProxy.connect()
- connectionProxy.disconnect()
- }
-
- @Test
- fun `connect should catch DeadObjectException`() {
- // Arrange
- every { connection.send(any()) } throws DeadObjectException()
- every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs
- // Act and Assert no crashes
- connectionProxy = ConnectionProxy(connection, mockedDispatchingHandler)
- connectionProxy.connect()
- }
-}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt
deleted file mode 100644
index 81b518199c..0000000000
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/ServiceConnectionDeviceDataSourceTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package net.mullvad.mullvadvpn.ui.serviceconnection
-
-import android.os.DeadObjectException
-import android.os.Looper
-import android.os.Messenger
-import io.mockk.MockKAnnotations
-import io.mockk.Runs
-import io.mockk.every
-import io.mockk.impl.annotations.MockK
-import io.mockk.just
-import io.mockk.mockk
-import io.mockk.mockkObject
-import io.mockk.mockkStatic
-import io.mockk.unmockkAll
-import kotlin.reflect.KClass
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-
-class ServiceConnectionDeviceDataSourceTest {
- @MockK private lateinit var mockedMainLooper: Looper
-
- @MockK private lateinit var mockedDispatchingHandler: EventDispatcher
-
- @MockK private lateinit var connection: Messenger
-
- lateinit var serviceConnectionDeviceDataSource: ServiceConnectionDeviceDataSource
-
- @BeforeEach
- fun setup() {
- mockkStatic(Looper::class)
- mockkStatic(android.util.Log::class)
- MockKAnnotations.init(this)
- mockkObject(Request.GetDevice, Request.RefreshDeviceState)
- every { Request.GetDevice.message } returns mockk()
- every { Request.RefreshDeviceState.message } returns mockk()
- every { Looper.getMainLooper() } returns mockedMainLooper
- every { android.util.Log.e(any(), any()) } returns mockk(relaxed = true)
- }
-
- @AfterEach
- fun tearDown() {
- unmockkAll()
- }
-
- @Test
- fun `get device should work`() {
- // Arrange
- every { connection.send(any()) } just Runs
- every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs
- // Act and Assert no crashes
- serviceConnectionDeviceDataSource =
- ServiceConnectionDeviceDataSource(connection, mockedDispatchingHandler)
- serviceConnectionDeviceDataSource.getDevice()
- }
-
- @Test
- fun `get device should catch DeadObjectException`() {
- // Arrange
- every { connection.send(any()) } throws DeadObjectException()
- every { mockedDispatchingHandler.registerHandler(any<KClass<Event>>(), any()) } just Runs
- // Act and Assert no crashes
- serviceConnectionDeviceDataSource =
- ServiceConnectionDeviceDataSource(connection, mockedDispatchingHandler)
- serviceConnectionDeviceDataSource.getDevice()
- }
-}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt
index 39bfae63d8..11d574b663 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/AccountExpiryNotificationUseCaseTest.kt
@@ -10,8 +10,8 @@ import kotlin.test.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.repository.AccountRepository
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class AccountExpiryNotificationUseCaseTest {
- private val accountExpiry = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
+ private val accountExpiry = MutableStateFlow<AccountData?>(null)
private lateinit var accountExpiryNotificationUseCase: AccountExpiryNotificationUseCase
@BeforeEach
@@ -30,7 +30,7 @@ class AccountExpiryNotificationUseCaseTest {
MockKAnnotations.init(this)
val accountRepository = mockk<AccountRepository>()
- every { accountRepository.accountExpiryState } returns accountExpiry
+ every { accountRepository.accountData } returns accountExpiry
accountExpiryNotificationUseCase = AccountExpiryNotificationUseCase(accountRepository)
}
@@ -53,11 +53,11 @@ class AccountExpiryNotificationUseCaseTest {
// Arrange, Act, Assert
accountExpiryNotificationUseCase.notifications().test {
assertTrue { awaitItem().isEmpty() }
- val closeToExpiry = AccountExpiry.Available(DateTime.now().plusDays(2))
+ val closeToExpiry = AccountData(mockk(relaxed = true), DateTime.now().plusDays(2))
accountExpiry.value = closeToExpiry
assertEquals(
- listOf(InAppNotification.AccountExpiry(closeToExpiry.expiryDateTime)),
+ listOf(InAppNotification.AccountExpiry(closeToExpiry.expiryDate)),
awaitItem()
)
}
@@ -68,7 +68,7 @@ class AccountExpiryNotificationUseCaseTest {
// Arrange, Act, Assert
accountExpiryNotificationUseCase.notifications().test {
assertTrue { awaitItem().isEmpty() }
- accountExpiry.value = AccountExpiry.Available(DateTime.now().plusDays(4))
+ accountExpiry.value = AccountData(mockk(relaxed = true), DateTime.now().plusDays(4))
expectNoEvents()
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt
index 4dfb95768b..bb19d42d13 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/CustomListActionUseCaseTest.kt
@@ -1,78 +1,80 @@
package net.mullvad.mullvadvpn.usecase
+import arrow.core.left
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
-import kotlin.test.assertIs
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.communication.Created
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
-import net.mullvad.mullvadvpn.model.CreateCustomListResult
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.model.CustomListsError
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.UpdateCustomListResult
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.relaylist.getRelayItemsByCodes
+import net.mullvad.mullvadvpn.compose.communication.Deleted
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
+import net.mullvad.mullvadvpn.compose.communication.Renamed
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.repository.CustomListsRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
-import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException
+import net.mullvad.mullvadvpn.usecase.customlists.RenameError
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class CustomListActionUseCaseTest {
private val mockCustomListsRepository: CustomListsRepository = mockk()
- private val mockRelayListUseCase: RelayListUseCase = mockk()
+ private val mockRelayListRepository: RelayListRepository = mockk()
private val customListActionUseCase =
CustomListActionUseCase(
customListsRepository = mockCustomListsRepository,
- relayListUseCase = mockRelayListUseCase
+ relayListRepository = mockRelayListRepository
)
+ private val relayListFlow = MutableStateFlow(emptyList<RelayItem.Location.Country>())
+
@BeforeEach
fun setup() {
mockkStatic(RELAY_LIST_EXTENSIONS)
+ every { mockRelayListRepository.relayList } returns relayListFlow
}
@Test
fun `create action should return success when ok`() = runTest {
// Arrange
val name = CustomListName.fromString("test")
- val locationCode = "AB"
+ val locationId = GeoLocationId.Country("se")
val locationName = "Acklaba"
- val createdId = "1"
- val action = CustomListAction.Create(name = name, locations = listOf(locationCode))
+ val createdId = CustomListId("1")
+ val action = CustomListAction.Create(name = name, locations = listOf(locationId))
val expectedResult =
- Result.success(
- CustomListResult.Created(
+ Created(
id = createdId,
name = name,
- locationName = locationName,
+ locationNames = listOf(locationName),
undo = action.not(createdId)
)
- )
- val relayItem =
- RelayItem.Country(
- name = locationName,
- code = locationCode,
- expanded = false,
- cities = emptyList()
- )
- val mockLocations: List<RelayItem.Country> = listOf(relayItem)
- coEvery { mockCustomListsRepository.createCustomList(name) } returns
- CreateCustomListResult.Ok(createdId)
+ .right()
+ coEvery { mockCustomListsRepository.createCustomList(name) } returns createdId.right()
coEvery {
- mockCustomListsRepository.updateCustomListLocationsFromCodes(
- createdId,
- listOf(locationCode)
+ mockCustomListsRepository.updateCustomListLocations(createdId, listOf(locationId))
+ } returns Unit.right()
+ relayListFlow.value =
+ listOf(
+ RelayItem.Location.Country(
+ id = locationId,
+ name = locationName,
+ expanded = false,
+ cities = emptyList()
+ )
)
- } returns UpdateCustomListResult.Ok
- coEvery { mockRelayListUseCase.fullRelayList() } returns flowOf(mockLocations)
- every { mockLocations.getRelayItemsByCodes(listOf(locationCode)) } returns mockLocations
// Act
val result = customListActionUseCase.performAction(action)
@@ -85,20 +87,17 @@ class CustomListActionUseCaseTest {
fun `create action should return error when name already exists`() = runTest {
// Arrange
val name = CustomListName.fromString("test")
- val locationCode = "AB"
- val action = CustomListAction.Create(name = name, locations = listOf(locationCode))
- val expectedError = CustomListsError.CustomListExists
+ val locationId = GeoLocationId.Country("AB")
+ val action = CustomListAction.Create(name = name, locations = listOf(locationId))
+ val expectedError = CreateWithLocationsError.Create(CustomListAlreadyExists).left()
coEvery { mockCustomListsRepository.createCustomList(name) } returns
- CreateCustomListResult.Error(CustomListsError.CustomListExists)
+ CustomListAlreadyExists.left()
// Act
val result = customListActionUseCase.performAction(action)
// Assert
- assertIs<Result<CustomListsException>>(result)
- val exception = result.exceptionOrNull()
- assertIs<CustomListsException>(exception)
- assertEquals(expectedError, exception.error)
+ assertEquals(expectedError, result)
}
@Test
@@ -106,13 +105,12 @@ class CustomListActionUseCaseTest {
// Arrange
val name = CustomListName.fromString("test")
val newName = CustomListName.fromString("test2")
- val customListId = "1"
- val action =
- CustomListAction.Rename(customListId = customListId, name = name, newName = newName)
- val expectedResult = Result.success(CustomListResult.Renamed(undo = action.not()))
+ val customListId = CustomListId("1")
+ val action = CustomListAction.Rename(id = customListId, name = name, newName = newName)
+ val expectedResult = Renamed(undo = action.not()).right()
coEvery {
mockCustomListsRepository.updateCustomListName(id = customListId, name = newName)
- } returns UpdateCustomListResult.Ok
+ } returns Unit.right()
// Act
val result = customListActionUseCase.performAction(action)
@@ -126,45 +124,38 @@ class CustomListActionUseCaseTest {
// Arrange
val name = CustomListName.fromString("test")
val newName = CustomListName.fromString("test2")
- val customListId = "1"
- val action =
- CustomListAction.Rename(customListId = customListId, name = name, newName = newName)
- val expectedError = CustomListsError.CustomListExists
+ val customListId = CustomListId("1")
+ val action = CustomListAction.Rename(id = customListId, name = name, newName = newName)
coEvery {
mockCustomListsRepository.updateCustomListName(id = customListId, name = newName)
- } returns UpdateCustomListResult.Error(expectedError)
+ } returns NameAlreadyExists(newName.value).left()
+
+ val expectedError = RenameError(NameAlreadyExists(newName.value)).left()
// Act
val result = customListActionUseCase.performAction(action)
// Assert
- assertIs<Result<CustomListsException>>(result)
- val exception = result.exceptionOrNull()
- assertIs<CustomListsException>(exception)
- assertEquals(expectedError, exception.error)
+ assertEquals(expectedError, result)
}
@Test
fun `delete action should return successful with deleted list`() = runTest {
// Arrange
- val mockCustomList: CustomList = mockk()
- val mockLocation: GeographicLocationConstraint.Country = mockk()
- val mockLocations: ArrayList<GeographicLocationConstraint> = arrayListOf(mockLocation)
+ val mockLocation: GeoLocationId.Country = mockk()
+ val mockLocations: List<GeoLocationId> = listOf(mockLocation)
val name = CustomListName.fromString("test")
- val customListId = "1"
- val locationCode = "AB"
- val action = CustomListAction.Delete(customListId = customListId)
+ val customListId = CustomListId("1")
+ val mockCustomList = CustomList(id = customListId, name = name, locations = mockLocations)
+ val location = GeoLocationId.Country("AB")
+ val action = CustomListAction.Delete(id = customListId)
val expectedResult =
- Result.success(
- CustomListResult.Deleted(
- undo = action.not(name = name, locations = listOf(locationCode))
- )
- )
- every { mockCustomList.locations } returns mockLocations
- every { mockCustomList.name } returns name.value
- every { mockLocation.countryCode } returns locationCode
- coEvery { mockCustomListsRepository.deleteCustomList(id = customListId) } returns true
- every { mockCustomListsRepository.getCustomListById(customListId) } returns mockCustomList
+ Deleted(undo = action.not(name = name, locations = listOf(location))).right()
+ every { mockLocation.countryCode } returns location.countryCode
+ coEvery { mockCustomListsRepository.deleteCustomList(id = customListId) } returns
+ Unit.right()
+ coEvery { mockCustomListsRepository.getCustomListById(customListId) } returns
+ mockCustomList.right()
// Act
val result = customListActionUseCase.performAction(action)
@@ -177,35 +168,20 @@ class CustomListActionUseCaseTest {
fun `update locations action should return success with changed locations`() = runTest {
// Arrange
val name = CustomListName.fromString("test")
- val oldLocationCodes = listOf("AB", "CD")
- val newLocationCodes = listOf("EF", "GH")
- val oldLocations: ArrayList<GeographicLocationConstraint> =
- arrayListOf(
- GeographicLocationConstraint.Country("AB"),
- GeographicLocationConstraint.Country("CD")
- )
- val customListId = "1"
- val customList = CustomList(id = customListId, name = name.value, locations = oldLocations)
- val action =
- CustomListAction.UpdateLocations(
- customListId = customListId,
- locations = newLocationCodes
- )
+ val newLocations = listOf(GeoLocationId.Country("EF"), GeoLocationId.Country("GH"))
+ val oldLocations: ArrayList<GeoLocationId> =
+ arrayListOf(GeoLocationId.Country("AB"), GeoLocationId.Country("CD"))
+ val customListId = CustomListId("1")
+ val customList = CustomList(id = customListId, name = name, locations = oldLocations)
+ val action = CustomListAction.UpdateLocations(id = customListId, locations = newLocations)
val expectedResult =
- Result.success(
- CustomListResult.LocationsChanged(
- name = name,
- undo = action.not(locations = oldLocationCodes)
- )
- )
- coEvery { mockCustomListsRepository.getCustomListById(customListId) } returns customList
+ LocationsChanged(name = name, undo = action.not(locations = oldLocations)).right()
+ coEvery { mockCustomListsRepository.getCustomListById(customListId) } returns
+ customList.right()
coEvery {
- mockCustomListsRepository.updateCustomListLocationsFromCodes(
- customListId,
- newLocationCodes
- )
- } returns UpdateCustomListResult.Ok
+ mockCustomListsRepository.updateCustomListLocations(customListId, newLocations)
+ } returns Unit.right()
// Act
val result = customListActionUseCase.performAction(action)
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt
index 691bb99131..b55da83f51 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/NewDeviceUseNotificationCaseTest.kt
@@ -10,11 +10,13 @@ import kotlin.test.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.AccountAndDevice
-import net.mullvad.mullvadvpn.model.Device
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
+import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -25,9 +27,14 @@ class NewDeviceUseNotificationCaseTest {
private val deviceName = "Frank Zebra"
private val deviceState =
- MutableStateFlow<DeviceState>(
+ MutableStateFlow<DeviceState?>(
DeviceState.LoggedIn(
- accountAndDevice = AccountAndDevice("", Device("", deviceName, byteArrayOf(), ""))
+ AccountToken("1234123412341234"),
+ Device(
+ id = DeviceId.fromString(UUID),
+ name = deviceName,
+ creationDate = DateTime.now()
+ )
)
)
private lateinit var newDeviceNotificationUseCase: NewDeviceNotificationUseCase
@@ -79,4 +86,8 @@ class NewDeviceUseNotificationCaseTest {
assertEquals(awaitItem(), emptyList())
}
}
+
+ companion object {
+ private const val UUID = "12345678-1234-5678-1234-567812345678"
+ }
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt
index 326e183445..088c9a435c 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt
@@ -9,21 +9,19 @@ import kotlin.time.Duration.Companion.days
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
-import net.mullvad.mullvadvpn.lib.ipc.events
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.tunnel.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -31,10 +29,10 @@ import org.junit.jupiter.api.Test
class OutOfTimeUseCaseTest {
private val mockAccountRepository: AccountRepository = mockk()
- private val mockMessageHandler: MessageHandler = mockk()
+ private val mockConnectionProxy: ConnectionProxy = mockk()
- private lateinit var events: Channel<Event.TunnelStateChange>
- private lateinit var expiry: MutableStateFlow<AccountExpiry>
+ private lateinit var events: Channel<TunnelState>
+ private lateinit var expiry: MutableStateFlow<AccountData?>
private val dispatcher = StandardTestDispatcher()
private val scope = TestScope(dispatcher)
@@ -44,15 +42,14 @@ class OutOfTimeUseCaseTest {
@BeforeEach
fun setup() {
events = Channel()
- expiry = MutableStateFlow(AccountExpiry.Missing)
- every { mockAccountRepository.accountExpiryState } returns expiry
- every { mockMessageHandler.events<Event.TunnelStateChange>() } returns
- events.receiveAsFlow()
+ expiry = MutableStateFlow(null)
+ every { mockAccountRepository.accountData } returns expiry
+ every { mockConnectionProxy.tunnelState } returns events.consumeAsFlow()
Dispatchers.setMain(dispatcher)
outOfTimeUseCase =
- OutOfTimeUseCase(mockAccountRepository, mockMessageHandler, scope.backgroundScope)
+ OutOfTimeUseCase(mockConnectionProxy, mockAccountRepository, scope.backgroundScope)
}
@AfterEach
@@ -73,14 +70,13 @@ class OutOfTimeUseCaseTest {
fun `tunnel is blocking because out of time should emit true`() =
scope.runTest {
// Arrange
- // Act, Assert
val errorStateCause = ErrorStateCause.AuthFailed("[EXPIRED_ACCOUNT]")
val tunnelStateError = TunnelState.Error(ErrorState(errorStateCause, true))
- val errorChange = Event.TunnelStateChange(tunnelStateError)
+ // Act, Assert
outOfTimeUseCase.isOutOfTime.test {
assertEquals(null, awaitItem())
- events.send(errorChange)
+ events.send(tunnelStateError)
assertEquals(true, awaitItem())
}
}
@@ -89,16 +85,16 @@ class OutOfTimeUseCaseTest {
fun `tunnel is connected should emit false`() =
scope.runTest {
// Arrange
- val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1))
+ val expiredAccountExpiry =
+ AccountData(mockk(relaxed = true), DateTime.now().plusDays(1))
val tunnelStateChanges =
listOf(
- TunnelState.Disconnected(),
- TunnelState.Connected(mockk(), null),
- TunnelState.Connecting(null, null),
- TunnelState.Disconnecting(mockk()),
- TunnelState.Error(ErrorState(ErrorStateCause.StartTunnelError, false)),
- )
- .map(Event::TunnelStateChange)
+ TunnelState.Disconnected(),
+ TunnelState.Connected(mockk(), null),
+ TunnelState.Connecting(null, null),
+ TunnelState.Disconnecting(mockk()),
+ TunnelState.Error(ErrorState(ErrorStateCause.StartTunnelError, false)),
+ )
// Act, Assert
outOfTimeUseCase.isOutOfTime.test {
@@ -118,7 +114,8 @@ class OutOfTimeUseCaseTest {
fun `account expiry that has expired should emit true`() =
scope.runTest {
// Arrange
- val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().minusDays(1))
+ val expiredAccountExpiry =
+ AccountData(mockk(relaxed = true), DateTime.now().minusDays(1))
// Act, Assert
outOfTimeUseCase.isOutOfTime.test {
assertEquals(null, awaitItem())
@@ -131,7 +128,8 @@ class OutOfTimeUseCaseTest {
fun `account expiry that has not expired should emit false`() =
scope.runTest {
// Arrange
- val notExpiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusDays(1))
+ val notExpiredAccountExpiry =
+ AccountData(mockk(relaxed = true), DateTime.now().plusDays(1))
// Act, Assert
outOfTimeUseCase.isOutOfTime.test {
@@ -145,7 +143,8 @@ class OutOfTimeUseCaseTest {
fun `account that expires without new expiry event should emit true`() =
runTest(dispatcher) {
// Arrange
- val expiredAccountExpiry = AccountExpiry.Available(DateTime.now().plusSeconds(100))
+ val expiredAccountExpiry =
+ AccountData(mockk(relaxed = true), DateTime.now().plusSeconds(100))
// Act, Assert
outOfTimeUseCase.isOutOfTime.test {
// Initial event
@@ -167,9 +166,10 @@ class OutOfTimeUseCaseTest {
@Test
fun `account that is about to expire but is refilled should emit false`() = runTest {
// Arrange
- val initialAccountExpiry = AccountExpiry.Available(DateTime.now().plusSeconds(100))
+ val initialAccountExpiry =
+ AccountData(mockk(relaxed = true), DateTime.now().plusSeconds(100))
val updatedExpiry =
- AccountExpiry.Available(initialAccountExpiry.expiryDateTime.plusDays(30))
+ AccountData(mockk(relaxed = true), initialAccountExpiry.expiryDate.plusDays(30))
// Act, Assert
outOfTimeUseCase.isOutOfTime.test {
@@ -196,9 +196,10 @@ class OutOfTimeUseCaseTest {
@Test
fun `expired account that is refilled should emit false`() = runTest {
// Arrange
- val initialAccountExpiry = AccountExpiry.Available(DateTime.now().plusSeconds(100))
+ val initialAccountExpiry =
+ AccountData(mockk(relaxed = true), DateTime.now().plusSeconds(100))
val updatedExpiry =
- AccountExpiry.Available(initialAccountExpiry.expiryDateTime.plusDays(30))
+ AccountData(mockk(relaxed = true), initialAccountExpiry.expiryDate.plusDays(30))
// Act, Assert
outOfTimeUseCase.isOutOfTime.test {
// Initial event
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
index 82126099d8..a2e8db36fd 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
@@ -10,15 +10,11 @@ import kotlin.test.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.repository.InAppNotification
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.util.EventNotifier
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -27,26 +23,19 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class TunnelStateNotificationUseCaseTest {
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
private val mockConnectionProxy: ConnectionProxy = mockk()
- private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
private lateinit var tunnelStateNotificationUseCase: TunnelStateNotificationUseCase
- private val eventNotifierTunnelUiState = EventNotifier<TunnelState>(TunnelState.Disconnected())
+ private val tunnelState = MutableStateFlow<TunnelState>(TunnelState.Disconnected())
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
- every { mockConnectionProxy.onUiStateChange } returns eventNotifierTunnelUiState
-
- every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
- every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy
+ every { mockConnectionProxy.tunnelState } returns tunnelState
tunnelStateNotificationUseCase =
- TunnelStateNotificationUseCase(serviceConnectionManager = mockServiceConnectionManager)
+ TunnelStateNotificationUseCase(connectionProxy = mockConnectionProxy)
}
@AfterEach
@@ -65,10 +54,8 @@ class TunnelStateNotificationUseCaseTest {
tunnelStateNotificationUseCase.notifications().test {
// Arrange, Act
assertEquals(emptyList(), awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
val errorState: ErrorState = mockk()
- eventNotifierTunnelUiState.notify(TunnelState.Error(errorState))
+ tunnelState.emit(TunnelState.Error(errorState))
// Assert
assertEquals(listOf(InAppNotification.TunnelStateError(errorState)), awaitItem())
@@ -81,11 +68,7 @@ class TunnelStateNotificationUseCaseTest {
tunnelStateNotificationUseCase.notifications().test {
// Arrange, Act
assertEquals(emptyList(), awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
- eventNotifierTunnelUiState.notify(
- TunnelState.Disconnecting(ActionAfterDisconnect.Block)
- )
+ tunnelState.emit(TunnelState.Disconnecting(ActionAfterDisconnect.Block))
// Assert
assertEquals(listOf(InAppNotification.TunnelStateBlocked), awaitItem())
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt
index fbc677b461..1630ed757f 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/VersionNotificationUseCaseTest.kt
@@ -4,7 +4,6 @@ import app.cash.turbine.test
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.mockk
-import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@@ -13,11 +12,7 @@ import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.ui.VersionInfo
-import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.util.appVersionCallbackFlow
+import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -26,39 +21,22 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class VersionNotificationUseCaseTest {
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
- private lateinit var mockAppVersionInfoCache: AppVersionInfoCache
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
+ private val mockAppVersionInfoRepository: AppVersionInfoRepository = mockk()
- private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
private val versionInfo =
MutableStateFlow(
- VersionInfo(
- currentVersion = null,
- upgradeVersion = null,
- isOutdated = false,
- isSupported = true
- )
+ VersionInfo(currentVersion = "", isSupported = true, suggestedUpgradeVersion = null)
)
private lateinit var versionNotificationUseCase: VersionNotificationUseCase
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
- mockkStatic(CACHE_EXTENSION_CLASS)
- mockAppVersionInfoCache =
- mockk<AppVersionInfoCache>().apply {
- every { appVersionCallbackFlow() } returns versionInfo
- }
-
- every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
- every { mockServiceConnectionContainer.appVersionInfoCache } returns mockAppVersionInfoCache
- every { mockAppVersionInfoCache.onUpdate = any() } answers {}
+ every { mockAppVersionInfoRepository.versionInfo() } returns versionInfo
versionNotificationUseCase =
VersionNotificationUseCase(
- serviceConnectionManager = mockServiceConnectionManager,
+ appVersionInfoRepository = mockAppVersionInfoRepository,
isVersionInfoNotificationEnabled = true
)
}
@@ -80,9 +58,11 @@ class VersionNotificationUseCaseTest {
versionNotificationUseCase.notifications().test {
// Arrange, Act
val upgradeVersionInfo =
- VersionInfo("1.0", "1.1", isOutdated = true, isSupported = true)
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ VersionInfo(
+ currentVersion = "1.0",
+ isSupported = true,
+ suggestedUpgradeVersion = "1.1"
+ )
awaitItem()
versionInfo.value = upgradeVersionInfo
@@ -100,9 +80,11 @@ class VersionNotificationUseCaseTest {
versionNotificationUseCase.notifications().test {
// Arrange, Act
val upgradeVersionInfo =
- VersionInfo("1.0", "", isOutdated = false, isSupported = false)
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ VersionInfo(
+ currentVersion = "1.0",
+ isSupported = false,
+ suggestedUpgradeVersion = null
+ )
awaitItem()
versionInfo.value = upgradeVersionInfo
@@ -113,8 +95,4 @@ class VersionNotificationUseCaseTest {
)
}
}
-
- companion object {
- private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
- }
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
index 61758c2d1d..362fc457f5 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModelTest.kt
@@ -8,7 +8,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
-import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
@@ -16,20 +15,18 @@ import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.DeviceState
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
-import net.mullvad.mullvadvpn.model.AccountAndDevice
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.Device
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
+import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -39,43 +36,35 @@ import org.junit.jupiter.api.extension.ExtendWith
class AccountViewModelTest {
private val mockAccountRepository: AccountRepository = mockk(relaxUnitFun = true)
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
- private val mockDeviceRepository: DeviceRepository = mockk()
- private val mockAuthTokenCache: AuthTokenCache = mockk()
+ private val mockDeviceRepository: DeviceRepository = mockk(relaxUnitFun = true)
private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
- private val deviceState: MutableStateFlow<DeviceState> = MutableStateFlow(DeviceState.Initial)
+ private val deviceState: MutableStateFlow<DeviceState?> = MutableStateFlow(null)
private val paymentAvailability = MutableStateFlow<PaymentAvailability?>(null)
private val purchaseResult = MutableStateFlow<PurchaseResult?>(null)
- private val accountExpiryState = MutableStateFlow(AccountExpiry.Missing)
+ private val accountExpiryState = MutableStateFlow(null)
- private val dummyAccountAndDevice: AccountAndDevice =
- AccountAndDevice(
+ private val dummyDevice =
+ Device(id = DeviceId.fromString(UUID), name = "fake_name", creationDate = DateTime.now())
+ private val dummyAccountToken: AccountToken =
+ AccountToken(
DUMMY_DEVICE_NAME,
- Device(
- id = "fake_id",
- name = "fake_name",
- pubkey = byteArrayOf(),
- created = "mock_date"
- )
)
private lateinit var viewModel: AccountViewModel
@BeforeEach
fun setup() {
- mockkStatic(CACHE_EXTENSION_CLASS)
mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS)
- every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache
+ every { mockAccountRepository.accountData } returns accountExpiryState
every { mockDeviceRepository.deviceState } returns deviceState
- every { mockAccountRepository.accountExpiryState } returns accountExpiryState
coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResult
coEvery { mockPaymentUseCase.paymentAvailability } returns paymentAvailability
+ coEvery { mockAccountRepository.getAccountData() } returns null
viewModel =
AccountViewModel(
accountRepository = mockAccountRepository,
- serviceConnectionManager = mockServiceConnectionManager,
deviceRepository = mockDeviceRepository,
paymentUseCase = mockPaymentUseCase,
isPlayBuild = false
@@ -92,7 +81,8 @@ class AccountViewModelTest {
// Act, Assert
viewModel.uiState.test {
awaitItem() // Default state
- deviceState.value = DeviceState.LoggedIn(accountAndDevice = dummyAccountAndDevice)
+ deviceState.value =
+ DeviceState.LoggedIn(accountToken = dummyAccountToken, device = dummyDevice)
val result = awaitItem()
assertEquals(DUMMY_DEVICE_NAME, result.accountNumber)
}
@@ -104,7 +94,7 @@ class AccountViewModelTest {
viewModel.onLogoutClick()
// Assert
- verify { mockAccountRepository.logout() }
+ coVerify { mockAccountRepository.logout() }
}
@Test
@@ -184,7 +174,7 @@ class AccountViewModelTest {
viewModel.onClosePurchaseResultDialog(success = true)
// Assert
- verify { mockAccountRepository.fetchAccountExpiry() }
+ coVerify { mockAccountRepository.getAccountData() }
}
@Test
@@ -221,9 +211,9 @@ class AccountViewModelTest {
}
companion object {
- private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
private const val PURCHASE_RESULT_EXTENSIONS_CLASS =
"net.mullvad.mullvadvpn.util.PurchaseResultExtensionsKt"
private const val DUMMY_DEVICE_NAME = "fake_name"
+ private const val UUID = "12345678-1234-5678-1234-567812345678"
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt
index 46126f5ad8..7888f02a4d 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ChangelogViewModelTest.kt
@@ -6,12 +6,11 @@ import io.mockk.Runs
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
-import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.model.BuildVersion
import net.mullvad.mullvadvpn.repository.ChangelogRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -25,10 +24,11 @@ class ChangelogViewModelTest {
private lateinit var viewModel: ChangelogViewModel
+ private val buildVersion = BuildVersion("1.0", 10)
+
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
- mockkStatic(EVENT_NOTIFIER_EXTENSION_CLASS)
every { mockedChangelogRepository.setVersionCodeOfMostRecentChangelogShowed(any()) } just
Runs
}
@@ -42,8 +42,8 @@ class ChangelogViewModelTest {
fun `given up to date version code uiSideEffect should not emit`() = runTest {
// Arrange
every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns
- buildVersionCode
- viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersionCode, false)
+ buildVersion.code
+ viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersion, false)
// If we have the most up to date version code, we should not show the changelog dialog
viewModel.uiSideEffect.test { expectNoEvents() }
@@ -58,13 +58,10 @@ class ChangelogViewModelTest {
version
every { mockedChangelogRepository.getLastVersionChanges() } returns changes
- viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersionCode, false)
+ viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersion, false)
// Given a new version with a change log we should return it
viewModel.uiSideEffect.test {
- assertEquals(
- awaitItem(),
- Changelog(version = BuildConfig.VERSION_NAME, changes = changes)
- )
+ assertEquals(awaitItem(), Changelog(version = buildVersion.name, changes = changes))
}
}
@@ -74,14 +71,8 @@ class ChangelogViewModelTest {
every { mockedChangelogRepository.getVersionCodeOfMostRecentChangelogShowed() } returns -1
every { mockedChangelogRepository.getLastVersionChanges() } returns emptyList()
- viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersionCode, false)
+ viewModel = ChangelogViewModel(mockedChangelogRepository, buildVersion, false)
// Given a new version with a change log we should not return it
viewModel.uiSideEffect.test { expectNoEvents() }
}
-
- companion object {
- private const val EVENT_NOTIFIER_EXTENSION_CLASS =
- "net.mullvad.talpid.util.EventNotifierExtensionsKt"
- private const val buildVersionCode = 10
- }
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
index 7e207a15a4..2de7724c69 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
@@ -2,12 +2,13 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
-import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNull
@@ -15,33 +16,31 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.GeoIpLocation
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
+import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
import net.mullvad.mullvadvpn.repository.InAppNotification
import net.mullvad.mullvadvpn.repository.InAppNotificationController
-import net.mullvad.mullvadvpn.ui.VersionInfo
-import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
+import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
-import net.mullvad.mullvadvpn.util.appVersionCallbackFlow
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.util.EventNotifier
+import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
+import net.mullvad.mullvadvpn.util.toInAddress
+import net.mullvad.mullvadvpn.util.toOutAddress
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -54,23 +53,12 @@ class ConnectViewModelTest {
private lateinit var viewModel: ConnectViewModel
private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
- private val versionInfo =
- MutableStateFlow(
- VersionInfo(
- currentVersion = null,
- upgradeVersion = null,
- isOutdated = false,
- isSupported = true
- )
- )
- private val accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
- private val deviceState = MutableStateFlow<DeviceState>(DeviceState.Initial)
+ MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Unbound)
+ private val accountExpiryState = MutableStateFlow<AccountData?>(null)
+ private val device = MutableStateFlow<DeviceState?>(null)
private val notifications = MutableStateFlow<List<InAppNotification>>(emptyList())
// Service connections
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
- private lateinit var mockAppVersionInfoCache: AppVersionInfoCache
private val mockConnectionProxy: ConnectionProxy = mockk()
private val mockLocation: GeoIpLocation = mockk(relaxed = true)
@@ -83,66 +71,62 @@ class ConnectViewModelTest {
// In App Notifications
private val mockInAppNotificationController: InAppNotificationController = mockk()
- // Relay list use case
- private val mockRelayListUseCase: RelayListUseCase = mockk()
+ // Select location use case
+ private val mockSelectedLocationTitleUseCase: SelectedLocationTitleUseCase = mockk()
// Payment use case
private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
- // Event notifiers
- private val eventNotifierTunnelUiState = EventNotifier<TunnelState>(TunnelState.Disconnected())
- private val eventNotifierTunnelRealState =
- EventNotifier<TunnelState>(TunnelState.Disconnected())
-
// Flows
- private val selectedRelayItemFlow = MutableStateFlow<RelayItem?>(null)
+ private val tunnelState = MutableStateFlow<TunnelState>(TunnelState.Disconnected())
+ private val selectedRelayItemFlow = MutableStateFlow<String?>(null)
// Out Of Time Use Case
private val outOfTimeUseCase: OutOfTimeUseCase = mockk()
private val outOfTimeViewFlow = MutableStateFlow(false)
+ // Last known location
+ private val mockLastKnownLocationUseCase: LastKnownLocationUseCase = mockk()
+
+ // VpnPermissionRepository
+ private val mockVpnPermissionRepository: VpnPermissionRepository = mockk(relaxed = true)
+
@BeforeEach
fun setup() {
- mockkStatic(CACHE_EXTENSION_CLASS)
- mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS)
-
- mockAppVersionInfoCache =
- mockk<AppVersionInfoCache>().apply {
- every { appVersionCallbackFlow() } returns versionInfo
- }
+ mockkStatic(TUNNEL_ENDPOINT_EXTENSIONS)
+ mockkStatic(GEO_IP_LOCATIONS_EXTENSIONS)
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
- every { mockServiceConnectionContainer.appVersionInfoCache } returns mockAppVersionInfoCache
- every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy
- every { mockAccountRepository.accountExpiryState } returns accountExpiryState
+ every { mockAccountRepository.accountData } returns accountExpiryState
- every { mockDeviceRepository.deviceState } returns deviceState
+ every { mockDeviceRepository.deviceState } returns device
every { mockInAppNotificationController.notifications } returns notifications
- every { mockConnectionProxy.onUiStateChange } returns eventNotifierTunnelUiState
- every { mockConnectionProxy.onStateChange } returns eventNotifierTunnelRealState
+ every { mockConnectionProxy.tunnelState } returns tunnelState
- every { mockLocation.country } returns "dummy country"
+ every { mockLastKnownLocationUseCase.lastKnownDisconnectedLocation } returns flowOf(null)
- // Listeners
- every { mockAppVersionInfoCache.onUpdate = any() } answers {}
+ every { mockLocation.country } returns "dummy country"
// Flows
- every { mockRelayListUseCase.selectedRelayItem() } returns selectedRelayItemFlow
+ every { mockSelectedLocationTitleUseCase.selectedLocationTitle() } returns
+ selectedRelayItemFlow
every { outOfTimeUseCase.isOutOfTime } returns outOfTimeViewFlow
viewModel =
ConnectViewModel(
- serviceConnectionManager = mockServiceConnectionManager,
accountRepository = mockAccountRepository,
deviceRepository = mockDeviceRepository,
inAppNotificationController = mockInAppNotificationController,
- relayListUseCase = mockRelayListUseCase,
newDeviceNotificationUseCase = mockk(),
outOfTimeUseCase = outOfTimeUseCase,
paymentUseCase = mockPaymentUseCase,
+ selectedLocationTitleUseCase = mockSelectedLocationTitleUseCase,
+ connectionProxy = mockConnectionProxy,
+ lastKnownLocationUseCase = mockLastKnownLocationUseCase,
+ vpnPermissionRepository = mockVpnPermissionRepository,
isPlayBuild = false
)
}
@@ -164,46 +148,41 @@ class ConnectViewModelTest {
viewModel.uiState.test {
assertEquals(ConnectUiState.INITIAL, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
- eventNotifierTunnelRealState.notify(tunnelRealStateTestItem)
+ tunnelState.emit(tunnelRealStateTestItem)
val result = awaitItem()
- assertEquals(tunnelRealStateTestItem, result.tunnelRealState)
+ assertEquals(tunnelRealStateTestItem, result.tunnelState)
}
}
@Test
- fun `given change in tunnelUiState uiState should emit new tunnelUiState`() = runTest {
- val tunnelUiStateTestItem = TunnelState.Connected(mockk(), mockk())
+ fun `given change in tunnelState uiState should emit new tunnelState`() = runTest {
+ // Arrange
+ val tunnelEndpoint: TunnelEndpoint = mockk()
+ val location: GeoIpLocation = mockk()
+ val tunnelStateTestItem = TunnelState.Connected(tunnelEndpoint, location)
+ every { tunnelEndpoint.toInAddress() } returns mockk(relaxed = true)
+ every { location.toOutAddress() } returns "1.1.1.1"
+ every { location.hostname } returns "hostname"
+ // Act, Assert
viewModel.uiState.test {
assertEquals(ConnectUiState.INITIAL, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
- eventNotifierTunnelUiState.notify(tunnelUiStateTestItem)
+ tunnelState.emit(tunnelStateTestItem)
val result = awaitItem()
- assertEquals(tunnelUiStateTestItem, result.tunnelUiState)
+ assertEquals(tunnelStateTestItem, result.tunnelState)
}
}
@Test
fun `given RelayListUseCase returns new selectedRelayItem uiState should emit new selectedRelayItem`() =
runTest {
- val selectedRelayItem =
- RelayItem.Country(
- name = "Name",
- code = "Code",
- expanded = false,
- cities = emptyList()
- )
- selectedRelayItemFlow.value = selectedRelayItem
+ val selectedRelayItemTitle = "Item"
+ selectedRelayItemFlow.value = selectedRelayItemTitle
viewModel.uiState.test {
assertEquals(ConnectUiState.INITIAL, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
val result = awaitItem()
- assertEquals(selectedRelayItem, result.selectedRelayItem)
+ assertEquals(selectedRelayItemTitle, result.selectedRelayItemTitle)
}
}
@@ -223,15 +202,13 @@ class ConnectViewModelTest {
// Act, Assert
viewModel.uiState.test {
assertEquals(ConnectUiState.INITIAL, awaitItem())
- eventNotifierTunnelRealState.notify(TunnelState.Disconnected(null))
+ tunnelState.emit(TunnelState.Disconnected(null))
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Start of with no location
assertNull(awaitItem().location)
// After updated we show latest
- eventNotifierTunnelRealState.notify(TunnelState.Disconnected(locationTestItem))
+ tunnelState.emit(TunnelState.Disconnected(locationTestItem))
assertEquals(locationTestItem, awaitItem().location)
}
}
@@ -245,8 +222,6 @@ class ConnectViewModelTest {
// Act, Assert
viewModel.uiState.test {
assertEquals(ConnectUiState.INITIAL, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
expectNoEvents()
val result = awaitItem()
assertEquals(locationTestItem, result.location)
@@ -255,34 +230,50 @@ class ConnectViewModelTest {
@Test
fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest {
- val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true)
- every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy
+ // Arrange
+ coEvery { mockConnectionProxy.disconnect() } returns true
+
+ // Act
viewModel.onDisconnectClick()
- verify { mockConnectionProxy.disconnect() }
+
+ // Assert
+ coVerify { mockConnectionProxy.disconnect() }
}
@Test
fun `onReconnectClick should invoke reconnect on ConnectionProxy`() = runTest {
- val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true)
- every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy
+ // Arrange
+ coEvery { mockConnectionProxy.reconnect() } returns true
+
+ // Act
viewModel.onReconnectClick()
- verify { mockConnectionProxy.reconnect() }
+
+ // Assert
+ coVerify { mockConnectionProxy.reconnect() }
}
@Test
fun `onConnectClick should invoke connect on ConnectionProxy`() = runTest {
- val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true)
- every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy
+ // Arrange
+ coEvery { mockConnectionProxy.connect() } returns true.right()
+
+ // Act
viewModel.onConnectClick()
- verify { mockConnectionProxy.connect() }
+
+ // Asser
+ coVerify { mockConnectionProxy.connect() }
}
@Test
fun `onCancelClick should invoke disconnect on ConnectionProxy`() = runTest {
- val mockConnectionProxy: ConnectionProxy = mockk(relaxed = true)
- every { mockServiceConnectionManager.connectionProxy() } returns mockConnectionProxy
+ // Arrange
+ coEvery { mockConnectionProxy.disconnect() } returns true
+
+ // Act
viewModel.onCancelClick()
- verify { mockConnectionProxy.disconnect() }
+
+ // Assert
+ coVerify { mockConnectionProxy.disconnect() }
}
@Test
@@ -292,15 +283,13 @@ class ConnectViewModelTest {
val mockErrorState: ErrorState = mockk()
val expectedConnectNotificationState =
InAppNotification.TunnelStateError(mockErrorState)
- val tunnelUiState = TunnelState.Error(mockErrorState)
+ val tunnelStateError = TunnelState.Error(mockErrorState)
notifications.value = listOf(expectedConnectNotificationState)
// Act, Assert
viewModel.uiState.test {
assertEquals(ConnectUiState.INITIAL, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
- eventNotifierTunnelUiState.notify(tunnelUiState)
+ tunnelState.emit(tunnelStateError)
val result = awaitItem()
assertEquals(expectedConnectNotificationState, result.inAppNotification)
}
@@ -310,10 +299,8 @@ class ConnectViewModelTest {
fun `onShowAccountClick call should result in uiSideEffect emitting OpenAccountManagementPageInBrowser`() =
runTest {
// Arrange
- val mockToken = "4444 5555 6666 7777"
- val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true)
- every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache
- coEvery { mockAuthTokenCache.fetchAuthToken() } returns mockToken
+ val mockToken = WebsiteAuthToken.fromString("154c4cc94810fddac78398662b7fa0c7")
+ coEvery { mockAccountRepository.getWebsiteAuthToken() } returns mockToken
// Act, Assert
viewModel.uiSideEffect.test {
@@ -332,8 +319,6 @@ class ConnectViewModelTest {
// Act
viewModel.uiState.test {
awaitItem()
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
outOfTimeViewFlow.value = true
awaitItem()
}
@@ -343,8 +328,9 @@ class ConnectViewModelTest {
}
companion object {
- private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
- private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS =
- "net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManagerExtensionsKt"
+ private const val TUNNEL_ENDPOINT_EXTENSIONS =
+ "net.mullvad.mullvadvpn.util.TunnelEndpointExtensionsKt"
+ private const val GEO_IP_LOCATIONS_EXTENSIONS =
+ "net.mullvad.mullvadvpn.util.GeoIpLocationExtensionsKt"
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt
index 7b14db3ffb..83675794f5 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt
@@ -1,17 +1,22 @@
package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlin.test.assertIs
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.communication.Created
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
-import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
@@ -25,13 +30,13 @@ class CreateCustomListDialogViewModelTest {
fun `when successfully creating a list with locations should emit return with result side effect`() =
runTest {
// Arrange
- val expectedResult: CustomListResult.Created = mockk()
+ val expectedResult: Created = mockk()
val customListName = "list"
- val viewModel = createViewModelWithLocationCode("AB")
+ val viewModel = createViewModelWithLocationCode(GeoLocationId.Country("AB"))
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Create>())
- } returns Result.success(expectedResult)
- every { expectedResult.locationName } returns "locationName"
+ } returns expectedResult.right()
+ every { expectedResult.locationNames } returns listOf("locationName")
// Act, Assert
viewModel.uiSideEffect.test {
@@ -46,19 +51,23 @@ class CreateCustomListDialogViewModelTest {
fun `when successfully creating a list without locations should emit with navigate to location screen`() =
runTest {
// Arrange
- val expectedResult: CustomListResult.Created = mockk()
- val customListName = "list"
- val createdId = "1"
- val viewModel = createViewModelWithLocationCode("")
+ val customListName = CustomListName.fromString("list")
+ val createdId = CustomListId("1")
+ val expectedResult =
+ Created(
+ id = createdId,
+ name = customListName,
+ locationNames = emptyList(),
+ undo = CustomListAction.Delete(createdId)
+ )
+ val viewModel = createViewModelWithLocationCode(GeoLocationId.Country("AB"))
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Create>())
- } returns Result.success(expectedResult)
- every { expectedResult.locationName } returns null
- every { expectedResult.id } returns createdId
+ } returns expectedResult.right()
// Act, Assert
viewModel.uiSideEffect.test {
- viewModel.createCustomList(customListName)
+ viewModel.createCustomList(customListName.value)
val sideEffect = awaitItem()
assertIs<CreateCustomListDialogSideEffect.NavigateToCustomListLocationsScreen>(
sideEffect
@@ -70,12 +79,12 @@ class CreateCustomListDialogViewModelTest {
@Test
fun `when failing to creating a list should update ui state with error`() = runTest {
// Arrange
- val expectedError = CustomListsError.CustomListExists
+ val expectedError = CreateWithLocationsError.Create(CustomListAlreadyExists)
val customListName = "list"
- val viewModel = createViewModelWithLocationCode("")
+ val viewModel = createViewModelWithLocationCode(GeoLocationId.Country("AB"))
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Create>())
- } returns Result.failure(CustomListsException(expectedError))
+ } returns expectedError.left()
// Act, Assert
viewModel.uiState.test {
@@ -89,12 +98,12 @@ class CreateCustomListDialogViewModelTest {
fun `given error state when calling clear error then should update to state without error`() =
runTest {
// Arrange
- val expectedError = CustomListsError.CustomListExists
+ val expectedError = CreateWithLocationsError.Create(CustomListAlreadyExists)
val customListName = "list"
- val viewModel = createViewModelWithLocationCode("")
+ val viewModel = createViewModelWithLocationCode(GeoLocationId.Country("AB"))
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Create>())
- } returns Result.failure(CustomListsException(expectedError))
+ } returns expectedError.left()
// Act, Assert
viewModel.uiState.test {
@@ -106,7 +115,7 @@ class CreateCustomListDialogViewModelTest {
}
}
- private fun createViewModelWithLocationCode(locationCode: String) =
+ private fun createViewModelWithLocationCode(locationCode: GeoLocationId) =
CreateCustomListDialogViewModel(
locationCode = locationCode,
customListActionUseCase = mockCustomListActionUseCase
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
index d21789d36f..321e2d53b5 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@@ -8,15 +9,21 @@ import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.relaylist.RelayItem
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.ProviderId
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.relaylist.descendants
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -24,23 +31,31 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class CustomListLocationsViewModelTest {
- private val mockRelayListUseCase: RelayListUseCase = mockk()
+ private val mockRelayListRepository: RelayListRepository = mockk()
private val mockCustomListUseCase: CustomListActionUseCase = mockk()
+ private val mockCustomListRelayItemsUseCase: CustomListRelayItemsUseCase = mockk()
- private val relayListFlow = MutableStateFlow<List<RelayItem.Country>>(emptyList())
- private val customListFlow = MutableStateFlow<List<RelayItem.CustomList>>(emptyList())
+ private val relayListFlow = MutableStateFlow<List<RelayItem.Location.Country>>(emptyList())
+ private val selectedLocationsFlow = MutableStateFlow<List<RelayItem.Location>>(emptyList())
@BeforeEach
fun setup() {
- every { mockRelayListUseCase.fullRelayList() } returns relayListFlow
- every { mockRelayListUseCase.customLists() } returns customListFlow
+ every { mockRelayListRepository.relayList } returns relayListFlow
+ every { mockCustomListRelayItemsUseCase.getRelayItemLocationsForCustomList(any()) } returns
+ selectedLocationsFlow
}
@Test
- fun `given new list false state should return new list false`() = runTest {
+ fun `given new list false state uiState newList should be false`() = runTest {
// Arrange
val newList = false
- val viewModel = createViewModel("id", newList)
+ val customList =
+ CustomList(
+ id = CustomListId("id"),
+ name = CustomListName.fromString("name"),
+ locations = emptyList()
+ )
+ val viewModel = createViewModel(customListId = customList.id, newList = newList)
// Act, Assert
viewModel.uiState.test { assertEquals(newList, awaitItem().newList) }
@@ -51,14 +66,7 @@ class CustomListLocationsViewModelTest {
runTest {
// Arrange
val expectedList = DUMMY_COUNTRIES
- val customListId = "id"
- val customListName = "name"
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns emptyList()
- }
- customListFlow.value = listOf(customList)
+ val customListId = CustomListId("id")
val expectedState =
CustomListLocationsUiState.Content.Data(
newList = true,
@@ -75,14 +83,7 @@ class CustomListLocationsViewModelTest {
fun `when selecting parent should select children`() = runTest {
// Arrange
val expectedList = DUMMY_COUNTRIES
- val customListId = "id"
- val customListName = "name"
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns emptyList()
- }
- customListFlow.value = listOf(customList)
+ val customListId = CustomListId("id")
val expectedSelection =
(DUMMY_COUNTRIES + DUMMY_COUNTRIES.flatMap { it.descendants() }).toSet()
val viewModel = createViewModel(customListId, true)
@@ -108,17 +109,11 @@ class CustomListLocationsViewModelTest {
val expectedList = DUMMY_COUNTRIES
val initialSelection =
(DUMMY_COUNTRIES + DUMMY_COUNTRIES.flatMap { it.descendants() }).toSet()
- val customListId = "id"
- val customListName = "name"
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns initialSelection.toList()
- }
- customListFlow.value = listOf(customList)
+ val customListId = CustomListId("id")
val expectedSelection = emptySet<RelayItem>()
- val viewModel = createViewModel(customListId, true)
relayListFlow.value = expectedList
+ selectedLocationsFlow.value = initialSelection.toList()
+ val viewModel = createViewModel(customListId, true)
// Act, Assert
viewModel.uiState.test {
@@ -140,17 +135,11 @@ class CustomListLocationsViewModelTest {
val expectedList = DUMMY_COUNTRIES
val initialSelection =
(DUMMY_COUNTRIES + DUMMY_COUNTRIES.flatMap { it.descendants() }).toSet()
- val customListId = "id"
- val customListName = "name"
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns initialSelection.toList()
- }
- customListFlow.value = listOf(customList)
+ val customListId = CustomListId("id")
val expectedSelection = emptySet<RelayItem>()
- val viewModel = createViewModel(customListId, true)
relayListFlow.value = expectedList
+ selectedLocationsFlow.value = initialSelection.toList()
+ val viewModel = createViewModel(customListId, true)
// Act, Assert
viewModel.uiState.test {
@@ -170,14 +159,7 @@ class CustomListLocationsViewModelTest {
fun `when selecting child should not select parent`() = runTest {
// Arrange
val expectedList = DUMMY_COUNTRIES
- val customListId = "id"
- val customListName = "name"
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns emptyList()
- }
- customListFlow.value = listOf(customList)
+ val customListId = CustomListId("id")
val expectedSelection = DUMMY_COUNTRIES[0].cities[0].relays.toSet()
val viewModel = createViewModel(customListId, true)
relayListFlow.value = expectedList
@@ -200,19 +182,12 @@ class CustomListLocationsViewModelTest {
fun `given new list true when saving successfully should emit close screen side effect`() =
runTest {
// Arrange
- val customListId = "1"
- val customListName = "name"
+ val customListId = CustomListId("1")
val newList = true
- val expectedResult: CustomListResult.LocationsChanged = mockk()
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns DUMMY_COUNTRIES
- }
- customListFlow.value = listOf(customList)
+ val expectedResult: LocationsChanged = mockk()
coEvery {
mockCustomListUseCase.performAction(any<CustomListAction.UpdateLocations>())
- } returns Result.success(expectedResult)
+ } returns expectedResult.right()
val viewModel = createViewModel(customListId, newList)
// Act, Assert
@@ -227,19 +202,12 @@ class CustomListLocationsViewModelTest {
fun `given new list false when saving successfully should emit return with result side effect`() =
runTest {
// Arrange
- val customListId = "1"
- val customListName = "name"
+ val customListId = CustomListId("1")
val newList = false
- val expectedResult: CustomListResult.LocationsChanged = mockk()
- val customList: RelayItem.CustomList = mockk {
- every { id } returns customListId
- every { name } returns customListName
- every { locations } returns DUMMY_COUNTRIES
- }
- customListFlow.value = listOf(customList)
+ val expectedResult: LocationsChanged = mockk()
coEvery {
mockCustomListUseCase.performAction(any<CustomListAction.UpdateLocations>())
- } returns Result.success(expectedResult)
+ } returns expectedResult.right()
val viewModel = createViewModel(customListId, newList)
// Act, Assert
@@ -251,42 +219,49 @@ class CustomListLocationsViewModelTest {
}
}
- private fun createViewModel(customListId: String, newList: Boolean) =
- CustomListLocationsViewModel(
+ private fun createViewModel(
+ customListId: CustomListId,
+ newList: Boolean
+ ): CustomListLocationsViewModel {
+ return CustomListLocationsViewModel(
customListId = customListId,
newList = newList,
- relayListUseCase = mockRelayListUseCase,
+ relayListRepository = mockRelayListRepository,
+ customListRelayItemsUseCase = mockCustomListRelayItemsUseCase,
customListActionUseCase = mockCustomListUseCase
)
+ }
companion object {
private val DUMMY_COUNTRIES =
listOf(
- RelayItem.Country(
+ RelayItem.Location.Country(
name = "Sweden",
- code = "SE",
+ id = GeoLocationId.Country("SE"),
expanded = false,
cities =
listOf(
- RelayItem.City(
+ RelayItem.Location.City(
name = "Gothenburg",
- code = "GBG",
expanded = false,
- location = GeographicLocationConstraint.City("SE", "GBG"),
+ id = GeoLocationId.City(GeoLocationId.Country("SE"), "GBG"),
relays =
listOf(
- RelayItem.Relay(
- name = "gbg-1",
- locationName = "GBG gbg-1",
- active = true,
- location =
- GeographicLocationConstraint.Hostname(
- "SE",
- "GBG",
+ RelayItem.Location.Relay(
+ id =
+ GeoLocationId.Hostname(
+ GeoLocationId.City(
+ GeoLocationId.Country("SE"),
+ "GBG"
+ ),
"gbg-1"
),
- providerName = "Provider",
- ownership = Ownership.MullvadOwned
+ active = true,
+ provider =
+ Provider(
+ ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
)
)
)
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModelTest.kt
index 612ae38a3a..ed615fe0af 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListsViewModelTest.kt
@@ -4,13 +4,13 @@ import app.cash.turbine.test
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.state.CustomListsUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -18,15 +18,15 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class CustomListsViewModelTest {
- private val mockRelayListUseCase: RelayListUseCase = mockk(relaxed = true)
+ private val mockCustomListsRepository: CustomListsRepository = mockk(relaxed = true)
private val mockCustomListsActionUseCase: CustomListActionUseCase = mockk(relaxed = true)
@Test
fun `given custom list from relay list use case should be in state`() = runTest {
// Arrange
- val customLists: List<RelayItem.CustomList> = mockk()
+ val customLists: List<CustomList> = mockk()
val expectedState = CustomListsUiState.Content(customLists)
- every { mockRelayListUseCase.customLists() } returns flowOf(customLists)
+ every { mockCustomListsRepository.customLists } returns MutableStateFlow(customLists)
val viewModel = createViewModel()
// Act, Assert
@@ -48,7 +48,7 @@ class CustomListsViewModelTest {
private fun createViewModel() =
CustomListsViewModel(
- relayListUseCase = mockRelayListUseCase,
+ customListsRepository = mockCustomListsRepository,
customListActionUseCase = mockCustomListsActionUseCase
)
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt
index 9f7f3f1f0b..6356719c42 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt
@@ -1,13 +1,15 @@
package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.mockk
import kotlin.test.assertIs
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Deleted
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -20,11 +22,11 @@ class DeleteCustomListConfirmationViewModelTest {
@Test
fun `when successfully deleting a list should emit return with result side effect`() = runTest {
// Arrange
- val expectedResult: CustomListResult.Deleted = mockk()
+ val expectedResult: Deleted = mockk()
val viewModel = createViewModel()
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Delete>())
- } returns Result.success(expectedResult)
+ } returns expectedResult.right()
// Act, Assert
viewModel.uiSideEffect.test {
@@ -37,7 +39,7 @@ class DeleteCustomListConfirmationViewModelTest {
private fun createViewModel() =
DeleteCustomListConfirmationViewModel(
- customListId = "1",
+ customListId = CustomListId("1"),
customListActionUseCase = mockCustomListActionUseCase
)
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt
index 11244e9df4..b63f59b302 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModelTest.kt
@@ -3,27 +3,22 @@ package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
import io.mockk.MockKAnnotations
import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
-import io.mockk.mockkStatic
import io.mockk.unmockkAll
-import io.mockk.verify
-import io.mockk.verifyOrder
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.DeviceRevokedUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.talpid.util.EventNotifier
-import net.mullvad.talpid.util.callbackFlowFromSubscription
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
@@ -35,23 +30,21 @@ class DeviceRevokedViewModelTest {
@MockK private lateinit var mockedAccountRepository: AccountRepository
- @MockK private lateinit var mockedServiceConnectionManager: ServiceConnectionManager
-
- private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
+ @MockK private lateinit var mockConnectionProxy: ConnectionProxy
private lateinit var viewModel: DeviceRevokedViewModel
+ private val tunnelStateFlow = MutableSharedFlow<TunnelState>()
+
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
- mockkStatic(EVENT_NOTIFIER_EXTENSION_CLASS)
- every { mockedServiceConnectionManager.connectionState } returns serviceConnectionState
+ every { mockConnectionProxy.tunnelState } returns tunnelStateFlow
viewModel =
DeviceRevokedViewModel(
- mockedServiceConnectionManager,
- mockedAccountRepository,
- UnconfinedTestDispatcher()
+ accountRepository = mockedAccountRepository,
+ connectionProxy = mockConnectionProxy,
+ dispatcher = UnconfinedTestDispatcher()
)
}
@@ -61,44 +54,15 @@ class DeviceRevokedViewModelTest {
}
@Test
- fun `when service connection is Disconnected then uiState should be UNKNOWN`() = runTest {
- // Arrange, Act, Assert
- viewModel.uiState.test {
- serviceConnectionState.value = ServiceConnectionState.Disconnected
- assertEquals(DeviceRevokedUiState.UNKNOWN, awaitItem())
- }
- }
-
- @Test
- fun `when service connection is ConnectedNotReady then uiState should be UNKNOWN`() = runTest {
- // Arrange, Act, Assert
- viewModel.uiState.test {
- serviceConnectionState.value = ServiceConnectionState.ConnectedNotReady(mockk())
- assertEquals(DeviceRevokedUiState.UNKNOWN, awaitItem())
- }
- }
-
- @Test
- fun `when service connection is ConnectedReady uiState should be SECURED`() = runTest {
+ fun `when tunnel state is secured uiState should be SECURED`() = runTest {
// Arrange
- val mockedContainer =
- mockk<ServiceConnectionContainer>().apply {
- val eventNotifierMock =
- mockk<EventNotifier<TunnelState>>().apply {
- every { callbackFlowFromSubscription(any()) } returns
- MutableStateFlow(TunnelState.Connected(mockk(), mockk()))
- }
- val mockedConnectionProxy =
- mockk<ConnectionProxy>().apply {
- every { onUiStateChange } returns eventNotifierMock
- }
- every { connectionProxy } returns mockedConnectionProxy
- }
+ val tunnelState: TunnelState = mockk()
+ every { tunnelState.isSecured() } returns true
// Act, Assert
viewModel.uiState.test {
assertEquals(DeviceRevokedUiState.UNKNOWN, awaitItem())
- serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockedContainer)
+ tunnelStateFlow.emit(tunnelState)
assertEquals(DeviceRevokedUiState.SECURED, awaitItem())
}
}
@@ -106,44 +70,29 @@ class DeviceRevokedViewModelTest {
@Test
fun `onGoToLoginClicked should invoke logout on AccountRepository`() {
// Arrange
- val mockedContainer =
- mockk<ServiceConnectionContainer>().also {
- every { it.connectionProxy.state } returns TunnelState.Disconnected()
- every { it.connectionProxy.disconnect() } just Runs
- every { mockedAccountRepository.logout() } just Runs
- }
- serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockedContainer)
+ coEvery { mockConnectionProxy.disconnect() } returns true
+ coEvery { mockedAccountRepository.logout() } just Runs
// Act
viewModel.onGoToLoginClicked()
// Assert
- verify { mockedAccountRepository.logout() }
+ coVerify { mockedAccountRepository.logout() }
}
@Test
fun `onGoToLoginClicked should invoke disconnect before logout when connected`() {
// Arrange
- val mockedContainer =
- mockk<ServiceConnectionContainer>().also {
- every { it.connectionProxy.state } returns TunnelState.Connected(mockk(), mockk())
- every { it.connectionProxy.disconnect() } just Runs
- every { mockedAccountRepository.logout() } just Runs
- }
- serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockedContainer)
+ coEvery { mockConnectionProxy.disconnect() } returns true
+ coEvery { mockedAccountRepository.logout() } just Runs
// Act
viewModel.onGoToLoginClicked()
// Assert
- verifyOrder {
- mockedContainer.connectionProxy.disconnect()
+ coVerifyOrder {
+ mockConnectionProxy.disconnect()
mockedAccountRepository.logout()
}
}
-
- companion object {
- private const val EVENT_NOTIFIER_EXTENSION_CLASS =
- "net.mullvad.talpid.util.EventNotifierExtensionsKt"
- }
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt
index e9592d0336..29afc8de0d 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt
@@ -1,16 +1,20 @@
package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.mockk
import kotlin.test.assertIs
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.Renamed
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.CustomListsError
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
-import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException
+import net.mullvad.mullvadvpn.usecase.customlists.RenameError
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
@@ -23,13 +27,13 @@ class EditCustomListNameDialogViewModelTest {
@Test
fun `when successfully renamed list should emit return with result side effect`() = runTest {
// Arrange
- val expectedResult: CustomListResult.Renamed = mockk()
- val customListId = "id"
+ val expectedResult: Renamed = mockk()
+ val customListId = CustomListId("id")
val customListName = "list"
val viewModel = createViewModel(customListId, customListName)
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Rename>())
- } returns Result.success(expectedResult)
+ } returns expectedResult.right()
// Act, Assert
viewModel.uiSideEffect.test {
@@ -41,15 +45,15 @@ class EditCustomListNameDialogViewModelTest {
}
@Test
- fun `when failing to creating a list should update ui state with error`() = runTest {
+ fun `when failing to rename a list should update ui state with error`() = runTest {
// Arrange
- val expectedError = CustomListsError.CustomListExists
- val customListId = "id2"
+ val customListId = CustomListId("id2")
val customListName = "list2"
+ val expectedError = RenameError(NameAlreadyExists(customListName))
val viewModel = createViewModel(customListId, customListName)
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Rename>())
- } returns Result.failure(CustomListsException(expectedError))
+ } returns expectedError.left()
// Act, Assert
viewModel.uiState.test {
@@ -63,13 +67,13 @@ class EditCustomListNameDialogViewModelTest {
fun `given error state when calling clear error then should update to state without error`() =
runTest {
// Arrange
- val expectedError = CustomListsError.CustomListExists
- val customListId = "id"
+ val customListId = CustomListId("id")
val customListName = "list"
+ val expectedError = RenameError(NameAlreadyExists(customListName))
val viewModel = createViewModel(customListId, customListName)
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.Rename>())
- } returns Result.failure(CustomListsException(expectedError))
+ } returns expectedError.left()
// Act, Assert
viewModel.uiState.test {
@@ -81,10 +85,10 @@ class EditCustomListNameDialogViewModelTest {
}
}
- private fun createViewModel(customListId: String, initialName: String) =
+ private fun createViewModel(customListId: CustomListId, initialName: String) =
EditCustomListNameDialogViewModel(
customListId = customListId,
- initialName = initialName,
+ initialName = CustomListName.fromString(initialName),
customListActionUseCase = mockCustomListActionUseCase
)
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt
index cbc5ff1c50..c3f233846a 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt
@@ -4,33 +4,33 @@ import app.cash.turbine.test
import io.mockk.every
import io.mockk.mockk
import kotlin.test.assertIs
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.EditCustomListState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.CustomListName
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class EditCustomListViewModelTest {
- private val mockRelayListUseCase: RelayListUseCase = mockk(relaxed = true)
+ private val mockCustomListsRepository: CustomListsRepository = mockk(relaxed = true)
@Test
fun `given a custom list id that does not exists should return not found ui state`() = runTest {
// Arrange
- val customListId = "2"
+ val customListId = CustomListId("2")
val customList =
- RelayItem.CustomList(
- id = "1",
- customListName = CustomListName.fromString("test"),
- expanded = false,
+ CustomList(
+ id = CustomListId("1"),
+ name = CustomListName.fromString("test"),
locations = emptyList()
)
- every { mockRelayListUseCase.customLists() } returns flowOf(listOf(customList))
+ every { mockCustomListsRepository.customLists } returns MutableStateFlow(listOf(customList))
val viewModel = createViewModel(customListId)
// Act, Assert
@@ -43,15 +43,14 @@ class EditCustomListViewModelTest {
@Test
fun `given a custom list id that exists should return content ui state`() = runTest {
// Arrange
- val customListId = "1"
+ val customListId = CustomListId("1")
val customList =
- RelayItem.CustomList(
+ CustomList(
id = customListId,
- customListName = CustomListName.fromString("test"),
- expanded = false,
+ name = CustomListName.fromString("test"),
locations = emptyList()
)
- every { mockRelayListUseCase.customLists() } returns flowOf(listOf(customList))
+ every { mockCustomListsRepository.customLists } returns MutableStateFlow(listOf(customList))
val viewModel = createViewModel(customListId)
// Act, Assert
@@ -64,9 +63,9 @@ class EditCustomListViewModelTest {
}
}
- private fun createViewModel(customListId: String) =
+ private fun createViewModel(customListId: CustomListId) =
EditCustomListViewModel(
customListId = customListId,
- relayListUseCase = mockRelayListUseCase
+ customListsRepository = mockCustomListsRepository
)
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt
index fda88bff79..5333a481be 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModelTest.kt
@@ -2,6 +2,8 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
+import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
@@ -15,11 +17,13 @@ import net.mullvad.mullvadvpn.compose.state.toConstraintProviders
import net.mullvad.mullvadvpn.compose.state.toOwnershipConstraint
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Providers
-import net.mullvad.mullvadvpn.relaylist.Provider
-import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.ProviderId
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
+import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -27,41 +31,52 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class FilterViewModelTest {
- private val mockRelayListFilterUseCase: RelayListFilterUseCase = mockk(relaxed = true)
+ private val mockAvailableProvidersUseCase: AvailableProvidersUseCase = mockk(relaxed = true)
+ private val mockRelayListFilterRepository: RelayListFilterRepository = mockk()
private lateinit var viewModel: FilterViewModel
private val selectedOwnership =
MutableStateFlow<Constraint<Ownership>>(Constraint.Only(Ownership.MullvadOwned))
private val dummyListOfAllProviders =
listOf(
- Provider("31173", true),
- Provider("100TB", false),
- Provider("Blix", true),
- Provider("Creanova", true),
- Provider("DataPacket", false),
- Provider("HostRoyale", false),
- Provider("hostuniversal", false),
- Provider("iRegister", false),
- Provider("M247", false),
- Provider("Makonix", false),
- Provider("PrivateLayer", false),
- Provider("ptisp", false),
- Provider("Qnax", false),
- Provider("Quadranet", false),
- Provider("techfutures", false),
- Provider("Tzulo", false),
- Provider("xtom", false)
+ Provider(ProviderId("31173"), Ownership.MullvadOwned),
+ Provider(ProviderId("100TB"), Ownership.Rented),
+ Provider(ProviderId("Blix"), Ownership.MullvadOwned),
+ Provider(ProviderId("Creanova"), Ownership.MullvadOwned),
+ Provider(ProviderId("DataPacket"), Ownership.Rented),
+ Provider(ProviderId("HostRoyale"), Ownership.Rented),
+ Provider(ProviderId("hostuniversal"), Ownership.Rented),
+ Provider(ProviderId("iRegister"), Ownership.Rented),
+ Provider(ProviderId("M247"), Ownership.Rented),
+ Provider(ProviderId("Makonix"), Ownership.Rented),
+ Provider(ProviderId("PrivateLayer"), Ownership.Rented),
+ Provider(ProviderId("ptisp"), Ownership.Rented),
+ Provider(ProviderId("Qnax"), Ownership.Rented),
+ Provider(ProviderId("Quadranet"), Ownership.Rented),
+ Provider(ProviderId("techfutures"), Ownership.Rented),
+ Provider(ProviderId("Tzulo"), Ownership.Rented),
+ Provider(ProviderId("xtom"), Ownership.Rented)
)
private val mockSelectedProviders: List<Provider> =
- listOf(Provider("31173", true), Provider("Blix", true), Provider("Creanova", true))
+ listOf(
+ Provider(ProviderId("31173"), Ownership.MullvadOwned),
+ Provider(ProviderId("Blix"), Ownership.MullvadOwned),
+ Provider(ProviderId("Creanova"), Ownership.MullvadOwned)
+ )
@BeforeEach
fun setup() {
- every { mockRelayListFilterUseCase.selectedOwnership() } returns selectedOwnership
- every { mockRelayListFilterUseCase.availableProviders() } returns
+ every { mockRelayListFilterRepository.selectedOwnership } returns selectedOwnership
+ every { mockAvailableProvidersUseCase.availableProviders() } returns
flowOf(dummyListOfAllProviders)
- every { mockRelayListFilterUseCase.selectedProviders() } returns
- flowOf(Constraint.Only(Providers(mockSelectedProviders.map { it.name }.toHashSet())))
- viewModel = FilterViewModel(mockRelayListFilterUseCase)
+ every { mockRelayListFilterRepository.selectedProviders } returns
+ MutableStateFlow(
+ Constraint.Only(Providers(mockSelectedProviders.map { it.providerId }.toSet()))
+ )
+ viewModel =
+ FilterViewModel(
+ availableProvidersUseCase = mockAvailableProvidersUseCase,
+ relayListFilterRepository = mockRelayListFilterRepository
+ )
}
@AfterEach
@@ -87,7 +102,7 @@ class FilterViewModelTest {
fun `setSelectionProvider should emit uiState where selectedProviders include the selected provider`() =
runTest {
// Arrange
- val mockSelectedProvidersList = Provider("ptisp", false)
+ val mockSelectedProvidersList = Provider(ProviderId("ptisp"), Ownership.Rented)
// Assert
viewModel.uiState.test {
assertLists(awaitItem().selectedProviders, mockSelectedProviders)
@@ -120,11 +135,19 @@ class FilterViewModelTest {
val mockOwnership = Ownership.MullvadOwned.toOwnershipConstraint()
val mockSelectedProviders =
mockSelectedProviders.toConstraintProviders(dummyListOfAllProviders)
+ coEvery {
+ mockRelayListFilterRepository.updateSelectedOwnershipAndProviderFilter(
+ mockOwnership,
+ mockSelectedProviders
+ )
+ } returns Unit.right()
+
// Act
viewModel.onApplyButtonClicked()
+
// Assert
coVerify {
- mockRelayListFilterUseCase.updateOwnershipAndProviderFilter(
+ mockRelayListFilterRepository.updateSelectedOwnershipAndProviderFilter(
mockOwnership,
mockSelectedProviders
)
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
index 3271fe57eb..d6eee6d941 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt
@@ -3,11 +3,15 @@ package net.mullvad.mullvadvpn.viewmodel
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import app.cash.turbine.turbineScope
+import arrow.core.left
+import arrow.core.right
import io.mockk.MockKAnnotations
import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.MockK
-import io.mockk.verify
+import io.mockk.mockk
+import kotlin.test.assertIs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -19,14 +23,10 @@ import net.mullvad.mullvadvpn.compose.state.LoginState.Loading
import net.mullvad.mullvadvpn.compose.state.LoginState.Success
import net.mullvad.mullvadvpn.compose.state.LoginUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.AccountCreationResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.AccountHistory
-import net.mullvad.mullvadvpn.model.AccountToken
-import net.mullvad.mullvadvpn.model.DeviceListEvent
-import net.mullvad.mullvadvpn.model.LoginResult
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.LoginAccountError
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.usecase.ConnectivityUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
import org.joda.time.DateTime
@@ -40,27 +40,23 @@ class LoginViewModelTest {
@MockK private lateinit var connectivityUseCase: ConnectivityUseCase
@MockK private lateinit var mockedAccountRepository: AccountRepository
- @MockK private lateinit var mockedDeviceRepository: DeviceRepository
@MockK private lateinit var mockedNewDeviceNotificationUseCase: NewDeviceNotificationUseCase
private lateinit var loginViewModel: LoginViewModel
- private val accountHistoryTestEvents = MutableStateFlow<AccountHistory>(AccountHistory.Missing)
@BeforeEach
fun setup() {
-
Dispatchers.setMain(UnconfinedTestDispatcher())
MockKAnnotations.init(this, relaxUnitFun = true)
every { connectivityUseCase.isInternetAvailable() } returns true
- every { mockedAccountRepository.accountHistory } returns accountHistoryTestEvents
every { mockedNewDeviceNotificationUseCase.newDeviceCreated() } returns Unit
+ coEvery { mockedAccountRepository.fetchAccountHistory() } returns null
loginViewModel =
LoginViewModel(
- mockedAccountRepository,
- mockedDeviceRepository,
- mockedNewDeviceNotificationUseCase,
- connectivityUseCase,
+ accountRepository = mockedAccountRepository,
+ newDeviceNotificationUseCase = mockedNewDeviceNotificationUseCase,
+ connectivityUseCase = connectivityUseCase,
UnconfinedTestDispatcher()
)
}
@@ -97,8 +93,7 @@ class LoginViewModelTest {
// Arrange
val uiStates = loginViewModel.uiState.testIn(backgroundScope)
val sideEffects = loginViewModel.uiSideEffect.testIn(backgroundScope)
- coEvery { mockedAccountRepository.createAccount() } returns
- AccountCreationResult.Success(DUMMY_ACCOUNT_TOKEN)
+ coEvery { mockedAccountRepository.createAccount() } returns DUMMY_ACCOUNT_TOKEN.right()
// Act, Assert
uiStates.skipDefaultItem()
@@ -114,13 +109,13 @@ class LoginViewModelTest {
// Arrange
val uiStates = loginViewModel.uiState.testIn(backgroundScope)
val sideEffects = loginViewModel.uiSideEffect.testIn(backgroundScope)
- coEvery { mockedAccountRepository.login(any()) } returns LoginResult.Ok
- coEvery { mockedAccountRepository.accountExpiryState } returns
- MutableStateFlow(AccountExpiry.Available(DateTime.now().plusDays(3)))
+ coEvery { mockedAccountRepository.login(any()) } returns Unit.right()
+ coEvery { mockedAccountRepository.accountData } returns
+ MutableStateFlow(AccountData(mockk(relaxed = true), DateTime.now().plusDays(3)))
// Act, Assert
uiStates.skipDefaultItem()
- loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN.value)
assertEquals(Loading.LoggingIn, uiStates.awaitItem().loginState)
assertEquals(Success, uiStates.awaitItem().loginState)
assertEquals(LoginUiSideEffect.NavigateToConnect, sideEffects.awaitItem())
@@ -131,11 +126,12 @@ class LoginViewModelTest {
fun `given invalid account when logging in then show invalid credentials`() = runTest {
loginViewModel.uiState.test {
// Arrange
- coEvery { mockedAccountRepository.login(any()) } returns LoginResult.InvalidAccount
+ coEvery { mockedAccountRepository.login(any()) } returns
+ LoginAccountError.InvalidAccount.left()
// Act, Assert
skipDefaultItem()
- loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN.value)
assertEquals(Loading.LoggingIn, awaitItem().loginState)
assertEquals(Idle(loginError = LoginError.InvalidCredentials), awaitItem().loginState)
}
@@ -148,23 +144,15 @@ class LoginViewModelTest {
// Arrange
val uiStates = loginViewModel.uiState.testIn(backgroundScope)
val sideEffects = loginViewModel.uiSideEffect.testIn(backgroundScope)
- coEvery {
- mockedDeviceRepository.refreshAndAwaitDeviceListWithTimeout(
- any(),
- any(),
- any(),
- any()
- )
- } returns DeviceListEvent.Available(DUMMY_ACCOUNT_TOKEN, listOf())
coEvery { mockedAccountRepository.login(any()) } returns
- LoginResult.MaxDevicesReached
+ LoginAccountError.MaxDevicesReached(DUMMY_ACCOUNT_TOKEN).left()
// Act, Assert
uiStates.skipDefaultItem()
- loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN.value)
assertEquals(Loading.LoggingIn, uiStates.awaitItem().loginState)
assertEquals(
- LoginUiSideEffect.TooManyDevices(AccountToken(DUMMY_ACCOUNT_TOKEN)),
+ LoginUiSideEffect.TooManyDevices(DUMMY_ACCOUNT_TOKEN),
sideEffects.awaitItem()
)
}
@@ -174,11 +162,12 @@ class LoginViewModelTest {
fun `given RpcError when logging in then show unknown error with message`() = runTest {
loginViewModel.uiState.test {
// Arrange
- coEvery { mockedAccountRepository.login(any()) } returns LoginResult.RpcError
+ coEvery { mockedAccountRepository.login(any()) } returns
+ LoginAccountError.RpcError.left()
// Act, Assert
skipDefaultItem()
- loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN.value)
assertEquals(Loading.LoggingIn, awaitItem().loginState)
assertEquals(
Idle(LoginError.Unknown(EXPECTED_RPC_ERROR_MESSAGE)),
@@ -188,31 +177,32 @@ class LoginViewModelTest {
}
@Test
- fun `given OtherError when logging in then show unknown error with message`() = runTest {
+ fun `given unknown error when logging in then show unknown error with message`() = runTest {
loginViewModel.uiState.test {
// Arrange
- coEvery { mockedAccountRepository.login(any()) } returns LoginResult.OtherError
+ coEvery { mockedAccountRepository.login(any()) } returns
+ LoginAccountError.Unknown(mockk()).left()
// Act, Assert
skipDefaultItem()
- loginViewModel.login(DUMMY_ACCOUNT_TOKEN)
+ loginViewModel.login(DUMMY_ACCOUNT_TOKEN.value)
assertEquals(Loading.LoggingIn, awaitItem().loginState)
- assertEquals(
- Idle(LoginError.Unknown(EXPECTED_OTHER_ERROR_MESSAGE)),
- awaitItem().loginState
- )
+ val loginState = awaitItem().loginState
+ assertIs<Idle>(loginState)
+ assertIs<LoginError.Unknown>(loginState.loginError)
}
}
@Test
fun `on new accountHistory emission uiState should include lastUsedAccount matching accountHistory`() =
runTest {
+ // Arrange
+ coEvery { mockedAccountRepository.fetchAccountHistory() } returns DUMMY_ACCOUNT_TOKEN
+
+ // Act, Assert
loginViewModel.uiState.test {
- // Act, Assert
- skipDefaultItem()
- accountHistoryTestEvents.emit(AccountHistory.Available(DUMMY_ACCOUNT_TOKEN))
assertEquals(
- LoginUiState.INITIAL.copy(lastUsedAccount = AccountToken(DUMMY_ACCOUNT_TOKEN)),
+ LoginUiState.INITIAL.copy(lastUsedAccount = DUMMY_ACCOUNT_TOKEN),
awaitItem()
)
}
@@ -222,7 +212,7 @@ class LoginViewModelTest {
fun `clearAccountHistory should invoke clearAccountHistory on AccountRepository`() = runTest {
// Act, Assert
loginViewModel.clearAccountHistory()
- verify { mockedAccountRepository.clearAccountHistory() }
+ coVerify { mockedAccountRepository.clearAccountHistory() }
}
private suspend fun <T> ReceiveTurbine<T>.skipDefaultItem() where T : Any? {
@@ -230,8 +220,7 @@ class LoginViewModelTest {
}
companion object {
- private const val DUMMY_ACCOUNT_TOKEN = "DUMMY"
+ private val DUMMY_ACCOUNT_TOKEN = AccountToken("DUMMY")
private const val EXPECTED_RPC_ERROR_MESSAGE = "RpcError"
- private const val EXPECTED_OTHER_ERROR_MESSAGE = "OtherError"
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
index e489c01d41..bd26effe82 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt
@@ -8,34 +8,28 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
-import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
-import net.mullvad.talpid.util.EventNotifier
import org.joda.time.DateTime
import org.joda.time.ReadableInstant
import org.junit.jupiter.api.AfterEach
@@ -47,23 +41,21 @@ import org.junit.jupiter.api.extension.ExtendWith
class OutOfTimeViewModelTest {
private val serviceConnectionStateFlow =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
- private val accountExpiryStateFlow = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
- private val deviceStateFlow = MutableStateFlow<DeviceState>(DeviceState.Initial)
+ MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Unbound)
+ private val accountExpiryStateFlow = MutableStateFlow<AccountData?>(null)
+ private val accountStateFlow = MutableStateFlow<DeviceState?>(null)
private val paymentAvailabilityFlow = MutableStateFlow<PaymentAvailability?>(null)
private val purchaseResultFlow = MutableStateFlow<PurchaseResult?>(null)
private val outOfTimeFlow = MutableStateFlow(true)
- // Service connections
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
+ // Connection Proxy
private val mockConnectionProxy: ConnectionProxy = mockk()
// Event notifiers
- private val eventNotifierTunnelRealState =
- EventNotifier<TunnelState>(TunnelState.Disconnected())
+ private val tunnelState = MutableStateFlow<TunnelState>(TunnelState.Disconnected())
private val mockAccountRepository: AccountRepository = mockk(relaxed = true)
- private val mockDeviceRepository: DeviceRepository = mockk()
+ private val mockDeviceRepository: DeviceRepository = mockk(relaxed = true)
private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
private val mockOutOfTimeUseCase: OutOfTimeUseCase = mockk(relaxed = true)
@@ -72,18 +64,15 @@ class OutOfTimeViewModelTest {
@BeforeEach
fun setup() {
- mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS)
mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS)
every { mockServiceConnectionManager.connectionState } returns serviceConnectionStateFlow
- every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy
+ every { mockConnectionProxy.tunnelState } returns tunnelState
- every { mockConnectionProxy.onStateChange } returns eventNotifierTunnelRealState
+ every { mockAccountRepository.accountData } returns accountExpiryStateFlow
- every { mockAccountRepository.accountExpiryState } returns accountExpiryStateFlow
-
- every { mockDeviceRepository.deviceState } returns deviceStateFlow
+ every { mockDeviceRepository.deviceState } returns accountStateFlow
coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResultFlow
@@ -94,10 +83,10 @@ class OutOfTimeViewModelTest {
viewModel =
OutOfTimeViewModel(
accountRepository = mockAccountRepository,
- serviceConnectionManager = mockServiceConnectionManager,
deviceRepository = mockDeviceRepository,
paymentUseCase = mockPaymentUseCase,
outOfTimeUseCase = mockOutOfTimeUseCase,
+ connectionProxy = mockConnectionProxy,
pollAccountExpiry = false,
isPlayBuild = false
)
@@ -112,10 +101,8 @@ class OutOfTimeViewModelTest {
@Test
fun `when clicking on site payment then open website account view`() = runTest {
// Arrange
- val mockToken = "4444 5555 6666 7777"
- val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true)
- every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache
- coEvery { mockAuthTokenCache.fetchAuthToken() } returns mockToken
+ val mockToken = WebsiteAuthToken.fromString("154c4cc94810fddac78398662b7fa0c7")
+ coEvery { mockAccountRepository.getWebsiteAuthToken() } returns mockToken
// Act, Assert
viewModel.uiSideEffect.test {
@@ -133,10 +120,9 @@ class OutOfTimeViewModelTest {
// Act, Assert
viewModel.uiState.test {
- assertEquals(OutOfTimeUiState(deviceName = ""), awaitItem())
- eventNotifierTunnelRealState.notify(tunnelRealStateTestItem)
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ // Default item
+ awaitItem()
+ tunnelState.emit(tunnelRealStateTestItem)
val result = awaitItem()
assertEquals(tunnelRealStateTestItem, result.tunnelState)
}
@@ -160,14 +146,13 @@ class OutOfTimeViewModelTest {
@Test
fun `onDisconnectClick should invoke disconnect on ConnectionProxy`() = runTest {
// Arrange
- val mockProxy: ConnectionProxy = mockk(relaxed = true)
- every { mockServiceConnectionManager.connectionProxy() } returns mockProxy
+ coEvery { mockConnectionProxy.disconnect() } returns true
// Act
viewModel.onDisconnectClick()
// Assert
- verify { mockProxy.disconnect() }
+ coVerify { mockConnectionProxy.disconnect() }
}
@Test
@@ -176,8 +161,6 @@ class OutOfTimeViewModelTest {
// Arrange
val productsUnavailable = PaymentAvailability.ProductsUnavailable
paymentAvailabilityFlow.value = productsUnavailable
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -192,8 +175,6 @@ class OutOfTimeViewModelTest {
// Arrange
val paymentAvailabilityError = PaymentAvailability.Error.Other(mockk())
paymentAvailabilityFlow.value = paymentAvailabilityError
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -208,8 +189,6 @@ class OutOfTimeViewModelTest {
// Arrange
val paymentAvailabilityError = PaymentAvailability.Error.BillingUnavailable
paymentAvailabilityFlow.value = paymentAvailabilityError
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -226,8 +205,6 @@ class OutOfTimeViewModelTest {
val expectedProductList = listOf(mockProduct)
val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct))
paymentAvailabilityFlow.value = productsAvailable
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -238,14 +215,12 @@ class OutOfTimeViewModelTest {
}
@Test
- fun `onClosePurchaseResultDialog with success should invoke fetchAccountExpiry on AccountRepository`() {
- // Arrange
-
+ fun `onClosePurchaseResultDialog with success should invoke getAccountData on AccountRepository`() {
// Act
viewModel.onClosePurchaseResultDialog(success = true)
// Assert
- verify { mockAccountRepository.fetchAccountExpiry() }
+ coVerify { mockAccountRepository.getAccountData() }
}
@Test
@@ -282,8 +257,6 @@ class OutOfTimeViewModelTest {
}
companion object {
- private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS =
- "net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManagerExtensionsKt"
private const val PURCHASE_RESULT_EXTENSIONS_CLASS =
"net.mullvad.mullvadvpn.util.PurchaseResultExtensionsKt"
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModelTest.kt
index 9be365e7ae..17394c39db 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ResetServerIpOverridesConfirmationViewModelTest.kt
@@ -2,17 +2,17 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
-import io.mockk.every
+import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.unmockkAll
-import io.mockk.verify
import kotlin.test.assertEquals
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.RelayOverride
+import net.mullvad.mullvadvpn.lib.model.RelayOverride
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -44,7 +44,7 @@ class ResetServerIpOverridesConfirmationViewModelTest {
@Test
fun `successful clear of override should result in side effect`() = runTest {
- every { mockRelayOverridesRepository.clearAllOverrides() } returns Unit
+ coEvery { mockRelayOverridesRepository.clearAllOverrides() } returns Unit.right()
viewModel.uiSideEffect.test {
viewModel.clearAllOverrides()
assertEquals(
@@ -56,8 +56,8 @@ class ResetServerIpOverridesConfirmationViewModelTest {
@Test
fun `clear overrides should invoke repository`() = runTest {
- every { mockRelayOverridesRepository.clearAllOverrides() } returns Unit
+ coEvery { mockRelayOverridesRepository.clearAllOverrides() } returns Unit.right()
viewModel.clearAllOverrides()
- verify { mockRelayOverridesRepository.clearAllOverrides() }
+ coVerify { mockRelayOverridesRepository.clearAllOverrides() }
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt
index 5d0ab5f604..80f62dba4a 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt
@@ -2,42 +2,40 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
-import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
-import io.mockk.runs
import io.mockk.unmockkAll
-import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
-import net.mullvad.mullvadvpn.compose.communication.CustomListResult
+import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.GeographicLocationConstraint
-import net.mullvad.mullvadvpn.model.LocationConstraint
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.Providers
-import net.mullvad.mullvadvpn.relaylist.Provider
-import net.mullvad.mullvadvpn.relaylist.RelayItem
-import net.mullvad.mullvadvpn.relaylist.RelayList
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+import net.mullvad.mullvadvpn.lib.model.CustomListName
+import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.Provider
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm
-import net.mullvad.mullvadvpn.relaylist.toLocationConstraint
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
-import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
+import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.usecase.AvailableProvidersUseCase
+import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -46,36 +44,44 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class SelectLocationViewModelTest {
- private val mockRelayListFilterUseCase: RelayListFilterUseCase = mockk(relaxed = true)
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
- private lateinit var viewModel: SelectLocationViewModel
- private val relayListWithSelectionFlow =
- MutableStateFlow(RelayList(emptyList(), emptyList(), emptyList(), null))
- private val mockRelayListUseCase: RelayListUseCase = mockk()
+ private val mockRelayListFilterRepository: RelayListFilterRepository = mockk()
+ private val mockAvailableProvidersUseCase: AvailableProvidersUseCase = mockk(relaxed = true)
private val mockCustomListActionUseCase: CustomListActionUseCase = mockk(relaxed = true)
- private val selectedOwnership = MutableStateFlow<Constraint<Ownership>>(Constraint.Any())
- private val selectedProvider = MutableStateFlow<Constraint<Providers>>(Constraint.Any())
- private val allProvider = MutableStateFlow<List<Provider>>(emptyList())
+ private val mockCustomListsRelayItemUseCase: CustomListsRelayItemUseCase = mockk()
+ private val mockFilteredRelayListUseCase: FilteredRelayListUseCase = mockk()
+ private val mockRelayListRepository: RelayListRepository = mockk()
+
+ private lateinit var viewModel: SelectLocationViewModel
+
+ private val allProviders = MutableStateFlow<List<Provider>>(emptyList())
+ private val selectedOwnership = MutableStateFlow<Constraint<Ownership>>(Constraint.Any)
+ private val selectedProviders = MutableStateFlow<Constraint<Providers>>(Constraint.Any)
+ private val selectedRelayItemFlow = MutableStateFlow<Constraint<RelayItemId>>(Constraint.Any)
+ private val filteredRelayList = MutableStateFlow<List<RelayItem.Location.Country>>(emptyList())
+ private val customRelayListItems = MutableStateFlow<List<RelayItem.CustomList>>(emptyList())
@BeforeEach
fun setup() {
- every { mockRelayListFilterUseCase.selectedOwnership() } returns selectedOwnership
- every { mockRelayListFilterUseCase.selectedProviders() } returns selectedProvider
- every { mockRelayListFilterUseCase.availableProviders() } returns allProvider
- every { mockRelayListUseCase.relayListWithSelection() } returns relayListWithSelectionFlow
- every { mockRelayListUseCase.fetchRelayList() } just runs
+ every { mockRelayListFilterRepository.selectedOwnership } returns selectedOwnership
+ every { mockRelayListFilterRepository.selectedProviders } returns selectedProviders
+ every { mockAvailableProvidersUseCase.availableProviders() } returns allProviders
+ every { mockRelayListRepository.selectedLocation } returns selectedRelayItemFlow
+ every { mockFilteredRelayListUseCase.filteredRelayList() } returns filteredRelayList
+ every { mockCustomListsRelayItemUseCase.relayItemCustomLists() } returns
+ customRelayListItems
- mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS)
mockkStatic(RELAY_LIST_EXTENSIONS)
mockkStatic(RELAY_ITEM_EXTENSIONS)
mockkStatic(CUSTOM_LIST_EXTENSIONS)
viewModel =
SelectLocationViewModel(
- mockServiceConnectionManager,
- mockRelayListUseCase,
- mockRelayListFilterUseCase,
- mockCustomListActionUseCase
+ relayListFilterRepository = mockRelayListFilterRepository,
+ availableProvidersUseCase = mockAvailableProvidersUseCase,
+ customListsRelayItemUseCase = mockCustomListsRelayItemUseCase,
+ customListActionUseCase = mockCustomListActionUseCase,
+ filteredRelayListUseCase = mockFilteredRelayListUseCase,
+ relayListRepository = mockRelayListRepository
)
}
@@ -93,12 +99,11 @@ class SelectLocationViewModelTest {
@Test
fun `given relayListWithSelection emits update uiState should contain new update`() = runTest {
// Arrange
- val mockCountries = listOf<RelayItem.Country>(mockk(), mockk())
- val mockCustomList = listOf<RelayItem.CustomList>(mockk(relaxed = true))
- val selectedItem: RelayItem = mockk()
+ val mockCountries = listOf<RelayItem.Location.Country>(mockk(), mockk())
+ val selectedItem: RelayItemId = mockk()
every { mockCountries.filterOnSearchTerm(any(), selectedItem) } returns mockCountries
- relayListWithSelectionFlow.value =
- RelayList(mockCustomList, mockCountries, mockCountries, selectedItem)
+ filteredRelayList.value = mockCountries
+ selectedRelayItemFlow.value = Constraint.Only(selectedItem)
// Act, Assert
viewModel.uiState.test {
@@ -113,12 +118,11 @@ class SelectLocationViewModelTest {
fun `given relayListWithSelection emits update with no selections selectedItem should be null`() =
runTest {
// Arrange
- val mockCustomList = listOf<RelayItem.CustomList>(mockk(relaxed = true))
- val mockCountries = listOf<RelayItem.Country>(mockk(), mockk())
- val selectedItem: RelayItem? = null
+ val mockCountries = listOf<RelayItem.Location.Country>(mockk(), mockk())
+ val selectedItem: RelayItemId? = null
every { mockCountries.filterOnSearchTerm(any(), selectedItem) } returns mockCountries
- relayListWithSelectionFlow.value =
- RelayList(mockCustomList, mockCountries, mockCountries, selectedItem)
+ filteredRelayList.value = mockCountries
+ selectedRelayItemFlow.value = Constraint.Any
// Act, Assert
viewModel.uiState.test {
@@ -132,25 +136,18 @@ class SelectLocationViewModelTest {
@Test
fun `on selectRelay call uiSideEffect should emit CloseScreen and connect`() = runTest {
// Arrange
- val mockRelayItem: RelayItem.Country = mockk()
- val mockLocation: GeographicLocationConstraint.Country = mockk(relaxed = true)
- val mockLocationConstraint: LocationConstraint = mockk()
- val connectionProxyMock: ConnectionProxy = mockk(relaxUnitFun = true)
- every { mockRelayItem.location } returns mockLocation
- every { mockServiceConnectionManager.connectionProxy() } returns connectionProxyMock
- every { mockRelayListUseCase.updateSelectedRelayLocation(mockLocationConstraint) } returns
- Unit
- every { mockRelayItem.toLocationConstraint() } returns mockLocationConstraint
+ val mockRelayItem: RelayItem.Location.Country = mockk()
+ val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
+ every { mockRelayItem.id } returns relayItemId
+ coEvery { mockRelayListRepository.updateSelectedRelayLocation(relayItemId) } returns
+ Unit.right()
// Act, Assert
viewModel.uiSideEffect.test {
viewModel.selectRelay(mockRelayItem)
// Await an empty item
assertEquals(SelectLocationSideEffect.CloseScreen, awaitItem())
- verify {
- connectionProxyMock.connect()
- mockRelayListUseCase.updateSelectedRelayLocation(mockLocationConstraint)
- }
+ coVerify { mockRelayListRepository.updateSelectedRelayLocation(relayItemId) }
}
}
@@ -158,15 +155,15 @@ class SelectLocationViewModelTest {
fun `on onSearchTermInput call uiState should emit with filtered countries`() = runTest {
// Arrange
val mockCustomList = listOf<RelayItem.CustomList>(mockk(relaxed = true))
- val mockCountries = listOf<RelayItem.Country>(mockk(), mockk())
- val selectedItem: RelayItem? = null
- val mockRelayList: List<RelayItem.Country> = mockk(relaxed = true)
+ val mockCountries = listOf<RelayItem.Location.Country>(mockk(), mockk())
+ val selectedItem: RelayItemId? = null
+ val mockRelayList: List<RelayItem.Location.Country> = mockk(relaxed = true)
val mockSearchString = "SEARCH"
every { mockRelayList.filterOnSearchTerm(mockSearchString, selectedItem) } returns
mockCountries
every { mockCustomList.filterOnSearchTerm(mockSearchString) } returns mockCustomList
- relayListWithSelectionFlow.value =
- RelayList(mockCustomList, mockRelayList, mockRelayList, selectedItem)
+ filteredRelayList.value = mockRelayList
+ selectedRelayItemFlow.value = Constraint.Any
// Act, Assert
viewModel.uiState.test {
@@ -188,15 +185,13 @@ class SelectLocationViewModelTest {
fun `when onSearchTermInput returns empty result uiState should return empty list`() = runTest {
// Arrange
val mockCustomList = listOf<RelayItem.CustomList>(mockk(relaxed = true))
- val mockCountries = emptyList<RelayItem.Country>()
- val selectedItem: RelayItem? = null
- val mockRelayList: List<RelayItem.Country> = mockk(relaxed = true)
+ val mockCountries = emptyList<RelayItem.Location.Country>()
+ val selectedItem: RelayItemId? = null
+ val mockRelayList: List<RelayItem.Location.Country> = mockk(relaxed = true)
val mockSearchString = "SEARCH"
every { mockRelayList.filterOnSearchTerm(mockSearchString, selectedItem) } returns
mockCountries
every { mockCustomList.filterOnSearchTerm(mockSearchString) } returns mockCustomList
- relayListWithSelectionFlow.value =
- RelayList(mockCustomList, mockRelayList, mockRelayList, selectedItem)
// Act, Assert
viewModel.uiState.test {
@@ -217,36 +212,30 @@ class SelectLocationViewModelTest {
fun `removeOwnerFilter should invoke use case with Constraint Any Ownership`() = runTest {
// Arrange
val mockSelectedProviders: Constraint<Providers> = mockk()
- every { mockRelayListFilterUseCase.selectedProviders() } returns
+ every { mockRelayListFilterRepository.selectedProviders } returns
MutableStateFlow(mockSelectedProviders)
+ coEvery { mockRelayListFilterRepository.updateSelectedOwnership(Constraint.Any) } returns
+ Unit.right()
// Act
viewModel.removeOwnerFilter()
// Assert
- verify {
- mockRelayListFilterUseCase.updateOwnershipAndProviderFilter(
- any<Constraint.Any<Ownership>>(),
- mockSelectedProviders
- )
- }
+ coVerify { mockRelayListFilterRepository.updateSelectedOwnership(Constraint.Any) }
}
@Test
fun `removeProviderFilter should invoke use case with Constraint Any Provider`() = runTest {
// Arrange
val mockSelectedOwnership: Constraint<Ownership> = mockk()
- every { mockRelayListFilterUseCase.selectedOwnership() } returns
+ every { mockRelayListFilterRepository.selectedOwnership } returns
MutableStateFlow(mockSelectedOwnership)
+ coEvery { mockRelayListFilterRepository.updateSelectedProviders(Constraint.Any) } returns
+ Unit.right()
// Act
viewModel.removeProviderFilter()
// Assert
- verify {
- mockRelayListFilterUseCase.updateOwnershipAndProviderFilter(
- mockSelectedOwnership,
- any<Constraint.Any<Providers>>()
- )
- }
+ coVerify { mockRelayListFilterRepository.updateSelectedProviders(Constraint.Any) }
}
@Test
@@ -264,18 +253,21 @@ class SelectLocationViewModelTest {
@Test
fun `after adding a location to a list should emit location added side effect`() = runTest {
// Arrange
- val expectedResult: CustomListResult.LocationsChanged = mockk()
- val location: RelayItem = mockk {
- every { code } returns "code"
+ val expectedResult: LocationsChanged = mockk()
+ val location: RelayItem.Location.Country = mockk {
+ every { id } returns GeoLocationId.Country("se")
every { descendants() } returns emptyList()
}
- val customList: RelayItem.CustomList = mockk {
- every { id } returns "1"
- every { locations } returns emptyList()
- }
+ val customList =
+ RelayItem.CustomList(
+ id = CustomListId("1"),
+ customListName = CustomListName.fromString("custom"),
+ locations = emptyList(),
+ expanded = false
+ )
coEvery {
mockCustomListActionUseCase.performAction(any<CustomListAction.UpdateLocations>())
- } returns Result.success(expectedResult)
+ } returns expectedResult.right()
// Act, Assert
viewModel.uiSideEffect.test {
@@ -287,8 +279,6 @@ class SelectLocationViewModelTest {
}
companion object {
- private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS =
- "net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManagerExtensionsKt"
private const val RELAY_LIST_EXTENSIONS =
"net.mullvad.mullvadvpn.relaylist.RelayListExtensionsKt"
private const val RELAY_ITEM_EXTENSIONS =
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt
index 16e89ac20b..b39d4357de 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ServerIpOverridesViewModelTest.kt
@@ -4,6 +4,8 @@ import android.content.ContentResolver
import android.net.Uri
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -17,13 +19,9 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.model.RelayOverride
-import net.mullvad.mullvadvpn.model.SettingsPatchError
+import net.mullvad.mullvadvpn.lib.model.RelayOverride
+import net.mullvad.mullvadvpn.lib.model.SettingsPatchError
import net.mullvad.mullvadvpn.repository.RelayOverridesRepository
-import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -33,27 +31,20 @@ import org.junit.jupiter.api.extension.ExtendWith
class ServerIpOverridesViewModelTest {
private lateinit var viewModel: ServerIpOverridesViewModel
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockRelayOverridesRepository: RelayOverridesRepository = mockk()
- private val mockSettingsRepository: SettingsRepository = mockk(relaxed = true)
private val mockContentResolver: ContentResolver = mockk()
private val relayOverrides = MutableStateFlow<List<RelayOverride>?>(null)
- private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.ConnectedReady(mockk()))
@BeforeEach
fun setup() {
coEvery { mockRelayOverridesRepository.relayOverrides } returns relayOverrides
- coEvery { mockServiceConnectionManager.connectionState } returns serviceConnectionState
mockkStatic(READ_TEXT)
viewModel =
ServerIpOverridesViewModel(
- serviceConnectionManager = mockServiceConnectionManager,
relayOverridesRepository = mockRelayOverridesRepository,
- settingsRepository = mockSettingsRepository,
contentResolver = mockContentResolver
)
}
@@ -80,10 +71,12 @@ class ServerIpOverridesViewModelTest {
@Test
fun `when import is finished we should get side effect`() = runTest {
+ // Arrange
val mockkResult: SettingsPatchError = mockk()
- coEvery { mockSettingsRepository.applySettingsPatch(TEXT_INPUT) } returns
- Event.ApplyJsonSettingsResult(mockkResult)
+ coEvery { mockRelayOverridesRepository.applySettingsPatch(TEXT_INPUT) } returns
+ mockkResult.left()
+ // Act, Assert
viewModel.uiSideEffect.test {
viewModel.importText(TEXT_INPUT)
assertEquals(ServerIpOverridesUiSideEffect.ImportResult(mockkResult), awaitItem())
@@ -92,22 +85,30 @@ class ServerIpOverridesViewModelTest {
@Test
fun `ensure import text invokes repository`() = runTest {
+ // Arrange
+ coEvery { mockRelayOverridesRepository.applySettingsPatch(TEXT_INPUT) } returns Unit.right()
+
+ // Act
viewModel.importText(TEXT_INPUT)
- coVerify { mockSettingsRepository.applySettingsPatch(TEXT_INPUT) }
+ // Assert
+ coVerify { mockRelayOverridesRepository.applySettingsPatch(TEXT_INPUT) }
}
@Test
fun `ensure import file invokes repository`() = runTest {
+ // Arrange
val uri: Uri = mockk()
-
val mockInputStream: InputStream = mockk()
every { mockContentResolver.openInputStream(uri) } returns mockInputStream
every { any<InputStreamReader>().readText() } returns TEXT_INPUT
+ coEvery { mockRelayOverridesRepository.applySettingsPatch(TEXT_INPUT) } returns Unit.right()
+ // Act
viewModel.importFile(uri)
- coVerify { mockSettingsRepository.applySettingsPatch(TEXT_INPUT) }
+ // Assert
+ coVerify { mockRelayOverridesRepository.applySettingsPatch(TEXT_INPUT) }
}
companion object {
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt
index 0eace9ca43..c76e2cd278 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt
@@ -4,21 +4,16 @@ import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import io.mockk.every
import io.mockk.mockk
-import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlin.test.assertEquals
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.repository.DeviceRepository
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.ui.VersionInfo
-import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.util.appVersionCallbackFlow
+import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -28,42 +23,26 @@ import org.junit.jupiter.api.extension.ExtendWith
class SettingsViewModelTest {
private val mockDeviceRepository: DeviceRepository = mockk()
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
- private lateinit var mockAppVersionInfoCache: AppVersionInfoCache
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
+ private val mockAppVersionInfoRepository: AppVersionInfoRepository = mockk()
- private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
private val versionInfo =
MutableStateFlow(
- VersionInfo(
- currentVersion = null,
- upgradeVersion = null,
- isOutdated = false,
- isSupported = false
- )
+ VersionInfo(currentVersion = "", isSupported = false, suggestedUpgradeVersion = null)
)
private lateinit var viewModel: SettingsViewModel
@BeforeEach
fun setup() {
- mockkStatic(CACHE_EXTENSION_CLASS)
val deviceState = MutableStateFlow<DeviceState>(DeviceState.LoggedOut)
- mockAppVersionInfoCache =
- mockk<AppVersionInfoCache>().apply {
- every { appVersionCallbackFlow() } returns versionInfo
- }
- every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
- every { mockServiceConnectionContainer.appVersionInfoCache } returns mockAppVersionInfoCache
every { mockDeviceRepository.deviceState } returns deviceState
- every { mockAppVersionInfoCache.onUpdate = any() } answers {}
+ every { mockAppVersionInfoRepository.versionInfo() } returns versionInfo
viewModel =
SettingsViewModel(
deviceRepository = mockDeviceRepository,
- serviceConnectionManager = mockServiceConnectionManager,
+ appVersionInfoRepository = mockAppVersionInfoRepository,
isPlayBuild = false
)
}
@@ -87,20 +66,14 @@ class SettingsViewModelTest {
val versionInfoTestItem =
VersionInfo(
currentVersion = "1.0",
- upgradeVersion = "1.0",
- isOutdated = false,
- isSupported = true
+ isSupported = true,
+ suggestedUpgradeVersion = null
)
- every { mockAppVersionInfoCache.version } returns "1.0"
- every { mockAppVersionInfoCache.isSupported } returns true
- every { mockAppVersionInfoCache.isOutdated } returns false
// Act, Assert
viewModel.uiState.test {
awaitItem() // Wait for initial value
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
versionInfo.value = versionInfoTestItem
val result = awaitItem()
assertEquals(false, result.isUpdateAvailable)
@@ -111,16 +84,12 @@ class SettingsViewModelTest {
fun `when AppVersionInfoCache returns isSupported false uiState should return isUpdateAvailable true`() =
runTest {
// Arrange
- every { mockAppVersionInfoCache.isSupported } returns false
- every { mockAppVersionInfoCache.isOutdated } returns false
- every { mockAppVersionInfoCache.version } returns ""
+ val versionInfoTestItem =
+ VersionInfo(currentVersion = "", isSupported = false, suggestedUpgradeVersion = "")
+ versionInfo.value = versionInfoTestItem
// Act, Assert
viewModel.uiState.test {
- awaitItem()
-
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
val result = awaitItem()
assertEquals(true, result.isUpdateAvailable)
}
@@ -130,22 +99,14 @@ class SettingsViewModelTest {
fun `when AppVersionInfoCache returns isOutdated true uiState should return isUpdateAvailable true`() =
runTest {
// Arrange
- every { mockAppVersionInfoCache.isSupported } returns true
- every { mockAppVersionInfoCache.isOutdated } returns true
- every { mockAppVersionInfoCache.version } returns ""
+ val versionInfoTestItem =
+ VersionInfo(currentVersion = "", isSupported = true, suggestedUpgradeVersion = "")
+ versionInfo.value = versionInfoTestItem
// Act, Assert
viewModel.uiState.test {
- awaitItem()
-
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
val result = awaitItem()
assertEquals(true, result.isUpdateAvailable)
}
}
-
- companion object {
- private const val CACHE_EXTENSION_CLASS = "net.mullvad.mullvadvpn.util.CacheExtensionsKt"
- }
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt
index 11b253e5ea..aa1ccc82f0 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt
@@ -2,15 +2,13 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
+import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
-import io.mockk.invoke
-import io.mockk.just
import io.mockk.mockk
-import io.mockk.runs
-import io.mockk.slot
import io.mockk.unmockkAll
import io.mockk.verify
-import io.mockk.verifyAll
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlinx.coroutines.cancel
@@ -21,10 +19,8 @@ import net.mullvad.mullvadvpn.applist.AppData
import net.mullvad.mullvadvpn.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
+import net.mullvad.mullvadvpn.lib.model.AppId
+import net.mullvad.mullvadvpn.repository.SplitTunnelingRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -36,14 +32,16 @@ import org.junit.jupiter.api.extension.ExtendWith
class SplitTunnelingViewModelTest {
private val mockedApplicationsProvider = mockk<ApplicationsProvider>()
- private val mockedSplitTunneling = mockk<SplitTunneling>()
- private val mockedServiceConnectionManager = mockk<ServiceConnectionManager>()
- private val mockedServiceConnectionContainer = mockk<ServiceConnectionContainer>()
+ private val mockedSplitTunnelingRepository = mockk<SplitTunnelingRepository>()
private lateinit var testSubject: SplitTunnelingViewModel
+ private val excludedApps: MutableStateFlow<Set<AppId>> = MutableStateFlow(emptySet())
+ private val enabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
+
@BeforeEach
fun setup() {
- every { mockedSplitTunneling.enabled } returns true
+ every { mockedSplitTunnelingRepository.splitTunnelingEnabled } returns enabled
+ every { mockedSplitTunnelingRepository.excludedApps } returns excludedApps
}
@AfterEach
@@ -66,14 +64,6 @@ class SplitTunnelingViewModelTest {
@Test
fun `empty app list should work`() = runTest {
- every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers
- {
- lambda<(Set<String>) -> Unit>().invoke(emptySet())
- }
- every { mockedSplitTunneling.enabledChange = captureLambda() } answers
- {
- lambda<(Boolean) -> Unit>().invoke(true)
- }
initTestSubject(emptyList())
val expectedState =
SplitTunnelingUiState.ShowAppList(
@@ -89,16 +79,9 @@ class SplitTunnelingViewModelTest {
fun `includedApps and excludedApps should both be included in uiState`() = runTest {
val appExcluded = AppData("test.excluded", 0, "testName1")
val appNotExcluded = AppData("test.not.excluded", 0, "testName2")
- every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers
- {
- lambda<(Set<String>) -> Unit>().invoke(setOf(appExcluded.packageName))
- }
- every { mockedSplitTunneling.enabledChange = captureLambda() } answers
- {
- lambda<(Boolean) -> Unit>().invoke(true)
- }
initTestSubject(listOf(appExcluded, appNotExcluded))
+ excludedApps.value = setOf(AppId(appExcluded.packageName))
val expectedState =
SplitTunnelingUiState.ShowAppList(
@@ -111,29 +94,15 @@ class SplitTunnelingViewModelTest {
testSubject.uiState.test {
val actualState = awaitItem()
assertEquals(expectedState, actualState)
- verifyAll {
- mockedSplitTunneling.enabledChange = any()
- mockedSplitTunneling.excludedAppsChange = any()
- }
}
}
@Test
fun `include app should work`() = runTest {
- var excludedAppsCallback = slot<(Set<String>) -> Unit>()
val app = AppData("test", 0, "testName")
- every { mockedSplitTunneling.includeApp(app.packageName) } just runs
- every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers
- {
- excludedAppsCallback = lambda()
- excludedAppsCallback.invoke(setOf(app.packageName))
- }
- every { mockedSplitTunneling.enabledChange = captureLambda() } answers
- {
- lambda<(Boolean) -> Unit>().invoke(true)
- }
initTestSubject(listOf(app))
+ excludedApps.value = setOf(AppId(app.packageName))
val expectedStateBeforeAction =
SplitTunnelingUiState.ShowAppList(
@@ -149,35 +118,22 @@ class SplitTunnelingViewModelTest {
includedApps = listOf(app),
showSystemApps = false
)
+ coEvery { mockedSplitTunnelingRepository.includeApp(AppId(app.packageName)) } returns
+ Unit.right()
testSubject.uiState.test {
assertEquals(expectedStateBeforeAction, awaitItem())
testSubject.onIncludeAppClick(app.packageName)
- excludedAppsCallback.invoke(emptySet())
+ excludedApps.value = emptySet()
assertEquals(expectedStateAfterAction, awaitItem())
- verifyAll {
- mockedSplitTunneling.enabledChange = any()
- mockedSplitTunneling.excludedAppsChange = any()
- mockedSplitTunneling.includeApp(app.packageName)
- }
+ coVerify { mockedSplitTunnelingRepository.includeApp(AppId(app.packageName)) }
}
}
@Test
fun `onExcludeApp should result in new uiState with app excluded`() = runTest {
- var excludedAppsCallback = slot<(Set<String>) -> Unit>()
val app = AppData("test", 0, "testName")
- every { mockedSplitTunneling.excludeApp(app.packageName) } just runs
- every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers
- {
- excludedAppsCallback = lambda()
- excludedAppsCallback.invoke(emptySet())
- }
- every { mockedSplitTunneling.enabledChange = captureLambda() } answers
- {
- lambda<(Boolean) -> Unit>().invoke(true)
- }
initTestSubject(listOf(app))
@@ -197,32 +153,23 @@ class SplitTunnelingViewModelTest {
showSystemApps = false
)
+ coEvery { mockedSplitTunnelingRepository.excludeApp(AppId(app.packageName)) } returns
+ Unit.right()
+
testSubject.uiState.test {
assertEquals(expectedStateBeforeAction, awaitItem())
testSubject.onExcludeAppClick(app.packageName)
- excludedAppsCallback.invoke(setOf(app.packageName))
+ excludedApps.value = setOf(AppId(app.packageName))
assertEquals(expectedStateAfterAction, awaitItem())
- verifyAll {
- mockedSplitTunneling.enabledChange = any()
- mockedSplitTunneling.excludedAppsChange = any()
- mockedSplitTunneling.excludeApp(app.packageName)
- }
+ coVerify { mockedSplitTunnelingRepository.excludeApp(AppId(app.packageName)) }
}
}
@Test
fun `when split tunneling is disabled uiState should be disabled`() = runTest {
- every { mockedSplitTunneling.excludedAppsChange = captureLambda() } answers
- {
- lambda<(Set<String>) -> Unit>().invoke(emptySet())
- }
- every { mockedSplitTunneling.enabledChange = captureLambda() } answers
- {
- lambda<(Boolean) -> Unit>().invoke(false)
- }
-
initTestSubject(emptyList())
+ enabled.value = false
val expectedState = SplitTunnelingUiState.ShowAppList(enabled = false)
@@ -234,15 +181,10 @@ class SplitTunnelingViewModelTest {
private fun initTestSubject(appList: List<AppData>) {
every { mockedApplicationsProvider.getAppsList() } returns appList
- every { mockedServiceConnectionManager.connectionState } returns
- MutableStateFlow(
- ServiceConnectionState.ConnectedReady(mockedServiceConnectionContainer)
- )
- every { mockedServiceConnectionContainer.splitTunneling } returns mockedSplitTunneling
testSubject =
SplitTunnelingViewModel(
mockedApplicationsProvider,
- mockedServiceConnectionManager,
+ mockedSplitTunnelingRepository,
UnconfinedTestDispatcher()
)
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt
index 6934384643..ef3b34effc 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VoucherDialogViewModelTest.kt
@@ -1,7 +1,8 @@
package net.mullvad.mullvadvpn.viewmodel
-import android.content.res.Resources
import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -10,18 +11,13 @@ import io.mockk.unmockkAll
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.VoucherDialogState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.VoucherSubmission
-import net.mullvad.mullvadvpn.model.VoucherSubmissionError
-import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.VoucherRedeemer
-import net.mullvad.mullvadvpn.ui.serviceconnection.voucherRedeemer
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess
+import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
+import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -30,26 +26,15 @@ import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(TestCoroutineRule::class)
class VoucherDialogViewModelTest {
- private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
- private val mockVoucherSubmission: VoucherSubmission = mockk()
- private val serviceConnectionState =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
+ private val mockVoucherSubmission: RedeemVoucherSuccess = mockk()
- private val mockVoucherRedeemer: VoucherRedeemer = mockk()
- private val mockResources: Resources = mockk()
+ private val mockVoucherRepository: VoucherRepository = mockk()
private lateinit var viewModel: VoucherDialogViewModel
@BeforeEach
fun setup() {
- every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
-
- viewModel =
- VoucherDialogViewModel(
- serviceConnectionManager = mockServiceConnectionManager,
- resources = mockResources
- )
+ viewModel = VoucherDialogViewModel(voucherRepository = mockVoucherRepository)
}
@AfterEach
@@ -62,36 +47,31 @@ class VoucherDialogViewModelTest {
val voucher = DUMMY_INVALID_VOUCHER
// Arrange
- every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
- every { mockVoucherSubmission.timeAdded } returns 0
- coEvery { mockVoucherRedeemer.submit(voucher) } returns
- VoucherSubmissionResult.Ok(mockVoucherSubmission)
+ val timeAdded = 0L
+ val newExpiry = DateTime()
+ coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
+ RedeemVoucherSuccess(timeAdded, newExpiry).right()
// Act
assertIs<VoucherDialogState.Default>(viewModel.uiState.value.voucherState)
viewModel.onRedeem(voucher)
// Assert
- coVerify(exactly = 1) { mockVoucherRedeemer.submit(voucher) }
+ coVerify(exactly = 1) { mockVoucherRepository.submitVoucher(voucher) }
}
@Test
fun `given invalid voucher when redeeming then show error`() = runTest {
val voucher = DUMMY_INVALID_VOUCHER
- val dummyStringResource = DUMMY_STRING_RESOURCE
// Arrange
- every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
- every { mockResources.getString(any()) } returns dummyStringResource
every { mockVoucherSubmission.timeAdded } returns 0
- coEvery { mockVoucherRedeemer.submit(voucher) } returns
- VoucherSubmissionResult.Error(VoucherSubmissionError.OtherError)
+ coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
+ RedeemVoucherError.InvalidVoucher.left()
// Act, Assert
viewModel.uiState.test {
assertEquals(viewModel.uiState.value, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
viewModel.onRedeem(voucher)
assertTrue { awaitItem().voucherState is VoucherDialogState.Verifying }
assertTrue { awaitItem().voucherState is VoucherDialogState.Error }
@@ -101,20 +81,15 @@ class VoucherDialogViewModelTest {
@Test
fun `given valid voucher when redeeming then show success`() = runTest {
val voucher = DUMMY_VALID_VOUCHER
- val dummyStringResource = DUMMY_STRING_RESOURCE
// Arrange
- every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
- every { mockResources.getString(any()) } returns dummyStringResource
every { mockVoucherSubmission.timeAdded } returns 0
- coEvery { mockVoucherRedeemer.submit(voucher) } returns
- VoucherSubmissionResult.Ok(VoucherSubmission(0, DUMMY_STRING_RESOURCE))
+ coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
+ RedeemVoucherSuccess(0, DateTime()).right()
// Act, Assert
viewModel.uiState.test {
assertEquals(viewModel.uiState.value, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
viewModel.onRedeem(voucher)
assertTrue { awaitItem().voucherState is VoucherDialogState.Verifying }
assertTrue { awaitItem().voucherState is VoucherDialogState.Success }
@@ -124,20 +99,15 @@ class VoucherDialogViewModelTest {
@Test
fun `when voucher input is changed then clear error`() = runTest {
val voucher = DUMMY_INVALID_VOUCHER
- val dummyStringResource = DUMMY_STRING_RESOURCE
// Arrange
- every { mockServiceConnectionManager.voucherRedeemer() } returns mockVoucherRedeemer
- every { mockResources.getString(any()) } returns dummyStringResource
every { mockVoucherSubmission.timeAdded } returns 0
- coEvery { mockVoucherRedeemer.submit(voucher) } returns
- VoucherSubmissionResult.Error(VoucherSubmissionError.OtherError)
+ coEvery { mockVoucherRepository.submitVoucher(voucher) } returns
+ RedeemVoucherError.VoucherAlreadyUsed.left()
// Act, Assert
viewModel.uiState.test {
assertEquals(viewModel.uiState.value, awaitItem())
- serviceConnectionState.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
viewModel.onRedeem(voucher)
assertTrue { awaitItem().voucherState is VoucherDialogState.Verifying }
assertTrue { awaitItem().voucherState is VoucherDialogState.Error }
@@ -149,6 +119,5 @@ class VoucherDialogViewModelTest {
companion object {
private const val DUMMY_VALID_VOUCHER = "dummy_valid_voucher"
private const val DUMMY_INVALID_VOUCHER = "dummy_invalid_voucher"
- private const val DUMMY_STRING_RESOURCE = "dummy_string_resource"
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
index 11992c40c0..29a6c764ba 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
@@ -1,12 +1,13 @@
package net.mullvad.mullvadvpn.viewmodel
-import android.content.res.Resources
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
+import arrow.core.right
+import io.mockk.coEvery
+import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
-import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.cancel
@@ -14,19 +15,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.Port
-import net.mullvad.mullvadvpn.model.PortRange
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.RelayConstraints
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.TunnelOptions
-import net.mullvad.mullvadvpn.model.WireguardConstraints
-import net.mullvad.mullvadvpn.model.WireguardTunnelOptions
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.RelayConstraints
+import net.mullvad.mullvadvpn.lib.model.RelaySettings
+import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.lib.model.TunnelOptions
+import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions
+import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
-import net.mullvad.mullvadvpn.usecase.RelayListUseCase
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -37,10 +38,8 @@ import org.junit.jupiter.api.extension.ExtendWith
class VpnSettingsViewModelTest {
private val mockSettingsRepository: SettingsRepository = mockk()
- private val mockResources: Resources = mockk()
- private val mockPortRangeUseCase: PortRangeUseCase = mockk()
- private val mockRelayListUseCase: RelayListUseCase = mockk()
private val mockSystemVpnSettingsUseCase: SystemVpnSettingsUseCase = mockk(relaxed = true)
+ private val mockRelayListRepository: RelayListRepository = mockk()
private val mockSettingsUpdate = MutableStateFlow<Settings?>(null)
private val portRangeFlow = MutableStateFlow(emptyList<PortRange>())
@@ -50,15 +49,13 @@ class VpnSettingsViewModelTest {
@BeforeEach
fun setup() {
every { mockSettingsRepository.settingsUpdates } returns mockSettingsUpdate
- every { mockPortRangeUseCase.portRanges() } returns portRangeFlow
+ every { mockRelayListRepository.portRanges } returns portRangeFlow
viewModel =
VpnSettingsViewModel(
repository = mockSettingsRepository,
- resources = mockResources,
- portRangeUseCase = mockPortRangeUseCase,
- relayListUseCase = mockRelayListUseCase,
systemVpnSettingsUseCase = mockSystemVpnSettingsUseCase,
+ relayListRepository = mockRelayListRepository,
dispatcher = UnconfinedTestDispatcher()
)
}
@@ -73,11 +70,11 @@ class VpnSettingsViewModelTest {
fun `onSelectQuantumResistanceSetting should invoke setWireguardQuantumResistant on SettingsRepository`() =
runTest {
val quantumResistantState = QuantumResistantState.On
- every {
+ coEvery {
mockSettingsRepository.setWireguardQuantumResistant(quantumResistantState)
- } returns Unit
+ } returns Unit.right()
viewModel.onSelectQuantumResistanceSetting(quantumResistantState)
- verify(exactly = 1) {
+ coVerify(exactly = 1) {
mockSettingsRepository.setWireguardQuantumResistant(quantumResistantState)
}
}
@@ -105,7 +102,8 @@ class VpnSettingsViewModelTest {
every { mockSettings.tunnelOptions } returns mockTunnelOptions
every { mockTunnelOptions.wireguard } returns mockWireguardTunnelOptions
every { mockWireguardTunnelOptions.quantumResistant } returns expectedResistantState
- every { mockSettings.relaySettings } returns mockk<RelaySettings.Normal>(relaxed = true)
+ every { mockWireguardTunnelOptions.mtu } returns Mtu(0)
+ every { mockSettings.relaySettings } returns mockk<RelaySettings>(relaxed = true)
viewModel.uiState.test {
assertEquals(defaultResistantState, awaitItem().quantumResistant)
@@ -120,7 +118,7 @@ class VpnSettingsViewModelTest {
// Arrange
val expectedPort: Constraint<Port> = Constraint.Only(Port(99))
val mockSettings: Settings = mockk(relaxed = true)
- val mockRelaySettings: RelaySettings.Normal = mockk()
+ val mockRelaySettings: RelaySettings = mockk()
val mockRelayConstraints: RelayConstraints = mockk()
val mockWireguardConstraints: WireguardConstraints = mockk()
@@ -128,10 +126,19 @@ class VpnSettingsViewModelTest {
every { mockRelaySettings.relayConstraints } returns mockRelayConstraints
every { mockRelayConstraints.wireguardConstraints } returns mockWireguardConstraints
every { mockWireguardConstraints.port } returns expectedPort
+ every { mockSettings.tunnelOptions } returns
+ TunnelOptions(
+ wireguard =
+ WireguardTunnelOptions(
+ mtu = null,
+ quantumResistant = QuantumResistantState.Off
+ ),
+ dnsOptions = mockk(relaxed = true)
+ )
// Act, Assert
viewModel.uiState.test {
- assertIs<Constraint.Any<Port>>(awaitItem().selectedWireguardPort)
+ assertIs<Constraint.Any>(awaitItem().selectedWireguardPort)
mockSettingsUpdate.value = mockSettings
assertEquals(expectedPort, awaitItem().customWireguardPort)
assertEquals(expectedPort, awaitItem().selectedWireguardPort)
@@ -144,14 +151,15 @@ class VpnSettingsViewModelTest {
// Arrange
val wireguardPort: Constraint<Port> = Constraint.Only(Port(99))
val wireguardConstraints = WireguardConstraints(port = wireguardPort)
- every { mockRelayListUseCase.updateSelectedWireguardConstraints(any()) } returns Unit
+ coEvery { mockRelayListRepository.updateSelectedWireguardConstraints(any()) } returns
+ Unit.right()
// Act
viewModel.onWireguardPortSelected(wireguardPort)
// Assert
- verify(exactly = 1) {
- mockRelayListUseCase.updateSelectedWireguardConstraints(wireguardConstraints)
+ coVerify(exactly = 1) {
+ mockRelayListRepository.updateSelectedWireguardConstraints(wireguardConstraints)
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
index 91554193bc..3113450276 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModelTest.kt
@@ -13,27 +13,23 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.state.PaymentState
-import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
-import net.mullvad.mullvadvpn.model.AccountAndDevice
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.Device
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.repository.AccountRepository
-import net.mullvad.mullvadvpn.repository.DeviceRepository
-import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache
-import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
-import net.mullvad.talpid.util.EventNotifier
import org.joda.time.DateTime
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -44,21 +40,20 @@ import org.junit.jupiter.api.extension.ExtendWith
class WelcomeViewModelTest {
private val serviceConnectionStateFlow =
- MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected)
- private val deviceStateFlow = MutableStateFlow<DeviceState>(DeviceState.Initial)
- private val accountExpiryStateFlow = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing)
+ MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Unbound)
+ private val deviceStateFlow = MutableStateFlow<DeviceState?>(DeviceState.LoggedOut)
+ private val accountExpiryStateFlow = MutableStateFlow<AccountData?>(null)
private val purchaseResultFlow = MutableStateFlow<PurchaseResult?>(null)
private val paymentAvailabilityFlow = MutableStateFlow<PaymentAvailability?>(null)
- // Service connections
- private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk()
+ // ConnectionProxy
private val mockConnectionProxy: ConnectionProxy = mockk()
// Event notifiers
- private val eventNotifierTunnelUiState = EventNotifier<TunnelState>(TunnelState.Disconnected())
+ private val tunnelState = MutableStateFlow<TunnelState>(TunnelState.Disconnected())
private val mockAccountRepository: AccountRepository = mockk(relaxed = true)
- private val mockDeviceRepository: DeviceRepository = mockk()
+ private val mockDeviceRepository: DeviceRepository = mockk(relaxed = true)
private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockPaymentUseCase: PaymentUseCase = mockk(relaxed = true)
@@ -66,18 +61,15 @@ class WelcomeViewModelTest {
@BeforeEach
fun setup() {
- mockkStatic(SERVICE_CONNECTION_MANAGER_EXTENSIONS)
mockkStatic(PURCHASE_RESULT_EXTENSIONS_CLASS)
every { mockDeviceRepository.deviceState } returns deviceStateFlow
every { mockServiceConnectionManager.connectionState } returns serviceConnectionStateFlow
- every { mockServiceConnectionContainer.connectionProxy } returns mockConnectionProxy
+ every { mockConnectionProxy.tunnelState } returns tunnelState
- every { mockConnectionProxy.onUiStateChange } returns eventNotifierTunnelUiState
-
- every { mockAccountRepository.accountExpiryState } returns accountExpiryStateFlow
+ every { mockAccountRepository.accountData } returns accountExpiryStateFlow
coEvery { mockPaymentUseCase.purchaseResult } returns purchaseResultFlow
@@ -87,8 +79,8 @@ class WelcomeViewModelTest {
WelcomeViewModel(
accountRepository = mockAccountRepository,
deviceRepository = mockDeviceRepository,
- serviceConnectionManager = mockServiceConnectionManager,
paymentUseCase = mockPaymentUseCase,
+ connectionProxy = mockConnectionProxy,
pollAccountExpiry = false,
isPlayBuild = false
)
@@ -103,10 +95,8 @@ class WelcomeViewModelTest {
@Test
fun `on onSitePaymentClick call uiSideEffect should emit OpenAccountView`() = runTest {
// Arrange
- val mockToken = "4444 5555 6666 7777"
- val mockAuthTokenCache: AuthTokenCache = mockk(relaxed = true)
- every { mockServiceConnectionManager.authTokenCache() } returns mockAuthTokenCache
- coEvery { mockAuthTokenCache.fetchAuthToken() } returns mockToken
+ val mockToken = WebsiteAuthToken.fromString("154c4cc94810fddac78398662b7fa0c7")
+ coEvery { mockAccountRepository.getWebsiteAuthToken() } returns mockToken
// Act, Assert
viewModel.uiSideEffect.test {
@@ -124,10 +114,9 @@ class WelcomeViewModelTest {
// Act, Assert
viewModel.uiState.test {
- assertEquals(WelcomeUiState(), awaitItem())
- eventNotifierTunnelUiState.notify(tunnelUiStateTestItem)
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ // Default state
+ awaitItem()
+ tunnelState.emit(tunnelUiStateTestItem)
val result = awaitItem()
assertEquals(tunnelUiStateTestItem, result.tunnelState)
}
@@ -137,21 +126,17 @@ class WelcomeViewModelTest {
fun `when DeviceRepository returns LoggedIn uiState should include new accountNumber`() =
runTest {
// Arrange
- val expectedAccountNumber = "4444555566667777"
+ val expectedAccountNumber = AccountToken("4444555566667777")
val device: Device = mockk()
every { device.displayName() } returns ""
// Act, Assert
viewModel.uiState.test {
- assertEquals(WelcomeUiState(), awaitItem())
+ // Default state
+ awaitItem()
paymentAvailabilityFlow.value = null
deviceStateFlow.value =
- DeviceState.LoggedIn(
- accountAndDevice =
- AccountAndDevice(account_token = expectedAccountNumber, device = device)
- )
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
+ DeviceState.LoggedIn(accountToken = expectedAccountNumber, device = device)
assertEquals(expectedAccountNumber, awaitItem().accountNumber)
}
}
@@ -159,7 +144,7 @@ class WelcomeViewModelTest {
@Test
fun `when user has added time then uiSideEffect should emit OpenConnectScreen`() = runTest {
// Arrange
- accountExpiryStateFlow.emit(AccountExpiry.Available(DateTime().plusDays(1)))
+ accountExpiryStateFlow.emit(AccountData(mockk(relaxed = true), DateTime().plusDays(1)))
// Act, Assert
viewModel.uiSideEffect.test {
@@ -179,8 +164,6 @@ class WelcomeViewModelTest {
// Default item
awaitItem()
paymentAvailabilityFlow.tryEmit(productsUnavailable)
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
val result = awaitItem().billingPaymentState
assertIs<PaymentState.NoPayment>(result)
}
@@ -192,8 +175,6 @@ class WelcomeViewModelTest {
// Arrange
val paymentOtherError = PaymentAvailability.Error.Other(mockk())
paymentAvailabilityFlow.tryEmit(paymentOtherError)
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -207,8 +188,6 @@ class WelcomeViewModelTest {
runTest { // Arrange
val paymentBillingError = PaymentAvailability.Error.BillingUnavailable
paymentAvailabilityFlow.value = paymentBillingError
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -225,8 +204,6 @@ class WelcomeViewModelTest {
val expectedProductList = listOf(mockProduct)
val productsAvailable = PaymentAvailability.ProductsAvailable(listOf(mockProduct))
paymentAvailabilityFlow.value = productsAvailable
- serviceConnectionStateFlow.value =
- ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer)
// Act, Assert
viewModel.uiState.test {
@@ -237,8 +214,6 @@ class WelcomeViewModelTest {
}
companion object {
- private const val SERVICE_CONNECTION_MANAGER_EXTENSIONS =
- "net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManagerExtensionsKt"
private const val PURCHASE_RESULT_EXTENSIONS_CLASS =
"net.mullvad.mullvadvpn.util.PurchaseResultExtensionsKt"
}