summaryrefslogtreecommitdiffhomepage
path: root/android
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
parent8e14a8d4287af66a57a98db79d3ac320c2dad4a1 (diff)
parent767b97eda756f4ec4e67fb5fa2ae664277291e8f (diff)
downloadmullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.tar.xz
mullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.zip
Merge branch 'android-grpc'
Diffstat (limited to 'android')
-rw-r--r--android/CHANGELOG.md4
-rw-r--r--android/app/build.gradle.kts25
-rw-r--r--android/app/lint-baseline.xml15
-rw-r--r--android/app/proguard-rules.pro13
-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/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
-rw-r--r--android/buildSrc/src/main/kotlin/Dependencies.kt24
-rw-r--r--android/buildSrc/src/main/kotlin/Versions.kt11
-rw-r--r--android/config/baseline.xml22
-rw-r--r--android/gradle/verification-metadata.xml305
-rw-r--r--android/lib/billing/build.gradle.kts9
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt58
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt34
-rw-r--r--android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt26
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassNames.kt (renamed from android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassesAndActions.kt)7
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt6
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt3
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt46
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt15
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt45
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt10
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt87
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt91
-rw-r--r--android/lib/daemon-grpc/build.gradle.kts85
-rw-r--r--android/lib/daemon-grpc/src/main/AndroidManifest.xml (renamed from android/lib/ipc/src/main/AndroidManifest.xml)0
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt565
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparator.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt)8
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt140
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt540
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt20
-rw-r--r--android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt24
-rw-r--r--android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt282
-rw-r--r--android/lib/intent-provider/build.gradle.kts (renamed from android/lib/ipc/build.gradle.kts)16
-rw-r--r--android/lib/intent-provider/src/main/AndroidManifest.xml1
-rw-r--r--android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt16
-rw-r--r--android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt44
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt53
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt86
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt38
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt31
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt7
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt14
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt134
-rw-r--r--android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt40
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt6
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt2
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt2
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt2
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt2
-rw-r--r--android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt2
-rw-r--r--android/lib/model/build.gradle.kts8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt21
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt9
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomListName.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListName.kt)2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DefaultDnsOptions.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DefaultDnsOptions.kt)11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt13
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt21
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsOptions.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsOptions.kt)11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsState.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsState.kt)2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt34
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GeoIpLocation.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt)7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt)4
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Latitude.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt)2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt15
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Longitude.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt)2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt28
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt25
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt20
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt15
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt25
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ObfuscationSettings.kt)11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ParameterGenerationError.kt (renamed from android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt)2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt30
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt13
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt88
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt48
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayOverride.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt)11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt9
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt5
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt)16
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SettingsPatchError.kt (renamed from android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt)8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TransportProtocol.kt (renamed from android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt)2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt (renamed from android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt)8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt35
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt35
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt15
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt3
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt40
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt7
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt13
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt9
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt15
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt28
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt28
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt13
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt9
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt16
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt14
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt11
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt23
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt8
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt46
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt12
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt10
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt8
-rw-r--r--android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatLongTest.kt (renamed from android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt)2
-rw-r--r--android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatitudeTest.kt (renamed from android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt)2
-rw-r--r--android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LongitudeTest.kt (renamed from android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt)2
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml16
-rw-r--r--android/lib/shared/build.gradle.kts47
-rw-r--r--android/lib/shared/src/main/AndroidManifest.xml1
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt84
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt26
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt36
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt13
-rw-r--r--android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt11
-rw-r--r--android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt54
-rw-r--r--android/lib/talpid/build.gradle.kts3
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt56
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt30
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt (renamed from android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt)6
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/InetNetwork.kt (renamed from android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt)2
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/TunConfig.kt (renamed from android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt)3
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt8
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt9
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt11
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt6
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt36
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt9
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt76
-rw-r--r--android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt9
-rw-r--r--android/service/build.gradle.kts16
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt102
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt133
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt391
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt334
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt48
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt184
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt56
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt49
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt85
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt136
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomLists.kt55
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt62
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt48
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/PlayPurchaseHandler.kt49
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt109
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt37
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt175
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt147
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt66
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt55
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt46
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/migration/MigrateSplitTunneling.kt51
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt147
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt80
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt97
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannelFactory.kt57
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationManager.kt59
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationProvider.kt8
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ShouldBeOnForegroundProvider.kt7
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt152
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt59
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt62
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt64
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt106
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt143
-rw-r--r--android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt34
-rw-r--r--android/settings.gradle.kts13
-rw-r--r--android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ArchitectureTest.kt2
-rw-r--r--android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/ForgetAllVpnAppsInSettingsTestRule.kt2
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt1
-rw-r--r--android/tile/build.gradle.kts6
-rw-r--r--android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt48
-rw-r--r--android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt132
491 files changed, 8550 insertions, 10512 deletions
diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md
index 811fc2dcc7..2998e2757c 100644
--- a/android/CHANGELOG.md
+++ b/android/CHANGELOG.md
@@ -22,6 +22,10 @@ Line wrap the file at 100 chars. Th
* **Security**: in case of vulnerabilities.
## [Unreleased]
+### Changed
+- Migrate underlaying communication wtih daemon to gRPC. This also implies major changes and
+ improvements throughout the app.
+
### Security
- Fix DNS leaks in blocking states or when no valid DNS has been configured due to an underlying OS
issue. In these cases a dummy DNS will be set to prevent leaks.
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 750b98aa67..798667ff3f 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -174,22 +174,23 @@ android {
"META-INF/LGPL2.1",
// Fixes packaging error caused by: jetified-junit-*
"META-INF/LICENSE.md",
- "META-INF/LICENSE-notice.md"
+ "META-INF/LICENSE-notice.md",
+ "META-INF/io.netty.versions.properties",
+ "META-INF/INDEX.LIST"
)
}
}
applicationVariants.configureEach {
val alwaysShowChangelog =
- gradleLocalProperties(rootProject.projectDir, providers).getProperty("ALWAYS_SHOW_CHANGELOG")
- ?: "false"
+ gradleLocalProperties(rootProject.projectDir, providers)
+ .getProperty("ALWAYS_SHOW_CHANGELOG") ?: "false"
buildConfigField("boolean", "ALWAYS_SHOW_CHANGELOG", alwaysShowChangelog)
val enableInAppVersionNotifications =
gradleLocalProperties(rootProject.projectDir, providers)
- .getProperty("ENABLE_IN_APP_VERSION_NOTIFICATIONS")
- ?: "true"
+ .getProperty("ENABLE_IN_APP_VERSION_NOTIFICATIONS") ?: "true"
buildConfigField(
"boolean",
@@ -305,18 +306,19 @@ afterEvaluate {
play { serviceAccountCredentials.set(file("play-api-key.json")) }
dependencies {
- implementation(project(Dependencies.Mullvad.vpnService))
- implementation(project(Dependencies.Mullvad.tileService))
-
implementation(project(Dependencies.Mullvad.commonLib))
+ implementation(project(Dependencies.Mullvad.daemonGrpc))
implementation(project(Dependencies.Mullvad.endpointLib))
- implementation(project(Dependencies.Mullvad.ipcLib))
+ implementation(project(Dependencies.Mullvad.intentLib))
+ implementation(project(Dependencies.Mullvad.mapLib))
implementation(project(Dependencies.Mullvad.modelLib))
+ implementation(project(Dependencies.Mullvad.paymentLib))
implementation(project(Dependencies.Mullvad.resourceLib))
+ implementation(project(Dependencies.Mullvad.sharedLib))
implementation(project(Dependencies.Mullvad.talpidLib))
+ implementation(project(Dependencies.Mullvad.tileService))
implementation(project(Dependencies.Mullvad.themeLib))
- implementation(project(Dependencies.Mullvad.paymentLib))
- implementation(project(Dependencies.Mullvad.mapLib))
+ implementation(project(Dependencies.Mullvad.vpnService))
// Play implementation
playImplementation(project(Dependencies.Mullvad.billingLib))
@@ -326,6 +328,7 @@ dependencies {
implementation(Dependencies.AndroidX.lifecycleRuntimeKtx)
implementation(Dependencies.AndroidX.lifecycleViewmodelKtx)
implementation(Dependencies.AndroidX.lifecycleRuntimeCompose)
+ implementation(Dependencies.Arrow.core)
implementation(Dependencies.Compose.constrainLayout)
implementation(Dependencies.Compose.foundation)
implementation(Dependencies.Compose.material3)
diff --git a/android/app/lint-baseline.xml b/android/app/lint-baseline.xml
index ae420539c8..0ab48b375d 100644
--- a/android/app/lint-baseline.xml
+++ b/android/app/lint-baseline.xml
@@ -1,16 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.1.4" type="baseline" client="gradle" dependencies="false" name="AGP (8.1.4)" variant="all" version="8.1.4">
-
- <issue
- id="MissingSuperCall"
- message="Overriding method should call `super.onActivityResult`"
- errorLine1=" override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {"
- errorLine2=" ~~~~~~~~~~~~~~~~">
- <location
- file="src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt"
- line="70"
- column="18"/>
- </issue>
+<issues format="6" by="lint 8.3.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0)" variant="all" version="8.3.0">
<issue
id="UseCheckPermission"
@@ -30,7 +19,7 @@
errorLine2=" ~~~~~">
<location
file="src/main/AndroidManifest.xml"
- line="25"
+ line="23"
column="39"/>
</issue>
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index 7d80dc6e39..b72bed1d05 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -18,3 +18,16 @@
-dontwarn org.joda.time.**
-keep class org.joda.time.** { *; }
-keep interface org.joda.time.** { *; }
+
+# grpc
+-keep class io.grpc.okhttp.OkHttpChannelBuilder { *; }
+-keep class mullvad_daemon.management_interface.** { *; }
+-keep class com.google.protobuf.Timestamp { *; }
+-keepnames class com.google.protobuf.** { *; }
+-dontwarn com.google.j2objc.annotations.ReflectionSupport
+-dontwarn com.google.j2objc.annotations.RetainedWith
+-dontwarn com.squareup.okhttp.CipherSuite
+-dontwarn com.squareup.okhttp.ConnectionSpec
+-dontwarn com.squareup.okhttp.TlsVersion
+
+
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/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"
}
diff --git a/android/buildSrc/src/main/kotlin/Dependencies.kt b/android/buildSrc/src/main/kotlin/Dependencies.kt
index 82fe6d70d7..b2417befba 100644
--- a/android/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/android/buildSrc/src/main/kotlin/Dependencies.kt
@@ -28,6 +28,8 @@ object Dependencies {
"androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.AndroidX.lifecycle}"
const val lifecycleRuntimeCompose =
"androidx.lifecycle:lifecycle-runtime-compose:${Versions.AndroidX.lifecycle}"
+ const val lifecycleService =
+ "androidx.lifecycle:lifecycle-service:${Versions.AndroidX.lifecycle}"
const val espressoCore =
"androidx.test.espresso:espresso-core:${Versions.AndroidX.espresso}"
const val testCore = "androidx.test:core:${Versions.AndroidX.test}"
@@ -40,6 +42,12 @@ object Dependencies {
"androidx.test:orchestrator:${Versions.AndroidX.testOrchestrator}"
}
+ object Arrow {
+ const val core = "io.arrow-kt:arrow-core:${Versions.Arrow.base}"
+ const val optics = "io.arrow-kt:arrow-optics:${Versions.Arrow.base}"
+ const val opticsKsp = "io.arrow-kt:arrow-optics-ksp-plugin:${Versions.Arrow.base}"
+ }
+
object Compose {
const val constrainLayout =
"androidx.constraintlayout:constraintlayout-compose:${Versions.Compose.constrainLayout}"
@@ -62,6 +70,15 @@ object Dependencies {
const val uiUtil = "androidx.compose.ui:ui-util:${Versions.Compose.base}"
}
+ object Grpc {
+ const val grpcOkHttp = "io.grpc:grpc-okhttp:${Versions.Grpc.grpcVersion}"
+ const val grpcAndroid = "io.grpc:grpc-android:${Versions.Grpc.grpcVersion}"
+ const val grpcKotlinStub = "io.grpc:grpc-kotlin-stub:${Versions.Grpc.grpcKotlinVersion}"
+ const val protobufLite = "io.grpc:grpc-protobuf-lite:${Versions.Grpc.grpcVersion}"
+ const val protobufKotlinLite =
+ "com.google.protobuf:protobuf-kotlin-lite:${Versions.Grpc.protobufVersion}"
+ }
+
object Koin {
const val core = "io.insert-koin:koin-core:${Versions.Koin.base}"
const val android = "io.insert-koin:koin-android:${Versions.Koin.base}"
@@ -76,6 +93,8 @@ object Dependencies {
}
object KotlinX {
+ const val coroutinesCore =
+ "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.kotlinx}"
const val coroutinesAndroid =
"org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.kotlinx}"
const val coroutinesTest =
@@ -88,12 +107,12 @@ object Dependencies {
}
object Mullvad {
+ const val daemonGrpc = ":lib:daemon-grpc"
const val vpnService = ":service"
const val tileService = ":tile"
const val commonLib = ":lib:common"
const val endpointLib = ":lib:endpoint"
- const val ipcLib = ":lib:ipc"
const val modelLib = ":lib:model"
const val resourceLib = ":lib:resource"
const val talpidLib = ":lib:talpid"
@@ -102,6 +121,8 @@ object Dependencies {
const val billingLib = ":lib:billing"
const val paymentLib = ":lib:payment"
const val mapLib = ":lib:map"
+ const val sharedLib = ":lib:shared"
+ const val intentLib = ":lib:intent-provider"
}
object Plugin {
@@ -130,5 +151,6 @@ object Dependencies {
const val playPublisher =
"com.github.triplet.gradle:play-publisher:${Versions.Plugin.playPublisher}"
const val playPublisherId = "com.github.triplet.play"
+ const val protobufId = "com.google.protobuf"
}
}
diff --git a/android/buildSrc/src/main/kotlin/Versions.kt b/android/buildSrc/src/main/kotlin/Versions.kt
index 606ed2e575..37f0f2c029 100644
--- a/android/buildSrc/src/main/kotlin/Versions.kt
+++ b/android/buildSrc/src/main/kotlin/Versions.kt
@@ -34,6 +34,10 @@ object Versions {
const val uiautomator = "2.3.0"
}
+ object Arrow {
+ const val base = "1.2.3"
+ }
+
object Compose {
const val destinations = "1.10.2"
const val base = "1.6.3"
@@ -42,6 +46,12 @@ object Versions {
const val material3 = "1.2.1"
}
+ object Grpc {
+ const val grpcVersion = "1.63.0"
+ const val grpcKotlinVersion = "1.4.1"
+ const val protobufVersion = "3.25.3"
+ }
+
object Plugin {
// The androidAapt plugin version must be in sync with the android plugin version.
// Required for Gradle metadata verification to work properly, see:
@@ -49,6 +59,7 @@ object Versions {
const val android = "8.3.0"
const val androidAapt = "$android-10880808"
const val playPublisher = "3.9.0"
+ const val protobuf = "0.9.4"
const val dependencyCheck = "9.0.9"
const val detekt = "1.23.5"
const val gradleVersions = "0.51.0"
diff --git a/android/config/baseline.xml b/android/config/baseline.xml
index cccc81b703..e79b02aa94 100644
--- a/android/config/baseline.xml
+++ b/android/config/baseline.xml
@@ -49,8 +49,8 @@
<ID>ConstructorParameterNaming:ExcludedProcessKt.kt$ExcludedProcessKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.ExcludedProcess.Builder</ID>
<ID>ConstructorParameterNaming:ExcludedProcessListKt.kt$ExcludedProcessListKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.ExcludedProcessList.Builder</ID>
<ID>ConstructorParameterNaming:GeoIpLocationKt.kt$GeoIpLocationKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.GeoIpLocation.Builder</ID>
- <ID>ConstructorParameterNaming:GeographicLocationConstraintKt.kt$GeographicLocationConstraintKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.GeographicLocationConstraint.Builder</ID>
- <ID>ConstructorParameterNaming:LocationConstraintKt.kt$LocationConstraintKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.LocationConstraint.Builder</ID>
+ <ID>ConstructorParameterNaming:GeoLocationIdKt.kt$GeoLocationIdKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.GeoLocationId.Builder</ID>
+ <ID>ConstructorParameterNaming:RelayItemIdKt.kt$RelayItemIdKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.RelayItemId.Builder</ID>
<ID>ConstructorParameterNaming:LocationKt.kt$LocationKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.Location.Builder</ID>
<ID>ConstructorParameterNaming:NewAccessMethodSettingKt.kt$NewAccessMethodSettingKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.NewAccessMethodSetting.Builder</ID>
<ID>ConstructorParameterNaming:NormalRelaySettingsKt.kt$NormalRelaySettingsKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.NormalRelaySettings.Builder</ID>
@@ -193,10 +193,10 @@
<ID>FunctionNaming:ExcludedProcessListKt.kt$ExcludedProcessListKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.ExcludedProcessList.Builder, ): Dsl</ID>
<ID>FunctionNaming:GeoIpLocationKt.kt$GeoIpLocationKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.GeoIpLocation</ID>
<ID>FunctionNaming:GeoIpLocationKt.kt$GeoIpLocationKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.GeoIpLocation.Builder, ): Dsl</ID>
- <ID>FunctionNaming:GeographicLocationConstraintKt.kt$GeographicLocationConstraintKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.GeographicLocationConstraint</ID>
- <ID>FunctionNaming:GeographicLocationConstraintKt.kt$GeographicLocationConstraintKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.GeographicLocationConstraint.Builder, ): Dsl</ID>
- <ID>FunctionNaming:LocationConstraintKt.kt$LocationConstraintKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.LocationConstraint</ID>
- <ID>FunctionNaming:LocationConstraintKt.kt$LocationConstraintKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.LocationConstraint.Builder, ): Dsl</ID>
+ <ID>FunctionNaming:GeoLocationIdKt.kt$GeoLocationIdKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.GeoLocationId</ID>
+ <ID>FunctionNaming:GeoLocationIdKt.kt$GeoLocationIdKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.GeoLocationId.Builder, ): Dsl</ID>
+ <ID>FunctionNaming:RelayItemIdKt.kt$RelayItemIdKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.RelayItemId</ID>
+ <ID>FunctionNaming:RelayItemIdKt.kt$RelayItemIdKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.RelayItemId.Builder, ): Dsl</ID>
<ID>FunctionNaming:LocationKt.kt$LocationKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.Location</ID>
<ID>FunctionNaming:LocationKt.kt$LocationKt.Dsl.Companion$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _create( builder: mullvad_daemon.management_interface.ManagementInterface.Location.Builder, ): Dsl</ID>
<ID>FunctionNaming:NewAccessMethodSettingKt.kt$NewAccessMethodSettingKt.Dsl$@kotlin.jvm.JvmSynthetic @kotlin.PublishedApi internal fun _build(): mullvad_daemon.management_interface.ManagementInterface.NewAccessMethodSetting</ID>
@@ -378,7 +378,7 @@
<ID>MaxLineLength:ConnectionConfigKt.kt$public inline</ID>
<ID>MaxLineLength:CustomDnsOptionsKt.kt$CustomDnsOptionsKt.Dsl$@kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class)</ID>
<ID>MaxLineLength:CustomListKt.kt$CustomListKt.Dsl$public</ID>
- <ID>MaxLineLength:CustomListKt.kt$CustomListKt.Dsl$values: kotlin.collections.Iterable&lt;mullvad_daemon.management_interface.ManagementInterface.GeographicLocationConstraint&gt;</ID>
+ <ID>MaxLineLength:CustomListKt.kt$CustomListKt.Dsl$values: kotlin.collections.Iterable&lt;mullvad_daemon.management_interface.ManagementInterface.GeoLocationId&gt;</ID>
<ID>MaxLineLength:CustomListSettingsKt.kt$CustomListSettingsKt.Dsl$public</ID>
<ID>MaxLineLength:CustomRelaySettingsKt.kt$public</ID>
<ID>MaxLineLength:DaemonEventKt.kt$public</ID>
@@ -396,8 +396,8 @@
<ID>MaxLineLength:ErrorStateKt.kt$public</ID>
<ID>MaxLineLength:ExcludedProcessListKt.kt$ExcludedProcessListKt.Dsl$public</ID>
<ID>MaxLineLength:ExcludedProcessListKt.kt$ExcludedProcessListKt.Dsl$values: kotlin.collections.Iterable&lt;mullvad_daemon.management_interface.ManagementInterface.ExcludedProcess&gt;</ID>
- <ID>MaxLineLength:GeographicLocationConstraintKt.kt$GeographicLocationConstraintKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.GeographicLocationConstraint.Builder</ID>
- <ID>MaxLineLength:LocationConstraintKt.kt$public</ID>
+ <ID>MaxLineLength:GeoLocationIdKt.kt$GeoLocationIdKt.Dsl$private val _builder: mullvad_daemon.management_interface.ManagementInterface.GeoLocationId.Builder</ID>
+ <ID>MaxLineLength:RelayItemIdKt.kt$public</ID>
<ID>MaxLineLength:ManagementInterfaceGrpcKt.kt$ManagementServiceGrpcKt$public</ID>
<ID>MaxLineLength:ManagementInterfaceGrpcKt.kt$ManagementServiceGrpcKt.ManagementServiceCoroutineImplBase$"Method mullvad_daemon.management_interface.ManagementService.AddSplitTunnelProcess is unimplemented"</ID>
<ID>MaxLineLength:ManagementInterfaceGrpcKt.kt$ManagementServiceGrpcKt.ManagementServiceCoroutineImplBase$"Method mullvad_daemon.management_interface.ManagementService.ClearSplitTunnelProcesses is unimplemented"</ID>
@@ -540,9 +540,9 @@
<ID>PackageNaming:ExcludedProcessKt.kt$package mullvad_daemon.management_interface</ID>
<ID>PackageNaming:ExcludedProcessListKt.kt$package mullvad_daemon.management_interface</ID>
<ID>PackageNaming:GeoIpLocationKt.kt$package mullvad_daemon.management_interface</ID>
- <ID>PackageNaming:GeographicLocationConstraintKt.kt$package mullvad_daemon.management_interface</ID>
+ <ID>PackageNaming:GeoLocationIdKt.kt$package mullvad_daemon.management_interface</ID>
<ID>PackageNaming:InetNetwork.kt$package net.mullvad.talpid.tun_provider</ID>
- <ID>PackageNaming:LocationConstraintKt.kt$package mullvad_daemon.management_interface</ID>
+ <ID>PackageNaming:RelayItemIdKt.kt$package mullvad_daemon.management_interface</ID>
<ID>PackageNaming:LocationKt.kt$package mullvad_daemon.management_interface</ID>
<ID>PackageNaming:ManagementInterfaceGrpcKt.kt$package mullvad_daemon.management_interface</ID>
<ID>PackageNaming:ManagementInterfaceKt.kt$package mullvad_daemon.management_interface</ID>
diff --git a/android/gradle/verification-metadata.xml b/android/gradle/verification-metadata.xml
index 13a615352c..11faf12b98 100644
--- a/android/gradle/verification-metadata.xml
+++ b/android/gradle/verification-metadata.xml
@@ -111,6 +111,11 @@
<sha256 value="c5769b13afe55023c1dc30c72ea86189ab70aa3ca770ecfb04c970f4d6e6be65" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.annotation" name="annotation" version="1.1.0">
+ <artifact name="annotation-1.1.0.jar">
+ <sha256 value="d38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.annotation" name="annotation" version="1.2.0">
<artifact name="annotation-1.2.0.jar">
<sha256 value="9029262bddce116e6d02be499e4afdba21f24c239087b76b3b57d7e98b490a36" origin="Generated by Gradle"/>
@@ -139,6 +144,9 @@
<artifact name="annotation-1.6.0.module">
<sha256 value="6146b6138643b2ac0590df509dd51abaea769c79fd7602eb217168fe5af78cd2" origin="Generated by Gradle"/>
</artifact>
+ <artifact name="annotation-metadata-1.6.0.jar">
+ <sha256 value="fbc64f5c44a7added8b6eab517cf7d70555e25153bf5d44a6ed9b0e5312f7de9" origin="Generated by Gradle"/>
+ </artifact>
</component>
<component group="androidx.annotation" name="annotation" version="1.7.0">
<artifact name="annotation-1.7.0.module">
@@ -173,6 +181,9 @@
</artifact>
</component>
<component group="androidx.annotation" name="annotation-jvm" version="1.6.0">
+ <artifact name="annotation-jvm-1.6.0.jar">
+ <sha256 value="60b10b5ef5769b79570172e015b8159405c92f034ba88b9391a977589c9deb4e" origin="Generated by Gradle"/>
+ </artifact>
<artifact name="annotation-jvm-1.6.0.module">
<sha256 value="3f5a8faa19de667e63dca9730ff8ef0e478e4bafb5feeb8258e5c086246dc90c" origin="Generated by Gradle"/>
</artifact>
@@ -1114,11 +1125,6 @@
<sha256 value="33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.lifecycle" name="lifecycle-common" version="2.0.0">
- <artifact name="lifecycle-common-2.0.0.jar">
- <sha256 value="7bad7a188804adea6fa1f35d5ef99b705f20bd93ecadde484760ff86b535fefc" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.lifecycle" name="lifecycle-common" version="2.3.1">
<artifact name="lifecycle-common-2.3.1.jar">
<sha256 value="15848fb56db32f4c7cdc72b324003183d52a4884d6bf09be708ac7f587d139b5" origin="Generated by Gradle"/>
@@ -1297,14 +1303,6 @@
<sha256 value="58c9e27371ccf7a22a233f44926d348c9d07e78c41a56588a4265ff6ae76645a" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.lifecycle" name="lifecycle-process" version="2.6.2">
- <artifact name="lifecycle-process-2.6.2.aar">
- <sha256 value="0f33b1bd017f965a6afb2e7bf3e3bd343c624061c1986dea8ba2469d04349437" origin="Generated by Gradle"/>
- </artifact>
- <artifact name="lifecycle-process-2.6.2.module">
- <sha256 value="d927d41903a2ff02ba1b9fefa4c25cb58187b1ce3a054945f43ce6a29918a3f3" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.lifecycle" name="lifecycle-process" version="2.7.0">
<artifact name="lifecycle-process-2.7.0.aar">
<sha256 value="6fb33d9473a4933da9d98a0b12b606127e80681bd9b90309ccd2c2230863b939" origin="Generated by Gradle"/>
@@ -1395,6 +1393,14 @@
<sha256 value="ebc28fc834248353425a95b35a10e8a0edb112deacdc8cc03c3a37d8167f178d" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.lifecycle" name="lifecycle-service" version="2.7.0">
+ <artifact name="lifecycle-service-2.7.0.aar">
+ <sha256 value="e31bd0e92bd0b31b360448ca5a5b80fc1f63507bb4ee8e9bd300437449d1789b" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="lifecycle-service-2.7.0.module">
+ <sha256 value="4786f3f890aa8db65ace14f34370b557e6f82252477f006d285a280eae3990a8" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.3.0">
<artifact name="lifecycle-viewmodel-2.3.0.module">
<sha256 value="feeb5ec453c20d8c1900b0849d2066edc8a41532ce0088d641c4a886bae57a08" origin="Generated by Gradle"/>
@@ -1628,11 +1634,6 @@
<sha256 value="c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.test" name="core" version="1.4.0">
- <artifact name="core-1.4.0.aar">
- <sha256 value="671284e62e393f16ceae1a99a3a9a07bf1aacda29f8fe7b6b884355ef34c09cf" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.test" name="core" version="1.5.0">
<artifact name="core-1.5.0.aar">
<sha256 value="2c06715c0d0843cee2143ab8bb322bb3f34d5247630402fc8c1b6a0eafa15b9f" origin="Generated by Gradle"/>
@@ -2454,6 +2455,11 @@
<sha256 value="d1f3c66aa91ac52549e00ae3b208ba4b9af7d72d68f230643553beb38e6118ac" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.google.errorprone" name="error_prone_annotations" version="2.23.0">
+ <artifact name="error_prone_annotations-2.23.0.jar">
+ <sha256 value="ec6f39f068b6ff9ac323c68e28b9299f8c0a80ca512dccb1d4a70f40ac3ec054" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.google.firebase" name="firebase-encoders" version="17.0.0">
<artifact name="firebase-encoders-17.0.0.jar">
<sha256 value="282a5a703f9b7eb56508dde97ea918e95d73318b157050f457f7a86dca750150" origin="Generated by Gradle"/>
@@ -2479,6 +2485,11 @@
<sha256 value="4e12b232d7f65cb1dcfffe0b44f97f20977288162dcdec9df09255f298003b98" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.google.gradle" name="osdetector-gradle-plugin" version="1.7.3">
+ <artifact name="osdetector-gradle-plugin-1.7.3.jar">
+ <sha256 value="6b4692f913a21b1fb603169ee78ba8f3e4ab2af9d762af9ca88b79126c1c0ad1" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.google.guava" name="failureaccess" version="1.0.1">
<artifact name="failureaccess-1.0.1.jar">
<sha256 value="a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26" origin="Generated by Gradle"/>
@@ -2489,11 +2500,27 @@
<sha256 value="39f3550b0343d8d19dd4e83bd165b58ea3389d2ddb9f2148e63903f79ecdb114" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.google.guava" name="guava" version="32.0.1-android">
+ <artifact name="guava-32.0.1-android.jar">
+ <sha256 value="12429ff9ac33b7afd8b1a5071179757cf4d3c47f0f4099bc0384e644ecbf82dd" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.google.guava" name="guava" version="32.0.1-jre">
<artifact name="guava-32.0.1-jre.jar">
<sha256 value="bd7fa227591fb8509677d0d1122cf95158f3b8a9f45653f58281d879f6dc48c5" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.google.guava" name="guava" version="32.1.3-android">
+ <artifact name="guava-32.1.3-android.jar">
+ <sha256 value="20e6ac8902ddf49e7806cc70f3054c8d91accb5eefdc10f3207e80e0a336b263" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="guava-32.1.3-android.module">
+ <sha256 value="f8a87cef72c0e8027a6fae3cbdf0072a241ce6d8e760f96e7087b742a6605a61" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="guava-32.1.3-jre.jar">
+ <sha256 value="6d4e2b5a118aab62e6e5e29d185a0224eed82c85c40ac3d33cf04a270c3b3744" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.google.guava" name="guava" version="32.1.3-jre">
<artifact name="guava-32.1.3-jre.jar">
<sha256 value="6d4e2b5a118aab62e6e5e29d185a0224eed82c85c40ac3d33cf04a270c3b3744" origin="Generated by Gradle"/>
@@ -2542,6 +2569,11 @@
<sha256 value="193edf97aefa28b93c5892bdc598bac34fa4c396588030084f290b1440e8b98a" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.google.protobuf" name="protobuf-gradle-plugin" version="0.9.4">
+ <artifact name="protobuf-gradle-plugin-0.9.4.jar">
+ <sha256 value="7e554bdec3202ede0a2407f20141d8ca2e9e3ab62429ffa9b21ab0c02f435223" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.google.protobuf" name="protobuf-java" version="3.22.3">
<artifact name="protobuf-java-3.22.3.jar">
<sha256 value="59d388ea6a2d2d76ae8efff7fd4d0c60c6f0f464c3d3ab9be8e5add092975708" origin="Generated by Gradle"/>
@@ -2552,6 +2584,24 @@
<sha256 value="c615f76879dc5c303e4df5b94a6afa39534058c7545db2d483fd95d9f63c8bfe" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.google.protobuf" name="protobuf-javalite" version="3.25.3">
+ <artifact name="protobuf-javalite-3.25.3.jar">
+ <sha256 value="b3cc1b80204c49a31786bd904c78ae5654d4b8490065cd53a3b9019d635000c9" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.google.protobuf" name="protobuf-kotlin-lite" version="3.25.3">
+ <artifact name="protobuf-kotlin-lite-3.25.3.jar">
+ <sha256 value="430410a282a8543365313fdecbf7404e855b3ba64d04be00fe8017c511032890" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="com.google.protobuf" name="protoc" version="3.25.3">
+ <artifact name="protoc-3.25.3-linux-x86_64.exe">
+ <sha256 value="e718992c5733ece01264f160c0890229770791e51c9612a2b34dc17f83ab3773" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="protoc-3.25.3-osx-aarch_64.exe">
+ <sha256 value="af8c1bd4fb34ca9215e32a9911d2b5ccd275d120155c42e8d910dcfecc51362d" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.google.testing.platform" name="android-device-provider-local" version="0.0.9-alpha02">
<artifact name="android-device-provider-local-0.0.9-alpha02.jar">
<sha256 value="446d0fca4e3711e3e37219cf888ec12b4dec3f14999ee439735385fb787e914b" origin="Generated by Gradle"/>
@@ -2824,16 +2874,16 @@
<sha256 value="607e220ff8215b929d829bbf54f332894f1459b4d795979aeafcbcc1cea54cf3" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="com.squareup.okio" name="okio" version="2.2.2">
- <artifact name="okio-2.2.2.jar">
- <sha256 value="e58c97406a6bb1138893750299ac63c6aa04b38b6b49eae1bfcad1a63ef9ba1b" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="com.squareup.okio" name="okio" version="3.2.0">
<artifact name="okio-3.2.0.module">
<sha256 value="681f5cec170de45b95493e9ee705868eb35adf898ee06f96e22d197250c1b104" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.squareup.okio" name="okio" version="3.4.0">
+ <artifact name="okio-3.4.0.module">
+ <sha256 value="69173608417a2113e6fa6afeb7b4540b20df70cfda3fa16c73aaa4fa702ffad3" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.squareup.okio" name="okio" version="3.6.0">
<artifact name="okio-3.6.0.module">
<sha256 value="6a47ac50364e6598459401fb86f9b6cfcdf637b9b3a3045b1cc33cbf4c408218" origin="Generated by Gradle"/>
@@ -2850,6 +2900,14 @@
<sha256 value="a778f39085ed6abfcd687112986c6385aa57ac2d8840b21d946b3c2095f30ea4" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="com.squareup.okio" name="okio-jvm" version="3.4.0">
+ <artifact name="okio-jvm-3.4.0.jar">
+ <sha256 value="0139ec7a506dbbd54cad62291b019cb850534be097c8c66c1000d5fbe8edef3e" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="okio-jvm-3.4.0.module">
+ <sha256 value="6fec13cf3361600364bfcc68004af7c760ceaf1fc1bbcc958742ae4d61db1512" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="com.squareup.okio" name="okio-jvm" version="3.6.0">
<artifact name="okio-jvm-3.6.0.jar">
<sha256 value="67543f0736fc422ae927ed0e504b98bc5e269fda0d3500579337cb713da28412" origin="Generated by Gradle"/>
@@ -2971,6 +3029,126 @@
<sha256 value="9c17ca630fc6f92a3ad41025eee82a2b6d09cce05b82975daa1b8c10420d8a9e" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.arrow-kt" name="arrow-annotations" version="1.2.3">
+ <artifact name="arrow-annotations-1.2.3.module">
+ <sha256 value="bade7720daef525b27c2176ae8d374ddc45f8dae0a3b3aaf26505f60bad2d6f4" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-annotations-metadata-1.2.3.jar">
+ <sha256 value="7757b1d2794d6b85bd6ec588b0e3a50954aaefcb3538566cf66614f19d204daa" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-annotations-jvm" version="1.2.3">
+ <artifact name="arrow-annotations-jvm-1.2.3.jar">
+ <sha256 value="ac1927dbb0e736c93179daef633cf8631a9f1c3481120f5dd1fe939ff92159e9" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-annotations-jvm-1.2.3.module">
+ <sha256 value="74fac2415acbba3a684ec786479fc4f551c7ce1290959208732a975ee0b1a1a4" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-atomic" version="1.2.3">
+ <artifact name="arrow-atomic-1.2.3.module">
+ <sha256 value="afb7e330c44ce46b7039531cf52cf669808ed5726cfae87e0f7a54abf7de4fe5" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-atomic-metadata-1.2.3.jar">
+ <sha256 value="ba91f80c1beb36dfc472a020b0a3902ad4b245828e0e937c04a11f0a0a94431a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-atomic-jvm" version="1.2.3">
+ <artifact name="arrow-atomic-jvm-1.2.3.jar">
+ <sha256 value="a5b69ea5a3e81dcddd870a18881e6347d0bdb1199fa5bd5305eed6896b6ddeab" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-atomic-jvm-1.2.3.module">
+ <sha256 value="860c30ddd4fc86b4dd169e4bfece64661501419309c6471c53ec6cb466a8be6f" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-autoclose" version="1.2.3">
+ <artifact name="arrow-autoclose-1.2.3.module">
+ <sha256 value="387840ab5ac3f84be2ae48154271f22a332bf11dc0edb60c322c4faba2638180" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-autoclose-metadata-1.2.3.jar">
+ <sha256 value="87799eb6b7cc44b18a2fba1af38fbb43a5e057d6b8a3de934181554dc00e1f43" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-autoclose-jvm" version="1.2.3">
+ <artifact name="arrow-autoclose-jvm-1.2.3.jar">
+ <sha256 value="125f9800e0bbfb9b8fa21c0a7a740fdde6f822fc4acc68b0480753cdb1cbd17f" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-autoclose-jvm-1.2.3.module">
+ <sha256 value="beace0a9954fbed210430d66b5467d42bfd36a368606c444712d4827354202f6" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-continuations" version="1.2.3">
+ <artifact name="arrow-continuations-1.2.3.module">
+ <sha256 value="e1a2bfce36818c8e0b8f1f870ea64effccbf9b2c23966eab3ac9d1cfe84927fc" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-continuations-metadata-1.2.3.jar">
+ <sha256 value="3101c2680ab998878b601b07bbed040747c9c6451a37808d8790ac87c549e0ef" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-continuations-jvm" version="1.2.3">
+ <artifact name="arrow-continuations-jvm-1.2.3.jar">
+ <sha256 value="6fd09cac2f4a43c97db16c8db62e67b9e3e4d834b8df5a02cb84f09b4dffec89" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-continuations-jvm-1.2.3.module">
+ <sha256 value="50e92e1f858b340c1839159440a6a4f76d3a8a4c3c726d67dc61345abd122bde" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-core" version="1.2.3">
+ <artifact name="arrow-core-1.2.3.module">
+ <sha256 value="f0af428f82c17f10ce01d803eaeaa3a4c9afa326698da3ab13684e1f610993e5" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-core-metadata-1.2.3.jar">
+ <sha256 value="0d161002b94259546a94e180df0dc448cbee7313d64b7a706dfa0684cfa65e96" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-core-jvm" version="1.2.3">
+ <artifact name="arrow-core-jvm-1.2.3.jar">
+ <sha256 value="66fdcf7d26475d0d94704079aed38b3401236072315da15c44245feadf0a8d67" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-core-jvm-1.2.3.module">
+ <sha256 value="1ff5a88b276200b1a9e7b7dbd6c1f183f86a2010c8e077f82056a2e72c0fffe7" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-fx-coroutines" version="1.2.3">
+ <artifact name="arrow-fx-coroutines-1.2.3.module">
+ <sha256 value="5987e27c5b5b6b9a6657e394f614d4f1f548b5896cf1c218ea8d50b53a37f2a5" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-fx-coroutines-metadata-1.2.3.jar">
+ <sha256 value="491b616943afe2e11ae237c892c2c525e32ad7ca94a2cb6812f27677a8aa705f" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-fx-coroutines-jvm" version="1.2.3">
+ <artifact name="arrow-fx-coroutines-jvm-1.2.3.jar">
+ <sha256 value="b13dce3b153338fbbd23bbe6fa8d218f4b0ab13e82c07bc7851379828dfe00fb" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-fx-coroutines-jvm-1.2.3.module">
+ <sha256 value="07fe2a0a10ebb377e2ecf83f8bfcd5d7d14edccfa5f9c036c79c1ef5d0e97d12" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-optics" version="1.2.3">
+ <artifact name="arrow-optics-1.2.3.module">
+ <sha256 value="eeb3691404db8a52866f3db713ddbd919fdfaa9a6302437d3b7f3b8c5aed213b" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-optics-metadata-1.2.3.jar">
+ <sha256 value="8bf4081ce4050a0d02f2061d00720a6645d616f0f5504840bd3935a4430c9c14" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-optics-jvm" version="1.2.3">
+ <artifact name="arrow-optics-jvm-1.2.3.jar">
+ <sha256 value="f83e7e20842341ece1814b19a5bccdeb3bc1aad46fdfc56a4b5125ae8ed1c456" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-optics-jvm-1.2.3.module">
+ <sha256 value="7e6fbd063a906e573287ab113399bff5a4fb92b0c43cae96d3750b36cc097989" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.arrow-kt" name="arrow-optics-ksp-plugin" version="1.2.3">
+ <artifact name="arrow-optics-ksp-plugin-1.2.3.jar">
+ <sha256 value="a84ff4659fe064523db5a50507aed46701c231c4f684b604078a3e462d1c4333" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="arrow-optics-ksp-plugin-1.2.3.module">
+ <sha256 value="1458ec6f2cff3f22df3f57ff712e93d15c2540ee242243e167ed6d9f106bfcf8" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.github.davidburstrom.contester" name="contester-breakpoint" version="0.2.0">
<artifact name="contester-breakpoint-0.2.0.jar">
<sha256 value="672cbebb5d45a72b35dd81fd6127e187451bb6fb7fba35315bbdf2f57cfce835" origin="Generated by Gradle"/>
@@ -3229,26 +3407,59 @@
<sha256 value="3432fc9644176c4ad94bf7bc9a6bc29826ea4dd0b12004cdc65e209014846f65" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-android" version="1.63.0">
+ <artifact name="grpc-android-1.63.0.aar">
+ <sha256 value="2544920ea5f46720dcf75e8200a374d0d376fdc8f294287bb57975bab382a639" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.grpc" name="grpc-api" version="1.57.0">
<artifact name="grpc-api-1.57.0.jar">
<sha256 value="8d2c384299f84ee8aa7f670f00e7cb26b87e231cf3091474307b32b76910f71c" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-api" version="1.63.0">
+ <artifact name="grpc-api-1.63.0.jar">
+ <sha256 value="21d747911e1e5931004f1b058417f3c3f72f1fbf8aea16f5fc6af7a3f0caf35a" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.grpc" name="grpc-context" version="1.57.0">
<artifact name="grpc-context-1.57.0.jar">
<sha256 value="953fcacd82f531e69b76e3834f5830bad4c22ae84144e058d71dc80a7430275d" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-context" version="1.63.0">
+ <artifact name="grpc-context-1.63.0.jar">
+ <sha256 value="d7f50185bb858131d02314de23ea3cc797131ed98b215e845429f45a81dd5fed" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.grpc" name="grpc-core" version="1.57.0">
<artifact name="grpc-core-1.57.0.jar">
<sha256 value="3bee48c73bc4c5b55bed79be0e484adf26ba56bebbe5798ddbf34714ef1e1cea" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-core" version="1.63.0">
+ <artifact name="grpc-core-1.63.0.jar">
+ <sha256 value="246e4e583cc11c70ed15cc8f988eff8e76fed5c06426e8310d51c4da6f5cc81b" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.grpc" name="grpc-kotlin-stub" version="1.4.1">
+ <artifact name="grpc-kotlin-stub-1.4.1.jar">
+ <sha256 value="9403d4c826039dc869f036087569cc686b3c901da2d5be3db75d154ddd3f8209" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="grpc-kotlin-stub-1.4.1.module">
+ <sha256 value="98e854a64bb3313158bd93a669900fc2804bbb72c3b748b96acfa14dc47e609d" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.grpc" name="grpc-netty" version="1.57.0">
<artifact name="grpc-netty-1.57.0.jar">
<sha256 value="81d43f2d4ed18fa341bd840a3735f1403a70074a046e157e27f679b721b4c9ad" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-okhttp" version="1.63.0">
+ <artifact name="grpc-okhttp-1.63.0.jar">
+ <sha256 value="5673e22fc937234983b16f2ff0228e8955ffd3a497bdd707b77726d3b72a3f62" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.grpc" name="grpc-protobuf" version="1.57.0">
<artifact name="grpc-protobuf-1.57.0.jar">
<sha256 value="49f986d4eab12610fdba4a6890fca52d5eb653598916fdb863a366d5e28eecf7" origin="Generated by Gradle"/>
@@ -3259,11 +3470,39 @@
<sha256 value="2c507c02d981b84a21763d44e09af4f279881dd3e25be3080f6361258607f198" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-protobuf-lite" version="1.63.0">
+ <artifact name="grpc-protobuf-lite-1.63.0.jar">
+ <sha256 value="5e7f36c03600c7cfa8e10d2d0321f0ba8c32d74cd044873f44b026704f355fb7" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.grpc" name="grpc-stub" version="1.57.0">
<artifact name="grpc-stub-1.57.0.jar">
<sha256 value="6e6ee141539fa14d9fa479f7f511605544443c7e011e78e273cf9468aa183060" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="io.grpc" name="grpc-stub" version="1.57.2">
+ <artifact name="grpc-stub-1.57.2.jar">
+ <sha256 value="84d2af12719168f76375f2afdfd6eb5133a865edba9244d40e6b968e3adde1d3" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.grpc" name="grpc-util" version="1.63.0">
+ <artifact name="grpc-util-1.63.0.jar">
+ <sha256 value="b15006f22e76a6631dfb137a6930aaf9e0cb3a797499ec42394d79641fb770ee" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.grpc" name="protoc-gen-grpc-java" version="1.63.0">
+ <artifact name="protoc-gen-grpc-java-1.63.0-linux-x86_64.exe">
+ <sha256 value="0e3e8db80ba1fbddeed97ea3220b52cfaa95764ff8bf00716df7322883ce47e8" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="protoc-gen-grpc-java-1.63.0-osx-aarch_64.exe">
+ <sha256 value="28290117a2ee9ea60f50f94273ab139dc2b3be4b8f2a557bef7e6efefee5b363" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
+ <component group="io.grpc" name="protoc-gen-grpc-kotlin" version="1.4.1">
+ <artifact name="protoc-gen-grpc-kotlin-1.4.1-jdk8.jar">
+ <sha256 value="62a9956b4c9aad4a06ecbbb5d425e078a0391ec57c5a26bbfbbb19f6717bcd69" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="io.insert-koin" name="koin-android" version="3.5.3">
<artifact name="koin-android-3.5.3.aar">
<sha256 value="73e86381fa4c7969c0a97fc8ce9cbec3be7f7e1ef24c085abc4b1b5c3b512d6c" origin="Generated by Gradle"/>
@@ -3552,6 +3791,11 @@
<sha256 value="8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="kr.motd.maven" name="os-maven-plugin" version="1.7.1">
+ <artifact name="os-maven-plugin-1.7.1.jar">
+ <sha256 value="f47aeef86821e52b2b18758978bd045f03d722292e32e747082122c6228952e0" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="net.bytebuddy" name="byte-buddy" version="1.14.6">
<artifact name="byte-buddy-1.14.6.jar">
<sha256 value="6eaf0190ee02731820e9925a544e7cfb48f6dfc3bf29e6dd87d267ad59862df0" origin="Generated by Gradle"/>
@@ -4105,11 +4349,6 @@
<sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.9.10">
- <artifact name="kotlin-reflect-1.9.10.jar">
- <sha256 value="8a835f5176355083668aff0ed6eef5b3eb030e10e89679ed3eeb703fd2d5b900" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.9.20">
<artifact name="kotlin-reflect-1.9.20.jar">
<sha256 value="49b66f9a89d50fd2954c2e8aeac80e4f488b0a09322a25efad6261576713dc0f" origin="Generated by Gradle"/>
@@ -4208,6 +4447,11 @@
<sha256 value="03a5c3965cc37051128e64e46748e394b6bd4c97fa81c6de6fc72bfd44e3421b" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.0">
+ <artifact name="kotlin-stdlib-1.9.0.jar">
+ <sha256 value="35aeffbe2db5aa446072cee50fcee48b7fa9e2fc51ca37c0cc7d7d0bc39d952e" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.20">
<artifact name="kotlin-stdlib-1.9.20-all.jar">
<sha256 value="cec38bc3302e72a8aaf9cde436b5a9071ee0331e2ad05e84d8bb897334d7e9d4" origin="Generated by Gradle"/>
@@ -4258,6 +4502,11 @@
<sha256 value="d0c2365e2437ef70f34586d50f055743f79716bcfe65e4bc7239cdd2669ef7c5" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.0">
+ <artifact name="kotlin-stdlib-common-1.9.0.jar">
+ <sha256 value="283274204bd7c020789ec46f8f8e72af4244d7f550b3392a57e5ca006ad7aa2c" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.20">
<artifact name="kotlin-stdlib-common-1.9.20.module">
<sha256 value="858828bc5191b9e602affa14e01d66489dafb08c4c18d2faee3cbed7ba7d9992" origin="Generated by Gradle"/>
diff --git a/android/lib/billing/build.gradle.kts b/android/lib/billing/build.gradle.kts
index 26cc345556..6bb4e5e7a6 100644
--- a/android/lib/billing/build.gradle.kts
+++ b/android/lib/billing/build.gradle.kts
@@ -49,12 +49,15 @@ dependencies {
//Model
implementation(project(Dependencies.Mullvad.modelLib))
- //IPC
- implementation(project(Dependencies.Mullvad.ipcLib))
-
//Payment library
implementation(project(Dependencies.Mullvad.paymentLib))
+ //Either
+ implementation(Dependencies.Arrow.core)
+
+ // Management service
+ implementation(project(Dependencies.Mullvad.daemonGrpc))
+
// Test dependencies
testRuntimeOnly(Dependencies.junitEngine)
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt
index 76df623ada..8b3ad66171 100644
--- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt
@@ -15,15 +15,14 @@ import net.mullvad.mullvadvpn.lib.billing.extension.toPaymentStatus
import net.mullvad.mullvadvpn.lib.billing.extension.toPurchaseResult
import net.mullvad.mullvadvpn.lib.billing.model.BillingException
import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent
+import net.mullvad.mullvadvpn.lib.model.PlayPurchase
+import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken
import net.mullvad.mullvadvpn.lib.payment.PaymentRepository
import net.mullvad.mullvadvpn.lib.payment.ProductIds
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
-import net.mullvad.mullvadvpn.model.PlayPurchase
-import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
class BillingPaymentRepository(
private val billingRepository: BillingRepository,
@@ -74,19 +73,20 @@ class BillingPaymentRepository(
// Get transaction id
emit(PurchaseResult.FetchingObfuscationId)
- val obfuscatedId: String =
- when (val result = initialisePurchase()) {
- is PlayPurchaseInitResult.Ok -> result.obfuscatedId
- else -> {
- emit(PurchaseResult.Error.TransactionIdError(productId, null))
- return@flow
- }
- }
+ val obfuscatedId: PlayPurchasePaymentToken =
+ initialisePurchase()
+ .fold(
+ {
+ emit(PurchaseResult.Error.TransactionIdError(productId, null))
+ return@flow
+ },
+ { it }
+ )
val result =
billingRepository.startPurchaseFlow(
productDetails = productDetails,
- obfuscatedId = obfuscatedId,
+ obfuscatedId = obfuscatedId.value,
activityProvider = activityProvider
)
@@ -115,11 +115,13 @@ class BillingPaymentRepository(
emit(PurchaseResult.Completed.Pending)
} else {
emit(PurchaseResult.VerificationStarted)
- if (verifyPurchase(event.purchases.first()) == PlayPurchaseVerifyResult.Ok) {
- emit(PurchaseResult.Completed.Success)
- } else {
- emit(PurchaseResult.Error.VerificationError(null))
- }
+ emit(
+ verifyPurchase(event.purchases.first())
+ .fold(
+ { PurchaseResult.Error.VerificationError(null) },
+ { PurchaseResult.Completed.Success }
+ )
+ )
}
}
PurchaseEvent.UserCanceled -> emit(event.toPurchaseResult())
@@ -135,13 +137,12 @@ class BillingPaymentRepository(
val purchases = purchasesResult.nonPendingPurchases()
if (purchases.isNotEmpty()) {
emit(VerificationResult.VerificationStarted)
- val verificationResult = verifyPurchase(purchases.first())
emit(
- when (verificationResult) {
- is PlayPurchaseVerifyResult.Error ->
- VerificationResult.Error.VerificationError(null)
- PlayPurchaseVerifyResult.Ok -> VerificationResult.Success
- }
+ verifyPurchase(purchases.first())
+ .fold(
+ { VerificationResult.Error.VerificationError(null) },
+ { VerificationResult.Success }
+ )
)
} else {
emit(VerificationResult.NothingToVerify)
@@ -152,16 +153,13 @@ class BillingPaymentRepository(
}
}
- private suspend fun initialisePurchase(): PlayPurchaseInitResult {
- return playPurchaseRepository.initializePlayPurchase()
- }
+ private suspend fun initialisePurchase() = playPurchaseRepository.initializePlayPurchase()
- private suspend fun verifyPurchase(purchase: Purchase): PlayPurchaseVerifyResult {
- return playPurchaseRepository.verifyPlayPurchase(
+ private suspend fun verifyPurchase(purchase: Purchase) =
+ playPurchaseRepository.verifyPlayPurchase(
PlayPurchase(
productId = purchase.products.first(),
- purchaseToken = purchase.purchaseToken,
+ purchaseToken = PlayPurchasePaymentToken(purchase.purchaseToken),
)
)
- }
}
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt
index ac71372f76..8e89cb8f95 100644
--- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt
@@ -1,33 +1,11 @@
package net.mullvad.mullvadvpn.lib.billing
-import kotlinx.coroutines.flow.first
-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.PlayPurchase
-import net.mullvad.mullvadvpn.model.PlayPurchaseInitError
-import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyError
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.PlayPurchase
-class PlayPurchaseRepository(private val messageHandler: MessageHandler) {
- suspend fun initializePlayPurchase(): PlayPurchaseInitResult {
- val result = messageHandler.trySendRequest(Request.InitPlayPurchase)
+class PlayPurchaseRepository(private val managementService: ManagementService) {
+ suspend fun initializePlayPurchase() = managementService.initializePlayPurchase()
- return if (result) {
- messageHandler.events<Event.PlayPurchaseInitResultEvent>().first().result
- } else {
- PlayPurchaseInitResult.Error(PlayPurchaseInitError.OtherError)
- }
- }
-
- suspend fun verifyPlayPurchase(purchase: PlayPurchase): PlayPurchaseVerifyResult {
- val result = messageHandler.trySendRequest(Request.VerifyPlayPurchase(purchase))
- return if (result) {
- messageHandler.events<Event.PlayPurchaseVerifyResultEvent>().first().result
- } else {
- PlayPurchaseVerifyResult.Error(PlayPurchaseVerifyError.OtherError)
- }
- }
+ suspend fun verifyPlayPurchase(purchase: PlayPurchase) =
+ managementService.verifyPlayPurchase(purchase)
}
diff --git a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt
index c4d1b04905..ad716cd30c 100644
--- a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt
+++ b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt
@@ -1,6 +1,8 @@
package net.mullvad.mullvadvpn.lib.billing
import app.cash.turbine.test
+import arrow.core.left
+import arrow.core.right
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ProductDetails
@@ -17,14 +19,13 @@ import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.billing.extension.toPaymentProduct
import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.model.PlayPurchaseInitError
+import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken
+import net.mullvad.mullvadvpn.lib.model.PlayPurchaseVerifyError
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.PlayPurchaseInitError
-import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyError
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -170,7 +171,7 @@ class BillingPaymentRepositoryTest {
coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns
mockProductDetailsResult
coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns
- PlayPurchaseInitResult.Error(PlayPurchaseInitError.OtherError)
+ PlayPurchaseInitError.OtherError.left()
// Act, Assert
paymentRepository.purchaseProduct(mockProductId, mockk()).test {
@@ -206,7 +207,7 @@ class BillingPaymentRepositoryTest {
)
} returns mockBillingResult
coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns
- PlayPurchaseInitResult.Ok("MOCK")
+ PlayPurchasePaymentToken("MOCK").right()
// Act, Assert
paymentRepository.purchaseProduct(mockProductId, mockk()).test {
@@ -241,7 +242,7 @@ class BillingPaymentRepositoryTest {
)
} returns mockBillingResult
coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns
- PlayPurchaseInitResult.Ok(mockObfuscatedId)
+ PlayPurchasePaymentToken(mockObfuscatedId).right()
// Act, Assert
paymentRepository.purchaseProduct(mockProductId, mockk()).test {
@@ -283,9 +284,9 @@ class BillingPaymentRepositoryTest {
)
} returns mockBillingResult
coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns
- PlayPurchaseInitResult.Ok("MOCK-ID")
+ PlayPurchasePaymentToken("MOCK-ID").right()
coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns
- PlayPurchaseVerifyResult.Error(PlayPurchaseVerifyError.OtherError)
+ PlayPurchaseVerifyError.OtherError.left()
// Act, Assert
paymentRepository.purchaseProduct(mockProductId, mockk()).test {
@@ -326,9 +327,8 @@ class BillingPaymentRepositoryTest {
)
} returns mockBillingResult
coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns
- PlayPurchaseInitResult.Ok("MOCK")
- coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns
- PlayPurchaseVerifyResult.Ok
+ PlayPurchasePaymentToken("MOCK").right()
+ coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns Unit.right()
// Act, Assert
paymentRepository.purchaseProduct(mockProductId, mockk()).test {
@@ -368,7 +368,7 @@ class BillingPaymentRepositoryTest {
)
} returns mockBillingResult
coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns
- PlayPurchaseInitResult.Ok("MOCK")
+ PlayPurchasePaymentToken("MOCK").right()
// Act, Assert
paymentRepository.purchaseProduct(mockProductId, mockk()).test {
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassesAndActions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassNames.kt
index 09210ffa03..1636bbd46f 100644
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassesAndActions.kt
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassNames.kt
@@ -2,13 +2,8 @@ package net.mullvad.mullvadvpn.lib.common.constant
// Do not use in cases where the application id is expected since the application id will differ
// between different builds.
-private const val MULLVAD_PACKAGE_NAME = "net.mullvad.mullvadvpn"
+internal const val MULLVAD_PACKAGE_NAME = "net.mullvad.mullvadvpn"
// Classes
const val MAIN_ACTIVITY_CLASS = "$MULLVAD_PACKAGE_NAME.ui.MainActivity"
const val VPN_SERVICE_CLASS = "$MULLVAD_PACKAGE_NAME.service.MullvadVpnService"
-
-// Actions
-const val KEY_CONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.connect_action"
-const val KEY_DISCONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.disconnect_action"
-const val KEY_QUIT_ACTION = "$MULLVAD_PACKAGE_NAME.quit_action"
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt
new file mode 100644
index 0000000000..ea420f2d0a
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.common.constant
+
+// Actions
+const val KEY_CONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.connect_action"
+const val KEY_DISCONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.disconnect_action"
+const val KEY_REQUEST_VPN_PERMISSION = "$MULLVAD_PACKAGE_NAME.request_vpn_permission"
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt
new file mode 100644
index 0000000000..d2ae3f1871
--- /dev/null
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.common.constant
+
+const val TAG = "mullvad"
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt
index 42f0663967..bf94c80778 100644
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt
@@ -1,47 +1,9 @@
package net.mullvad.mullvadvpn.lib.common.util
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.IBinder
-import android.util.Log
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.SendChannel
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import net.mullvad.mullvadvpn.model.ServiceResult
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.withTimeoutOrNull
-fun <T> SendChannel<T>.safeOffer(element: T): Boolean {
- return runCatching { trySend(element).isSuccess }.getOrDefault(false)
-}
-
-fun Context.bindServiceFlow(intent: Intent, flags: Int = 0): Flow<ServiceResult> = callbackFlow {
- val connectionCallback =
- object : ServiceConnection {
- override fun onServiceConnected(className: ComponentName, binder: IBinder) {
- safeOffer(ServiceResult(binder))
- }
-
- override fun onServiceDisconnected(className: ComponentName) {
- safeOffer(ServiceResult.NOT_CONNECTED)
- bindService(intent, this, flags)
- }
- }
-
- bindService(intent, connectionCallback, flags)
-
- awaitClose {
- safeOffer(ServiceResult.NOT_CONNECTED)
-
- Dispatchers.Default.dispatch(EmptyCoroutineContext) {
- try {
- unbindService(connectionCallback)
- } catch (e: IllegalArgumentException) {
- Log.e("mullvad", "Cannot unbind as no binding exists.")
- }
- }
- }
+suspend fun <T> Flow<T>.firstOrNullWithTimeout(timeMillis: Long): T? {
+ return withTimeoutOrNull(timeMillis) { firstOrNull() }
}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt
index 8ef70dad92..d714dae327 100644
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt
@@ -4,15 +4,20 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.Settings
-import net.mullvad.mullvadvpn.lib.common.R
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.getInstalledPackagesList
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app"
-fun Context.openAccountPageInBrowser(authToken: String) {
- startActivity(
- Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.account_url) + "?token=$authToken"))
- )
+fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): Uri {
+ val urlString = buildString {
+ append(accountUri)
+ if (websiteAuthToken != null) {
+ append("?token=")
+ append(websiteAuthToken.value)
+ }
+ }
+ return Uri.parse(urlString)
}
fun Context.getAlwaysOnVpnAppName(): String? {
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt
deleted file mode 100644
index 7fc37a752c..0000000000
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package net.mullvad.mullvadvpn.lib.common.util
-
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.reflect.KClass
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedSendChannelException
-import kotlinx.coroutines.channels.SendChannel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.consumeAsFlow
-
-class DispatchingFlow<T : Any>(private val upstream: Flow<T>) : Flow<T> {
- private val subscribers = ConcurrentHashMap<KClass<out T>, SendChannel<T>>()
-
- fun <V : T> subscribe(variant: KClass<V>, capacity: Int = Channel.CONFLATED): Flow<V> {
- val channel = Channel<V>(capacity)
-
- // This is safe because `collect` will only send to this channel if the instance class is V
- @Suppress("UNCHECKED_CAST")
- subscribers[variant] = channel as SendChannel<T>
-
- return channel.consumeAsFlow()
- }
-
- fun <V : T> unsubscribe(variant: KClass<V>) = subscribers.remove(variant)
-
- @InternalCoroutinesApi
- override suspend fun collect(collector: FlowCollector<T>) {
- upstream.collect { event ->
- try {
- subscribers[event::class]?.send(event)
- } catch (closedException: ClosedSendChannelException) {
- subscribers.remove(event::class)
- }
-
- collector.emit(event)
- }
-
- subscribers.clear()
- }
-}
-
-fun <T : Any> Flow<T>.dispatchTo(configureSubscribers: DispatchingFlow<T>.() -> Unit) =
- DispatchingFlow(this).also(configureSubscribers)
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt
index f906ee8f6d..2c9554a842 100644
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt
+++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt
@@ -2,9 +2,9 @@ package net.mullvad.mullvadvpn.lib.common.util
import android.content.Context
import net.mullvad.mullvadvpn.lib.common.R
-import net.mullvad.talpid.tunnel.ErrorState
-import net.mullvad.talpid.tunnel.ErrorStateCause
-import net.mullvad.talpid.tunnel.ParameterGenerationError
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
import net.mullvad.talpid.util.addressString
fun ErrorState.getErrorNotificationResources(context: Context): ErrorNotificationMessage {
@@ -48,8 +48,8 @@ fun ErrorStateCause.errorMessageId(): Int {
is ErrorStateCause.InvalidDnsServers -> R.string.invalid_dns_servers
is ErrorStateCause.AuthFailed -> R.string.auth_failed
is ErrorStateCause.Ipv6Unavailable -> R.string.ipv6_unavailable
- is ErrorStateCause.SetFirewallPolicyError -> R.string.set_firewall_policy_error
- is ErrorStateCause.SetDnsError -> R.string.set_dns_error
+ is ErrorStateCause.FirewallPolicyError -> R.string.set_firewall_policy_error
+ is ErrorStateCause.DnsError -> R.string.set_dns_error
is ErrorStateCause.StartTunnelError -> R.string.start_tunnel_error
is ErrorStateCause.IsOffline -> R.string.is_offline
is ErrorStateCause.TunnelParameterError -> {
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt
deleted file mode 100644
index 448d96778f..0000000000
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-package net.mullvad.mullvadvpn.lib.common.util
-
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.Semaphore
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.sync.withPermit
-import net.mullvad.talpid.util.EventNotifier
-
-// Wrapper to allow awaiting for intermittent values.
-//
-// Wraps a property that is changed from time to time and that can become unavailable (null). This
-// behaves in a way similar to `CompletableDeferred`, but the value can be set and reset multiple
-// times.
-//
-// Calling `await` will either provide the value if it's available, or suspend until it becomes
-// available and then return it.
-//
-// Calling `update` will set the internal value after it guarantees that no other coroutine is
-// currently reading the value (through a permit from the semaphore). After the value is set, it
-// provides a permit to the semaphore so that suspended coroutines can use the new value.
-//
-// Extra initialization can be done on the intermittent value when it becomes available and before
-// it is provided to the awaiting coroutines, through the use of listener callbacks. These are
-// called after the value is updated but before it is made available to the coroutines.
-class Intermittent<T> {
- private val notifier = EventNotifier<T?>(null)
- private val semaphore = Semaphore(1, 1)
- private val writeLock = Mutex()
-
- private var updateJob: Job? = null
- private var value by notifier.notifiable()
-
- // When the internal value is updated, listeners can be notified before the awaiting coroutines
- // resume execution. This allows performing any extra initialization before the value is made
- // available for usage.
- fun registerListener(id: Any, listener: (T?) -> Unit) = notifier.subscribe(id, listener)
-
- fun unregisterListener(id: Any) = notifier.unsubscribe(id)
-
- suspend fun await(): T {
- return semaphore.withPermit { value!! }
- }
-
- suspend fun update(newValue: T?) {
- writeLock.withLock {
- if (newValue != value) {
- if (value != null) {
- semaphore.acquire()
- }
-
- // This will trigger the listeners to run before the awaiting coroutines resume
- value = newValue
-
- if (newValue != null) {
- semaphore.release()
- }
- }
- }
- }
-
- // Helper method that spawns a coroutine to update the value.
- fun spawnUpdate(newValue: T?) {
- synchronized(this@Intermittent) {
- val previousUpdate = updateJob
-
- updateJob =
- GlobalScope.launch(Dispatchers.Default) {
- previousUpdate?.join()
- update(newValue)
- }
- }
- }
-
- // Helper method that provides a simple way to change the wrapped value.
- // The method returns a property delegate that will spawn a coroutine to update the wrapped
- // value every time the property is written to.
- fun source() = observable<T?>(null) { _, _, newValue -> spawnUpdate(newValue) }
-
- fun onDestroy() {
- notifier.unsubscribeAll()
- }
-}
diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt
deleted file mode 100644
index edb76ed4ae..0000000000
--- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package net.mullvad.mullvadvpn.lib.common.util
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-
-class JobTracker {
- private val jobs = HashMap<Long, Job>()
- private val reaperJobs = HashMap<Long, Job>()
- private val namedJobs = HashMap<String, Long>()
-
- private var jobIdCounter = 0L
-
- fun newJob(job: Job): Long {
- synchronized(jobs) {
- val jobId = jobIdCounter
-
- jobIdCounter += 1
-
- jobs.put(jobId, job)
-
- reaperJobs.put(
- jobId,
- GlobalScope.launch(Dispatchers.Default) {
- job.join()
-
- synchronized(jobs) { jobs.remove(jobId) }
- }
- )
-
- return jobId
- }
- }
-
- fun newJob(name: String, job: Job): Long {
- synchronized(namedJobs) {
- cancelJob(name)
-
- val newJobId = newJob(job)
-
- namedJobs.put(name, newJobId)
-
- return newJobId
- }
- }
-
- fun newBackgroundJob(name: String, jobBody: suspend () -> Unit): Long {
- return newJob(name, GlobalScope.launch(Dispatchers.Default) { jobBody() })
- }
-
- fun newUiJob(name: String, jobBody: suspend () -> Unit): Long {
- return newJob(name, GlobalScope.launch(Dispatchers.Main) { jobBody() })
- }
-
- suspend fun <T> runOnBackground(jobBody: suspend () -> T): T {
- val job = GlobalScope.async(Dispatchers.Default) { jobBody() }
-
- newJob(job)
-
- return job.await()
- }
-
- fun cancelJob(name: String) {
- synchronized(namedJobs) { namedJobs.remove(name)?.let { oldJobId -> cancelJob(oldJobId) } }
- }
-
- fun cancelJob(jobId: Long) {
- synchronized(jobs) {
- jobs.remove(jobId)?.cancel()
- reaperJobs.remove(jobId)?.cancel()
- }
- }
-
- fun cancelAllJobs() {
- synchronized(jobs) {
- for (job in jobs.values) {
- job.cancel()
- }
-
- for (job in reaperJobs.values) {
- job.cancel()
- }
-
- jobs.clear()
- reaperJobs.clear()
- namedJobs.clear()
- }
- }
-}
diff --git a/android/lib/daemon-grpc/build.gradle.kts b/android/lib/daemon-grpc/build.gradle.kts
new file mode 100644
index 0000000000..ed33aa4d09
--- /dev/null
+++ b/android/lib/daemon-grpc/build.gradle.kts
@@ -0,0 +1,85 @@
+import com.google.protobuf.gradle.proto
+
+plugins {
+ id(Dependencies.Plugin.androidLibraryId)
+ id(Dependencies.Plugin.kotlinAndroidId)
+ id(Dependencies.Plugin.kotlinParcelizeId)
+ id(Dependencies.Plugin.protobufId) version Versions.Plugin.protobuf
+ id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.lib.daemon.grpc"
+ compileSdk = Versions.Android.compileSdkVersion
+
+ defaultConfig { minSdk = Versions.Android.minSdkVersion }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions { jvmTarget = Versions.jvmTarget }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+
+ sourceSets {
+ getByName("main") {
+ proto { srcDir("${rootProject.projectDir}/../mullvad-management-interface/proto") }
+ }
+ }
+}
+
+protobuf {
+ protoc { artifact = "com.google.protobuf:protoc:${Versions.Grpc.protobufVersion}" }
+ plugins {
+ create("java") { artifact = "io.grpc:protoc-gen-grpc-java:${Versions.Grpc.grpcVersion}" }
+ create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:${Versions.Grpc.grpcVersion}" }
+ create("grpckt") {
+ artifact = "io.grpc:protoc-gen-grpc-kotlin:${Versions.Grpc.grpcKotlinVersion}:jdk8@jar"
+ }
+ }
+ generateProtoTasks {
+ all().forEach {
+ it.plugins {
+ create("java") { option("lite") }
+ create("grpc") { option("lite") }
+ create("grpckt") { option("lite") }
+ }
+ it.builtins { create("kotlin") { option("lite") } }
+ }
+ }
+}
+
+dependencies {
+ implementation(project(Dependencies.Mullvad.commonLib))
+ implementation(project(Dependencies.Mullvad.modelLib))
+ implementation(project(Dependencies.Mullvad.talpidLib))
+
+ implementation(Dependencies.jodaTime)
+ implementation(Dependencies.Kotlin.stdlib)
+ implementation(Dependencies.KotlinX.coroutinesCore)
+ implementation(Dependencies.KotlinX.coroutinesAndroid)
+
+ implementation(Dependencies.Grpc.grpcOkHttp)
+ implementation(Dependencies.Grpc.grpcAndroid)
+ implementation(Dependencies.Grpc.grpcKotlinStub)
+ implementation(Dependencies.Grpc.protobufLite)
+ implementation(Dependencies.Grpc.protobufKotlinLite)
+
+ implementation(Dependencies.Arrow.core)
+ implementation(Dependencies.Arrow.optics)
+
+ testImplementation(project(Dependencies.Mullvad.commonTestLib))
+ testImplementation(Dependencies.Kotlin.test)
+ testImplementation(Dependencies.KotlinX.coroutinesTest)
+ testImplementation(Dependencies.MockK.core)
+ testImplementation(Dependencies.turbine)
+ testImplementation(Dependencies.junitApi)
+ testRuntimeOnly(Dependencies.junitEngine)
+ testImplementation(Dependencies.junitParams)
+}
diff --git a/android/lib/ipc/src/main/AndroidManifest.xml b/android/lib/daemon-grpc/src/main/AndroidManifest.xml
index cc947c5679..cc947c5679 100644
--- a/android/lib/ipc/src/main/AndroidManifest.xml
+++ b/android/lib/daemon-grpc/src/main/AndroidManifest.xml
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
new file mode 100644
index 0000000000..80b79b707a
--- /dev/null
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt
@@ -0,0 +1,565 @@
+package net.mullvad.mullvadvpn.lib.daemon.grpc
+
+import android.net.LocalSocketAddress
+import android.util.Log
+import arrow.core.Either
+import arrow.optics.copy
+import arrow.optics.dsl.index
+import arrow.optics.typeclasses.Index
+import com.google.protobuf.BoolValue
+import com.google.protobuf.Empty
+import com.google.protobuf.StringValue
+import com.google.protobuf.UInt32Value
+import io.grpc.ConnectivityState
+import io.grpc.Status
+import io.grpc.StatusException
+import io.grpc.android.UdsChannelBuilder
+import java.net.InetAddress
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mullvad_daemon.management_interface.ManagementInterface
+import mullvad_daemon.management_interface.ManagementServiceGrpcKt
+import net.mullvad.mullvadvpn.lib.common.constant.TAG
+import net.mullvad.mullvadvpn.lib.daemon.grpc.mapper.fromDomain
+import net.mullvad.mullvadvpn.lib.daemon.grpc.mapper.toDomain
+import net.mullvad.mullvadvpn.lib.daemon.grpc.util.LogInterceptor
+import net.mullvad.mullvadvpn.lib.daemon.grpc.util.connectivityFlow
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.AddSplitTunnelingAppError
+import net.mullvad.mullvadvpn.lib.model.AppId
+import net.mullvad.mullvadvpn.lib.model.AppVersionInfo as ModelAppVersionInfo
+import net.mullvad.mullvadvpn.lib.model.ClearAllOverridesError
+import net.mullvad.mullvadvpn.lib.model.ConnectError
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.CreateAccountError
+import net.mullvad.mullvadvpn.lib.model.CreateCustomListError
+import net.mullvad.mullvadvpn.lib.model.CustomList as ModelCustomList
+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.DeleteCustomListError
+import net.mullvad.mullvadvpn.lib.model.DeleteDeviceError
+import net.mullvad.mullvadvpn.lib.model.Device
+import net.mullvad.mullvadvpn.lib.model.DeviceId
+import net.mullvad.mullvadvpn.lib.model.DeviceState as ModelDeviceState
+import net.mullvad.mullvadvpn.lib.model.DnsOptions as ModelDnsOptions
+import net.mullvad.mullvadvpn.lib.model.DnsOptions
+import net.mullvad.mullvadvpn.lib.model.DnsState as ModelDnsState
+import net.mullvad.mullvadvpn.lib.model.GetAccountDataError
+import net.mullvad.mullvadvpn.lib.model.GetAccountHistoryError
+import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
+import net.mullvad.mullvadvpn.lib.model.GetDeviceStateError
+import net.mullvad.mullvadvpn.lib.model.LoginAccountError
+import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings as ModelObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.Ownership as ModelOwnership
+import net.mullvad.mullvadvpn.lib.model.PlayPurchase
+import net.mullvad.mullvadvpn.lib.model.PlayPurchaseInitError
+import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken
+import net.mullvad.mullvadvpn.lib.model.PlayPurchaseVerifyError
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState as ModelQuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess
+import net.mullvad.mullvadvpn.lib.model.RelayConstraints
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId as ModelRelayItemId
+import net.mullvad.mullvadvpn.lib.model.RelayList as ModelRelayList
+import net.mullvad.mullvadvpn.lib.model.RelayList
+import net.mullvad.mullvadvpn.lib.model.RelaySettings
+import net.mullvad.mullvadvpn.lib.model.RemoveSplitTunnelingAppError
+import net.mullvad.mullvadvpn.lib.model.SetAllowLanError
+import net.mullvad.mullvadvpn.lib.model.SetAutoConnectError
+import net.mullvad.mullvadvpn.lib.model.SetDnsOptionsError
+import net.mullvad.mullvadvpn.lib.model.SetObfuscationOptionsError
+import net.mullvad.mullvadvpn.lib.model.SetRelayLocationError
+import net.mullvad.mullvadvpn.lib.model.SetWireguardConstraintsError
+import net.mullvad.mullvadvpn.lib.model.SetWireguardMtuError
+import net.mullvad.mullvadvpn.lib.model.SetWireguardQuantumResistantError
+import net.mullvad.mullvadvpn.lib.model.Settings as ModelSettings
+import net.mullvad.mullvadvpn.lib.model.SettingsPatchError
+import net.mullvad.mullvadvpn.lib.model.TunnelState as ModelTunnelState
+import net.mullvad.mullvadvpn.lib.model.UnknownCustomListError
+import net.mullvad.mullvadvpn.lib.model.UpdateCustomListError
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
+import net.mullvad.mullvadvpn.lib.model.WireguardConstraints as ModelWireguardConstraints
+import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData as ModelWireguardEndpointData
+import net.mullvad.mullvadvpn.lib.model.addresses
+import net.mullvad.mullvadvpn.lib.model.customOptions
+import net.mullvad.mullvadvpn.lib.model.location
+import net.mullvad.mullvadvpn.lib.model.ownership
+import net.mullvad.mullvadvpn.lib.model.providers
+import net.mullvad.mullvadvpn.lib.model.relayConstraints
+import net.mullvad.mullvadvpn.lib.model.state
+import net.mullvad.mullvadvpn.lib.model.wireguardConstraints
+
+@Suppress("TooManyFunctions")
+class ManagementService(
+ rpcSocketPath: String,
+ private val extensiveLogging: Boolean,
+ private val scope: CoroutineScope,
+) {
+ private var job: Job? = null
+
+ private val channel =
+ UdsChannelBuilder.forPath(rpcSocketPath, LocalSocketAddress.Namespace.FILESYSTEM).build()
+
+ val connectionState: StateFlow<GrpcConnectivityState> =
+ channel
+ .connectivityFlow()
+ .map(ConnectivityState::toDomain)
+ .stateIn(scope, SharingStarted.Eagerly, channel.getState(false).toDomain())
+
+ private val grpc =
+ ManagementServiceGrpcKt.ManagementServiceCoroutineStub(channel)
+ .withExecutor(Dispatchers.IO.asExecutor())
+ .let {
+ if (extensiveLogging) {
+ it.withInterceptors(LogInterceptor())
+ } else it
+ }
+ .withWaitForReady()
+
+ private val _mutableDeviceState = MutableStateFlow<ModelDeviceState?>(null)
+ val deviceState: Flow<ModelDeviceState> = _mutableDeviceState.filterNotNull()
+
+ private val _mutableTunnelState = MutableStateFlow<ModelTunnelState?>(null)
+ val tunnelState: Flow<ModelTunnelState> = _mutableTunnelState.filterNotNull()
+
+ private val _mutableSettings = MutableStateFlow<ModelSettings?>(null)
+ val settings: Flow<ModelSettings> = _mutableSettings.filterNotNull()
+
+ private val _mutableVersionInfo = MutableStateFlow<ModelAppVersionInfo?>(null)
+ val versionInfo: Flow<ModelAppVersionInfo> = _mutableVersionInfo.filterNotNull()
+
+ private val _mutableRelayList = MutableStateFlow<RelayList?>(null)
+ val relayList: Flow<RelayList> = _mutableRelayList.filterNotNull()
+
+ val relayCountries: Flow<List<RelayItem.Location.Country>> =
+ relayList.mapNotNull { it.countries }
+
+ val wireguardEndpointData: Flow<ModelWireguardEndpointData> =
+ relayList.mapNotNull { it.wireguardEndpointData }
+
+ fun start() {
+ // Just to ensure that connection is set up since the connection won't be setup without a
+ // call to the daemon
+ if (job != null) {
+ error("ManagementService already started")
+ }
+
+ job = scope.launch { subscribeEvents() }
+ }
+
+ fun stop() {
+ job?.cancel(message = "ManagementService stopped")
+ ?: error("ManagementService already stopped")
+ job = null
+ }
+
+ private suspend fun subscribeEvents() =
+ withContext(Dispatchers.IO) {
+ launch {
+ grpc.eventsListen(Empty.getDefaultInstance()).collect { event ->
+ if (extensiveLogging) {
+ Log.d(TAG, "Event: $event")
+ }
+ @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
+ when (event.eventCase) {
+ ManagementInterface.DaemonEvent.EventCase.TUNNEL_STATE ->
+ _mutableTunnelState.update { event.tunnelState.toDomain() }
+ ManagementInterface.DaemonEvent.EventCase.SETTINGS ->
+ _mutableSettings.update { event.settings.toDomain() }
+ ManagementInterface.DaemonEvent.EventCase.RELAY_LIST ->
+ _mutableRelayList.update { event.relayList.toDomain() }
+ ManagementInterface.DaemonEvent.EventCase.VERSION_INFO ->
+ _mutableVersionInfo.update { event.versionInfo.toDomain() }
+ ManagementInterface.DaemonEvent.EventCase.DEVICE ->
+ _mutableDeviceState.update { event.device.newState.toDomain() }
+ ManagementInterface.DaemonEvent.EventCase.REMOVE_DEVICE -> {}
+ ManagementInterface.DaemonEvent.EventCase.EVENT_NOT_SET -> {}
+ ManagementInterface.DaemonEvent.EventCase.NEW_ACCESS_METHOD -> {}
+ }
+ }
+ }
+ getInitialServiceState()
+ }
+
+ suspend fun getDevice(): Either<GetDeviceStateError, ModelDeviceState> =
+ Either.catch { grpc.getDevice(Empty.getDefaultInstance()) }
+ .map { it.toDomain() }
+ .mapLeft { GetDeviceStateError.Unknown(it) }
+
+ suspend fun getDeviceList(token: AccountToken): Either<GetDeviceListError, List<Device>> =
+ Either.catch { grpc.listDevices(StringValue.of(token.value)) }
+ .map { it.devicesList.map(ManagementInterface.Device::toDomain) }
+ .mapLeft { GetDeviceListError.Unknown(it) }
+
+ suspend fun removeDevice(
+ token: AccountToken,
+ deviceId: DeviceId
+ ): Either<DeleteDeviceError, Unit> =
+ Either.catch {
+ grpc.removeDevice(
+ ManagementInterface.DeviceRemoval.newBuilder()
+ .setAccountToken(token.value)
+ .setDeviceId(deviceId.value.toString())
+ .build(),
+ )
+ }
+ .mapEmpty()
+ .mapLeft { DeleteDeviceError.Unknown(it) }
+
+ suspend fun connect(): Either<ConnectError, Boolean> =
+ Either.catch { grpc.connectTunnel(Empty.getDefaultInstance()).value }
+ .mapLeft(ConnectError::Unknown)
+
+ suspend fun disconnect(): Boolean = grpc.disconnectTunnel(Empty.getDefaultInstance()).value
+
+ suspend fun reconnect(): Boolean = grpc.reconnectTunnel(Empty.getDefaultInstance()).value
+
+ private suspend fun getTunnelState(): ModelTunnelState =
+ grpc.getTunnelState(Empty.getDefaultInstance()).toDomain()
+
+ private suspend fun getSettings(): ModelSettings =
+ grpc.getSettings(Empty.getDefaultInstance()).toDomain()
+
+ private suspend fun getDeviceState(): ModelDeviceState =
+ grpc.getDevice(Empty.getDefaultInstance()).toDomain()
+
+ private suspend fun getRelayList(): ModelRelayList =
+ grpc.getRelayLocations(Empty.getDefaultInstance()).toDomain()
+
+ private suspend fun getVersionInfo(): ModelAppVersionInfo =
+ grpc.getVersionInfo(Empty.getDefaultInstance()).toDomain()
+
+ suspend fun logoutAccount() {
+ grpc.logoutAccount(Empty.getDefaultInstance())
+ }
+
+ suspend fun loginAccount(accountToken: AccountToken): Either<LoginAccountError, Unit> =
+ Either.catch { grpc.loginAccount(StringValue.of(accountToken.value)) }
+ .mapLeftStatus {
+ when (it.status.code) {
+ Status.Code.UNAUTHENTICATED -> LoginAccountError.InvalidAccount
+ Status.Code.RESOURCE_EXHAUSTED ->
+ LoginAccountError.MaxDevicesReached(accountToken)
+ Status.Code.UNAVAILABLE -> LoginAccountError.RpcError
+ else -> LoginAccountError.Unknown(it)
+ }
+ }
+ .mapEmpty()
+
+ suspend fun clearAccountHistory() {
+ grpc.clearAccountHistory(Empty.getDefaultInstance())
+ }
+
+ suspend fun getAccountHistory(): Either<GetAccountHistoryError, AccountToken?> =
+ Either.catch {
+ val history = grpc.getAccountHistory(Empty.getDefaultInstance())
+ if (history.hasToken()) {
+ AccountToken(history.token.value)
+ } else {
+ null
+ }
+ }
+ .mapLeft(GetAccountHistoryError::Unknown)
+
+ private suspend fun getInitialServiceState() {
+ withContext(Dispatchers.IO) {
+ awaitAll(
+ async { _mutableTunnelState.update { getTunnelState() } },
+ async { _mutableDeviceState.update { getDeviceState() } },
+ async { _mutableSettings.update { getSettings() } },
+ async { _mutableVersionInfo.update { getVersionInfo() } },
+ async { _mutableRelayList.update { getRelayList() } },
+ )
+ }
+ }
+
+ suspend fun getAccountData(
+ accountToken: AccountToken
+ ): Either<GetAccountDataError, AccountData> =
+ Either.catch { grpc.getAccountData(StringValue.of(accountToken.value)).toDomain() }
+ .mapLeft(GetAccountDataError::Unknown)
+
+ suspend fun createAccount(): Either<CreateAccountError, AccountToken> =
+ Either.catch {
+ val accountTokenStringValue = grpc.createNewAccount(Empty.getDefaultInstance())
+ AccountToken(accountTokenStringValue.value)
+ }
+ .mapLeft(CreateAccountError::Unknown)
+
+ suspend fun setDnsOptions(dnsOptions: ModelDnsOptions): Either<SetDnsOptionsError, Unit> =
+ Either.catch { grpc.setDnsOptions(dnsOptions.fromDomain()) }
+ .mapLeft(SetDnsOptionsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setDnsState(dnsState: ModelDnsState): Either<SetDnsOptionsError, Unit> =
+ Either.catch {
+ val currentDnsOptions = getSettings().tunnelOptions.dnsOptions
+ val updated = DnsOptions.state.set(currentDnsOptions, dnsState)
+ grpc.setDnsOptions(updated.fromDomain())
+ }
+ .mapLeft(SetDnsOptionsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setCustomDns(index: Int, address: InetAddress): Either<SetDnsOptionsError, Unit> =
+ Either.catch {
+ val currentDnsOptions = getSettings().tunnelOptions.dnsOptions
+ val updatedDnsOptions =
+ DnsOptions.customOptions.addresses
+ .index(Index.list(), index)
+ .set(currentDnsOptions, address)
+
+ grpc.setDnsOptions(updatedDnsOptions.fromDomain())
+ }
+ .mapLeft(SetDnsOptionsError::Unknown)
+ .mapEmpty()
+
+ suspend fun addCustomDns(address: InetAddress): Either<SetDnsOptionsError, Unit> =
+ Either.catch {
+ val currentDnsOptions = getSettings().tunnelOptions.dnsOptions
+ val updatedDnsOptions =
+ DnsOptions.customOptions.addresses.modify(currentDnsOptions) { it + address }
+ grpc.setDnsOptions(updatedDnsOptions.fromDomain())
+ }
+ .mapLeft(SetDnsOptionsError::Unknown)
+ .mapEmpty()
+
+ suspend fun deleteCustomDns(address: InetAddress): Either<SetDnsOptionsError, Unit> =
+ Either.catch {
+ val currentDnsOptions = getSettings().tunnelOptions.dnsOptions
+ val updatedDnsOptions =
+ DnsOptions.customOptions.addresses.modify(currentDnsOptions) { it - address }
+ grpc.setDnsOptions(updatedDnsOptions.fromDomain())
+ }
+ .mapLeft(SetDnsOptionsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setWireguardMtu(value: Int): Either<SetWireguardMtuError, Unit> =
+ Either.catch { grpc.setWireguardMtu(UInt32Value.of(value)) }
+ .mapLeft(SetWireguardMtuError::Unknown)
+ .mapEmpty()
+
+ suspend fun resetWireguardMtu(): Either<SetWireguardMtuError, Unit> =
+ Either.catch { grpc.setWireguardMtu(UInt32Value.newBuilder().clearValue().build()) }
+ .mapLeft(SetWireguardMtuError::Unknown)
+ .mapEmpty()
+
+ suspend fun setWireguardQuantumResistant(
+ value: ModelQuantumResistantState
+ ): Either<SetWireguardQuantumResistantError, Unit> =
+ Either.catch { grpc.setQuantumResistantTunnel(value.toDomain()) }
+ .mapLeft(SetWireguardQuantumResistantError::Unknown)
+ .mapEmpty()
+
+ // Todo needs to be more advanced
+ suspend fun setRelaySettings(value: RelaySettings) {
+ grpc.setRelaySettings(value.fromDomain())
+ }
+
+ suspend fun setObfuscationOptions(
+ value: ModelObfuscationSettings
+ ): Either<SetObfuscationOptionsError, Unit> =
+ Either.catch { grpc.setObfuscationSettings(value.fromDomain()) }
+ .mapLeft(SetObfuscationOptionsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setAutoConnect(isEnabled: Boolean): Either<SetAutoConnectError, Unit> =
+ Either.catch { grpc.setAutoConnect(BoolValue.of(isEnabled)) }
+ .mapLeft(SetAutoConnectError::Unknown)
+ .mapEmpty()
+
+ suspend fun setAllowLan(allow: Boolean): Either<SetAllowLanError, Unit> =
+ Either.catch { grpc.setAllowLan(BoolValue.of(allow)) }
+ .mapLeft(SetAllowLanError::Unknown)
+ .mapEmpty()
+
+ suspend fun setRelayLocation(location: ModelRelayItemId): Either<SetRelayLocationError, Unit> =
+ Either.catch {
+ val currentRelaySettings = getSettings().relaySettings
+ val updatedRelaySettings =
+ RelaySettings.relayConstraints.location.set(
+ currentRelaySettings,
+ Constraint.Only(location),
+ )
+ grpc.setRelaySettings(updatedRelaySettings.fromDomain())
+ }
+ .mapLeft(SetRelayLocationError::Unknown)
+ .mapEmpty()
+
+ suspend fun createCustomList(
+ name: CustomListName
+ ): Either<CreateCustomListError, CustomListId> =
+ Either.catch { grpc.createCustomList(StringValue.of(name.value)) }
+ .map { CustomListId(it.value) }
+ .mapLeftStatus {
+ when (it.status.code) {
+ Status.Code.ALREADY_EXISTS -> CustomListAlreadyExists
+ else -> UnknownCustomListError(it)
+ }
+ }
+
+ suspend fun updateCustomList(customList: ModelCustomList): Either<UpdateCustomListError, Unit> =
+ Either.catch { grpc.updateCustomList(customList.fromDomain()) }
+ .mapLeft(::UnknownCustomListError)
+ .mapEmpty()
+
+ suspend fun deleteCustomList(id: CustomListId): Either<DeleteCustomListError, Unit> =
+ Either.catch { grpc.deleteCustomList(StringValue.of(id.value)) }
+ .mapLeft(::UnknownCustomListError)
+ .mapEmpty()
+
+ suspend fun clearAllRelayOverrides(): Either<ClearAllOverridesError, Unit> =
+ Either.catch { grpc.clearAllRelayOverrides(Empty.getDefaultInstance()) }
+ .mapLeft(ClearAllOverridesError::Unknown)
+ .mapEmpty()
+
+ suspend fun applySettingsPatch(json: String): Either<SettingsPatchError, Unit> =
+ Either.catch { grpc.applyJsonSettings(StringValue.of(json)) }
+ .mapLeftStatus {
+ when (it.status.code) {
+ // Currently we only get invalid argument errors from daemon via gRPC
+ Status.Code.INVALID_ARGUMENT -> SettingsPatchError.ParsePatch
+ else -> SettingsPatchError.ApplyPatch
+ }
+ }
+ .mapEmpty()
+
+ suspend fun setWireguardConstraints(
+ value: ModelWireguardConstraints
+ ): Either<SetWireguardConstraintsError, Unit> =
+ Either.catch {
+ val relaySettings = getSettings().relaySettings
+ val updated =
+ RelaySettings.relayConstraints.wireguardConstraints.set(relaySettings, value)
+ grpc.setRelaySettings(updated.fromDomain())
+ }
+ .mapLeft(SetWireguardConstraintsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setOwnershipAndProviders(
+ ownershipConstraint: Constraint<ModelOwnership>,
+ providersConstraint: Constraint<Providers>
+ ): Either<SetWireguardConstraintsError, Unit> =
+ Either.catch {
+ val relaySettings = getSettings().relaySettings
+ val updated =
+ relaySettings.copy {
+ inside(RelaySettings.relayConstraints) {
+ RelayConstraints.providers set providersConstraint
+ RelayConstraints.ownership set ownershipConstraint
+ }
+ }
+ grpc.setRelaySettings(updated.fromDomain())
+ }
+ .mapLeft(SetWireguardConstraintsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setOwnership(
+ ownership: Constraint<ModelOwnership>
+ ): Either<SetWireguardConstraintsError, Unit> =
+ Either.catch {
+ val relaySettings = getSettings().relaySettings
+ val updated = RelaySettings.relayConstraints.ownership.set(relaySettings, ownership)
+ grpc.setRelaySettings(updated.fromDomain())
+ }
+ .mapLeft(SetWireguardConstraintsError::Unknown)
+ .mapEmpty()
+
+ suspend fun setProviders(
+ providersConstraint: Constraint<Providers>
+ ): Either<SetWireguardConstraintsError, Unit> =
+ Either.catch {
+ val relaySettings = getSettings().relaySettings
+ val updated =
+ RelaySettings.relayConstraints.providers.set(relaySettings, providersConstraint)
+ grpc.setRelaySettings(updated.fromDomain())
+ }
+ .mapLeft(SetWireguardConstraintsError::Unknown)
+ .mapEmpty()
+
+ suspend fun submitVoucher(voucher: String): Either<RedeemVoucherError, RedeemVoucherSuccess> =
+ Either.catch { grpc.submitVoucher(StringValue.of(voucher)).toDomain() }
+ .mapLeftStatus {
+ when (it.status.code) {
+ Status.Code.INVALID_ARGUMENT,
+ Status.Code.NOT_FOUND -> RedeemVoucherError.InvalidVoucher
+ Status.Code.ALREADY_EXISTS,
+ Status.Code.RESOURCE_EXHAUSTED -> RedeemVoucherError.VoucherAlreadyUsed
+ Status.Code.UNAVAILABLE -> RedeemVoucherError.RpcError
+ else -> RedeemVoucherError.Unknown(it)
+ }
+ }
+
+ suspend fun initializePlayPurchase(): Either<PlayPurchaseInitError, PlayPurchasePaymentToken> =
+ Either.catch { grpc.initPlayPurchase(Empty.getDefaultInstance()).toDomain() }
+ .mapLeft { PlayPurchaseInitError.OtherError }
+
+ suspend fun verifyPlayPurchase(purchase: PlayPurchase): Either<PlayPurchaseVerifyError, Unit> =
+ Either.catch { grpc.verifyPlayPurchase(purchase.fromDomain()) }
+ .mapLeft { PlayPurchaseVerifyError.OtherError }
+ .mapEmpty()
+
+ suspend fun addSplitTunnelingApp(app: AppId): Either<AddSplitTunnelingAppError, Unit> =
+ Either.catch { grpc.addSplitTunnelApp(StringValue.of(app.value)) }
+ .mapLeft(AddSplitTunnelingAppError::Unknown)
+ .mapEmpty()
+
+ suspend fun removeSplitTunnelingApp(app: AppId): Either<RemoveSplitTunnelingAppError, Unit> =
+ Either.catch { grpc.removeSplitTunnelApp(StringValue.of(app.value)) }
+ .mapLeft(RemoveSplitTunnelingAppError::Unknown)
+ .mapEmpty()
+
+ suspend fun setSplitTunnelingState(
+ enabled: Boolean
+ ): Either<RemoveSplitTunnelingAppError, Unit> =
+ Either.catch { grpc.setSplitTunnelState(BoolValue.of(enabled)) }
+ .mapLeft(RemoveSplitTunnelingAppError::Unknown)
+ .mapEmpty()
+
+ suspend fun getWebsiteAuthToken(): Either<Throwable, WebsiteAuthToken> =
+ Either.catch { grpc.getWwwAuthToken(Empty.getDefaultInstance()) }
+ .map { WebsiteAuthToken.fromString(it.value) }
+
+ private fun <A> Either<A, Empty>.mapEmpty() = map {}
+
+ private inline fun <B, C> Either<Throwable, B>.mapLeftStatus(
+ f: (StatusException) -> C
+ ): Either<C, B> = mapLeft {
+ if (it is StatusException) {
+ f(it)
+ } else {
+ throw it
+ }
+ }
+}
+
+sealed interface GrpcConnectivityState {
+ data object Connecting : GrpcConnectivityState
+
+ data object Ready : GrpcConnectivityState
+
+ data object Idle : GrpcConnectivityState
+
+ data object TransientFailure : GrpcConnectivityState
+
+ data object Shutdown : GrpcConnectivityState
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparator.kt
index c062fd1466..a1b1d3b092 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayNameComparator.kt
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparator.kt
@@ -1,7 +1,9 @@
-package net.mullvad.mullvadvpn.relaylist
+package net.mullvad.mullvadvpn.lib.daemon.grpc
-internal object RelayNameComparator : Comparator<RelayItem.Relay> {
- override fun compare(o1: RelayItem.Relay, o2: RelayItem.Relay): Int {
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+
+internal object RelayNameComparator : Comparator<RelayItem.Location.Relay> {
+ override fun compare(o1: RelayItem.Location.Relay, o2: RelayItem.Location.Relay): Int {
val partitions1 = o1.name.split(regex)
val partitions2 = o2.name.split(regex)
return if (partitions1.size > partitions2.size) partitions1 compareWith partitions2
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt
new file mode 100644
index 0000000000..df4625228f
--- /dev/null
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt
@@ -0,0 +1,140 @@
+package net.mullvad.mullvadvpn.lib.daemon.grpc.mapper
+
+import mullvad_daemon.management_interface.ManagementInterface
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions
+import net.mullvad.mullvadvpn.lib.model.CustomList
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+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.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.PlayPurchase
+import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.Providers
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.model.RelaySettings
+import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+
+internal fun Constraint<RelayItemId>.fromDomain(): ManagementInterface.LocationConstraint =
+ ManagementInterface.LocationConstraint.newBuilder()
+ .apply {
+ when (this@fromDomain) {
+ is Constraint.Any -> {}
+ is Constraint.Only -> {
+ when (val relayItemId = value) {
+ is CustomListId -> setCustomList(relayItemId.value)
+ is GeoLocationId -> setLocation(relayItemId.fromDomain())
+ }
+ }
+ }
+ }
+ .build()
+
+internal fun Constraint<Providers>.fromDomain(): List<String> =
+ when (this) {
+ is Constraint.Any -> emptyList()
+ is Constraint.Only -> value.providers.map { it.value }
+ }
+
+internal fun DnsOptions.fromDomain(): ManagementInterface.DnsOptions =
+ ManagementInterface.DnsOptions.newBuilder()
+ .setState(state.fromDomain())
+ .setCustomOptions(customOptions.fromDomain())
+ .setDefaultOptions(defaultOptions.fromDomain())
+ .build()
+
+internal fun DnsState.fromDomain(): ManagementInterface.DnsOptions.DnsState =
+ when (this) {
+ DnsState.Default -> ManagementInterface.DnsOptions.DnsState.DEFAULT
+ DnsState.Custom -> ManagementInterface.DnsOptions.DnsState.CUSTOM
+ }
+
+internal fun CustomDnsOptions.fromDomain(): ManagementInterface.CustomDnsOptions =
+ ManagementInterface.CustomDnsOptions.newBuilder()
+ .addAllAddresses(addresses.map { it.hostAddress })
+ .build()
+
+internal fun DefaultDnsOptions.fromDomain(): ManagementInterface.DefaultDnsOptions =
+ ManagementInterface.DefaultDnsOptions.newBuilder()
+ .setBlockAds(blockAds)
+ .setBlockGambling(blockGambling)
+ .setBlockMalware(blockMalware)
+ .setBlockTrackers(blockTrackers)
+ .setBlockAdultContent(blockAdultContent)
+ .setBlockSocialMedia(blockSocialMedia)
+ .build()
+
+internal fun ObfuscationSettings.fromDomain(): ManagementInterface.ObfuscationSettings =
+ ManagementInterface.ObfuscationSettings.newBuilder()
+ .setSelectedObfuscation(selectedObfuscation.toDomain())
+ .setUdp2Tcp(udp2tcp.toDomain())
+ .build()
+
+internal fun GeoLocationId.fromDomain(): ManagementInterface.GeographicLocationConstraint =
+ ManagementInterface.GeographicLocationConstraint.newBuilder()
+ .apply {
+ when (val id = this@fromDomain) {
+ is GeoLocationId.Country -> setCountry(id.countryCode)
+ is GeoLocationId.City -> setCountry(id.countryCode.countryCode).setCity(id.cityCode)
+ is GeoLocationId.Hostname ->
+ setCountry(id.country.countryCode)
+ .setCity(id.city.cityCode)
+ .setHostname(id.hostname)
+ }
+ }
+ .build()
+
+internal fun CustomList.fromDomain(): ManagementInterface.CustomList =
+ ManagementInterface.CustomList.newBuilder()
+ .setId(id.value)
+ .setName(name.value)
+ .addAllLocations(locations.map { it.fromDomain() })
+ .build()
+
+internal fun WireguardConstraints.fromDomain(): ManagementInterface.WireguardConstraints =
+ when (port) {
+ is Constraint.Any -> ManagementInterface.WireguardConstraints.newBuilder().build()
+ is Constraint.Only ->
+ ManagementInterface.WireguardConstraints.newBuilder()
+ .setPort((port as Constraint.Only<Port>).value.value)
+ .build()
+ }
+
+internal fun Ownership.fromDomain(): ManagementInterface.Ownership =
+ when (this) {
+ Ownership.MullvadOwned -> ManagementInterface.Ownership.MULLVAD_OWNED
+ Ownership.Rented -> ManagementInterface.Ownership.RENTED
+ }
+
+internal fun RelaySettings.fromDomain(): ManagementInterface.RelaySettings =
+ ManagementInterface.RelaySettings.newBuilder()
+ .setNormal(
+ ManagementInterface.NormalRelaySettings.newBuilder()
+ .setTunnelType(ManagementInterface.TunnelType.WIREGUARD)
+ .setWireguardConstraints(relayConstraints.wireguardConstraints.fromDomain())
+ .setOpenvpnConstraints(ManagementInterface.OpenvpnConstraints.getDefaultInstance())
+ .setLocation(relayConstraints.location.fromDomain())
+ .setOwnership(relayConstraints.ownership.fromDomain())
+ .addAllProviders(relayConstraints.providers.fromDomain())
+ .build()
+ )
+ .build()
+
+internal fun Constraint<Ownership>.fromDomain(): ManagementInterface.Ownership =
+ when (this) {
+ Constraint.Any -> ManagementInterface.Ownership.ANY
+ is Constraint.Only -> value.fromDomain()
+ }
+
+internal fun PlayPurchasePaymentToken.fromDomain(): ManagementInterface.PlayPurchasePaymentToken =
+ ManagementInterface.PlayPurchasePaymentToken.newBuilder().setToken(value).build()
+
+internal fun PlayPurchase.fromDomain(): ManagementInterface.PlayPurchase =
+ ManagementInterface.PlayPurchase.newBuilder()
+ .setPurchaseToken(purchaseToken.fromDomain())
+ .setProductId(productId)
+ .build()
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
new file mode 100644
index 0000000000..636e6c5176
--- /dev/null
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt
@@ -0,0 +1,540 @@
+@file:Suppress("TooManyFunctions")
+
+package net.mullvad.mullvadvpn.lib.daemon.grpc.mapper
+
+import io.grpc.ConnectivityState
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.util.UUID
+import mullvad_daemon.management_interface.ManagementInterface
+import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState
+import net.mullvad.mullvadvpn.lib.daemon.grpc.RelayNameComparator
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountId
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.AppId
+import net.mullvad.mullvadvpn.lib.model.AppVersionInfo
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions
+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.DefaultDnsOptions
+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.model.DnsOptions
+import net.mullvad.mullvadvpn.lib.model.DnsState
+import net.mullvad.mullvadvpn.lib.model.Endpoint
+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.GeoLocationId
+import net.mullvad.mullvadvpn.lib.model.Mtu
+import net.mullvad.mullvadvpn.lib.model.ObfuscationEndpoint
+import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.ObfuscationType
+import net.mullvad.mullvadvpn.lib.model.Ownership
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
+import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken
+import net.mullvad.mullvadvpn.lib.model.Port
+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.Providers
+import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
+import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess
+import net.mullvad.mullvadvpn.lib.model.RelayConstraints
+import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.model.RelayList
+import net.mullvad.mullvadvpn.lib.model.RelayOverride
+import net.mullvad.mullvadvpn.lib.model.RelaySettings
+import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
+import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.lib.model.SplitTunnelSettings
+import net.mullvad.mullvadvpn.lib.model.TransportProtocol
+import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
+import net.mullvad.mullvadvpn.lib.model.TunnelOptions
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings
+import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData
+import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions
+import org.joda.time.Instant
+
+internal fun ManagementInterface.TunnelState.toDomain(): TunnelState =
+ when (stateCase!!) {
+ ManagementInterface.TunnelState.StateCase.DISCONNECTED ->
+ TunnelState.Disconnected(
+ location =
+ with(disconnected) {
+ if (hasDisconnectedLocation()) {
+ disconnectedLocation.toDomain()
+ } else null
+ },
+ )
+ ManagementInterface.TunnelState.StateCase.CONNECTING ->
+ TunnelState.Connecting(
+ endpoint = connecting.relayInfo.tunnelEndpoint.toDomain(),
+ location =
+ with(connecting.relayInfo) {
+ if (hasLocation()) {
+ location.toDomain()
+ } else null
+ }
+ )
+ ManagementInterface.TunnelState.StateCase.CONNECTED ->
+ TunnelState.Connected(
+ endpoint = connected.relayInfo.tunnelEndpoint.toDomain(),
+ location =
+ with(connected.relayInfo) {
+ if (hasLocation()) {
+ location.toDomain()
+ } else {
+ null
+ }
+ }
+ )
+ ManagementInterface.TunnelState.StateCase.DISCONNECTING ->
+ TunnelState.Disconnecting(
+ actionAfterDisconnect = disconnecting.afterDisconnect.toDomain(),
+ )
+ ManagementInterface.TunnelState.StateCase.ERROR ->
+ TunnelState.Error(errorState = error.errorState.toDomain())
+ ManagementInterface.TunnelState.StateCase.STATE_NOT_SET ->
+ TunnelState.Disconnected(
+ location = disconnected.disconnectedLocation.toDomain(),
+ )
+ }
+
+internal fun ManagementInterface.GeoIpLocation.toDomain(): GeoIpLocation =
+ GeoIpLocation(
+ ipv4 =
+ if (hasIpv4()) {
+ InetAddress.getByName(ipv4)
+ } else {
+ null
+ },
+ ipv6 =
+ if (hasIpv6()) {
+ InetAddress.getByName(ipv6)
+ } else {
+ null
+ },
+ country = country,
+ city = city,
+ latitude = latitude,
+ longitude = longitude,
+ hostname = hostname
+ )
+
+internal fun ManagementInterface.TunnelEndpoint.toDomain(): TunnelEndpoint =
+ TunnelEndpoint(
+ endpoint =
+ with(address) {
+ val indexOfSeparator = indexOfLast { it == ':' }
+ val ipPart =
+ address.substring(0, indexOfSeparator).filter { it !in listOf('[', ']') }
+ val portPart = address.substring(indexOfSeparator + 1)
+
+ Endpoint(
+ address = InetSocketAddress(InetAddress.getByName(ipPart), portPart.toInt()),
+ protocol = protocol.toDomain()
+ )
+ },
+ quantumResistant = quantumResistant,
+ obfuscation =
+ if (hasObfuscation()) {
+ obfuscation.toDomain()
+ } else {
+ null
+ }
+ )
+
+internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndpoint =
+ ObfuscationEndpoint(
+ endpoint =
+ Endpoint(address = InetSocketAddress(address, port), protocol = protocol.toDomain()),
+ obfuscationType = obfuscationType.toDomain()
+ )
+
+internal fun ManagementInterface.ObfuscationType.toDomain(): ObfuscationType =
+ when (this) {
+ ManagementInterface.ObfuscationType.UDP2TCP -> ObfuscationType.Udp2Tcp
+ ManagementInterface.ObfuscationType.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized obfuscation type")
+ }
+
+internal fun ManagementInterface.TransportProtocol.toDomain(): TransportProtocol =
+ when (this) {
+ ManagementInterface.TransportProtocol.TCP -> TransportProtocol.Tcp
+ ManagementInterface.TransportProtocol.UDP -> TransportProtocol.Udp
+ ManagementInterface.TransportProtocol.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized transport protocol")
+ }
+
+internal fun ManagementInterface.AfterDisconnect.toDomain(): ActionAfterDisconnect =
+ when (this) {
+ ManagementInterface.AfterDisconnect.NOTHING -> ActionAfterDisconnect.Nothing
+ ManagementInterface.AfterDisconnect.RECONNECT -> ActionAfterDisconnect.Reconnect
+ ManagementInterface.AfterDisconnect.BLOCK -> ActionAfterDisconnect.Block
+ ManagementInterface.AfterDisconnect.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized action after disconnect")
+ }
+
+internal fun ManagementInterface.ErrorState.toDomain(): ErrorState =
+ ErrorState(
+ cause =
+ when (cause!!) {
+ ManagementInterface.ErrorState.Cause.AUTH_FAILED ->
+ ErrorStateCause.AuthFailed(authFailedError.name)
+ ManagementInterface.ErrorState.Cause.IPV6_UNAVAILABLE ->
+ ErrorStateCause.Ipv6Unavailable
+ ManagementInterface.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR ->
+ policyError.toDomain()
+ ManagementInterface.ErrorState.Cause.SET_DNS_ERROR -> ErrorStateCause.DnsError
+ ManagementInterface.ErrorState.Cause.START_TUNNEL_ERROR ->
+ ErrorStateCause.StartTunnelError
+ ManagementInterface.ErrorState.Cause.TUNNEL_PARAMETER_ERROR ->
+ ErrorStateCause.TunnelParameterError(parameterError.toDomain())
+ ManagementInterface.ErrorState.Cause.IS_OFFLINE -> ErrorStateCause.IsOffline
+ ManagementInterface.ErrorState.Cause.VPN_PERMISSION_DENIED ->
+ ErrorStateCause.VpnPermissionDenied
+ ManagementInterface.ErrorState.Cause.SPLIT_TUNNEL_ERROR ->
+ ErrorStateCause.StartTunnelError
+ ManagementInterface.ErrorState.Cause.UNRECOGNIZED,
+ ManagementInterface.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS,
+ ManagementInterface.ErrorState.Cause.CREATE_TUNNEL_DEVICE ->
+ throw IllegalArgumentException("Unrecognized error state cause")
+ },
+ isBlocking = !hasBlockingError()
+ )
+
+internal fun ManagementInterface.ErrorState.FirewallPolicyError.toDomain():
+ ErrorStateCause.FirewallPolicyError =
+ when (type!!) {
+ ManagementInterface.ErrorState.FirewallPolicyError.ErrorType.GENERIC ->
+ ErrorStateCause.FirewallPolicyError.Generic
+ ManagementInterface.ErrorState.FirewallPolicyError.ErrorType.LOCKED,
+ ManagementInterface.ErrorState.FirewallPolicyError.ErrorType.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized firewall policy error")
+ }
+
+internal fun ManagementInterface.ErrorState.GenerationError.toDomain(): ParameterGenerationError =
+ when (this) {
+ ManagementInterface.ErrorState.GenerationError.NO_MATCHING_RELAY ->
+ ParameterGenerationError.NoMatchingRelay
+ ManagementInterface.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY ->
+ ParameterGenerationError.NoMatchingBridgeRelay
+ ManagementInterface.ErrorState.GenerationError.NO_WIREGUARD_KEY ->
+ ParameterGenerationError.NoWireguardKey
+ ManagementInterface.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR ->
+ ParameterGenerationError.CustomTunnelHostResultionError
+ ManagementInterface.ErrorState.GenerationError.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized parameter generation error")
+ }
+
+internal fun ManagementInterface.Settings.toDomain(): Settings =
+ Settings(
+ relaySettings = relaySettings.toDomain(),
+ obfuscationSettings = obfuscationSettings.toDomain(),
+ customLists = customLists.customListsList.map { it.toDomain() },
+ allowLan = allowLan,
+ autoConnect = autoConnect,
+ tunnelOptions = tunnelOptions.toDomain(),
+ relayOverrides = relayOverridesList.map { it.toDomain() },
+ showBetaReleases = showBetaReleases,
+ splitTunnelSettings = splitTunnel.toDomain()
+ )
+
+internal fun ManagementInterface.RelayOverride.toDomain(): RelayOverride =
+ RelayOverride(
+ hostname = hostname,
+ ipv4AddressIn = if (hasIpv4AddrIn()) InetAddress.getByName(ipv4AddrIn) else null,
+ ipv6AddressIn = if (hasIpv6AddrIn()) InetAddress.getByName(ipv6AddrIn) else null
+ )
+
+internal fun ManagementInterface.RelaySettings.toDomain(): RelaySettings =
+ when (endpointCase) {
+ ManagementInterface.RelaySettings.EndpointCase.CUSTOM ->
+ throw IllegalArgumentException("CustomTunnelEndpoint is not supported")
+ ManagementInterface.RelaySettings.EndpointCase.NORMAL -> RelaySettings(normal.toDomain())
+ ManagementInterface.RelaySettings.EndpointCase.ENDPOINT_NOT_SET ->
+ throw IllegalArgumentException("RelaySettings endpoint not set")
+ else -> throw NullPointerException("RelaySettings endpoint is null")
+ }
+
+internal fun ManagementInterface.NormalRelaySettings.toDomain(): RelayConstraints =
+ RelayConstraints(
+ location = location.toDomain(),
+ providers = providersList.toDomain(),
+ ownership = ownership.toDomain(),
+ wireguardConstraints = wireguardConstraints.toDomain()
+ )
+
+internal fun ManagementInterface.LocationConstraint.toDomain(): Constraint<RelayItemId> =
+ when (typeCase) {
+ ManagementInterface.LocationConstraint.TypeCase.CUSTOM_LIST ->
+ Constraint.Only(CustomListId(customList))
+ ManagementInterface.LocationConstraint.TypeCase.LOCATION ->
+ Constraint.Only(location.toDomain())
+ ManagementInterface.LocationConstraint.TypeCase.TYPE_NOT_SET -> Constraint.Any
+ else -> throw IllegalArgumentException("Location constraint type is null")
+ }
+
+@Suppress("ReturnCount")
+internal fun ManagementInterface.GeographicLocationConstraint.toDomain(): GeoLocationId {
+ val country = GeoLocationId.Country(country)
+ if (!hasCity()) {
+ return country
+ }
+
+ val city = GeoLocationId.City(country, city)
+ if (!hasHostname()) {
+ return city
+ }
+ return GeoLocationId.Hostname(city, hostname)
+}
+
+internal fun List<String>.toDomain(): Constraint<Providers> =
+ if (isEmpty()) Constraint.Any else Constraint.Only(Providers(map { ProviderId(it) }.toSet()))
+
+internal fun ManagementInterface.WireguardConstraints.toDomain(): WireguardConstraints =
+ WireguardConstraints(
+ port =
+ if (hasPort()) {
+ Constraint.Only(Port(port))
+ } else {
+ Constraint.Any
+ },
+ )
+
+internal fun ManagementInterface.Ownership.toDomain(): Constraint<Ownership> =
+ when (this) {
+ ManagementInterface.Ownership.ANY -> Constraint.Any
+ ManagementInterface.Ownership.MULLVAD_OWNED -> Constraint.Only(Ownership.MullvadOwned)
+ ManagementInterface.Ownership.RENTED -> Constraint.Only(Ownership.Rented)
+ ManagementInterface.Ownership.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized ownership")
+ }
+
+internal fun ManagementInterface.ObfuscationSettings.toDomain(): ObfuscationSettings =
+ ObfuscationSettings(
+ selectedObfuscation = selectedObfuscation.toDomain(),
+ udp2tcp = udp2Tcp.toDomain()
+ )
+
+internal fun ManagementInterface.ObfuscationSettings.SelectedObfuscation.toDomain():
+ SelectedObfuscation =
+ when (this) {
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO -> SelectedObfuscation.Auto
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF -> SelectedObfuscation.Off
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP ->
+ SelectedObfuscation.Udp2Tcp
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized selected obfuscation")
+ }
+
+internal fun ManagementInterface.Udp2TcpObfuscationSettings.toDomain(): Udp2TcpObfuscationSettings =
+ if (hasPort()) {
+ Udp2TcpObfuscationSettings(Constraint.Only(Port(port)))
+ } else {
+ Udp2TcpObfuscationSettings(Constraint.Any)
+ }
+
+internal fun ManagementInterface.CustomList.toDomain(): CustomList =
+ CustomList(
+ id = CustomListId(id),
+ name = CustomListName.fromString(name),
+ locations = locationsList.map { it.toDomain() }
+ )
+
+internal fun ManagementInterface.TunnelOptions.toDomain(): TunnelOptions =
+ TunnelOptions(wireguard = wireguard.toDomain(), dnsOptions = dnsOptions.toDomain())
+
+internal fun ManagementInterface.TunnelOptions.WireguardOptions.toDomain(): WireguardTunnelOptions =
+ WireguardTunnelOptions(
+ mtu = if (hasMtu()) Mtu(mtu) else null,
+ quantumResistant = quantumResistant.toDomain(),
+ )
+
+internal fun ManagementInterface.QuantumResistantState.toDomain(): QuantumResistantState =
+ when (state) {
+ ManagementInterface.QuantumResistantState.State.AUTO -> QuantumResistantState.Auto
+ ManagementInterface.QuantumResistantState.State.ON -> QuantumResistantState.On
+ ManagementInterface.QuantumResistantState.State.OFF -> QuantumResistantState.Off
+ ManagementInterface.QuantumResistantState.State.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized quantum resistant state")
+ else -> throw NullPointerException("Quantum resistant state is null")
+ }
+
+internal fun ManagementInterface.DnsOptions.toDomain(): DnsOptions =
+ DnsOptions(
+ state = state.toDomain(),
+ defaultOptions = defaultOptions.toDomain(),
+ customOptions = customOptions.toDomain()
+ )
+
+internal fun ManagementInterface.DnsOptions.DnsState.toDomain(): DnsState =
+ when (this) {
+ ManagementInterface.DnsOptions.DnsState.DEFAULT -> DnsState.Default
+ ManagementInterface.DnsOptions.DnsState.CUSTOM -> DnsState.Custom
+ ManagementInterface.DnsOptions.DnsState.UNRECOGNIZED ->
+ throw IllegalArgumentException("Unrecognized dns state")
+ }
+
+internal fun ManagementInterface.DefaultDnsOptions.toDomain() =
+ DefaultDnsOptions(
+ blockAds = blockAds,
+ blockMalware = blockMalware,
+ blockAdultContent = blockAdultContent,
+ blockGambling = blockGambling,
+ blockSocialMedia = blockSocialMedia,
+ blockTrackers = blockTrackers
+ )
+
+internal fun ManagementInterface.CustomDnsOptions.toDomain() =
+ CustomDnsOptions(addressesList.map { InetAddress.getByName(it) })
+
+internal fun QuantumResistantState.toDomain(): ManagementInterface.QuantumResistantState =
+ ManagementInterface.QuantumResistantState.newBuilder()
+ .setState(
+ when (this) {
+ QuantumResistantState.Auto -> ManagementInterface.QuantumResistantState.State.AUTO
+ QuantumResistantState.On -> ManagementInterface.QuantumResistantState.State.ON
+ QuantumResistantState.Off -> ManagementInterface.QuantumResistantState.State.OFF
+ }
+ )
+ .build()
+
+internal fun SelectedObfuscation.toDomain():
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation =
+ when (this) {
+ SelectedObfuscation.Udp2Tcp ->
+ ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP
+ SelectedObfuscation.Auto -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO
+ SelectedObfuscation.Off -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF
+ }
+
+internal fun Udp2TcpObfuscationSettings.toDomain(): ManagementInterface.Udp2TcpObfuscationSettings =
+ when (val port = port) {
+ is Constraint.Any ->
+ ManagementInterface.Udp2TcpObfuscationSettings.newBuilder().clearPort().build()
+ is Constraint.Only ->
+ ManagementInterface.Udp2TcpObfuscationSettings.newBuilder()
+ .setPort(port.value.value)
+ .build()
+ }
+
+internal fun ManagementInterface.AppVersionInfo.toDomain(): AppVersionInfo =
+ AppVersionInfo(
+ supported = supported,
+ suggestedUpgrade = if (hasSuggestedUpgrade()) suggestedUpgrade else null
+ )
+
+internal fun ConnectivityState.toDomain(): GrpcConnectivityState =
+ when (this) {
+ ConnectivityState.CONNECTING -> GrpcConnectivityState.Connecting
+ ConnectivityState.READY -> GrpcConnectivityState.Ready
+ ConnectivityState.IDLE -> GrpcConnectivityState.Idle
+ ConnectivityState.TRANSIENT_FAILURE -> GrpcConnectivityState.TransientFailure
+ ConnectivityState.SHUTDOWN -> GrpcConnectivityState.Shutdown
+ }
+
+internal fun ManagementInterface.RelayList.toDomain(): RelayList =
+ RelayList(countriesList.toDomain(), wireguard.toDomain())
+
+internal fun ManagementInterface.WireguardEndpointData.toDomain(): WireguardEndpointData =
+ WireguardEndpointData(portRangesList.map { it.toDomain() })
+
+internal fun ManagementInterface.PortRange.toDomain(): PortRange = PortRange(first..last)
+
+/**
+ * Convert from a list of ManagementInterface.RelayListCountry to a model.RelayList. Non-wireguard
+ * relays are filtered out. So are also cities that only contains non-wireguard relays and countries
+ * that does not have any cities. Countries, cities and relays are ordered by name.
+ */
+internal fun List<ManagementInterface.RelayListCountry>.toDomain():
+ List<RelayItem.Location.Country> =
+ map(ManagementInterface.RelayListCountry::toDomain)
+ .filter { it.cities.isNotEmpty() }
+ .sortedBy { it.name }
+
+internal fun ManagementInterface.RelayListCountry.toDomain(): RelayItem.Location.Country {
+ val countryCode = GeoLocationId.Country(code)
+ return RelayItem.Location.Country(
+ countryCode,
+ name,
+ false,
+ citiesList
+ .map { city -> city.toDomain(countryCode) }
+ .filter { it.relays.isNotEmpty() }
+ .sortedBy { it.name }
+ )
+}
+
+internal fun ManagementInterface.RelayListCity.toDomain(
+ countryCode: GeoLocationId.Country
+): RelayItem.Location.City {
+ val cityCode = GeoLocationId.City(countryCode, code)
+ return RelayItem.Location.City(
+ name = name,
+ id = cityCode,
+ expanded = false,
+ relays =
+ relaysList
+ .filter { it.endpointType == ManagementInterface.Relay.RelayType.WIREGUARD }
+ .map { it.toDomain(cityCode) }
+ .sortedWith(RelayNameComparator)
+ )
+}
+
+internal fun ManagementInterface.Relay.toDomain(
+ cityCode: GeoLocationId.City
+): RelayItem.Location.Relay =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(cityCode, hostname),
+ active = active,
+ provider =
+ Provider(
+ ProviderId(provider),
+ ownership = if (owned) Ownership.MullvadOwned else Ownership.Rented
+ )
+ )
+
+internal fun ManagementInterface.Device.toDomain(): Device =
+ Device(DeviceId.fromString(id), name, Instant.ofEpochSecond(created.seconds).toDateTime())
+
+internal fun ManagementInterface.DeviceState.toDomain(): DeviceState =
+ when (state) {
+ ManagementInterface.DeviceState.State.LOGGED_IN ->
+ DeviceState.LoggedIn(AccountToken(device.accountToken), device.device.toDomain())
+ ManagementInterface.DeviceState.State.LOGGED_OUT -> DeviceState.LoggedOut
+ ManagementInterface.DeviceState.State.REVOKED -> DeviceState.Revoked
+ ManagementInterface.DeviceState.State.UNRECOGNIZED ->
+ throw IllegalArgumentException("Non valid device state")
+ else -> throw NullPointerException("Device state is null")
+ }
+
+internal fun ManagementInterface.AccountData.toDomain(): AccountData =
+ AccountData(
+ AccountId(UUID.fromString(id)),
+ expiryDate = Instant.ofEpochSecond(expiry.seconds).toDateTime()
+ )
+
+internal fun ManagementInterface.VoucherSubmission.toDomain(): RedeemVoucherSuccess =
+ RedeemVoucherSuccess(
+ timeAdded = secondsAdded,
+ newExpiryDate = Instant.ofEpochSecond(newExpiry.seconds).toDateTime()
+ )
+
+internal fun ManagementInterface.SplitTunnelSettings.toDomain(): SplitTunnelSettings =
+ SplitTunnelSettings(
+ enabled = enableExclusions,
+ excludedApps = appsList.map { AppId(it) }.toSet()
+ )
+
+internal fun ManagementInterface.PlayPurchasePaymentToken.toDomain(): PlayPurchasePaymentToken =
+ PlayPurchasePaymentToken(value = token)
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt
new file mode 100644
index 0000000000..fde87ecdd5
--- /dev/null
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.lib.daemon.grpc.util
+
+import android.util.Log
+import io.grpc.CallOptions
+import io.grpc.Channel
+import io.grpc.ClientCall
+import io.grpc.ClientInterceptor
+import io.grpc.MethodDescriptor
+import net.mullvad.mullvadvpn.lib.common.constant.TAG
+
+internal class LogInterceptor : ClientInterceptor {
+ override fun <ReqT : Any?, RespT : Any?> interceptCall(
+ method: MethodDescriptor<ReqT, RespT>?,
+ callOptions: CallOptions?,
+ next: Channel?
+ ): ClientCall<ReqT, RespT> {
+ Log.d(TAG, "Intercepted call: ${method?.fullMethodName}")
+ return next!!.newCall(method, callOptions)
+ }
+}
diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt
new file mode 100644
index 0000000000..3f98ae93d8
--- /dev/null
+++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt
@@ -0,0 +1,24 @@
+package net.mullvad.mullvadvpn.lib.daemon.grpc.util
+
+import io.grpc.ConnectivityState
+import io.grpc.ManagedChannel
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.isActive
+
+internal fun ManagedChannel.connectivityFlow(): Flow<ConnectivityState> {
+ return callbackFlow {
+ var currentState = getState(false)
+ send(currentState)
+
+ while (isActive) {
+ currentState =
+ suspendCoroutine<ConnectivityState> {
+ notifyWhenStateChanged(currentState) { it.resume(getState(false)) }
+ }
+ send(currentState)
+ }
+ }
+}
diff --git a/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt
new file mode 100644
index 0000000000..42cf745510
--- /dev/null
+++ b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt
@@ -0,0 +1,282 @@
+package net.mullvad.mullvadvpn.lib.daemon.grpc
+
+import io.mockk.mockk
+import io.mockk.unmockkAll
+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 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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se9-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ ),
+ )
+ val relay10 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se10-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se9-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay9b =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se9-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "001"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay1 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "1"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay3 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "3"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay100 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "100"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay9b =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se001-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay005 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se005-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+
+ relay001 assertOrderBothDirection relay005
+ }
+
+ @Test
+ fun `given 4 relays comparator should sort by prefix then number`() {
+ val relayAr2 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "ar2-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relayAr8 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "ar8-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relaySe5 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se5-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relaySe10 =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se10-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se2-cloud"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay2w =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se2-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("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.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se22"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+ val relay22b =
+ RelayItem.Location.Relay(
+ id = GeoLocationId.Hostname(city = mockk(), "se22-wireguard"),
+ active = false,
+ provider =
+ Provider(
+ providerId = ProviderId("Provider"),
+ ownership = Ownership.MullvadOwned
+ )
+ )
+
+ relay22a assertOrderBothDirection relay22b
+ }
+
+ private infix fun RelayItem.Location.Relay.assertOrderBothDirection(
+ other: RelayItem.Location.Relay
+ ) {
+ assertTrue(RelayNameComparator.compare(this, other) < 0)
+ assertTrue(RelayNameComparator.compare(other, this) > 0)
+ }
+}
diff --git a/android/lib/ipc/build.gradle.kts b/android/lib/intent-provider/build.gradle.kts
index 35fa3c4f1e..f63a9c7f69 100644
--- a/android/lib/ipc/build.gradle.kts
+++ b/android/lib/intent-provider/build.gradle.kts
@@ -2,17 +2,13 @@ plugins {
id(Dependencies.Plugin.androidLibraryId)
id(Dependencies.Plugin.kotlinAndroidId)
id(Dependencies.Plugin.kotlinParcelizeId)
- id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5
}
android {
- namespace = "net.mullvad.mullvadvpn.lib.ipc"
+ namespace = "net.mullvad.mullvadvpn.lib.intent"
compileSdk = Versions.Android.compileSdkVersion
- defaultConfig {
- minSdk = Versions.Android.minSdkVersion
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
+ defaultConfig { minSdk = Versions.Android.minSdkVersion }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -26,16 +22,10 @@ android {
abortOnError = true
warningsAsErrors = true
}
+ buildFeatures { buildConfig = true }
}
dependencies {
- implementation(project(Dependencies.Mullvad.modelLib))
-
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
-
- androidTestImplementation(Dependencies.junitApi)
- androidTestImplementation(Dependencies.junitEngine)
- androidTestImplementation(Dependencies.AndroidX.testRunner)
- androidTestImplementation(Dependencies.Kotlin.test)
}
diff --git a/android/lib/intent-provider/src/main/AndroidManifest.xml b/android/lib/intent-provider/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..cc947c5679
--- /dev/null
+++ b/android/lib/intent-provider/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest />
diff --git a/android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt b/android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt
new file mode 100644
index 0000000000..86ad970b5d
--- /dev/null
+++ b/android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt
@@ -0,0 +1,16 @@
+package net.mullvad.mullvadvpn.lib.intent
+
+import android.content.Intent
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class IntentProvider {
+ private val _intents = MutableStateFlow<Intent?>(null)
+ val intents: Flow<Intent?> = _intents
+
+ fun setStartIntent(intent: Intent?) {
+ _intents.tryEmit(intent)
+ }
+
+ fun getLatestIntent(): Intent? = _intents.value
+}
diff --git a/android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt b/android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt
deleted file mode 100644
index a125af6059..0000000000
--- a/android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import android.os.Bundle
-import android.os.Looper
-import android.os.Message
-import android.os.Parcelable
-import kotlin.test.assertEquals
-import kotlinx.coroutines.flow.take
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.runBlocking
-import kotlinx.parcelize.Parcelize
-import org.junit.jupiter.api.Test
-
-class HandlerFlowTest {
- val looper by lazy { Looper.getMainLooper() }
-
- val handler: HandlerFlow<Data?> by lazy {
- HandlerFlow(looper) { message -> message.data.getParcelable(DATA_KEY) }
- }
-
- @Test
- fun test_message_extraction() {
- sendMessage(Data(1))
- sendMessage(Data(2))
- sendMessage(Data(3))
-
- val extractedData = runBlocking { handler.take(3).toList() }
-
- assertEquals(listOf(Data(1), Data(2), Data(3)), extractedData)
- }
-
- private fun sendMessage(messageData: Data) {
- val message =
- Message().apply { data = Bundle().apply { putParcelable(DATA_KEY, messageData) } }
-
- handler.handleMessage(message)
- }
-
- companion object {
- const val DATA_KEY = "data"
-
- @Parcelize data class Data(val id: Int) : Parcelable
- }
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt
deleted file mode 100644
index efaa1b78f8..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import android.os.Handler
-import android.os.Looper
-import android.os.Message
-import android.util.Log
-import java.util.concurrent.locks.ReentrantReadWriteLock
-import kotlin.concurrent.withLock
-import kotlin.reflect.KClass
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
-
-class DispatchingHandler<T : Any>(looper: Looper, private val extractor: (Message) -> T?) :
- Handler(looper), MessageDispatcher<T> {
- private val handlers = HashMap<KClass<out T>, (T) -> Unit>()
- private val lock = ReentrantReadWriteLock()
-
- private val _parsedMessages =
- MutableSharedFlow<T>(extraBufferCapacity = MESSAGES_BUFFER_CAPACITY)
- val parsedMessages = _parsedMessages.asSharedFlow()
-
- @Deprecated("Use parsedMessages instead.")
- override fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) {
- lock.writeLock().withLock {
- handlers.put(variant) { instance -> @Suppress("UNCHECKED_CAST") handler(instance as V) }
- }
- }
-
- override fun handleMessage(message: Message) {
- lock.readLock().withLock {
- val instance = extractor(message)
-
- if (instance != null) {
- val handler = handlers.get(instance::class)
-
- handler?.invoke(instance)
- _parsedMessages.tryEmit(instance)
- } else {
- Log.e("mullvad", "Dispatching handler received an unexpected message")
- }
- }
- }
-
- fun onDestroy() {
- lock.writeLock().withLock { handlers.clear() }
-
- removeCallbacksAndMessages(null)
- }
-
- companion object {
- private const val MESSAGES_BUFFER_CAPACITY = 10
- }
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt
deleted file mode 100644
index 36ea17036e..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import android.os.Messenger
-import kotlinx.parcelize.Parcelize
-import net.mullvad.mullvadvpn.model.AccountCreationResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.AccountHistory
-import net.mullvad.mullvadvpn.model.CreateCustomListResult
-import net.mullvad.mullvadvpn.model.DeviceListEvent
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.LoginResult
-import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.RemoveDeviceResult
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.SettingsPatchError
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.model.UpdateCustomListResult
-
-// Events that can be sent from the service
-sealed class Event : Message.EventMessage() {
- override val messageKey = MESSAGE_KEY
-
- @Parcelize data class AccountCreationEvent(val result: AccountCreationResult) : Event()
-
- @Parcelize data class AccountExpiryEvent(val expiry: AccountExpiry) : Event()
-
- @Parcelize data class AccountHistoryEvent(val history: AccountHistory) : Event()
-
- @Parcelize
- data class AppVersionInfo(val versionInfo: net.mullvad.mullvadvpn.model.AppVersionInfo?) :
- Event()
-
- @Parcelize data class AuthToken(val token: String?) : Event()
-
- @Parcelize data class CurrentVersion(val version: String?) : Event()
-
- @Parcelize data class DeviceStateEvent(val newState: DeviceState) : Event()
-
- @Parcelize data class DeviceListUpdate(val event: DeviceListEvent) : Event()
-
- @Parcelize
- data class DeviceRemovalEvent(val deviceId: String, val result: RemoveDeviceResult) : Event()
-
- @Parcelize data class ListenerReady(val connection: Messenger, val listenerId: Int) : Event()
-
- @Parcelize data class LoginEvent(val result: LoginResult) : Event()
-
- @Parcelize data class NewRelayList(val relayList: RelayList?) : Event()
-
- @Parcelize data class SettingsUpdate(val settings: Settings?) : Event()
-
- @Parcelize data class SplitTunnelingUpdate(val excludedApps: List<String>?) : Event()
-
- @Parcelize data class TunnelStateChange(val tunnelState: TunnelState) : Event()
-
- @Parcelize
- data class VoucherSubmissionResult(
- val voucher: String,
- val result: net.mullvad.mullvadvpn.model.VoucherSubmissionResult
- ) : Event()
-
- @Parcelize data class PlayPurchaseInitResultEvent(val result: PlayPurchaseInitResult) : Event()
-
- @Parcelize
- data class PlayPurchaseVerifyResultEvent(val result: PlayPurchaseVerifyResult) : Event()
-
- @Parcelize object VpnPermissionRequest : Event()
-
- @Parcelize data class CreateCustomListResultEvent(val result: CreateCustomListResult) : Event()
-
- @Parcelize data class UpdateCustomListResultEvent(val result: UpdateCustomListResult) : Event()
-
- @Parcelize data class ExportJsonSettingsResult(val json: String) : Event()
-
- @Parcelize data class ApplyJsonSettingsResult(val error: SettingsPatchError?) : Event()
-
- companion object {
- private const val MESSAGE_KEY = "event"
-
- fun fromMessage(message: android.os.Message): Event? = fromMessage(message, MESSAGE_KEY)
- }
-}
-
-typealias EventDispatcher = MessageDispatcher<Event>
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt
deleted file mode 100644
index 7b839a3658..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import android.os.Handler
-import android.os.Looper
-import android.os.Message
-import android.util.Log
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedSendChannelException
-import kotlinx.coroutines.channels.trySendBlocking
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.consumeAsFlow
-import kotlinx.coroutines.flow.onCompletion
-
-class HandlerFlow<T>(looper: Looper, private val extractor: (Message) -> T) :
- Handler(looper), Flow<T> {
- private val channel = Channel<T>(Channel.UNLIMITED)
- private val flow = channel.consumeAsFlow().onCompletion { removeCallbacksAndMessages(null) }
-
- @InternalCoroutinesApi
- override suspend fun collect(collector: FlowCollector<T>) = flow.collect(collector)
-
- override fun handleMessage(message: Message) {
- val extractedData = extractor(message)
-
- try {
- channel.trySendBlocking(extractedData)
- } catch (exception: ClosedSendChannelException) {
- Log.w("mullvad", "Received a message after HandlerFlow was closed", exception)
- removeCallbacksAndMessages(null)
- } catch (exception: CancellationException) {
- Log.w("mullvad", "Received a message after HandlerFlow was cancelled", exception)
- removeCallbacksAndMessages(null)
- }
- }
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt
deleted file mode 100644
index 7cc293b373..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import android.os.Bundle
-import android.os.Message as RawMessage
-import android.os.Parcelable
-
-sealed class Message(private val messageId: Int) : Parcelable {
- abstract class EventMessage : Message(1)
-
- abstract class RequestMessage : Message(2)
-
- protected abstract val messageKey: String
-
- val message: RawMessage
- get() =
- RawMessage.obtain().also { message ->
- message.what = messageId
- message.data = Bundle()
- message.data.putParcelable(messageKey, this)
- }
-
- companion object {
- internal fun <T : Parcelable> fromMessage(message: RawMessage, key: String): T? {
- val data = message.data
-
- data.classLoader = Message::class.java.classLoader
-
- return data.getParcelable(key)
- }
- }
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt
deleted file mode 100644
index 8bb6703479..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import kotlin.reflect.KClass
-
-interface MessageDispatcher<T : Any> {
- fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit)
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt
deleted file mode 100644
index 04de35e3bd..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import kotlin.reflect.KClass
-import kotlinx.coroutines.flow.Flow
-
-interface MessageHandler {
- fun <R : Event> events(klass: KClass<R>): Flow<R>
-
- fun trySendRequest(request: Request): Boolean
-}
-
-inline fun <reified R : Event> MessageHandler.events(): Flow<R> {
- return this.events(R::class)
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt
deleted file mode 100644
index 4bcf871acc..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc
-
-import android.os.Message as RawMessage
-import android.os.Messenger
-import java.net.InetAddress
-import kotlinx.parcelize.Parcelize
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.DnsOptions
-import net.mullvad.mullvadvpn.model.LocationConstraint
-import net.mullvad.mullvadvpn.model.ObfuscationSettings
-import net.mullvad.mullvadvpn.model.Ownership
-import net.mullvad.mullvadvpn.model.PlayPurchase
-import net.mullvad.mullvadvpn.model.Providers
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.RelayOverride
-import net.mullvad.mullvadvpn.model.WireguardConstraints
-
-// Requests that the service can handle
-sealed class Request : Message.RequestMessage() {
- override val messageKey = MESSAGE_KEY
-
- @Parcelize
- @Deprecated("Use SetDnsOptions")
- data class AddCustomDnsServer(val address: InetAddress) : Request()
-
- @Parcelize object Connect : Request()
-
- @Parcelize object CreateAccount : Request()
-
- @Parcelize object Disconnect : Request()
-
- @Parcelize data class ExcludeApp(val packageName: String) : Request()
-
- @Parcelize object FetchAccountExpiry : Request()
-
- @Parcelize object FetchAccountHistory : Request()
-
- @Parcelize object FetchAuthToken : Request()
-
- @Parcelize data class IncludeApp(val packageName: String) : Request()
-
- @Parcelize data class Login(val account: String?) : Request()
-
- @Parcelize object RefreshDeviceState : Request()
-
- @Parcelize object GetDevice : Request()
-
- @Parcelize data class GetDeviceList(val accountToken: String) : Request()
-
- @Parcelize data class RemoveDevice(val accountToken: String, val deviceId: String) : Request()
-
- @Parcelize object Logout : Request()
-
- @Parcelize object PersistExcludedApps : Request()
-
- @Parcelize object Reconnect : Request()
-
- @Parcelize data class RegisterListener(val listener: Messenger) : Request()
-
- @Parcelize object ClearAccountHistory : Request()
-
- @Parcelize
- @Deprecated("Use SetDnsOptions")
- data class RemoveCustomDnsServer(val address: InetAddress) : Request()
-
- @Parcelize
- @Deprecated("Use SetDnsOptions")
- data class ReplaceCustomDnsServer(val oldAddress: InetAddress, val newAddress: InetAddress) :
- Request()
-
- @Parcelize data class SetAllowLan(val allow: Boolean) : Request()
-
- @Parcelize data class SetAutoConnect(val autoConnect: Boolean) : Request()
-
- @Parcelize
- @Deprecated("Use SetDnsOptions")
- data class SetEnableCustomDns(val enable: Boolean) : Request()
-
- @Parcelize data class SetEnableSplitTunneling(val enable: Boolean) : Request()
-
- @Parcelize data class SetRelayLocation(val locationConstraint: LocationConstraint) : Request()
-
- @Parcelize data class SetWireGuardMtu(val mtu: Int?) : Request()
-
- @Parcelize data class SubmitVoucher(val voucher: String) : Request()
-
- @Parcelize data object InitPlayPurchase : Request()
-
- @Parcelize data class VerifyPlayPurchase(val playPurchase: PlayPurchase) : Request()
-
- @Parcelize data class UnregisterListener(val listenerId: Int) : Request()
-
- @Parcelize data class VpnPermissionResponse(val isGranted: Boolean) : Request()
-
- @Parcelize data class SetDnsOptions(val dnsOptions: DnsOptions) : Request()
-
- @Parcelize data class SetObfuscationSettings(val settings: ObfuscationSettings?) : Request()
-
- @Parcelize
- data class SetWireguardConstraints(val wireguardConstraints: WireguardConstraints) : Request()
-
- @Parcelize
- data class SetWireGuardQuantumResistant(val quantumResistant: QuantumResistantState) :
- Request()
-
- @Parcelize data object FetchRelayList : Request()
-
- @Parcelize
- data class SetOwnershipAndProviders(
- val ownership: Constraint<Ownership>,
- val providers: Constraint<Providers>
- ) : Request()
-
- @Parcelize data class CreateCustomList(val name: String) : Request()
-
- @Parcelize data class DeleteCustomList(val id: String) : Request()
-
- @Parcelize data class UpdateCustomList(val customList: CustomList) : Request()
-
- @Parcelize data object ClearAllRelayOverrides : Request()
-
- @Parcelize data class ApplyJsonSettings(val json: String) : Request()
-
- @Parcelize data object ExportJsonSettings : Request()
-
- @Parcelize data class SetRelayOverride(val override: RelayOverride) : Request()
-
- companion object {
- private const val MESSAGE_KEY = "request"
-
- fun fromMessage(message: RawMessage): Request? = fromMessage(message, MESSAGE_KEY)
- }
-}
diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt
deleted file mode 100644
index 26cade5cb4..0000000000
--- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package net.mullvad.mullvadvpn.lib.ipc.extensions
-
-import android.os.DeadObjectException
-import android.os.Message
-import android.os.Messenger
-import android.os.RemoteException
-import android.util.Log
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-fun Messenger.trySendEvent(event: Event, logErrors: Boolean): Boolean {
- return trySend(event.message, logErrors, event::class.qualifiedName)
-}
-
-fun Messenger.trySendRequest(request: Request, logErrors: Boolean): Boolean {
- return trySend(request.message, logErrors, request::class.qualifiedName)
-}
-
-private fun Messenger.trySend(message: Message, logErrors: Boolean, messageName: String?): Boolean {
- return try {
- this.send(message)
- true
- } catch (deadObjectException: DeadObjectException) {
- if (logErrors) {
- Log.e(
- "mullvad",
- "Failed to send message ${messageName ?: "<missing>"} due to DeadObjectException"
- )
- }
- false
- } catch (remoteException: RemoteException) {
- if (logErrors) {
- Log.e(
- "mullvad",
- "Failed to send message ${messageName ?: "<missing>"} due to RemoteException"
- )
- }
- false
- }
-}
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt
index 6ce7690bc5..4047783825 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt
@@ -18,9 +18,9 @@ import net.mullvad.mullvadvpn.lib.map.internal.MAX_ANIMATION_MILLIS
import net.mullvad.mullvadvpn.lib.map.internal.MAX_MULTIPLIER_PEAK_TIMING
import net.mullvad.mullvadvpn.lib.map.internal.MIN_ANIMATION_MILLIS
import net.mullvad.mullvadvpn.lib.map.internal.SHORT_ANIMATION_CUTOFF_MILLIS
-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
@Composable
fun animatedCameraPosition(
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt
index a143a63cb8..b1ea1144f9 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt
@@ -13,7 +13,7 @@ import net.mullvad.mullvadvpn.lib.map.data.GlobeColors
import net.mullvad.mullvadvpn.lib.map.data.MapViewState
import net.mullvad.mullvadvpn.lib.map.data.Marker
import net.mullvad.mullvadvpn.lib.map.internal.MapGLSurfaceView
-import net.mullvad.mullvadvpn.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.LatLong
@Composable
fun Map(
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt
index d837bcadfc..b66b0ea657 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt
@@ -1,7 +1,7 @@
package net.mullvad.mullvadvpn.lib.map.data
import androidx.compose.runtime.Immutable
-import net.mullvad.mullvadvpn.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.LatLong
@Immutable
data class CameraPosition(val latLong: LatLong, val zoom: Float, val verticalBias: Float)
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt
index 9f464612f1..4d26348d45 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt
@@ -1,7 +1,7 @@
package net.mullvad.mullvadvpn.lib.map.data
import androidx.compose.runtime.Immutable
-import net.mullvad.mullvadvpn.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.LatLong
@Immutable
data class Marker(
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt
index 41ac903fb1..887e64bebd 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt
@@ -13,7 +13,7 @@ import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors
import net.mullvad.mullvadvpn.lib.map.data.MapViewState
import net.mullvad.mullvadvpn.lib.map.internal.shapes.Globe
import net.mullvad.mullvadvpn.lib.map.internal.shapes.LocationMarker
-import net.mullvad.mullvadvpn.model.toRadians
+import net.mullvad.mullvadvpn.lib.model.toRadians
internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.Renderer {
diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt
index 9d03a540c5..c67a0a1bb7 100644
--- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt
+++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt
@@ -12,7 +12,7 @@ import net.mullvad.mullvadvpn.lib.map.internal.VERTEX_COMPONENT_SIZE
import net.mullvad.mullvadvpn.lib.map.internal.initArrayBuffer
import net.mullvad.mullvadvpn.lib.map.internal.initShaderProgram
import net.mullvad.mullvadvpn.lib.map.internal.toFloatArray
-import net.mullvad.mullvadvpn.model.LatLong
+import net.mullvad.mullvadvpn.lib.model.LatLong
internal class LocationMarker(val colors: LocationMarkerColors) {
diff --git a/android/lib/model/build.gradle.kts b/android/lib/model/build.gradle.kts
index 7264c6041a..28a5804b5f 100644
--- a/android/lib/model/build.gradle.kts
+++ b/android/lib/model/build.gradle.kts
@@ -3,10 +3,11 @@ plugins {
id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5
id(Dependencies.Plugin.kotlinAndroidId)
id(Dependencies.Plugin.kotlinParcelizeId)
+ id(Dependencies.Plugin.ksp) version Versions.Plugin.ksp
}
android {
- namespace = "net.mullvad.mullvadvpn.model"
+ namespace = "net.mullvad.mullvadvpn.lib.model"
compileSdk = Versions.Android.compileSdkVersion
defaultConfig {
@@ -29,11 +30,12 @@ android {
}
dependencies {
- implementation(project(Dependencies.Mullvad.talpidLib))
-
implementation(Dependencies.jodaTime)
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
+ implementation(Dependencies.Arrow.core)
+ implementation(Dependencies.Arrow.optics)
+ ksp(Dependencies.Arrow.opticsKsp)
// Test dependencies
testRuntimeOnly(Dependencies.junitEngine)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt
new file mode 100644
index 0000000000..60395721d8
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import org.joda.time.DateTime
+
+data class AccountData(
+ val id: AccountId,
+ val expiryDate: DateTime,
+)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt
new file mode 100644
index 0000000000..75550259fd
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import java.util.UUID
+
+@JvmInline
+value class AccountId(val value: UUID) {
+ companion object {
+ fun fromString(value: String) = AccountId(UUID.fromString(value))
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt
new file mode 100644
index 0000000000..d03a0d6721
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@JvmInline @Parcelize value class AccountToken(val value: String) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt
new file mode 100644
index 0000000000..531fc1c073
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class ActionAfterDisconnect {
+ Nothing,
+ Block,
+ Reconnect
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt
new file mode 100644
index 0000000000..338162db8c
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+interface AddSplitTunnelingAppError {
+ data class Unknown(val throwable: Throwable) : AddSplitTunnelingAppError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt
new file mode 100644
index 0000000000..0663b530a1
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+@JvmInline value class AppId(val value: String)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt
new file mode 100644
index 0000000000..9af168bf28
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class AppVersionInfo(val supported: Boolean, val suggestedUpgrade: String?)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt
new file mode 100644
index 0000000000..980ea23961
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class BuildVersion(val name: String, val code: Int)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt
new file mode 100644
index 0000000000..ce1bc0af12
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface ClearAllOverridesError {
+ data class Unknown(val throwable: Throwable) : ClearAllOverridesError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt
new file mode 100644
index 0000000000..307a235314
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface ConnectError {
+ data class Unknown(val throwable: Throwable) : ConnectError
+
+ data object NoVpnPermission : ConnectError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt
new file mode 100644
index 0000000000..95e7d95154
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt
@@ -0,0 +1,21 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+sealed interface Constraint<out T> {
+ data object Any : Constraint<Nothing>
+
+ @optics
+ data class Only<T>(val value: T) : Constraint<T> {
+ companion object
+ }
+
+ fun getOrNull(): T? =
+ when (this) {
+ Any -> null
+ is Only -> value
+ }
+
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt
new file mode 100644
index 0000000000..eeeaf11fca
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed class CreateAccountError {
+ data class Unknown(val error: Throwable) : CreateAccountError()
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt
new file mode 100644
index 0000000000..adbac22d9b
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface CreateCustomListError
+
+data object CustomListAlreadyExists : CreateCustomListError
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt
new file mode 100644
index 0000000000..4fd64b2892
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt
@@ -0,0 +1,9 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+import java.net.InetAddress
+
+@optics
+data class CustomDnsOptions(val addresses: List<InetAddress>) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt
new file mode 100644
index 0000000000..ed43ac1097
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class CustomList(
+ val id: CustomListId,
+ val name: CustomListName,
+ val locations: List<GeoLocationId>
+) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListName.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomListName.kt
index 5822eec2b3..186d74dc92 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListName.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomListName.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DefaultDnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DefaultDnsOptions.kt
index 69f4d4d220..6979320ce6 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DefaultDnsOptions.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DefaultDnsOptions.kt
@@ -1,9 +1,8 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import arrow.optics.optics
-@Parcelize
+@optics
data class DefaultDnsOptions(
val blockAds: Boolean = false,
val blockTrackers: Boolean = false,
@@ -11,7 +10,7 @@ data class DefaultDnsOptions(
val blockAdultContent: Boolean = false,
val blockGambling: Boolean = false,
val blockSocialMedia: Boolean = false,
-) : Parcelable {
+) {
fun isAnyBlockerEnabled(): Boolean {
return blockAds ||
blockTrackers ||
@@ -20,4 +19,6 @@ data class DefaultDnsOptions(
blockGambling ||
blockSocialMedia
}
+
+ companion object
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt
new file mode 100644
index 0000000000..d9c93c87cf
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface DeleteCustomListError
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt
new file mode 100644
index 0000000000..1c6c54bcf0
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface DeleteDeviceError {
+ data class Unknown(val error: Throwable) : DeleteDeviceError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt
new file mode 100644
index 0000000000..e8303f0eca
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+import net.mullvad.mullvadvpn.lib.model.extensions.startCase
+import org.joda.time.DateTime
+
+@Parcelize
+data class Device(val id: DeviceId, private val name: String, val creationDate: DateTime) :
+ Parcelable {
+ fun displayName(): String = name.startCase()
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt
new file mode 100644
index 0000000000..863d15fd67
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import java.util.UUID
+import kotlinx.parcelize.Parcelize
+
+@JvmInline
+@Parcelize
+value class DeviceId(val value: UUID) : Parcelable {
+ companion object {
+ fun fromString(value: String): DeviceId = DeviceId(UUID.fromString(value))
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt
new file mode 100644
index 0000000000..4546cd46b3
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt
@@ -0,0 +1,21 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+sealed class DeviceState : Parcelable {
+ @Parcelize
+ data class LoggedIn(val accountToken: AccountToken, val device: Device) : DeviceState()
+
+ @Parcelize data object LoggedOut : DeviceState()
+
+ @Parcelize data object Revoked : DeviceState()
+
+ fun displayName(): String? {
+ return (this as? LoggedIn)?.device?.displayName()
+ }
+
+ fun token(): AccountToken? {
+ return (this as? LoggedIn)?.accountToken
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsOptions.kt
index 1ce3acc095..ae27e47457 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsOptions.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsOptions.kt
@@ -1,11 +1,12 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import arrow.optics.optics
-@Parcelize
+@optics
data class DnsOptions(
val state: DnsState,
val defaultOptions: DefaultDnsOptions,
val customOptions: CustomDnsOptions
-) : Parcelable
+) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsState.kt
index 9c8677ba7d..4bf053eef1 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsState.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsState.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
enum class DnsState {
Default,
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt
new file mode 100644
index 0000000000..43a4cc41b1
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+interface EnableSplitTunnelingError {
+ data class Unknown(val throwable: Throwable) : EnableSplitTunnelingError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt
new file mode 100644
index 0000000000..4eae8b08ec
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import java.net.InetSocketAddress
+
+data class Endpoint(val address: InetSocketAddress, val protocol: TransportProtocol)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt
new file mode 100644
index 0000000000..fb7673b7b5
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class ErrorState(val cause: ErrorStateCause, val isBlocking: Boolean)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt
new file mode 100644
index 0000000000..0ba63a4b08
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt
@@ -0,0 +1,34 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import java.net.InetAddress
+
+sealed class ErrorStateCause {
+ class AuthFailed(private val reason: String?) : ErrorStateCause() {
+ fun isCausedByExpiredAccount(): Boolean {
+ return reason == AUTH_FAILED_REASON_EXPIRED_ACCOUNT
+ }
+
+ companion object {
+ private const val AUTH_FAILED_REASON_EXPIRED_ACCOUNT = "[EXPIRED_ACCOUNT]"
+ }
+ }
+
+ data object Ipv6Unavailable : ErrorStateCause()
+
+ sealed class FirewallPolicyError : ErrorStateCause() {
+ data object Generic : FirewallPolicyError()
+ }
+
+ data object DnsError : ErrorStateCause()
+
+ // Regression
+ data class InvalidDnsServers(val addresses: List<InetAddress>) : ErrorStateCause()
+
+ data object StartTunnelError : ErrorStateCause()
+
+ data class TunnelParameterError(val error: ParameterGenerationError) : ErrorStateCause()
+
+ data object IsOffline : ErrorStateCause()
+
+ data object VpnPermissionDenied : ErrorStateCause()
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GeoIpLocation.kt
index 625de76b29..3334b458d7 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GeoIpLocation.kt
@@ -1,10 +1,7 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
import java.net.InetAddress
-import kotlinx.parcelize.Parcelize
-@Parcelize
data class GeoIpLocation(
val ipv4: InetAddress?,
val ipv6: InetAddress?,
@@ -13,4 +10,4 @@ data class GeoIpLocation(
val latitude: Double,
val longitude: Double,
val hostname: String?,
-) : Parcelable
+)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt
new file mode 100644
index 0000000000..6f3ba64848
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface GetAccountDataError {
+ data class Unknown(val error: Throwable) : GetAccountDataError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt
new file mode 100644
index 0000000000..7803a98ad1
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface GetAccountHistoryError {
+ data class Unknown(val error: Throwable) : GetAccountHistoryError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt
new file mode 100644
index 0000000000..bcad016580
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface GetDeviceListError {
+ data class Unknown(val error: Throwable) : GetDeviceListError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt
new file mode 100644
index 0000000000..675973ee1e
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface GetDeviceStateError {
+ data class Unknown(val error: Throwable) : GetDeviceStateError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt
index d6749a16a2..19f757ffc3 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt
@@ -1,9 +1,9 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sqrt
-import net.mullvad.mullvadvpn.model.Latitude.Companion.mean
+import net.mullvad.mullvadvpn.lib.model.Latitude.Companion.mean
data class LatLong(val latitude: Latitude, val longitude: Longitude) {
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Latitude.kt
index 21d113f3bc..9b0cc7fbbe 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Latitude.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import kotlin.math.absoluteValue
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt
new file mode 100644
index 0000000000..6530450d42
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+interface ListDevicesError {
+ data class Unknown(val throwable: Throwable) : ListDevicesError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt
new file mode 100644
index 0000000000..1c58f80bee
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt
@@ -0,0 +1,15 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+sealed class LoginAccountError : Parcelable {
+ data object InvalidAccount : LoginAccountError()
+
+ data class MaxDevicesReached(val accountToken: AccountToken) : LoginAccountError()
+
+ data object RpcError : LoginAccountError()
+
+ data class Unknown(val error: Throwable) : LoginAccountError()
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Longitude.kt
index 9f73a6ff17..b772801da7 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Longitude.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import kotlin.math.absoluteValue
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt
new file mode 100644
index 0000000000..68b4b71bd9
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt
@@ -0,0 +1,28 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import arrow.core.Either
+import arrow.core.raise.either
+import arrow.core.raise.ensure
+import kotlinx.parcelize.Parcelize
+
+@JvmInline
+@Parcelize
+value class Mtu(val value: Int) : Parcelable {
+ companion object {
+ fun fromString(value: String): Either<ParseMtuError, Mtu> = either {
+ val number = value.toIntOrNull() ?: raise(ParseMtuError.NotANumber)
+ ensure(number in MIN_VALUE..MAX_VALUE) { ParseMtuError.OutOfRange(number) }
+ Mtu(number)
+ }
+
+ private const val MIN_VALUE = 1280
+ private const val MAX_VALUE = 1420
+ }
+}
+
+sealed interface ParseMtuError {
+ data object NotANumber : ParseMtuError
+
+ data class OutOfRange(val number: Int) : ParseMtuError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt
new file mode 100644
index 0000000000..5dda03aa9d
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt
@@ -0,0 +1,25 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import org.joda.time.Duration
+
+sealed interface Notification {
+ val actions: List<NotificationAction>
+ val ongoing: Boolean
+ val channelId: NotificationChannelId
+
+ data class Tunnel(
+ override val channelId: NotificationChannelId,
+ val state: NotificationTunnelState,
+ override val actions: List<NotificationAction.Tunnel>,
+ override val ongoing: Boolean,
+ ) : Notification
+
+ data class AccountExpiry(
+ override val channelId: NotificationChannelId,
+ override val actions: List<NotificationAction.AccountExpiry>,
+ val websiteAuthToken: WebsiteAuthToken?,
+ val durationUntilExpiry: Duration
+ ) : Notification {
+ override val ongoing: Boolean = false
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt
new file mode 100644
index 0000000000..ec938a9fbf
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface NotificationAction {
+
+ sealed interface AccountExpiry : NotificationAction {
+ data object Open : AccountExpiry
+ }
+
+ sealed interface Tunnel : NotificationAction {
+ data object Connect : Tunnel
+
+ data object Disconnect : Tunnel
+
+ data object Cancel : Tunnel
+
+ data object Dismiss : Tunnel
+
+ data object RequestPermission : Tunnel
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt
new file mode 100644
index 0000000000..166c20b826
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt
@@ -0,0 +1,15 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface NotificationChannel {
+ val id: NotificationChannelId
+
+ data object TunnelUpdates : NotificationChannel {
+ private const val CHANNEL_ID = "vpn_tunnel_status"
+ override val id: NotificationChannelId = NotificationChannelId(CHANNEL_ID)
+ }
+
+ data object AccountUpdates : NotificationChannel {
+ private const val CHANNEL_ID = "mullvad_account_time"
+ override val id: NotificationChannelId = NotificationChannelId(CHANNEL_ID)
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt
new file mode 100644
index 0000000000..c4231deb8c
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+@JvmInline value class NotificationChannelId(val value: String)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt
new file mode 100644
index 0000000000..9c20bf9420
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+@JvmInline value class NotificationId(val value: Int)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt
new file mode 100644
index 0000000000..fffe86c247
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt
@@ -0,0 +1,25 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface NotificationTunnelState {
+ data class Disconnected(val hasVpnPermission: Boolean) : NotificationTunnelState
+
+ data object Connecting : NotificationTunnelState
+
+ data object Connected : NotificationTunnelState
+
+ data object Reconnecting : NotificationTunnelState
+
+ data object Disconnecting : NotificationTunnelState
+
+ sealed interface Error : NotificationTunnelState {
+ data object DeviceOffline : Error
+
+ data object Blocking : Error
+
+ data object VpnPermissionDenied : Error
+
+ data object AlwaysOnVpn : Error
+
+ data object Critical : Error
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt
new file mode 100644
index 0000000000..00d64cbc3e
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface NotificationUpdate<out D> {
+ val notificationId: NotificationId
+
+ data class Notify<D>(override val notificationId: NotificationId, val value: D) :
+ NotificationUpdate<D>
+
+ data class Cancel(override val notificationId: NotificationId) : NotificationUpdate<Nothing>
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt
new file mode 100644
index 0000000000..020ef8e5c1
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class ObfuscationEndpoint(val endpoint: Endpoint, val obfuscationType: ObfuscationType)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt
index 19b5c0e5f2..b8a26973a2 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ObfuscationSettings.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt
@@ -1,10 +1,11 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import arrow.optics.optics
-@Parcelize
+@optics
data class ObfuscationSettings(
val selectedObfuscation: SelectedObfuscation,
val udp2tcp: Udp2TcpObfuscationSettings
-) : Parcelable
+) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
new file mode 100644
index 0000000000..cd71d645af
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class ObfuscationType {
+ Udp2Tcp
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt
new file mode 100644
index 0000000000..5257f944d3
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class Ownership {
+ MullvadOwned,
+ Rented
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ParameterGenerationError.kt
index b1504c676f..476aed1407 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ParameterGenerationError.kt
@@ -1,4 +1,4 @@
-package net.mullvad.talpid.tunnel
+package net.mullvad.mullvadvpn.lib.model
enum class ParameterGenerationError {
NoMatchingRelay,
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt
new file mode 100644
index 0000000000..9384f9f5b8
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class PlayPurchase(val productId: String, val purchaseToken: PlayPurchasePaymentToken)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt
new file mode 100644
index 0000000000..6326bab8e8
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class PlayPurchaseInitError {
+ OtherError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt
new file mode 100644
index 0000000000..bfcae64d45
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+@JvmInline value class PlayPurchasePaymentToken(val value: String)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt
new file mode 100644
index 0000000000..dc06b8ffbf
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class PlayPurchaseVerifyError {
+ OtherError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt
new file mode 100644
index 0000000000..bcb5a8dd99
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@JvmInline @Parcelize value class Port(val value: Int) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt
new file mode 100644
index 0000000000..77767a1011
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt
@@ -0,0 +1,30 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcel
+import android.os.Parcelable
+import kotlinx.parcelize.Parceler
+import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.TypeParceler
+
+@JvmInline
+@Parcelize
+@TypeParceler<IntRange, IntRangeParceler>
+value class PortRange(val value: IntRange) : Parcelable {
+ operator fun contains(port: Port): Boolean = port.value in value
+
+ fun toFormattedString(): String =
+ if (value.first == value.last) {
+ value.first.toString()
+ } else {
+ "${value.first}-${value.last}"
+ }
+}
+
+object IntRangeParceler : Parceler<IntRange> {
+ override fun create(parcel: Parcel) = IntRange(parcel.readInt(), parcel.readInt())
+
+ override fun IntRange.write(parcel: Parcel, flags: Int) {
+ parcel.writeInt(start)
+ parcel.writeInt(endInclusive)
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt
new file mode 100644
index 0000000000..e704e9554d
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class Provider(val providerId: ProviderId, val ownership: Ownership)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt
new file mode 100644
index 0000000000..cc23c3e9b6
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+@JvmInline value class ProviderId(val value: String)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt
new file mode 100644
index 0000000000..73cf9facdb
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class Providers(val providers: Set<ProviderId>) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt
new file mode 100644
index 0000000000..c77dab72d3
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class QuantumResistantState {
+ Auto,
+ On,
+ Off
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt
new file mode 100644
index 0000000000..d14a2f236b
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt
@@ -0,0 +1,11 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed class RedeemVoucherError {
+ data object InvalidVoucher : RedeemVoucherError()
+
+ data object VoucherAlreadyUsed : RedeemVoucherError()
+
+ data object RpcError : RedeemVoucherError()
+
+ data class Unknown(val error: Throwable) : RedeemVoucherError()
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt
new file mode 100644
index 0000000000..9c81042b8c
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import org.joda.time.DateTime
+
+data class RedeemVoucherSuccess(val timeAdded: Long, val newExpiryDate: DateTime)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt
new file mode 100644
index 0000000000..f3573933e3
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class RelayConstraints(
+ val location: Constraint<RelayItemId>,
+ val providers: Constraint<Providers>,
+ val ownership: Constraint<Ownership>,
+ val wireguardConstraints: WireguardConstraints,
+) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
new file mode 100644
index 0000000000..a31a6f67df
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt
@@ -0,0 +1,88 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+sealed interface RelayItem {
+ val id: RelayItemId
+ val name: String
+ val active: Boolean
+ val hasChildren: Boolean
+ val expanded: Boolean
+
+ @optics
+ data class CustomList(
+ override val id: CustomListId,
+ val customListName: CustomListName,
+ val locations: List<Location>,
+ override val expanded: Boolean,
+ ) : RelayItem {
+ override val name: String = customListName.value
+
+ override val active
+ get() = locations.any { location -> location.active }
+
+ override val hasChildren
+ get() = locations.isNotEmpty()
+
+ companion object
+ }
+
+ @optics
+ sealed interface Location : RelayItem {
+ override val id: GeoLocationId
+
+ @optics
+ data class Country(
+ override val id: GeoLocationId.Country,
+ override val name: String,
+ override val expanded: Boolean,
+ val cities: List<City>
+ ) : Location {
+ val relays = cities.flatMap { city -> city.relays }
+
+ override val active
+ get() = cities.any { city -> city.active }
+
+ override val hasChildren
+ get() = cities.isNotEmpty()
+
+ companion object
+ }
+
+ @optics
+ data class City(
+ override val id: GeoLocationId.City,
+ override val name: String,
+ override val expanded: Boolean,
+ val relays: List<Relay>
+ ) : Location {
+
+ override val active
+ get() = relays.any { relay -> relay.active }
+
+ override val hasChildren
+ get() = relays.isNotEmpty()
+
+ companion object
+ }
+
+ @optics
+ data class Relay(
+ override val id: GeoLocationId.Hostname,
+ val provider: Provider,
+ override val active: Boolean,
+ ) : Location {
+ override val name: String = id.hostname
+
+ override val hasChildren = false
+ override val expanded = false
+
+ companion object
+ }
+
+ companion object
+ }
+
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt
new file mode 100644
index 0000000000..da59481269
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt
@@ -0,0 +1,48 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import android.os.Parcelable
+import arrow.optics.optics
+import kotlinx.parcelize.Parcelize
+
+@optics
+sealed interface RelayItemId : Parcelable {
+ companion object
+}
+
+@optics
+@Parcelize
+@JvmInline
+value class CustomListId(val value: String) : RelayItemId, Parcelable {
+ companion object
+}
+
+@optics
+sealed interface GeoLocationId : RelayItemId {
+ @optics
+ @Parcelize
+ data class Country(val countryCode: String) : GeoLocationId {
+ companion object
+ }
+
+ @optics
+ @Parcelize
+ data class City(val countryCode: Country, val cityCode: String) : GeoLocationId {
+ companion object
+ }
+
+ @optics
+ @Parcelize
+ data class Hostname(val city: City, val hostname: String) : GeoLocationId {
+ companion object
+ }
+
+ val country: Country
+ get() =
+ when (this) {
+ is Country -> this
+ is City -> this.countryCode
+ is Hostname -> this.city.countryCode
+ }
+
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt
new file mode 100644
index 0000000000..39e43a713e
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class RelayList(
+ val countries: List<RelayItem.Location.Country>,
+ val wireguardEndpointData: WireguardEndpointData
+)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayOverride.kt
index f738218ee7..3bd0a2f0a1 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayOverride.kt
@@ -1,12 +1,13 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
+import arrow.optics.optics
import java.net.InetAddress
-import kotlinx.parcelize.Parcelize
-@Parcelize
+@optics
data class RelayOverride(
val hostname: String,
val ipv4AddressIn: InetAddress?,
val ipv6AddressIn: InetAddress?
-) : Parcelable
+) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt
new file mode 100644
index 0000000000..ea40c980d0
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class RelaySettings(val relayConstraints: RelayConstraints) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt
new file mode 100644
index 0000000000..d00272ec63
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt
@@ -0,0 +1,9 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface RemoveDeviceError {
+ data object NotFound : RemoveDeviceError
+
+ data object RpcError : RemoveDeviceError
+
+ data class Unknown(val throwable: Throwable) : RemoveDeviceError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt
new file mode 100644
index 0000000000..aa4dcfd8be
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+interface RemoveSplitTunnelingAppError {
+ data class Unknown(val throwable: Throwable) : RemoveSplitTunnelingAppError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt
new file mode 100644
index 0000000000..1651d61db7
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.lib.model
+
+enum class SelectedObfuscation {
+ Auto,
+ Off,
+ Udp2Tcp
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt
new file mode 100644
index 0000000000..e30eba0d9e
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetAllowLanError {
+ data class Unknown(val throwable: Throwable) : SetAllowLanError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt
new file mode 100644
index 0000000000..b2b3b74edf
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetAutoConnectError {
+ data class Unknown(val throwable: Throwable) : SetAutoConnectError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt
new file mode 100644
index 0000000000..8d72d8cebe
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetDnsOptionsError {
+ data class Unknown(val throwable: Throwable) : SetDnsOptionsError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt
new file mode 100644
index 0000000000..d9c5acf650
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetObfuscationOptionsError {
+ data class Unknown(val throwable: Throwable) : SetObfuscationOptionsError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt
new file mode 100644
index 0000000000..4606c46125
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetRelayLocationError {
+ data class Unknown(val throwable: Throwable) : SetRelayLocationError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt
new file mode 100644
index 0000000000..ccf8b4c8dc
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetWireguardConstraintsError {
+ data class Unknown(val throwable: Throwable) : SetWireguardConstraintsError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt
new file mode 100644
index 0000000000..ca4f135fb1
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetWireguardMtuError {
+ data class Unknown(val throwable: Throwable) : SetWireguardMtuError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt
new file mode 100644
index 0000000000..8121120c67
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface SetWireguardQuantumResistantError {
+ data class Unknown(val throwable: Throwable) : SetWireguardQuantumResistantError
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt
index 847b80cd70..c5191531be 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt
@@ -1,16 +1,18 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
+import arrow.optics.optics
-@Parcelize
+@optics
data class Settings(
val relaySettings: RelaySettings,
val obfuscationSettings: ObfuscationSettings,
- val customLists: CustomListsSettings,
+ val customLists: List<CustomList>,
val allowLan: Boolean,
val autoConnect: Boolean,
val tunnelOptions: TunnelOptions,
- val relayOverrides: ArrayList<RelayOverride>,
+ val relayOverrides: List<RelayOverride>,
val showBetaReleases: Boolean,
-) : Parcelable
+ val splitTunnelSettings: SplitTunnelSettings
+) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SettingsPatchError.kt
index 5e3cb29911..1db1dc6f68 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SettingsPatchError.kt
@@ -1,10 +1,6 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-sealed class SettingsPatchError : Parcelable {
+sealed class SettingsPatchError {
// E.g hostname is number instead of String
data class InvalidOrMissingValue(val value: String) : SettingsPatchError()
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt
new file mode 100644
index 0000000000..a937d53bae
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class SplitTunnelSettings(val enabled: Boolean, val excludedApps: Set<AppId>)
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TransportProtocol.kt
index 89fdedaba1..b25e3061be 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TransportProtocol.kt
@@ -1,4 +1,4 @@
-package net.mullvad.talpid.net
+package net.mullvad.mullvadvpn.lib.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt
index 9c45833eb2..d715f16766 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt
@@ -1,11 +1,7 @@
-package net.mullvad.talpid.net
+package net.mullvad.mullvadvpn.lib.model
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
data class TunnelEndpoint(
val endpoint: Endpoint,
val quantumResistant: Boolean,
val obfuscation: ObfuscationEndpoint?
-) : Parcelable
+)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt
new file mode 100644
index 0000000000..de1d760d30
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class TunnelOptions(val wireguard: WireguardTunnelOptions, val dnsOptions: DnsOptions) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt
new file mode 100644
index 0000000000..3fae41802a
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt
@@ -0,0 +1,35 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed class TunnelState {
+ data class Disconnected(val location: GeoIpLocation? = null) : TunnelState()
+
+ data class Connecting(val endpoint: TunnelEndpoint?, val location: GeoIpLocation?) :
+ TunnelState()
+
+ data class Connected(val endpoint: TunnelEndpoint, val location: GeoIpLocation?) :
+ TunnelState()
+
+ data class Disconnecting(val actionAfterDisconnect: ActionAfterDisconnect) : TunnelState()
+
+ data class Error(val errorState: ErrorState) : TunnelState()
+
+ fun location(): GeoIpLocation? {
+ return when (this) {
+ is Connected -> location
+ is Connecting -> location
+ is Disconnecting -> null
+ is Disconnected -> location
+ is Error -> null
+ }
+ }
+
+ fun isSecured(): Boolean {
+ return when (this) {
+ is Connected,
+ is Connecting,
+ is Disconnecting, -> true
+ is Disconnected -> false
+ is Error -> this.errorState.isBlocking
+ }
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt
new file mode 100644
index 0000000000..7447f7a4cf
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class Udp2TcpObfuscationSettings(val port: Constraint<Port>)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt
new file mode 100644
index 0000000000..ef49018dca
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt
@@ -0,0 +1,35 @@
+package net.mullvad.mullvadvpn.lib.model
+
+sealed interface UpdateCustomListNameError {
+ companion object {
+ fun from(error: UpdateCustomListError): UpdateCustomListNameError =
+ when (error) {
+ is NameAlreadyExists -> error
+ is UnknownCustomListError -> error
+ }
+ }
+}
+
+sealed interface UpdateCustomListLocationsError {
+ companion object {
+ fun from(error: UpdateCustomListError): UpdateCustomListLocationsError =
+ when (error) {
+ is NameAlreadyExists -> error("Not supported error")
+ is UnknownCustomListError -> error
+ }
+ }
+}
+
+sealed interface UpdateCustomListError
+
+data class NameAlreadyExists(val name: String) : UpdateCustomListError, UpdateCustomListNameError
+
+data class UnknownCustomListError(val throwable: Throwable) :
+ UpdateCustomListError,
+ UpdateCustomListNameError,
+ UpdateCustomListLocationsError,
+ CreateCustomListError,
+ DeleteCustomListError
+
+data class GetCustomListError(val id: CustomListId) :
+ UpdateCustomListLocationsError, UpdateCustomListNameError
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt
new file mode 100644
index 0000000000..8ad9b85787
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+@JvmInline
+value class WebsiteAuthToken private constructor(val value: String) {
+ companion object {
+ fun fromString(value: String) = WebsiteAuthToken(value)
+ }
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt
new file mode 100644
index 0000000000..8affb81077
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.lib.model
+
+import arrow.optics.optics
+
+@optics
+data class WireguardConstraints(val port: Constraint<Port>) {
+ companion object
+}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt
new file mode 100644
index 0000000000..8aff7d2895
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class WireguardEndpointData(val portRanges: List<PortRange>)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt
new file mode 100644
index 0000000000..573f08213e
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.model
+
+data class WireguardTunnelOptions(val mtu: Mtu?, val quantumResistant: QuantumResistantState)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt
new file mode 100644
index 0000000000..0df57eb057
--- /dev/null
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.model.extensions
+
+fun String.startCase() =
+ split(" ").joinToString(" ") { word ->
+ word.replaceFirstChar { firstChar -> firstChar.uppercase() }
+ }
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt
deleted file mode 100644
index f5137ebbb7..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class AccountAndDevice(val account_token: String, val device: Device) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt
deleted file mode 100644
index 4bb4c61384..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class AccountCreationResult : Parcelable {
- @Parcelize data class Success(val accountToken: String) : AccountCreationResult()
-
- @Parcelize object Failure : AccountCreationResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt
deleted file mode 100644
index 6dda6b8352..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-data class AccountData(val expiry: String)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt
deleted file mode 100644
index f856ef8c89..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import org.joda.time.DateTime
-
-sealed class AccountExpiry : Parcelable {
- @Parcelize data class Available(val expiryDateTime: DateTime) : AccountExpiry()
-
- @Parcelize data object Missing : AccountExpiry()
-
- fun date(): DateTime? {
- return (this as? Available)?.expiryDateTime
- }
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt
deleted file mode 100644
index f003ee316b..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class AccountHistory : Parcelable {
- @Parcelize data class Available(val accountToken: String) : AccountHistory()
-
- @Parcelize object Missing : AccountHistory()
-
- fun accountToken() = (this as? Available)?.accountToken
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt
deleted file mode 100644
index 2aeca352d0..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-@JvmInline value class AccountToken(val value: String)
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt
deleted file mode 100644
index bbe99ce656..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class AppVersionInfo(val supported: Boolean, val suggestedUpgrade: String?) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt
deleted file mode 100644
index d9ca22b164..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class Constraint<T> : Parcelable {
- @Parcelize @Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY") class Any<T> : Constraint<T>()
-
- @Parcelize data class Only<T : Parcelable>(val value: T) : Constraint<T>()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt
deleted file mode 100644
index 73eaa209c8..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class CreateCustomListResult : Parcelable {
- @Parcelize data class Ok(val id: String) : CreateCustomListResult()
-
- @Parcelize data class Error(val error: CustomListsError) : CreateCustomListResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt
deleted file mode 100644
index bbf029dd4d..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import java.net.InetAddress
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class CustomDnsOptions(val addresses: ArrayList<InetAddress>) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt
deleted file mode 100644
index cdfa1b9687..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class CustomList(
- val id: String,
- val name: String,
- val locations: ArrayList<GeographicLocationConstraint>
-) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt
deleted file mode 100644
index 83806af4f7..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-enum class CustomListsError {
- CustomListExists,
- OtherError
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt
deleted file mode 100644
index 8a8c03ef05..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class CustomListsSettings(val customLists: ArrayList<CustomList>) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt
deleted file mode 100644
index 72276c65e4..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-class CustomTunnelEndpoint
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt
deleted file mode 100644
index 0f0a55d05d..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class Device(
- val id: String,
- private val name: String,
- val pubkey: ByteArray,
- val created: String
-) : Parcelable {
- // Generated by Android Studio
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as Device
-
- if (id != other.id) return false
- if (name != other.name) return false
- return pubkey.contentEquals(other.pubkey)
- }
-
- // Generated by Android Studio
- override fun hashCode(): Int {
- var result = id.hashCode()
- result = 31 * result + name.hashCode()
- result = 31 * result + pubkey.contentHashCode()
- return result
- }
-
- fun displayName(): String = name.capitalizeFirstCharOfEachWord()
-}
-
-private fun String.capitalizeFirstCharOfEachWord(): String {
- return split(" ")
- .joinToString(" ") { word -> word.replaceFirstChar { firstChar -> firstChar.uppercase() } }
- .trimEnd()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt
deleted file mode 100644
index 741108612d..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class DeviceEvent(val cause: DeviceEventCause, val newState: DeviceState) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt
deleted file mode 100644
index b4c1d21761..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class DeviceEventCause : Parcelable {
- LoggedIn,
- LoggedOut,
- Revoked,
- Updated,
- RotatedKey
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt
deleted file mode 100644
index afe5982ed5..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-sealed class DeviceList {
- object Unavailable : DeviceList()
-
- data class Available(val devices: List<Device>) : DeviceList()
-
- object Error : DeviceList()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt
deleted file mode 100644
index 7a2883617b..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class DeviceListEvent : Parcelable {
- @Parcelize
- data class Available(val accountToken: String, val devices: List<Device>) : DeviceListEvent()
-
- @Parcelize object Error : DeviceListEvent()
-
- fun isAvailable(): Boolean {
- return (this is Available)
- }
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt
deleted file mode 100644
index e43eae3e6b..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class DevicePort(val id: String) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt
deleted file mode 100644
index fb34c9e645..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class DeviceState : Parcelable {
- @Parcelize object Initial : DeviceState()
-
- @Parcelize object Unknown : DeviceState()
-
- @Parcelize data class LoggedIn(val accountAndDevice: AccountAndDevice) : DeviceState()
-
- @Parcelize object LoggedOut : DeviceState()
-
- @Parcelize object Revoked : DeviceState()
-
- fun isUnknown(): Boolean {
- return this is Unknown
- }
-
- fun deviceName(): String? {
- return (this as? LoggedIn)?.accountAndDevice?.device?.displayName()
- }
-
- fun token(): String? {
- return (this as? LoggedIn)?.accountAndDevice?.account_token
- }
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt
deleted file mode 100644
index 386257a72a..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class GeographicLocationConstraint : Parcelable {
- abstract val location: GeoIpLocation
-
- @Parcelize
- data class Country(val countryCode: String) : GeographicLocationConstraint() {
- override val location: GeoIpLocation
- get() = GeoIpLocation(null, null, countryCode, null, 0.0, 0.0, null)
- }
-
- @Parcelize
- data class City(val countryCode: String, val cityCode: String) :
- GeographicLocationConstraint() {
- override val location: GeoIpLocation
- get() = GeoIpLocation(null, null, countryCode, cityCode, 0.0, 0.0, null)
- }
-
- @Parcelize
- data class Hostname(val countryCode: String, val cityCode: String, val hostname: String) :
- GeographicLocationConstraint() {
- override val location: GeoIpLocation
- get() = GeoIpLocation(null, null, countryCode, cityCode, 0.0, 0.0, hostname)
- }
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt
deleted file mode 100644
index 2e94266e2a..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-sealed class GetAccountDataResult {
- class Ok(val accountData: AccountData) : GetAccountDataResult()
-
- object InvalidAccount : GetAccountDataResult()
-
- object RpcError : GetAccountDataResult()
-
- object OtherError : GetAccountDataResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt
deleted file mode 100644
index 0c9d331e3b..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class LocationConstraint : Parcelable {
- @Parcelize
- data class Location(val location: GeographicLocationConstraint) : LocationConstraint()
-
- @Parcelize data class CustomList(val listId: String) : LocationConstraint()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt
deleted file mode 100644
index 29fb68203d..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class LoginResult : Parcelable {
- Ok,
- InvalidAccount,
- MaxDevicesReached,
- RpcError,
- OtherError
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt
deleted file mode 100644
index 43037be676..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class Ownership : Parcelable {
- MullvadOwned,
- Rented
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt
deleted file mode 100644
index 8ae46a07a9..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class PlayPurchase(val productId: String, val purchaseToken: String) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt
deleted file mode 100644
index 39aebabbe2..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class PlayPurchaseInitError : Parcelable {
- // TODO: Add more errors here.
- OtherError
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt
deleted file mode 100644
index 41407474af..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class PlayPurchaseInitResult : Parcelable {
- @Parcelize data class Ok(val obfuscatedId: String) : PlayPurchaseInitResult()
-
- @Parcelize data class Error(val error: PlayPurchaseInitError) : PlayPurchaseInitResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt
deleted file mode 100644
index b0434c22f9..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class PlayPurchaseVerifyError : Parcelable {
- // TODO: Add more errors here.
- OtherError
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt
deleted file mode 100644
index 7c5ee4d953..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class PlayPurchaseVerifyResult : Parcelable {
- @Parcelize data object Ok : PlayPurchaseVerifyResult()
-
- @Parcelize data class Error(val error: PlayPurchaseVerifyError) : PlayPurchaseVerifyResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt
deleted file mode 100644
index 52f495a7a7..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class Port(val value: Int) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt
deleted file mode 100644
index 376f5ef7a4..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class PortRange(val from: Int, val to: Int) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt
deleted file mode 100644
index d3c6aacba9..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Suppress("ensure value classes property is named value")
-@JvmInline
-@Parcelize
-value class Providers(val providers: HashSet<String>) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt
deleted file mode 100644
index 169b6c3856..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class PublicKey(val key: ByteArray, val dateCreated: String) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt
deleted file mode 100644
index a19267388a..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class QuantumResistantState : Parcelable {
- Auto,
- On,
- Off
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt
deleted file mode 100644
index 461648209c..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class Relay(
- val hostname: String,
- val active: Boolean,
- val owned: Boolean,
- val provider: String,
- val endpointData: RelayEndpointData
-) : Parcelable {
- val isWireguardRelay
- get() = endpointData is RelayEndpointData.Wireguard
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt
deleted file mode 100644
index 031b09bace..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class RelayConstraints(
- val location: Constraint<LocationConstraint>,
- val providers: Constraint<Providers>,
- val ownership: Constraint<Ownership>,
- val wireguardConstraints: WireguardConstraints,
-) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt
deleted file mode 100644
index 86b3f0fa35..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class RelayEndpointData : Parcelable {
- @Parcelize object Openvpn : RelayEndpointData()
-
- @Parcelize object Bridge : RelayEndpointData()
-
- @Parcelize
- data class Wireguard(val wireguardRelayEndpointData: WireguardRelayEndpointData) :
- RelayEndpointData()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt
deleted file mode 100644
index 60d8b6dd35..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class RelayList(
- val countries: ArrayList<RelayListCountry>,
- val wireguardEndpointData: WireguardEndpointData
-) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt
deleted file mode 100644
index 2376609ced..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class RelayListCity(val name: String, val code: String, val relays: ArrayList<Relay>) :
- Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt
deleted file mode 100644
index d6d4b8ec6a..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class RelayListCountry(
- val name: String,
- val code: String,
- val cities: ArrayList<RelayListCity>
-) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt
deleted file mode 100644
index 642046f1b8..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class RelaySettings : Parcelable {
- @Parcelize data object CustomTunnelEndpoint : RelaySettings()
-
- @Parcelize data class Normal(val relayConstraints: RelayConstraints) : RelaySettings()
-
- fun relayConstraints(): RelayConstraints? = (this as? Normal)?.relayConstraints
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt
deleted file mode 100644
index cc6e7db2bb..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class RemoveDeviceEvent(val accountToken: String, val newDevices: ArrayList<Device>) :
- Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt
deleted file mode 100644
index 67bf165a37..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class RemoveDeviceResult : Parcelable {
- Ok,
- NotFound,
- RpcError,
- OtherError
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt
deleted file mode 100644
index 8124bcc6a6..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class SelectedObfuscation : Parcelable {
- Auto,
- Off,
- Udp2Tcp
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt
deleted file mode 100644
index e597797e5a..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.IBinder
-
-data class ServiceResult(val binder: IBinder?) {
- enum class ConnectionState {
- CONNECTED,
- DISCONNECTED
- }
-
- val connectionState: ConnectionState
- get() {
- return if (binder == null) {
- ConnectionState.DISCONNECTED
- } else {
- ConnectionState.CONNECTED
- }
- }
-
- companion object {
- val NOT_CONNECTED = ServiceResult(null)
- }
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt
deleted file mode 100644
index 108fd32e04..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class TunnelOptions(val wireguard: WireguardTunnelOptions, val dnsOptions: DnsOptions) :
- Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt
deleted file mode 100644
index 4ab925d014..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import net.mullvad.talpid.net.TunnelEndpoint
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorState
-
-sealed class TunnelState : Parcelable {
- @Parcelize
- data class Disconnected(val location: GeoIpLocation? = null) : TunnelState(), Parcelable
-
- @Parcelize
- class Connecting(val endpoint: TunnelEndpoint?, val location: GeoIpLocation?) :
- TunnelState(), Parcelable
-
- @Parcelize
- class Connected(val endpoint: TunnelEndpoint, val location: GeoIpLocation?) :
- TunnelState(), Parcelable
-
- @Parcelize
- class Disconnecting(val actionAfterDisconnect: ActionAfterDisconnect) :
- TunnelState(), Parcelable
-
- @Parcelize class Error(val errorState: ErrorState) : TunnelState(), Parcelable
-
- fun location(): GeoIpLocation? {
- return when (this) {
- is Connected -> location
- is Connecting -> location
- is Disconnecting -> null
- is Disconnected -> location
- is Error -> null
- }
- }
-
- fun isSecured(): Boolean {
- return when (this) {
- is Connected,
- is Connecting,
- is Disconnecting, -> true
- is Disconnected -> false
- is Error -> this.errorState.isBlocking
- }
- }
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt
deleted file mode 100644
index f01bb35c6f..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class Udp2TcpObfuscationSettings(val port: Constraint<Int>) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt
deleted file mode 100644
index ebfe9e8cd6..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class UpdateCustomListResult : Parcelable {
- @Parcelize data object Ok : UpdateCustomListResult()
-
- @Parcelize data class Error(val error: CustomListsError) : UpdateCustomListResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt
deleted file mode 100644
index efe05e2f5c..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class VoucherSubmission(val timeAdded: Long, val newExpiry: String) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt
deleted file mode 100644
index 1cf778400a..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class VoucherSubmissionError : Parcelable {
- InvalidVoucher,
- VoucherAlreadyUsed,
- RpcError,
- OtherError,
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt
deleted file mode 100644
index 4163b782d4..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-sealed class VoucherSubmissionResult : Parcelable {
- @Parcelize data class Ok(val submission: VoucherSubmission) : VoucherSubmissionResult()
-
- @Parcelize data class Error(val error: VoucherSubmissionError) : VoucherSubmissionResult()
-}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt
deleted file mode 100644
index 1725b01f0f..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class WireguardConstraints(val port: Constraint<Port>) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt
deleted file mode 100644
index 0a21221bb0..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class WireguardEndpointData(val portRanges: ArrayList<PortRange>) : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt
deleted file mode 100644
index 4a1930dd43..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize object WireguardRelayEndpointData : Parcelable
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt
deleted file mode 100644
index f4a869a4ea..0000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.mullvadvpn.model
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class WireguardTunnelOptions(val mtu: Int?, val quantumResistant: QuantumResistantState) :
- Parcelable
diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatLongTest.kt
index b8608ca55c..8abef5d9b3 100644
--- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt
+++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatLongTest.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import kotlin.math.sqrt
import org.junit.jupiter.api.Assertions.assertEquals
diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatitudeTest.kt
index c883f20bfc..214afef127 100644
--- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt
+++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatitudeTest.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import kotlin.math.absoluteValue
import kotlin.test.assertEquals
diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LongitudeTest.kt
index 69d3445417..88017cdcea 100644
--- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt
+++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LongitudeTest.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.model
+package net.mullvad.mullvadvpn.lib.model
import kotlin.math.absoluteValue
import kotlin.test.assertEquals
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 38a839bed3..f9acacb5d9 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -80,19 +80,18 @@
<string name="vpn_settings_not_found">There is no VPN settings on your device</string>
<string name="auto_connect_carousel_first_slide_top_text">The Auto-connect and Lockdown mode settings can be found in the Android system settings, follow this guide to enable one or both.</string>
<string name="auto_connect_carousel_first_slide_bottom_text">
- <![CDATA[1. After clicking on the <b>Go to VPN settings</b> button below, click on the cogwheel next to the <b>Mullvad VPN</b> name.]]>
+ <![CDATA[1. After clicking on the <b>Go to VPN settings</b> button below, click on the cogwheel next to the <b>Mullvad VPN</b> name.]]>
</string>
<string name="auto_connect_carousel_second_slide_top_text">Auto-connect is called Always-on VPN in the Android system settings and it makes sure you are constantly connected to the VPN tunnel and auto connects after restart.</string>
<string name="auto_connect_carousel_second_slide_bottom_text">
- <![CDATA[2. To enable Auto-connect, click on the toggle next to <b>Always-on VPN</b>.]]>
- </string>
+ <![CDATA[2. To enable Auto-connect, click on the toggle next to <b>Always-on VPN</b>.]]>
+ </string>
<string name="auto_connect_carousel_third_slide_top_text">
<![CDATA[The Lockdown mode blocks all internet access if the VPN tunnel is manually disconnected. <br/><b>Warning: This setting blocks split apps and the Local Network Sharing feature</b>.]]>
</string>
<string name="auto_connect_carousel_third_slide_bottom_text">
<![CDATA[3. To enable Lockdown mode, click on the toggle next to <b>Block connections without VPN</b>.]]>
</string>
-
<string name="auto_connect_footer">Automatically connect to a server when the app launches.</string>
<string name="wireguard_mtu">WireGuard MTU</string>
<string name="wireguard_mtu_footer">Set WireGuard MTU value. Valid range: %1$d - %2$d.</string>
@@ -281,7 +280,9 @@
<string name="loading_verifying">Verifying purchase...</string>
<string name="copied_logs_to_clipboard">Copied logs to clipboard</string>
<string name="auto_connect_legacy">Auto-connect (legacy)</string>
- <string name="auto_connect_footer_legacy"><![CDATA[Please use the <b>Always-on</b> system setting instead by following the guide in <b>%s</b> above.]]></string>
+ <string name="auto_connect_footer_legacy">
+ <![CDATA[Please use the <b>Always-on</b> system setting instead by following the guide in <b>%s</b> above.]]>
+ </string>
<string name="custom_lists">Custom lists</string>
<string name="all_locations">All locations</string>
<string name="edit_lists">Edit lists</string>
@@ -295,9 +296,7 @@
<string name="list_name">List name</string>
<string name="locations">Locations</string>
<string name="edit_locations">Edit locations</string>
- <string name="delete_custom_list_confirmation_description">
- Delete \"%s\"?
- </string>
+ <string name="delete_custom_list_confirmation_description">Delete \"%s\"?</string>
<string name="custom_list_error_list_exists">Name is already taken.</string>
<string name="update_list_name">Update list name</string>
<string name="no_custom_lists_available">No custom lists available</string>
@@ -344,4 +343,5 @@
<string name="settings_patch_error_recursion_limit">Recursion limit</string>
<string name="settings_patch_success">Import successful, overrides active</string>
<string name="overrides_cleared">Overrides cleared</string>
+ <string name="unsecured_vpn_permission_error">Unsecured (No VPN permission)</string>
</resources>
diff --git a/android/lib/shared/build.gradle.kts b/android/lib/shared/build.gradle.kts
new file mode 100644
index 0000000000..88b5cfb3c9
--- /dev/null
+++ b/android/lib/shared/build.gradle.kts
@@ -0,0 +1,47 @@
+plugins {
+ id(Dependencies.Plugin.androidLibraryId)
+ id(Dependencies.Plugin.kotlinAndroidId)
+ id(Dependencies.Plugin.kotlinParcelizeId)
+ id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.lib.shared"
+ compileSdk = Versions.Android.compileSdkVersion
+
+ defaultConfig { minSdk = Versions.Android.minSdkVersion }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions { jvmTarget = Versions.jvmTarget }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+ buildFeatures { buildConfig = true }
+}
+
+dependencies {
+ implementation(project(Dependencies.Mullvad.commonLib))
+ implementation(project(Dependencies.Mullvad.daemonGrpc))
+ implementation(project(Dependencies.Mullvad.modelLib))
+
+ implementation(Dependencies.Arrow.core)
+ implementation(Dependencies.Kotlin.stdlib)
+ implementation(Dependencies.KotlinX.coroutinesAndroid)
+ implementation(Dependencies.jodaTime)
+
+ testImplementation(Dependencies.Kotlin.test)
+ testImplementation(Dependencies.KotlinX.coroutinesTest)
+ testImplementation(Dependencies.MockK.core)
+ testImplementation(Dependencies.junitApi)
+ testImplementation(Dependencies.junitParams)
+ testImplementation(Dependencies.turbine)
+ testImplementation(project(Dependencies.Mullvad.commonTestLib))
+ testRuntimeOnly(Dependencies.junitEngine)
+}
diff --git a/android/lib/shared/src/main/AndroidManifest.xml b/android/lib/shared/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..cc947c5679
--- /dev/null
+++ b/android/lib/shared/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest />
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
new file mode 100644
index 0000000000..432d113fba
--- /dev/null
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt
@@ -0,0 +1,84 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import arrow.core.Either
+import arrow.core.raise.nullable
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.AccountData
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.CreateAccountError
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.LoginAccountError
+import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
+import org.joda.time.DateTime
+
+class AccountRepository(
+ private val managementService: ManagementService,
+ private val deviceRepository: DeviceRepository,
+ val scope: CoroutineScope
+) {
+
+ private val _mutableAccountDataCache: MutableSharedFlow<AccountData> = MutableSharedFlow()
+
+ private val _isNewAccount: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ val isNewAccount: StateFlow<Boolean> = _isNewAccount
+ val accountData: StateFlow<AccountData?> =
+ merge(
+ managementService.deviceState.filterNotNull().map { deviceState ->
+ when (deviceState) {
+ is DeviceState.LoggedIn -> {
+ managementService.getAccountData(deviceState.accountToken).getOrNull()
+ }
+ DeviceState.LoggedOut,
+ DeviceState.Revoked -> null
+ }
+ },
+ _mutableAccountDataCache
+ )
+ .distinctUntilChanged()
+ .stateIn(scope = scope, SharingStarted.Eagerly, null)
+
+ suspend fun createAccount(): Either<CreateAccountError, AccountToken> =
+ managementService.createAccount().onRight { _isNewAccount.update { true } }
+
+ suspend fun login(accountToken: AccountToken): Either<LoginAccountError, Unit> =
+ managementService.loginAccount(accountToken)
+
+ suspend fun logout() {
+ managementService.logoutAccount()
+ _isNewAccount.update { false }
+ }
+
+ suspend fun fetchAccountHistory(): AccountToken? =
+ managementService.getAccountHistory().getOrNull()
+
+ suspend fun clearAccountHistory() = managementService.clearAccountHistory()
+
+ suspend fun getAccountData(): AccountData? = nullable {
+ val deviceState = ensureNotNull(deviceRepository.deviceState.value as? DeviceState.LoggedIn)
+
+ val accountData =
+ managementService.getAccountData(deviceState.accountToken).getOrNull().bind()
+
+ // Update stateflow cache
+ _mutableAccountDataCache.emit(accountData)
+ accountData
+ }
+
+ suspend fun getWebsiteAuthToken(): WebsiteAuthToken? =
+ managementService.getWebsiteAuthToken().getOrNull()
+
+ internal suspend fun onVoucherRedeemed(newExpiry: DateTime) {
+ accountData.value?.copy(expiryDate = newExpiry)?.let { _mutableAccountDataCache.emit(it) }
+ }
+}
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt
new file mode 100644
index 0000000000..6ea373e426
--- /dev/null
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt
@@ -0,0 +1,26 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import arrow.core.Either
+import arrow.core.raise.either
+import arrow.core.raise.ensure
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.ConnectError
+
+class ConnectionProxy(
+ private val managementService: ManagementService,
+ private val vpnPermissionRepository: VpnPermissionRepository
+) {
+ val tunnelState = managementService.tunnelState
+
+ suspend fun connect(): Either<ConnectError, Boolean> = either {
+ ensure(vpnPermissionRepository.hasVpnPermission()) { ConnectError.NoVpnPermission }
+ managementService.connect().bind()
+ }
+
+ suspend fun connectWithoutPermissionCheck(): Either<ConnectError, Boolean> =
+ managementService.connect()
+
+ suspend fun disconnect() = managementService.disconnect()
+
+ suspend fun reconnect() = managementService.reconnect()
+}
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt
new file mode 100644
index 0000000000..b1b8f4fa41
--- /dev/null
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt
@@ -0,0 +1,36 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import arrow.core.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.stateIn
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.AccountToken
+import net.mullvad.mullvadvpn.lib.model.DeleteDeviceError
+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.model.GetDeviceListError
+
+class DeviceRepository(
+ private val managementService: ManagementService,
+ dispatcher: CoroutineDispatcher = Dispatchers.IO
+) {
+ val deviceState: StateFlow<DeviceState?> =
+ managementService.deviceState.stateIn(
+ CoroutineScope(dispatcher),
+ SharingStarted.Eagerly,
+ null
+ )
+
+ suspend fun removeDevice(
+ accountToken: AccountToken,
+ deviceId: DeviceId
+ ): Either<DeleteDeviceError, Unit> = managementService.removeDevice(accountToken, deviceId)
+
+ suspend fun deviceList(accountToken: AccountToken): Either<GetDeviceListError, List<Device>> =
+ managementService.getDeviceList(accountToken)
+}
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt
new file mode 100644
index 0000000000..a5783a832e
--- /dev/null
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+
+class VoucherRepository(
+ private val managementService: ManagementService,
+ private val accountRepository: AccountRepository
+) {
+ suspend fun submitVoucher(voucher: String) =
+ managementService.submitVoucher(voucher).onRight {
+ accountRepository.onVoucherRedeemed(it.newExpiryDate)
+ }
+}
diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt
new file mode 100644
index 0000000000..b97c60316c
--- /dev/null
+++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt
@@ -0,0 +1,11 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import android.content.Context
+import android.net.VpnService
+import net.mullvad.mullvadvpn.lib.common.util.getAlwaysOnVpnAppName
+
+class VpnPermissionRepository(private val applicationContext: Context) {
+ fun hasVpnPermission(): Boolean = VpnService.prepare(applicationContext) == null
+
+ fun getAlwaysOnVpnAppName() = applicationContext.getAlwaysOnVpnAppName()
+}
diff --git a/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt
new file mode 100644
index 0000000000..74ab4f6b64
--- /dev/null
+++ b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt
@@ -0,0 +1,54 @@
+package net.mullvad.mullvadvpn.lib.shared
+
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Test
+
+class ConnectionProxyTest {
+
+ private val mockManagementService: ManagementService = mockk(relaxed = true)
+ private val mockVpnPermissionRepository: VpnPermissionRepository = mockk()
+
+ private val connectionProxy: ConnectionProxy =
+ ConnectionProxy(
+ managementService = mockManagementService,
+ vpnPermissionRepository = mockVpnPermissionRepository
+ )
+
+ @Test
+ fun `connect with vpn permission allowed should call managementService connect`() = runTest {
+ every { mockVpnPermissionRepository.hasVpnPermission() } returns true
+ connectionProxy.connect()
+ coVerify(exactly = 1) { mockManagementService.connect() }
+ }
+
+ @Test
+ fun `connect with vpn permission not allowed should not call managementService connect`() =
+ runTest {
+ every { mockVpnPermissionRepository.hasVpnPermission() } returns false
+ connectionProxy.connect()
+ coVerify(exactly = 0) { mockManagementService.connect() }
+ }
+
+ @Test
+ fun `disconnect should call managementService disconnect`() = runTest {
+ connectionProxy.disconnect()
+ coVerify(exactly = 1) { mockManagementService.disconnect() }
+ }
+
+ @Test
+ fun `reconnect should call managementService reconnect`() = runTest {
+ connectionProxy.reconnect()
+ coVerify(exactly = 1) { mockManagementService.reconnect() }
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkAll()
+ }
+}
diff --git a/android/lib/talpid/build.gradle.kts b/android/lib/talpid/build.gradle.kts
index ac760a860e..00409f9482 100644
--- a/android/lib/talpid/build.gradle.kts
+++ b/android/lib/talpid/build.gradle.kts
@@ -25,6 +25,9 @@ android {
}
dependencies {
+ implementation(project(Dependencies.Mullvad.modelLib))
+
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
+ implementation(Dependencies.AndroidX.lifecycleService)
}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt
index cdc16567e1..905f59f313 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt
@@ -7,7 +7,6 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import kotlin.properties.Delegates.observable
-import net.mullvad.talpid.util.EventNotifier
class ConnectivityListener {
private val availableNetworks = HashSet<Network>()
@@ -21,22 +20,19 @@ class ConnectivityListener {
override fun onLost(network: Network) {
availableNetworks.remove(network)
- isConnected = !availableNetworks.isEmpty()
+ isConnected = availableNetworks.isNotEmpty()
}
}
private lateinit var connectivityManager: ConnectivityManager
- val connectivityNotifier = EventNotifier(false)
-
+ // Used by JNI
var isConnected by
observable(false) { _, oldValue, newValue ->
if (newValue != oldValue) {
if (senderAddress != 0L) {
notifyConnectivityChange(newValue, senderAddress)
}
-
- connectivityNotifier.notify(newValue)
}
}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt
new file mode 100644
index 0000000000..efb29c31c6
--- /dev/null
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt
@@ -0,0 +1,56 @@
+package net.mullvad.talpid
+
+import android.content.Intent
+import android.net.VpnService
+import android.os.IBinder
+import androidx.annotation.CallSuper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ServiceLifecycleDispatcher
+
+/**
+ * A VpnService that is also a [LifecycleOwner]. See source:
+ * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt?q=file:androidx%2Flifecycle%2FLifecycleService.kt%20class:androidx.lifecycle.LifecycleService
+ */
+open class LifecycleVpnService : VpnService(), LifecycleOwner {
+
+ private val dispatcher = ServiceLifecycleDispatcher(this)
+
+ @CallSuper
+ override fun onCreate() {
+ dispatcher.onServicePreSuperOnCreate()
+ super.onCreate()
+ }
+
+ @CallSuper
+ override fun onBind(intent: Intent?): IBinder? {
+ dispatcher.onServicePreSuperOnBind()
+ return super.onBind(intent)
+ }
+
+ @Deprecated("Deprecated in Java")
+ @Suppress("DEPRECATION")
+ @CallSuper
+ override fun onStart(intent: Intent?, startId: Int) {
+ dispatcher.onServicePreSuperOnStart()
+ super.onStart(intent, startId)
+ }
+
+ // this method is added only to annotate it with @CallSuper.
+ // In usual Service, super.onStartCommand is no-op, but in LifecycleService
+ // it results in dispatcher.onServicePreSuperOnStart() call, because
+ // super.onStartCommand calls onStart().
+ @CallSuper
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ @CallSuper
+ override fun onDestroy() {
+ dispatcher.onServicePreSuperOnDestroy()
+ super.onDestroy()
+ }
+
+ override val lifecycle: Lifecycle
+ get() = dispatcher.lifecycle
+}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt
index 76abde2a01..e89c841d25 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt
@@ -1,16 +1,17 @@
package net.mullvad.talpid
-import android.net.VpnService
import android.os.ParcelFileDescriptor
import android.util.Log
+import androidx.annotation.CallSuper
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import kotlin.properties.Delegates.observable
-import net.mullvad.talpid.tun_provider.TunConfig
+import net.mullvad.talpid.model.CreateTunResult
+import net.mullvad.talpid.model.TunConfig
import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported
-open class TalpidVpnService : VpnService() {
+open class TalpidVpnService : LifecycleVpnService() {
private var activeTunStatus by
observable<CreateTunResult?>(null) { _, oldTunStatus, _ ->
val oldTunFd =
@@ -29,17 +30,19 @@ open class TalpidVpnService : VpnService() {
get() = activeTunStatus?.isOpen ?: false
private var currentTunConfig = defaultTunConfig()
- private var tunIsStale = false
-
- protected var disallowedApps: List<String>? = null
+ // Used by JNI
val connectivityListener = ConnectivityListener()
+ @CallSuper
override fun onCreate() {
+ super.onCreate()
connectivityListener.register(this)
}
+ @CallSuper
override fun onDestroy() {
+ super.onDestroy()
connectivityListener.unregister()
}
@@ -47,14 +50,13 @@ open class TalpidVpnService : VpnService() {
synchronized(this) {
val tunStatus = activeTunStatus
- if (config == currentTunConfig && tunIsOpen && !tunIsStale) {
+ if (config == currentTunConfig && tunIsOpen) {
return tunStatus!!
} else {
val newTunStatus = createTun(config)
currentTunConfig = config
activeTunStatus = newTunStatus
- tunIsStale = false
return newTunStatus
}
@@ -78,17 +80,13 @@ open class TalpidVpnService : VpnService() {
synchronized(this) { activeTunStatus = null }
}
- fun markTunAsStale() {
- synchronized(this) { tunIsStale = true }
- }
-
private fun createTun(config: TunConfig): CreateTunResult {
if (prepare(this) != null) {
// VPN permission wasn't granted
return CreateTunResult.PermissionDenied
}
- var invalidDnsServerAddresses = ArrayList<InetAddress>()
+ val invalidDnsServerAddresses = ArrayList<InetAddress>()
val builder =
Builder().apply {
@@ -120,11 +118,7 @@ open class TalpidVpnService : VpnService() {
addRoute(route.address, route.prefixLength.toInt())
}
- disallowedApps?.let { apps ->
- for (app in apps) {
- addDisallowedApplication(app)
- }
- }
+ config.excludedPackages.forEach { app -> addDisallowedApplication(app) }
setMtu(config.mtu)
setBlocking(false)
setMeteredIfSupported(false)
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt
index 33f62026d6..089112e3ab 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt
@@ -1,4 +1,4 @@
-package net.mullvad.talpid
+package net.mullvad.talpid.model
import java.net.InetAddress
@@ -17,7 +17,7 @@ sealed class CreateTunResult {
get() = true
}
- object PermissionDenied : CreateTunResult()
+ data object PermissionDenied : CreateTunResult()
- object TunnelDeviceError : CreateTunResult()
+ data object TunnelDeviceError : CreateTunResult()
}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/InetNetwork.kt
index a8490b48bf..a9c257c3e4 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/InetNetwork.kt
@@ -1,4 +1,4 @@
-package net.mullvad.talpid.tun_provider
+package net.mullvad.talpid.model
import java.net.Inet6Address
import java.net.InetAddress
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/TunConfig.kt
index 7efd3f7763..955a6f4454 100644
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt
+++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/TunConfig.kt
@@ -1,4 +1,4 @@
-package net.mullvad.talpid.tun_provider
+package net.mullvad.talpid.model
import java.net.InetAddress
@@ -6,5 +6,6 @@ data class TunConfig(
val addresses: ArrayList<InetAddress>,
val dnsServers: ArrayList<InetAddress>,
val routes: ArrayList<InetNetwork>,
+ val excludedPackages: ArrayList<String>,
val mtu: Int
)
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt
deleted file mode 100644
index 8937bd0122..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.talpid.net
-
-import android.os.Parcelable
-import java.net.InetSocketAddress
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class Endpoint(val address: InetSocketAddress, val protocol: TransportProtocol) : Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt
deleted file mode 100644
index 9ec96b1494..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package net.mullvad.talpid.net
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class ObfuscationEndpoint(val endpoint: Endpoint, val obfuscationType: ObfuscationType) :
- Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt
deleted file mode 100644
index 72409d9026..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.mullvad.talpid.net
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class ObfuscationType : Parcelable {
- Udp2Tcp
-}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt
deleted file mode 100644
index a62abaacd0..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.talpid.tunnel
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class ActionAfterDisconnect : Parcelable {
- Nothing,
- Block,
- Reconnect
-}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt
deleted file mode 100644
index 070d190beb..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.talpid.tunnel
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize data class ErrorState(val cause: ErrorStateCause, val isBlocking: Boolean) : Parcelable
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt
deleted file mode 100644
index fc35e4e23e..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package net.mullvad.talpid.tunnel
-
-import android.os.Parcelable
-import java.net.InetAddress
-import kotlinx.parcelize.Parcelize
-
-private const val AUTH_FAILED_REASON_EXPIRED_ACCOUNT = "[EXPIRED_ACCOUNT]"
-
-sealed class ErrorStateCause : Parcelable {
- @Parcelize
- class AuthFailed(private val reason: String?) : ErrorStateCause() {
- fun isCausedByExpiredAccount(): Boolean {
- return reason == AUTH_FAILED_REASON_EXPIRED_ACCOUNT
- }
- }
-
- @Parcelize data object Ipv6Unavailable : ErrorStateCause()
-
- @Parcelize
- data class SetFirewallPolicyError(val firewallPolicyError: FirewallPolicyError) :
- ErrorStateCause()
-
- @Parcelize data object SetDnsError : ErrorStateCause()
-
- @Parcelize
- data class InvalidDnsServers(val addresses: ArrayList<InetAddress>) : ErrorStateCause()
-
- @Parcelize data object StartTunnelError : ErrorStateCause()
-
- @Parcelize
- data class TunnelParameterError(val error: ParameterGenerationError) : ErrorStateCause()
-
- @Parcelize data object IsOffline : ErrorStateCause()
-
- @Parcelize data object VpnPermissionDenied : ErrorStateCause()
-}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt
deleted file mode 100644
index c6f19e71af..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.mullvad.talpid.tunnel
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-enum class FirewallPolicyError : Parcelable {
- Generic
-}
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt
deleted file mode 100644
index 148b56eb45..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package net.mullvad.talpid.util
-
-import kotlin.properties.Delegates.observable
-
-// Manages listeners interested in receiving events of type T
-//
-// The listeners subscribe using an ID object. This ID is used later on for unsubscribing. The only
-// requirement is that the object uses the default implementation of the `hashCode` and `equals`
-// methods inherited from `Any` (or `Object` in Java).
-//
-// If the ID object class (or any of its super-classes) overrides `hashCode` or `equals`,
-// unsubscribe might not work correctly.
-class EventNotifier<T>(private val initialValue: T) {
- private val listeners = LinkedHashMap<Any, (T) -> Unit>()
-
- var latestEvent = initialValue
- private set
-
- fun notify(event: T) {
- synchronized(this) {
- latestEvent = event
-
- for (listener in listeners.values) {
- listener(event)
- }
- }
- }
-
- fun notifyIfChanged(event: T) {
- synchronized(this) {
- if (latestEvent != event) {
- notify(event)
- }
- }
- }
-
- fun subscribe(id: Any, listener: (T) -> Unit) {
- subscribe(id, true, listener)
- }
-
- fun subscribe(id: Any, startWithLatestEvent: Boolean, listener: (T) -> Unit) {
- synchronized(this) {
- listeners.put(id, listener)
- if (startWithLatestEvent) listener(latestEvent)
- }
- }
-
- fun hasListeners(): Boolean {
- synchronized(this) {
- return !listeners.isEmpty()
- }
- }
-
- fun unsubscribe(id: Any) {
- synchronized(this) { listeners.remove(id) }
- }
-
- fun unsubscribeAll() {
- synchronized(this) { listeners.clear() }
- }
-
- fun notifiable() = observable(latestEvent) { _, _, newValue -> notify(newValue) }
-}
-
-fun <T> autoSubscribable(id: Any, fallback: T, listener: (T) -> Unit) =
- observable<EventNotifier<T>?>(null) { _, old, new ->
- if (old != new) {
- old?.unsubscribe(id)
-
- if (new == null) {
- listener.invoke(fallback)
- } else {
- new.subscribe(id, listener)
- }
- }
- }
diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt
deleted file mode 100644
index add362fcb1..0000000000
--- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.mullvad.talpid.util
-
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.callbackFlow
-
-fun <T> EventNotifier<T>.callbackFlowFromSubscription(id: Any) = callbackFlow {
- this@callbackFlowFromSubscription.subscribe(id) { this.trySend(it) }
- awaitClose { this@callbackFlowFromSubscription.unsubscribe(id) }
-}
diff --git a/android/service/build.gradle.kts b/android/service/build.gradle.kts
index 73daa9bcb0..2dcedca5d8 100644
--- a/android/service/build.gradle.kts
+++ b/android/service/build.gradle.kts
@@ -44,21 +44,25 @@ android {
buildConfigField("String", "API_ENDPOINT", "\"api.stagemole.eu\"")
}
}
- buildFeatures {
- buildConfig = true
- }
+
+ buildFeatures { buildConfig = true }
}
dependencies {
implementation(project(Dependencies.Mullvad.commonLib))
+ implementation(project(Dependencies.Mullvad.daemonGrpc))
implementation(project(Dependencies.Mullvad.endpointLib))
- implementation(project(Dependencies.Mullvad.ipcLib))
+ implementation(project(Dependencies.Mullvad.intentLib))
implementation(project(Dependencies.Mullvad.modelLib))
+ implementation(project(Dependencies.Mullvad.sharedLib))
implementation(project(Dependencies.Mullvad.talpidLib))
- implementation(Dependencies.jodaTime)
- implementation(Dependencies.Koin.core)
+ implementation(Dependencies.AndroidX.coreKtx)
+ implementation(Dependencies.AndroidX.lifecycleService)
+ implementation(Dependencies.Arrow.core)
implementation(Dependencies.Koin.android)
+ implementation(Dependencies.Koin.core)
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
+ implementation(Dependencies.jodaTime)
}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt
deleted file mode 100644
index 8cc292fb95..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/DaemonInstance.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import java.io.File
-import kotlin.properties.Delegates.observable
-import kotlin.reflect.KClass
-import kotlin.reflect.safeCast
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.ReceiveChannel
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
-
-private const val RELAYS_FILE = "relays.json"
-
-class DaemonInstance(private val vpnService: MullvadVpnService) {
- sealed class Command {
- data class Start(val apiEndpointConfiguration: ApiEndpointConfiguration) : Command()
-
- object Stop : Command()
- }
-
- private val commandChannel = spawnActor()
-
- private var daemon by
- observable<MullvadDaemon?>(null) { _, oldInstance, _ -> oldInstance?.onDestroy() }
-
- val intermittentDaemon = Intermittent<MullvadDaemon>()
-
- fun start(apiEndpointConfiguration: ApiEndpointConfiguration) {
- commandChannel.trySendBlocking(Command.Start(apiEndpointConfiguration))
- }
-
- fun stop() {
- commandChannel.trySendBlocking(Command.Stop)
- }
-
- fun onDestroy() {
- commandChannel.close()
- intermittentDaemon.onDestroy()
- }
-
- private fun spawnActor() =
- GlobalScope.actor(Dispatchers.Default, Channel.UNLIMITED) {
- var isRunning = true
-
- prepareFiles()
-
- while (isRunning) {
- val startCommand = waitForCommand(channel, Command.Start::class) ?: break
- startDaemon(startCommand.apiEndpointConfiguration)
- isRunning = waitForCommand(channel, Command.Stop::class) is Command.Stop
- stopDaemon()
- }
- }
-
- private suspend fun <T : Command> waitForCommand(
- channel: ReceiveChannel<Command>,
- command: KClass<T>
- ): T? {
- return try {
- var receivedCommand: T?
- do {
- receivedCommand = command.safeCast(channel.receive())
- } while (receivedCommand == null)
- receivedCommand
- } catch (exception: ClosedReceiveChannelException) {
- null
- }
- }
-
- private fun prepareFiles() {
- val shouldOverwriteRelayList =
- lastUpdatedTime() > File(vpnService.filesDir, RELAYS_FILE).lastModified()
-
- FileResourceExtractor(vpnService).apply { extract(RELAYS_FILE, shouldOverwriteRelayList) }
- }
-
- private suspend fun startDaemon(apiEndpointConfiguration: ApiEndpointConfiguration) {
- val newDaemon =
- MullvadDaemon(vpnService, apiEndpointConfiguration).apply {
- onDaemonStopped = {
- intermittentDaemon.spawnUpdate(null)
- daemon = null
- }
- }
-
- daemon = newDaemon
- intermittentDaemon.update(newDaemon)
- }
-
- private fun stopDaemon() {
- daemon?.shutdown()
- }
-
- private fun lastUpdatedTime(): Long {
- return vpnService.run { packageManager.getPackageInfo(packageName, 0).lastUpdateTime }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt
deleted file mode 100644
index dad6ea5b56..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/ForegroundNotificationManager.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import android.app.Service
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
-import android.net.VpnService
-import android.os.Build
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.onStart
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.endpoint.ConnectionProxy
-import net.mullvad.mullvadvpn.service.notifications.TunnelStateNotification
-
-class ForegroundNotificationManager(
- val service: MullvadVpnService,
- val connectionProxy: ConnectionProxy,
- val intermittentDaemon: Intermittent<MullvadDaemon>
-) {
- private sealed class UpdaterMessage {
- class UpdateNotification : UpdaterMessage()
-
- class UpdateAction : UpdaterMessage()
-
- class NewTunnelState(val newState: TunnelState) : UpdaterMessage()
- }
-
- private val jobTracker = JobTracker()
- private val updater = runUpdater()
-
- private val tunnelStateNotification = TunnelStateNotification(service)
-
- private var loggedIn by
- observable(false) { _, _, _ -> updater.trySendBlocking(UpdaterMessage.UpdateAction()) }
-
- private val tunnelState
- get() = connectionProxy.onStateChange.latestEvent
-
- private val shouldBeOnForeground
- get() = lockedToForeground || !(tunnelState is TunnelState.Disconnected)
-
- var onForeground = false
- private set
-
- var lockedToForeground by
- observable(false) { _, _, _ ->
- updater.trySendBlocking(UpdaterMessage.UpdateNotification())
- }
-
- init {
- connectionProxy.onStateChange.subscribe(this) { newState ->
- updater.trySendBlocking(UpdaterMessage.NewTunnelState(newState))
- }
-
- intermittentDaemon.registerListener(this) { daemon ->
- jobTracker.newBackgroundJob("notificationLoggedInJob") {
- daemon
- ?.deviceStateUpdates
- ?.onStart { daemon.getAndEmitDeviceState()?.let { emit(it) } }
- ?.collect { deviceState -> loggedIn = deviceState is DeviceState.LoggedIn }
- }
- }
-
- updater.trySendBlocking(UpdaterMessage.UpdateNotification())
- }
-
- fun onDestroy() {
- jobTracker.cancelAllJobs()
- intermittentDaemon.unregisterListener(this)
- connectionProxy.onStateChange.unsubscribe(this)
- updater.close()
- }
-
- private fun runUpdater() =
- GlobalScope.actor<UpdaterMessage>(Dispatchers.Main, Channel.UNLIMITED) {
- for (message in channel) {
- when (message) {
- is UpdaterMessage.UpdateNotification -> updateNotification()
- is UpdaterMessage.UpdateAction -> updateNotificationAction()
- is UpdaterMessage.NewTunnelState -> {
- tunnelStateNotification.tunnelState = message.newState
- updateNotification()
- }
- }
- }
- }
-
- fun showOnForeground() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- if (VpnService.prepare(service) == null) {
- service.startForeground(
- TunnelStateNotification.NOTIFICATION_ID,
- tunnelStateNotification.build(),
- FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
- )
- } else {
- return
- }
- } else {
- service.startForeground(
- TunnelStateNotification.NOTIFICATION_ID,
- tunnelStateNotification.build(),
- )
- }
- onForeground = true
- }
-
- fun updateNotification() {
- if (shouldBeOnForeground != onForeground) {
- if (shouldBeOnForeground) {
- showOnForeground()
- } else {
- service.stopForeground(Service.STOP_FOREGROUND_DETACH)
- onForeground = false
- }
- }
- }
-
- fun cancelNotification() {
- tunnelStateNotification.visible = false
- }
-
- private fun updateNotificationAction() {
- tunnelStateNotification.showAction = loggedIn
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
index 1d87987cf3..aa6f07e9bb 100644
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt
@@ -1,227 +1,70 @@
package net.mullvad.mullvadvpn.service
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
+import android.annotation.SuppressLint
+import android.content.Context
+import java.io.File
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpoint
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
-import net.mullvad.mullvadvpn.model.AppVersionInfo
-import net.mullvad.mullvadvpn.model.CreateCustomListResult
-import net.mullvad.mullvadvpn.model.CustomList
-import net.mullvad.mullvadvpn.model.Device
-import net.mullvad.mullvadvpn.model.DeviceEvent
-import net.mullvad.mullvadvpn.model.DeviceListEvent
-import net.mullvad.mullvadvpn.model.DeviceState
-import net.mullvad.mullvadvpn.model.DnsOptions
-import net.mullvad.mullvadvpn.model.GetAccountDataResult
-import net.mullvad.mullvadvpn.model.LoginResult
-import net.mullvad.mullvadvpn.model.ObfuscationSettings
-import net.mullvad.mullvadvpn.model.PlayPurchase
-import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
-import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
-import net.mullvad.mullvadvpn.model.QuantumResistantState
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.RelayOverride
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.RemoveDeviceEvent
-import net.mullvad.mullvadvpn.model.RemoveDeviceResult
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.SettingsPatchError
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.model.UpdateCustomListResult
-import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
-import net.mullvad.talpid.util.EventNotifier
+import net.mullvad.mullvadvpn.service.migration.MigrateSplitTunneling
+private const val RELAYS_FILE = "relays.json"
+
+@SuppressLint("SdCardPath")
class MullvadDaemon(
vpnService: MullvadVpnService,
- apiEndpointConfiguration: ApiEndpointConfiguration
+ apiEndpointConfiguration: ApiEndpointConfiguration,
+ migrateSplitTunneling: MigrateSplitTunneling
) {
- protected var daemonInterfaceAddress = 0L
-
- val onSettingsChange = EventNotifier<Settings?>(null)
- var onTunnelStateChange = EventNotifier<TunnelState>(TunnelState.Disconnected())
-
- var onAppVersionInfoChange: ((AppVersionInfo) -> Unit)? = null
- var onRelayListChange: ((RelayList) -> Unit)? = null
- var onDaemonStopped: (() -> Unit)? = null
-
- private val _deviceStateUpdates = MutableSharedFlow<DeviceState>(extraBufferCapacity = 1)
- val deviceStateUpdates = _deviceStateUpdates.asSharedFlow()
+ // Used by JNI
+ @Suppress("ProtectedMemberInFinalClass") protected var daemonInterfaceAddress = 0L
- private val _deviceListUpdates = MutableSharedFlow<DeviceListEvent>(extraBufferCapacity = 1)
- val deviceListUpdates = _deviceListUpdates.asSharedFlow()
+ private val shutdownSignal = Channel<Unit>()
init {
System.loadLibrary("mullvad_jni")
+ prepareFiles(vpnService)
+
+ migrateSplitTunneling.migrate()
+
initialize(
vpnService = vpnService,
cacheDirectory = vpnService.cacheDir.absolutePath,
resourceDirectory = vpnService.filesDir.absolutePath,
apiEndpoint = apiEndpointConfiguration.apiEndpoint()
)
-
- onSettingsChange.notify(getSettings())
-
- onTunnelStateChange.notify(getState() ?: TunnelState.Disconnected())
- }
-
- fun connect() {
- connect(daemonInterfaceAddress)
- }
-
- fun createNewAccount(): String? {
- return createNewAccount(daemonInterfaceAddress)
- }
-
- fun disconnect() {
- disconnect(daemonInterfaceAddress)
- }
-
- fun getAccountData(accountToken: String): GetAccountDataResult {
- return getAccountData(daemonInterfaceAddress, accountToken)
- }
-
- fun getAccountHistory(): String? {
- return getAccountHistory(daemonInterfaceAddress)
- }
-
- fun getWwwAuthToken(): String {
- return getWwwAuthToken(daemonInterfaceAddress) ?: ""
- }
-
- fun getCurrentVersion(): String? {
- return getCurrentVersion(daemonInterfaceAddress)
- }
-
- fun getRelayLocations(): RelayList? {
- return getRelayLocations(daemonInterfaceAddress)
- }
-
- fun getSettings(): Settings? {
- return getSettings(daemonInterfaceAddress)
- }
-
- fun getState(): TunnelState? {
- return getState(daemonInterfaceAddress)
- }
-
- fun getVersionInfo(): AppVersionInfo? {
- return getVersionInfo(daemonInterfaceAddress)
- }
-
- fun reconnect() {
- reconnect(daemonInterfaceAddress)
- }
-
- fun clearAccountHistory() {
- clearAccountHistory(daemonInterfaceAddress)
- }
-
- fun loginAccount(accountToken: String): LoginResult {
- return loginAccount(daemonInterfaceAddress, accountToken)
}
- fun logoutAccount() = logoutAccount(daemonInterfaceAddress)
-
- fun getAndEmitDeviceList(accountToken: String): List<Device>? {
- return listDevices(daemonInterfaceAddress, accountToken).also { deviceList ->
- _deviceListUpdates.tryEmit(
- if (deviceList == null) {
- DeviceListEvent.Error
- } else {
- DeviceListEvent.Available(accountToken, deviceList)
- }
- )
+ suspend fun shutdown() =
+ withContext(Dispatchers.IO) {
+ val shutdownSignal = async { shutdownSignal.receive() }
+ shutdown(daemonInterfaceAddress)
+ shutdownSignal.await()
+ deinitialize()
}
- }
-
- fun getAndEmitDeviceState(): DeviceState? {
- return getDevice(daemonInterfaceAddress)?.also { deviceState ->
- _deviceStateUpdates.tryEmit(deviceState)
- }
- }
- fun refreshDevice() {
- updateDevice(daemonInterfaceAddress)
- getAndEmitDeviceState()
- }
-
- fun removeDevice(accountToken: String, deviceId: String): RemoveDeviceResult {
- return removeDevice(daemonInterfaceAddress, accountToken, deviceId)
- }
-
- fun setAllowLan(allowLan: Boolean) {
- setAllowLan(daemonInterfaceAddress, allowLan)
- }
+ private fun prepareFiles(context: Context) {
+ val shouldOverwriteRelayList =
+ lastUpdatedTime(context) > File(context.filesDir, RELAYS_FILE).lastModified()
- fun setAutoConnect(autoConnect: Boolean) {
- setAutoConnect(daemonInterfaceAddress, autoConnect)
+ FileResourceExtractor(context).apply { extract(RELAYS_FILE, shouldOverwriteRelayList) }
}
- fun setDnsOptions(dnsOptions: DnsOptions) {
- setDnsOptions(daemonInterfaceAddress, dnsOptions)
- }
-
- fun setWireguardMtu(wireguardMtu: Int?) {
- setWireguardMtu(daemonInterfaceAddress, wireguardMtu)
- }
-
- fun shutdown() {
- shutdown(daemonInterfaceAddress)
- }
-
- fun submitVoucher(voucher: String): VoucherSubmissionResult {
- return submitVoucher(daemonInterfaceAddress, voucher)
- }
-
- fun initPlayPurchase(): PlayPurchaseInitResult {
- return initPlayPurchase(daemonInterfaceAddress)
- }
-
- fun verifyPlayPurchase(playPurchase: PlayPurchase): PlayPurchaseVerifyResult {
- return verifyPlayPurchase(daemonInterfaceAddress, playPurchase)
- }
-
- fun setRelaySettings(update: RelaySettings) {
- setRelaySettings(daemonInterfaceAddress, update)
- }
-
- fun setObfuscationSettings(settings: ObfuscationSettings?) {
- setObfuscationSettings(daemonInterfaceAddress, settings)
- }
-
- fun setQuantumResistant(quantumResistant: QuantumResistantState) {
- setQuantumResistantTunnel(daemonInterfaceAddress, quantumResistant)
- }
-
- fun createCustomList(name: String): CreateCustomListResult =
- createCustomList(daemonInterfaceAddress, name)
-
- fun deleteCustomList(id: String) {
- deleteCustomList(daemonInterfaceAddress, id)
- }
-
- fun updateCustomList(customList: CustomList): UpdateCustomListResult =
- updateCustomList(daemonInterfaceAddress, customList)
-
- fun clearAllRelayOverrides() = clearAllRelayOverrides(daemonInterfaceAddress)
-
- fun applyJsonSettings(json: String) = applyJsonSettings(daemonInterfaceAddress, json)
-
- fun exportJsonSettings(): String = exportJsonSettings(daemonInterfaceAddress)
-
- fun setRelayOverride(relayOverride: RelayOverride) =
- setRelayOverride(daemonInterfaceAddress, relayOverride)
-
- fun onDestroy() {
- onSettingsChange.unsubscribeAll()
- onTunnelStateChange.unsubscribeAll()
-
- onAppVersionInfoChange = null
- onRelayListChange = null
- onDaemonStopped = null
+ private fun lastUpdatedTime(context: Context): Long =
+ context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
- deinitialize()
+ // Used by JNI
+ @Suppress("unused")
+ private fun notifyDaemonStopped() {
+ runBlocking {
+ shutdownSignal.send(Unit)
+ shutdownSignal.close()
+ }
}
private external fun initialize(
@@ -233,159 +76,5 @@ class MullvadDaemon(
private external fun deinitialize()
- private external fun connect(daemonInterfaceAddress: Long)
-
- private external fun createNewAccount(daemonInterfaceAddress: Long): String?
-
- private external fun disconnect(daemonInterfaceAddress: Long)
-
- private external fun getAccountData(
- daemonInterfaceAddress: Long,
- accountToken: String
- ): GetAccountDataResult
-
- private external fun getAccountHistory(daemonInterfaceAddress: Long): String?
-
- private external fun getWwwAuthToken(daemonInterfaceAddress: Long): String?
-
- private external fun getCurrentVersion(daemonInterfaceAddress: Long): String?
-
- private external fun getRelayLocations(daemonInterfaceAddress: Long): RelayList?
-
- private external fun getSettings(daemonInterfaceAddress: Long): Settings?
-
- private external fun getState(daemonInterfaceAddress: Long): TunnelState?
-
- private external fun getVersionInfo(daemonInterfaceAddress: Long): AppVersionInfo?
-
- private external fun reconnect(daemonInterfaceAddress: Long)
-
- private external fun clearAccountHistory(daemonInterfaceAddress: Long)
-
- private external fun loginAccount(
- daemonInterfaceAddress: Long,
- accountToken: String?
- ): LoginResult
-
- private external fun logoutAccount(daemonInterfaceAddress: Long)
-
- private external fun listDevices(
- daemonInterfaceAddress: Long,
- accountToken: String?
- ): List<Device>?
-
- // TODO: Review this method when redoing Daemon communication, it can be null which was not
- // considered when this method was initially added.
- private external fun getDevice(daemonInterfaceAddress: Long): DeviceState?
-
- private external fun updateDevice(daemonInterfaceAddress: Long)
-
- private external fun removeDevice(
- daemonInterfaceAddress: Long,
- accountToken: String?,
- deviceId: String
- ): RemoveDeviceResult
-
- private external fun setAllowLan(daemonInterfaceAddress: Long, allowLan: Boolean)
-
- private external fun setAutoConnect(daemonInterfaceAddress: Long, alwaysOn: Boolean)
-
- private external fun setDnsOptions(daemonInterfaceAddress: Long, dnsOptions: DnsOptions)
-
- private external fun setWireguardMtu(daemonInterfaceAddress: Long, wireguardMtu: Int?)
-
private external fun shutdown(daemonInterfaceAddress: Long)
-
- private external fun submitVoucher(
- daemonInterfaceAddress: Long,
- voucher: String
- ): VoucherSubmissionResult
-
- private external fun initPlayPurchase(daemonInterfaceAddress: Long): PlayPurchaseInitResult
-
- private external fun verifyPlayPurchase(
- daemonInterfaceAddress: Long,
- playPurchase: PlayPurchase,
- ): PlayPurchaseVerifyResult
-
- private external fun setRelaySettings(daemonInterfaceAddress: Long, update: RelaySettings)
-
- private external fun setObfuscationSettings(
- daemonInterfaceAddress: Long,
- settings: ObfuscationSettings?
- )
-
- private external fun setQuantumResistantTunnel(
- daemonInterfaceAddress: Long,
- quantumResistant: QuantumResistantState
- )
-
- // Used by JNI
-
- private external fun createCustomList(
- daemonInterfaceAddress: Long,
- name: String
- ): CreateCustomListResult
-
- private external fun deleteCustomList(daemonInterfaceAddress: Long, id: String)
-
- private external fun updateCustomList(
- daemonInterfaceAddress: Long,
- customList: CustomList
- ): UpdateCustomListResult
-
- private external fun clearAllRelayOverrides(daemonInterfaceAddress: Long)
-
- private external fun applyJsonSettings(
- daemonInterfaceAddress: Long,
- json: String
- ): SettingsPatchError
-
- private external fun exportJsonSettings(daemonInterfaceAddress: Long): String
-
- private external fun setRelayOverride(
- daemonInterfaceAddress: Long,
- relayOverride: RelayOverride
- )
-
- @Suppress("unused")
- private fun notifyAppVersionInfoEvent(appVersionInfo: AppVersionInfo) {
- onAppVersionInfoChange?.invoke(appVersionInfo)
- }
-
- // Used by JNI
- @Suppress("unused")
- private fun notifyRelayListEvent(relayList: RelayList) {
- onRelayListChange?.invoke(relayList)
- }
-
- // Used by JNI
- @Suppress("unused")
- private fun notifySettingsEvent(settings: Settings) {
- onSettingsChange.notify(settings)
- }
-
- // Used by JNI
- @Suppress("unused")
- private fun notifyTunnelStateEvent(event: TunnelState) {
- onTunnelStateChange.notify(event)
- }
-
- // Used by JNI
- @Suppress("unused")
- private fun notifyDaemonStopped() {
- onDaemonStopped?.invoke()
- }
-
- // Used by JNI
- @Suppress("unused")
- private fun notifyDeviceEvent(event: DeviceEvent) {
- _deviceStateUpdates.tryEmit(event.newState)
- }
-
- // Used by JNI
- @Suppress("unused")
- private fun notifyRemoveDeviceEvent(event: RemoveDeviceEvent) {
- _deviceListUpdates.tryEmit(DeviceListEvent.Available(event.accountToken, event.newDevices))
- }
}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt
index afd07d7584..e3940c8166 100644
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt
@@ -1,280 +1,202 @@
package net.mullvad.mullvadvpn.service
-import android.annotation.SuppressLint
import android.app.KeyguardManager
-import android.content.Context
import android.content.Intent
+import android.os.Binder
+import android.os.Build
import android.os.IBinder
-import android.os.Looper
import android.util.Log
-import kotlin.properties.Delegates.observable
+import androidx.core.content.getSystemService
+import androidx.lifecycle.lifecycleScope
+import arrow.atomic.AtomicInt
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
import net.mullvad.mullvadvpn.lib.common.constant.KEY_DISCONNECT_ACTION
-import net.mullvad.mullvadvpn.lib.common.constant.KEY_QUIT_ACTION
-import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
+import net.mullvad.mullvadvpn.lib.common.constant.TAG
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
import net.mullvad.mullvadvpn.lib.endpoint.getApiEndpointConfigurationExtras
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.model.TunnelState
+import net.mullvad.mullvadvpn.lib.intent.IntentProvider
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.service.di.apiEndpointModule
import net.mullvad.mullvadvpn.service.di.vpnServiceModule
-import net.mullvad.mullvadvpn.service.endpoint.ServiceEndpoint
-import net.mullvad.mullvadvpn.service.notifications.AccountExpiryNotification
+import net.mullvad.mullvadvpn.service.migration.MigrateSplitTunneling
+import net.mullvad.mullvadvpn.service.notifications.ForegroundNotificationManager
+import net.mullvad.mullvadvpn.service.notifications.NotificationChannelFactory
+import net.mullvad.mullvadvpn.service.notifications.NotificationManager
+import net.mullvad.mullvadvpn.service.notifications.ShouldBeOnForegroundProvider
import net.mullvad.talpid.TalpidVpnService
-import org.koin.android.ext.android.get
+import org.koin.android.ext.android.getKoin
import org.koin.core.context.loadKoinModules
-class MullvadVpnService : TalpidVpnService() {
+class MullvadVpnService : TalpidVpnService(), ShouldBeOnForegroundProvider {
+ private val _shouldBeOnForeground = MutableStateFlow(false)
+ override val shouldBeOnForeground: StateFlow<Boolean> = _shouldBeOnForeground
- private enum class PendingAction {
- Connect,
- Disconnect,
- }
-
- private enum class State {
- Running,
- Stopping,
- Stopped,
- }
-
- private val connectionProxy
- get() = endpoint.connectionProxy
-
- private var state = State.Running
-
- private var setUpDaemonJob: Job? = null
-
- private lateinit var accountExpiryNotification: AccountExpiryNotification
- private lateinit var daemonInstance: DaemonInstance
- private lateinit var endpoint: ServiceEndpoint
private lateinit var keyguardManager: KeyguardManager
- private lateinit var notificationManager: ForegroundNotificationManager
-
- private var pendingAction by
- observable<PendingAction?>(null) { _, _, _ ->
- endpoint.settingsListener.settings?.let { settings -> handlePendingAction(settings) }
- }
+ private lateinit var daemonInstance: MullvadDaemon
private lateinit var apiEndpointConfiguration: ApiEndpointConfiguration
+ private lateinit var managementService: ManagementService
+ private lateinit var migrateSplitTunneling: MigrateSplitTunneling
+ private lateinit var intentProvider: IntentProvider
+ private lateinit var connectionProxy: ConnectionProxy
+
+ private lateinit var foregroundNotificationHandler: ForegroundNotificationManager
+
+ // Count number of binds to know if the service is needed. If user actively using the VPN, a
+ // bind from the system, should always be present.
+ private val bindCount = AtomicInt()
- // Suppressing since the tunnel state pref should be writted immediately.
- @SuppressLint("ApplySharedPref")
override fun onCreate() {
super.onCreate()
- Log.d(TAG, "Initializing service")
+ Log.d(TAG, "MullvadVpnService: onCreate")
loadKoinModules(listOf(vpnServiceModule, apiEndpointModule))
+ with(getKoin()) {
+ // Needed to create all the notification channels
+ get<NotificationChannelFactory>()
- daemonInstance = DaemonInstance(this)
- keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+ managementService = get()
- endpoint =
- ServiceEndpoint(
- Looper.getMainLooper(),
- daemonInstance.intermittentDaemon,
- connectivityListener,
- this
- )
+ foregroundNotificationHandler =
+ ForegroundNotificationManager(this@MullvadVpnService, get(), lifecycleScope)
+ get<NotificationManager>()
- endpoint.splitTunneling.onChange.subscribe(this@MullvadVpnService) { excludedApps ->
- disallowedApps = excludedApps
- markTunAsStale()
- connectionProxy.reconnect()
+ apiEndpointConfiguration = get()
+ migrateSplitTunneling = get()
+ intentProvider = get()
+ connectionProxy = get()
}
- notificationManager =
- ForegroundNotificationManager(this, connectionProxy, daemonInstance.intermittentDaemon)
+ keyguardManager = getSystemService<KeyguardManager>()!!
- accountExpiryNotification =
- AccountExpiryNotification(
- this,
- daemonInstance.intermittentDaemon,
- endpoint.accountCache
- )
+ lifecycleScope.launch { foregroundNotificationHandler.start(this@MullvadVpnService) }
- // Remove any leftover tunnel state persistence data
- getSharedPreferences("tunnel_state", MODE_PRIVATE).edit().clear().commit()
+ // TODO We should avoid lifecycleScope.launch (current needed due to InetSocketAddress
+ // with intent from API)
+ lifecycleScope.launch(context = Dispatchers.IO) {
+ managementService.start()
+ daemonInstance =
+ MullvadDaemon(
+ vpnService = this@MullvadVpnService,
+ apiEndpointConfiguration =
+ intentProvider.getLatestIntent()?.getApiEndpointConfigurationExtras()
+ ?: apiEndpointConfiguration,
+ migrateSplitTunneling = migrateSplitTunneling
+ )
+ }
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- Log.d(TAG, "Starting service")
-
- val intentProvidedConfiguration =
- if (BuildConfig.DEBUG) {
- intent?.getApiEndpointConfigurationExtras()
- } else {
- null
- }
-
- apiEndpointConfiguration = intentProvidedConfiguration ?: get()
-
- daemonInstance.apply {
- intermittentDaemon.registerListener(this@MullvadVpnService) { daemon ->
- handleDaemonInstance(daemon)
- }
-
- start(apiEndpointConfiguration)
- }
+ Log.d(
+ TAG,
+ "onStartCommand (intent=$intent, action=${intent?.action}, flags=$flags, startId=$startId)"
+ )
val startResult = super.onStartCommand(intent, flags, startId)
- var quitCommand = false
// Always promote to foreground if connect/disconnect actions are provided to mitigate cases
// where the service would potentially otherwise be too slow running `startForeground`.
- if (intent?.action == KEY_CONNECT_ACTION || intent?.action == KEY_DISCONNECT_ACTION) {
- notificationManager.showOnForeground()
- }
-
- notificationManager.updateNotification()
-
- if (!keyguardManager.isDeviceLocked) {
- val action = intent?.action
-
- if (action == SERVICE_INTERFACE || action == KEY_CONNECT_ACTION) {
- pendingAction = PendingAction.Connect
- } else if (action == KEY_DISCONNECT_ACTION) {
- pendingAction = PendingAction.Disconnect
- } else if (action == KEY_QUIT_ACTION && !notificationManager.onForeground) {
- quitCommand = true
- stop()
+ when {
+ keyguardManager.isKeyguardLocked -> {
+ Log.d(TAG, "Keyguard is locked, ignoring command")
+ }
+ intent.isFromSystem() || intent?.action == KEY_CONNECT_ACTION -> {
+ // Only show on foreground if we have permission
+ if (prepare(this) == null) {
+ _shouldBeOnForeground.update { true }
+ }
+ lifecycleScope.launch { connectionProxy.connectWithoutPermissionCheck() }
+ }
+ intent?.action == KEY_DISCONNECT_ACTION -> {
+ lifecycleScope.launch { connectionProxy.disconnect() }
}
- }
-
- if (state == State.Stopping && !quitCommand) {
- restart()
}
return startResult
}
- override fun onBind(intent: Intent): IBinder {
- Log.d(TAG, "New connection to service")
- return super.onBind(intent) ?: endpoint.messenger.binder
- }
+ override fun onBind(intent: Intent?): IBinder {
+ bindCount.incrementAndGet()
+ Log.d(TAG, "onBind: $intent")
- override fun onRebind(intent: Intent) {
- Log.d(TAG, "Connection to service restored")
- if (state == State.Stopping) {
- restart()
+ if (intent.isFromSystem()) {
+ Log.d(TAG, "onBind from system")
+ _shouldBeOnForeground.update { true }
}
- }
- override fun onRevoke() {
- pendingAction = PendingAction.Disconnect
+ // We always need to return a binder. If the system binds to our VPN service, VpnService
+ // will return a binder that shall be user, otherwise we return an empty dummy binder to
+ // keep connection service alive since the actual communication happens over gRPC.
+ return super.onBind(intent) ?: emptyBinder()
}
- override fun onUnbind(intent: Intent): Boolean {
- Log.d(TAG, "Closed all connections to service")
-
- if (state != State.Running) {
- stop()
+ private fun emptyBinder() =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Binder(this.toString())
+ } else {
+ Binder()
}
- return true
- }
-
- override fun onDestroy() {
- Log.d(TAG, "Service has stopped")
- state = State.Stopped
- accountExpiryNotification.onDestroy()
- notificationManager.onDestroy()
- daemonInstance.onDestroy()
- super.onDestroy()
- }
-
- override fun onTaskRemoved(rootIntent: Intent?) {
- connectionProxy.onStateChange.latestEvent.let { tunnelState ->
- Log.d(TAG, "Task removed")
- if (tunnelState is TunnelState.Disconnected) {
- notificationManager.cancelNotification()
- stop()
- }
- }
+ override fun onRevoke() {
+ runBlocking { connectionProxy.disconnect() }
}
- private fun handleDaemonInstance(daemon: MullvadDaemon?) {
- setUpDaemonJob?.cancel()
-
- if (daemon != null) {
- setUpDaemonJob = setUpDaemon(daemon)
- } else {
- Log.d(TAG, "Daemon has stopped")
+ override fun onUnbind(intent: Intent): Boolean {
+ val count = bindCount.decrementAndGet()
- if (state == State.Running) {
- restart()
- }
+ // Foreground?
+ if (intent.isFromSystem()) {
+ Log.d(TAG, "onUnbind from system")
+ _shouldBeOnForeground.update { false }
}
- }
- private fun setUpDaemon(daemon: MullvadDaemon) =
- GlobalScope.launch(Dispatchers.Main) {
- if (state != State.Stopped) {
- val settings = daemon.getSettings()
+ if (count == 0) {
+ Log.d(TAG, "No one bound to the service, stopSelf()")
+ lifecycleScope.launch {
+ Log.d(TAG, "Waiting for disconnected state")
+ // TODO This needs reworking, we should not wait for the disconnected state, what we
+ // want is the notification of disconnected to go out before we start shutting down
+ connectionProxy.tunnelState
+ .filter {
+ it is TunnelState.Disconnected ||
+ (it is TunnelState.Error && !it.errorState.isBlocking)
+ }
+ .first()
- if (settings != null) {
- handlePendingAction(settings)
- } else {
- restart()
+ if (bindCount.get() == 0) {
+ Log.d(TAG, "Stopping service")
+ stopSelf()
}
}
}
-
- private fun stop() {
- Log.d(TAG, "Stopping service")
- state = State.Stopping
- daemonInstance.stop()
- stopSelf()
- }
-
- private fun restart() {
- if (state != State.Stopped) {
- Log.d(TAG, "Restarting service")
-
- state = State.Running
-
- daemonInstance.apply {
- stop()
- start(apiEndpointConfiguration)
- }
- } else {
- Log.d(TAG, "Ignoring restart because onDestroy has executed")
- }
+ return false
}
- private fun handlePendingAction(settings: Settings) {
- when (pendingAction) {
- PendingAction.Connect -> {
- if (settings != null) {
- connectionProxy.connect()
- } else {
- openUi()
- }
- }
- PendingAction.Disconnect -> connectionProxy.disconnect()
- null -> return
- }
+ override fun onDestroy() {
+ Log.d(TAG, "MullvadVpnService: onDestroy")
+ managementService.stop()
- pendingAction = null
+ // Shutting down the daemon gracefully
+ runBlocking { daemonInstance.shutdown() }
+ super.onDestroy()
}
- private fun openUi() {
- val intent =
- Intent().apply {
- setClassName(applicationContext.packageName, MAIN_ACTIVITY_CLASS)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- }
-
- startActivity(intent)
+ // If an intent is from the system it is because of the OS starting/stopping the VPN.
+ private fun Intent?.isFromSystem(): Boolean {
+ return this?.action == SERVICE_INTERFACE
}
companion object {
- private const val TAG = "mullvad"
-
init {
System.loadLibrary("mullvad_jni")
}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt
index 0a7d3dec39..e037039675 100644
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/di/VpnServiceModule.kt
@@ -1,7 +1,53 @@
package net.mullvad.mullvadvpn.service.di
import androidx.core.app.NotificationManagerCompat
+import kotlinx.coroutines.MainScope
+import net.mullvad.mullvadvpn.lib.model.NotificationChannel
+import net.mullvad.mullvadvpn.service.migration.MigrateSplitTunneling
+import net.mullvad.mullvadvpn.service.notifications.NotificationChannelFactory
+import net.mullvad.mullvadvpn.service.notifications.NotificationManager
+import net.mullvad.mullvadvpn.service.notifications.NotificationProvider
+import net.mullvad.mullvadvpn.service.notifications.accountexpiry.AccountExpiryNotificationProvider
+import net.mullvad.mullvadvpn.service.notifications.tunnelstate.TunnelStateNotificationProvider
import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.createdAtStart
+import org.koin.core.module.dsl.withOptions
+import org.koin.dsl.bind
import org.koin.dsl.module
-val vpnServiceModule = module { single { NotificationManagerCompat.from(androidContext()) } }
+val vpnServiceModule = module {
+ single { NotificationManagerCompat.from(androidContext()) }
+ single { androidContext().resources }
+
+ single { NotificationChannel.TunnelUpdates } bind NotificationChannel::class
+ single { NotificationChannel.AccountUpdates } bind NotificationChannel::class
+ single { NotificationChannelFactory(get(), get(), getAll()) } withOptions { createdAtStart() }
+
+ single {
+ TunnelStateNotificationProvider(
+ get(),
+ get(),
+ get(),
+ get<NotificationChannel.TunnelUpdates>().id,
+ MainScope()
+ )
+ } bind NotificationProvider::class
+ single {
+ AccountExpiryNotificationProvider(
+ get<NotificationChannel.AccountUpdates>().id,
+ get(),
+ get()
+ )
+ } bind NotificationProvider::class
+
+ single {
+ NotificationManager(
+ get(),
+ getAll(),
+ get(),
+ MainScope(),
+ )
+ } withOptions { createdAtStart() }
+
+ single { MigrateSplitTunneling(androidContext()) }
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt
deleted file mode 100644
index 093f13403d..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AccountCache.kt
+++ /dev/null
@@ -1,184 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.lib.common.util.parseAsDateTime
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.AccountCreationResult
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.AccountHistory
-import net.mullvad.mullvadvpn.model.GetAccountDataResult
-import net.mullvad.talpid.util.EventNotifier
-
-class AccountCache(private val endpoint: ServiceEndpoint) {
-
- private val commandChannel = spawnActor()
-
- private val daemon
- get() = endpoint.intermittentDaemon
-
- val onAccountExpiryChange = EventNotifier<AccountExpiry>(AccountExpiry.Missing)
- val onAccountHistoryChange = EventNotifier<AccountHistory>(AccountHistory.Missing)
-
- private val jobTracker = JobTracker()
-
- private var accountExpiry by onAccountExpiryChange.notifiable()
- private var accountHistory by onAccountHistoryChange.notifiable()
-
- private var cachedAccountToken: String? = null
- private var cachedCreatedAccountToken: String? = null
-
- val isNewAccount: Boolean
- get() = cachedAccountToken == cachedCreatedAccountToken
-
- init {
- jobTracker.newBackgroundJob("autoFetchAccountExpiry") {
- daemon.await().deviceStateUpdates.collect { deviceState ->
- accountExpiry =
- deviceState
- .token()
- .also { cachedAccountToken = it }
- ?.let { fetchAccountExpiry(it) } ?: AccountExpiry.Missing
- }
- }
-
- onAccountHistoryChange.subscribe(this) { history ->
- endpoint.sendEvent(Event.AccountHistoryEvent(history))
- }
-
- onAccountExpiryChange.subscribe(this) { endpoint.sendEvent(Event.AccountExpiryEvent(it)) }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.CreateAccount::class) { _ ->
- commandChannel.trySendBlocking(Command.CreateAccount)
- }
-
- registerHandler(Request.Login::class) { request ->
- request.account?.let { account ->
- commandChannel.trySendBlocking(Command.Login(account))
- }
- }
-
- registerHandler(Request.Logout::class) { _ ->
- commandChannel.trySendBlocking(Command.Logout)
- }
-
- registerHandler(Request.FetchAccountExpiry::class) { _ ->
- jobTracker.newBackgroundJob("fetchAccountExpiry") {
- val token = cachedAccountToken ?: return@newBackgroundJob
- val newAccountExpiry = fetchAccountExpiry(token) ?: return@newBackgroundJob
- accountExpiry = newAccountExpiry
- }
- }
-
- registerHandler(Request.FetchAccountHistory::class) { _ ->
- jobTracker.newBackgroundJob("fetchAccountHistory") {
- accountHistory = fetchAccountHistory()
- }
- }
-
- registerHandler(Request.ClearAccountHistory::class) { _ ->
- jobTracker.newBackgroundJob("clearAccountHistory") { clearAccountHistory() }
- }
- }
- }
-
- fun onDestroy() {
- jobTracker.cancelAllJobs()
-
- onAccountExpiryChange.unsubscribeAll()
- onAccountHistoryChange.unsubscribeAll()
-
- commandChannel.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- is Command.CreateAccount -> doCreateAccount()
- is Command.Login -> doLogin(command.account)
- is Command.Logout -> doLogout()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Command channel was closed, stop the actor
- }
- }
-
- private suspend fun clearAccountHistory() {
- daemon.await().clearAccountHistory()
- accountHistory = fetchAccountHistory()
- }
-
- private suspend fun doCreateAccount() {
- daemon
- .await()
- .createNewAccount()
- .also { newAccountToken -> cachedCreatedAccountToken = newAccountToken }
- .let { newAccountToken ->
- if (newAccountToken != null) {
- AccountCreationResult.Success(newAccountToken)
- } else {
- AccountCreationResult.Failure
- }
- }
- .also { result -> endpoint.sendEvent(Event.AccountCreationEvent(result)) }
- }
-
- private suspend fun doLogin(account: String) {
- daemon.await().loginAccount(account).also { result ->
- endpoint.sendEvent(Event.LoginEvent(result))
- }
- }
-
- private suspend fun doLogout() {
- daemon.await().logoutAccount()
- accountExpiry = AccountExpiry.Missing
- accountHistory = fetchAccountHistory()
- }
-
- private suspend fun fetchAccountHistory(): AccountHistory {
- return daemon.await().getAccountHistory().let { history ->
- if (history != null) {
- AccountHistory.Available(history)
- } else {
- AccountHistory.Missing
- }
- }
- }
-
- private suspend fun fetchAccountExpiry(accountToken: String): AccountExpiry? {
- return fetchAccountData(accountToken).let { result ->
- when (result) {
- is GetAccountDataResult.Ok -> {
- result.accountData.expiry.parseAsDateTime()?.let { AccountExpiry.Available(it) }
- }
- GetAccountDataResult.InvalidAccount -> AccountExpiry.Missing
- GetAccountDataResult.OtherError -> null
- GetAccountDataResult.RpcError -> null
- }
- }
- }
-
- private suspend fun fetchAccountData(accountToken: String): GetAccountDataResult {
- return daemon.await().getAccountData(accountToken)
- }
-
- companion object {
- private sealed class Command {
- object CreateAccount : Command()
-
- data class Login(val account: String) : Command()
-
- object Logout : Command()
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt
deleted file mode 100644
index 767ac3e251..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AppVersionInfoCache.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.model.AppVersionInfo
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-
-class AppVersionInfoCache(endpoint: ServiceEndpoint) {
- private val daemon = endpoint.intermittentDaemon
-
- var appVersionInfo by
- observable<AppVersionInfo?>(null) { _, _, info ->
- endpoint.sendEvent(Event.AppVersionInfo(info))
- }
- private set
-
- var currentVersion by
- observable<String?>(null) { _, _, version ->
- endpoint.sendEvent(Event.CurrentVersion(version))
- }
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- newDaemon?.let { daemon ->
- initializeCurrentVersion(daemon)
- registerVersionInfoListener(daemon)
- fetchInitialVersionInfo(daemon)
- }
- }
- }
-
- fun onDestroy() {
- daemon.unregisterListener(this)
- }
-
- private fun initializeCurrentVersion(daemon: MullvadDaemon) {
- if (currentVersion == null) {
- currentVersion = daemon.getCurrentVersion()
- }
- }
-
- private fun registerVersionInfoListener(daemon: MullvadDaemon) {
- daemon.onAppVersionInfoChange = { newAppVersionInfo ->
- synchronized(this@AppVersionInfoCache) { appVersionInfo = newAppVersionInfo }
- }
- }
-
- private fun fetchInitialVersionInfo(daemon: MullvadDaemon) {
- synchronized(this@AppVersionInfoCache) {
- if (appVersionInfo == null) {
- appVersionInfo = daemon.getVersionInfo()
- }
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt
deleted file mode 100644
index 08b0943c4d..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/AuthTokenCache.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class AuthTokenCache(endpoint: ServiceEndpoint) {
- private val daemon = endpoint.intermittentDaemon
- private val requestQueue = spawnActor()
-
- var authToken by
- observable<String?>(null) { _, _, token -> endpoint.sendEvent(Event.AuthToken(token)) }
- private set
-
- init {
- endpoint.dispatcher.registerHandler(Request.FetchAuthToken::class) { _ ->
- requestQueue.trySendBlocking(Command.Fetch)
- }
- }
-
- fun onDestroy() {
- requestQueue.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- Command.Fetch -> authToken = daemon.await().getWwwAuthToken()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-
- companion object {
- private enum class Command {
- Fetch
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt
deleted file mode 100644
index 65a27c8f69..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ConnectionProxy.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.talpid.util.EventNotifier
-
-class ConnectionProxy(val vpnPermission: VpnPermission, endpoint: ServiceEndpoint) {
- private enum class Command {
- CONNECT,
- RECONNECT,
- DISCONNECT,
- }
-
- private val commandChannel = spawnActor()
- private val daemon = endpoint.intermittentDaemon
- private val initialState = TunnelState.Disconnected()
-
- var onStateChange = EventNotifier<TunnelState>(initialState)
-
- var state by onStateChange.notifiable()
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- newDaemon?.onTunnelStateChange?.subscribe(this@ConnectionProxy) { newState ->
- state = newState
- }
- }
-
- onStateChange.subscribe(this) { tunnelState ->
- endpoint.sendEvent(Event.TunnelStateChange(tunnelState))
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.Connect::class) { _ -> connect() }
- registerHandler(Request.Reconnect::class) { _ -> reconnect() }
- registerHandler(Request.Disconnect::class) { _ -> disconnect() }
- }
- }
-
- fun connect() {
- commandChannel.trySendBlocking(Command.CONNECT)
- }
-
- fun reconnect() {
- commandChannel.trySendBlocking(Command.RECONNECT)
- }
-
- fun disconnect() {
- commandChannel.trySendBlocking(Command.DISCONNECT)
- }
-
- fun onDestroy() {
- commandChannel.close()
- onStateChange.unsubscribeAll()
- daemon.unregisterListener(this)
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- while (true) {
- val command = channel.receive()
-
- when (command) {
- Command.CONNECT -> {
- vpnPermission.request()
- daemon.await().connect()
- }
- Command.RECONNECT -> daemon.await().reconnect()
- Command.DISCONNECT -> daemon.await().disconnect()
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt
deleted file mode 100644
index 7ecfe02d58..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomDns.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import java.net.InetAddress
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.CustomDnsOptions
-import net.mullvad.mullvadvpn.model.DefaultDnsOptions
-import net.mullvad.mullvadvpn.model.DnsOptions
-import net.mullvad.mullvadvpn.model.DnsState
-
-class CustomDns(private val endpoint: ServiceEndpoint) {
- private sealed class Command {
- @Deprecated("Use SetDnsOptions") class AddDnsServer(val server: InetAddress) : Command()
-
- @Deprecated("Use SetDnsOptions") class RemoveDnsServer(val server: InetAddress) : Command()
-
- @Deprecated("Use SetDnsOptions")
- class ReplaceDnsServer(val oldServer: InetAddress, val newServer: InetAddress) : Command()
-
- @Deprecated("Use SetDnsOptions") class SetEnabled(val enabled: Boolean) : Command()
-
- class SetDnsOptions(val dnsOptions: DnsOptions) : Command()
- }
-
- private val commandChannel = spawnActor()
- private val dnsServers = ArrayList<InetAddress>()
-
- private val daemon
- get() = endpoint.intermittentDaemon
-
- private var enabled = false
-
- init {
- endpoint.settingsListener.dnsOptionsNotifier.subscribe(this) { maybeDnsOptions ->
- maybeDnsOptions?.let { dnsOptions ->
- enabled = dnsOptions.state == DnsState.Custom
- dnsServers.clear()
- dnsServers.addAll(dnsOptions.customOptions.addresses)
- }
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.AddCustomDnsServer::class) { request ->
- commandChannel.trySendBlocking(Command.AddDnsServer(request.address))
- }
-
- registerHandler(Request.RemoveCustomDnsServer::class) { request ->
- commandChannel.trySendBlocking(Command.RemoveDnsServer(request.address))
- }
-
- registerHandler(Request.ReplaceCustomDnsServer::class) { request ->
- commandChannel.trySendBlocking(
- Command.ReplaceDnsServer(request.oldAddress, request.newAddress)
- )
- }
-
- registerHandler(Request.SetEnableCustomDns::class) { request ->
- commandChannel.trySendBlocking(Command.SetEnabled(request.enable))
- }
-
- registerHandler(Request.SetDnsOptions::class) { request ->
- commandChannel.trySendBlocking(Command.SetDnsOptions(request.dnsOptions))
- }
- }
- }
-
- fun onDestroy() {
- endpoint.settingsListener.dnsOptionsNotifier.unsubscribe(this)
- commandChannel.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- while (true) {
- val command = channel.receive()
-
- when (command) {
- is Command.AddDnsServer -> doAddDnsServer(command.server)
- is Command.RemoveDnsServer -> doRemoveDnsServer(command.server)
- is Command.ReplaceDnsServer -> {
- doReplaceDnsServer(command.oldServer, command.newServer)
- }
- is Command.SetEnabled -> changeDnsOptions(command.enabled)
- is Command.SetDnsOptions -> setDnsOptions(command.dnsOptions)
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-
- private suspend fun doAddDnsServer(server: InetAddress) {
- if (!dnsServers.contains(server)) {
- dnsServers.add(server)
- changeDnsOptions(enabled)
- }
- }
-
- private suspend fun doReplaceDnsServer(oldServer: InetAddress, newServer: InetAddress) {
- if (oldServer != newServer && !dnsServers.contains(newServer)) {
- val index = dnsServers.indexOf(oldServer)
-
- if (index >= 0) {
- dnsServers.removeAt(index)
- dnsServers.add(index, newServer)
- changeDnsOptions(enabled)
- }
- }
- }
-
- private suspend fun doRemoveDnsServer(server: InetAddress) {
- if (dnsServers.remove(server)) {
- changeDnsOptions(enabled)
- }
- }
-
- private suspend fun changeDnsOptions(enable: Boolean) {
- val options =
- DnsOptions(
- state = if (enable) DnsState.Custom else DnsState.Default,
- customOptions = CustomDnsOptions(dnsServers),
- defaultOptions = DefaultDnsOptions()
- )
- daemon.await().setDnsOptions(options)
- }
-
- private suspend fun setDnsOptions(dnsOptions: DnsOptions) {
- daemon.await().setDnsOptions(dnsOptions)
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomLists.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomLists.kt
deleted file mode 100644
index 39702398c7..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/CustomLists.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.CustomList
-
-class CustomLists(
- private val endpoint: ServiceEndpoint,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
- private val daemon
- get() = endpoint.intermittentDaemon
-
- init {
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.CreateCustomList>()
- .collect { createCustomList(it.name) }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.DeleteCustomList>()
- .collect { daemon.await().deleteCustomList(it.id) }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.UpdateCustomList>()
- .collect { updateCustomList(it.customList) }
- }
- }
-
- private suspend fun createCustomList(name: String) {
- val result = daemon.await().createCustomList(name)
- endpoint.sendEvent(Event.CreateCustomListResultEvent(result))
- }
-
- private suspend fun updateCustomList(customList: CustomList) {
- val result = daemon.await().updateCustomList(customList)
- endpoint.sendEvent(Event.UpdateCustomListResultEvent(result))
- }
-
- fun onDestroy() {
- scope.cancel()
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
deleted file mode 100644
index db264ed1fe..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/DaemonDeviceDataSource.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.flow.collect
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-
-class DaemonDeviceDataSource(val endpoint: ServiceEndpoint) {
- private val tracker = JobTracker()
-
- init {
- endpoint.intermittentDaemon.registerListener(this) { daemon ->
- if (daemon != null) {
- launchDeviceEndpointJobs(daemon)
- } else {
- tracker.cancelAllJobs()
- }
- }
- }
-
- private fun launchDeviceEndpointJobs(daemon: MullvadDaemon) {
- tracker.newBackgroundJob("propagateDeviceUpdatesJob") {
- daemon.deviceStateUpdates.collect { newState ->
- endpoint.sendEvent(Event.DeviceStateEvent(newState))
- }
- }
-
- tracker.newBackgroundJob("propagateDeviceListUpdatesJob") {
- daemon.deviceListUpdates.collect { newState ->
- endpoint.sendEvent(Event.DeviceListUpdate(newState))
- }
- }
-
- endpoint.dispatcher.registerHandler(Request.GetDevice::class) {
- tracker.newBackgroundJob("getDeviceJob") { daemon.getAndEmitDeviceState() }
- }
-
- endpoint.dispatcher.registerHandler(Request.RefreshDeviceState::class) {
- tracker.newBackgroundJob("refreshDeviceJob") { daemon.refreshDevice() }
- }
-
- endpoint.dispatcher.registerHandler(Request.RemoveDevice::class) { request ->
- tracker.newBackgroundJob("removeDeviceJob") {
- daemon.removeDevice(request.accountToken, request.deviceId).also { result ->
- endpoint.sendEvent(Event.DeviceRemovalEvent(request.deviceId, result))
- }
- }
- }
-
- endpoint.dispatcher.registerHandler(Request.GetDeviceList::class) { request ->
- tracker.newBackgroundJob("getDeviceListJob") {
- daemon.getAndEmitDeviceList(request.accountToken)
- }
- }
- }
-
- fun onDestroy() {
- tracker.cancelAllJobs()
- endpoint.intermittentDaemon.unregisterListener(this)
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt
deleted file mode 100644
index 65d7b6cff0..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class JsonSettings(
- private val endpoint: ServiceEndpoint,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
- private val daemon
- get() = endpoint.intermittentDaemon
-
- init {
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.ApplyJsonSettings>()
- .collect { applyJsonSettings(it.json) }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.ExportJsonSettings>()
- .collect { exportJsonSettings() }
- }
- }
-
- private suspend fun applyJsonSettings(json: String) {
- val result = daemon.await().applyJsonSettings(json)
- endpoint.sendEvent(Event.ApplyJsonSettingsResult(result))
- }
-
- private suspend fun exportJsonSettings() {
- val json = daemon.await().exportJsonSettings()
- endpoint.sendEvent(Event.ExportJsonSettingsResult(json))
- }
-
- fun onDestroy() {
- scope.cancel()
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/PlayPurchaseHandler.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/PlayPurchaseHandler.kt
deleted file mode 100644
index 9a1e34b62a..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/PlayPurchaseHandler.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.PlayPurchase
-
-class PlayPurchaseHandler(
- private val endpoint: ServiceEndpoint,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
- private val daemon
- get() = endpoint.intermittentDaemon
-
- init {
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.InitPlayPurchase>()
- .collect { initializePurchase() }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.VerifyPlayPurchase>()
- .collect { verifyPlayPurchase(it.playPurchase) }
- }
- }
-
- fun onDestroy() {
- scope.cancel()
- }
-
- private suspend fun initializePurchase() {
- val result = daemon.await().initPlayPurchase()
- endpoint.sendEvent(Event.PlayPurchaseInitResultEvent(result))
- }
-
- private suspend fun verifyPlayPurchase(playPurchase: PlayPurchase) {
- val result = daemon.await().verifyPlayPurchase(playPurchase)
- endpoint.sendEvent(Event.PlayPurchaseVerifyResultEvent(result))
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt
deleted file mode 100644
index 8ba6234cf6..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.Constraint
-import net.mullvad.mullvadvpn.model.RelayConstraints
-import net.mullvad.mullvadvpn.model.RelayList
-import net.mullvad.mullvadvpn.model.RelaySettings
-import net.mullvad.mullvadvpn.model.WireguardConstraints
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-
-class RelayListListener(
- endpoint: ServiceEndpoint,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
- private val daemon = endpoint.intermittentDaemon
-
- var relayList by
- observable<RelayList?>(null) { _, _, relays ->
- endpoint.sendEvent(Event.NewRelayList(relays))
- }
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- newDaemon?.let { daemon ->
- setUpListener(daemon)
- fetchInitialRelayList(daemon)
- }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.SetRelayLocation>()
- .collect { request ->
- val update =
- getCurrentRelayConstraints()
- .copy(location = Constraint.Only(request.locationConstraint))
- daemon.await().setRelaySettings(RelaySettings.Normal(update))
- }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.SetWireguardConstraints>()
- .collect { request ->
- val update =
- getCurrentRelayConstraints()
- .copy(wireguardConstraints = request.wireguardConstraints)
- daemon.await().setRelaySettings(RelaySettings.Normal(update))
- }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages.filterIsInstance<Request.FetchRelayList>().collect {
- relayList = daemon.await().getRelayLocations()
- }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.SetOwnershipAndProviders>()
- .collect { request ->
- val update =
- getCurrentRelayConstraints()
- .copy(ownership = request.ownership, providers = request.providers)
- daemon.await().setRelaySettings(RelaySettings.Normal(update))
- }
- }
- }
-
- fun onDestroy() {
- daemon.unregisterListener(this)
- scope.cancel()
- }
-
- private fun setUpListener(daemon: MullvadDaemon) {
- daemon.onRelayListChange = { relayLocations -> relayList = relayLocations }
- }
-
- private fun fetchInitialRelayList(daemon: MullvadDaemon) {
- synchronized(this) {
- if (relayList == null) {
- relayList = daemon.getRelayLocations()
- }
- }
- }
-
- private suspend fun getCurrentRelayConstraints(): RelayConstraints =
- when (val relaySettings = daemon.await().getSettings()?.relaySettings) {
- is RelaySettings.Normal -> relaySettings.relayConstraints
- else ->
- RelayConstraints(
- location = Constraint.Any(),
- providers = Constraint.Any(),
- ownership = Constraint.Any(),
- wireguardConstraints = WireguardConstraints(Constraint.Any())
- )
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt
deleted file mode 100644
index cda7a5b94b..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class RelayOverrides(
- private val endpoint: ServiceEndpoint,
- dispatcher: CoroutineDispatcher = Dispatchers.IO
-) {
- private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
- private val daemon
- get() = endpoint.intermittentDaemon
-
- init {
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.SetRelayOverride>()
- .collect { daemon.await().setRelayOverride(it.override) }
- }
-
- scope.launch {
- endpoint.dispatcher.parsedMessages
- .filterIsInstance<Request.ClearAllRelayOverrides>()
- .collect { daemon.await().clearAllRelayOverrides() }
- }
- }
-
- fun onDestroy() {
- scope.cancel()
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
deleted file mode 100644
index f8fc6aaf64..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt
+++ /dev/null
@@ -1,175 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import android.content.Context
-import android.os.Looper
-import android.os.Messenger
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.SendChannel
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-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.trySendEvent
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-import net.mullvad.mullvadvpn.service.persistence.SplitTunnelingPersistence
-import net.mullvad.talpid.ConnectivityListener
-
-const val SHOULD_LOG_DEAD_OBJECT_EXCEPTION = true
-
-class ServiceEndpoint(
- looper: Looper,
- internal val intermittentDaemon: Intermittent<MullvadDaemon>,
- val connectivityListener: ConnectivityListener,
- context: Context
-) {
-
- private val listeners = mutableMapOf<Int, Messenger>()
- private val commands: SendChannel<Command> = startRegistrator()
-
- internal val dispatcher = DispatchingHandler(looper) { message -> Request.fromMessage(message) }
-
- private var listenerIdCounter = 0
-
- val messenger = Messenger(dispatcher)
-
- val vpnPermission = VpnPermission(context, this)
-
- val connectionProxy = ConnectionProxy(vpnPermission, this)
- val settingsListener = SettingsListener(this)
-
- val accountCache = AccountCache(this)
- val appVersionInfoCache = AppVersionInfoCache(this)
- val authTokenCache = AuthTokenCache(this)
- val customDns = CustomDns(this)
- val relayOverrides = RelayOverrides(this)
- val jsonSettings = JsonSettings(this)
- val relayListListener = RelayListListener(this)
- val splitTunneling = SplitTunneling(SplitTunnelingPersistence(context), this)
- val voucherRedeemer = VoucherRedeemer(this, accountCache)
-
- private val playPurchaseHandler = PlayPurchaseHandler(this)
- private val customLists = CustomLists(this)
-
- private val deviceRepositoryBackend = DaemonDeviceDataSource(this)
-
- init {
- dispatcher.apply {
- registerHandler(Request.RegisterListener::class) { request ->
- commands.trySendBlocking(Command.RegisterListener(request.listener))
- }
-
- registerHandler(Request.UnregisterListener::class) { request ->
- commands.trySendBlocking(Command.UnregisterListener(request.listenerId))
- }
- }
- }
-
- fun onDestroy() {
- dispatcher.onDestroy()
- commands.close()
-
- accountCache.onDestroy()
- appVersionInfoCache.onDestroy()
- authTokenCache.onDestroy()
- connectionProxy.onDestroy()
- customDns.onDestroy()
- deviceRepositoryBackend.onDestroy()
- relayListListener.onDestroy()
- settingsListener.onDestroy()
- splitTunneling.onDestroy()
- voucherRedeemer.onDestroy()
- playPurchaseHandler.onDestroy()
- customLists.onDestroy()
- relayOverrides.onDestroy()
- jsonSettings.onDestroy()
- }
-
- internal fun sendEvent(event: Event) {
- synchronized(this) {
- val deadListeners = mutableSetOf<Int>()
-
- for ((id, listener) in listeners) {
- if (!listener.trySendEvent(event, SHOULD_LOG_DEAD_OBJECT_EXCEPTION)) {
- deadListeners.add(id)
- }
- }
- deadListeners.forEach { listeners.remove(it) }
- }
- }
-
- private fun startRegistrator() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- is Command.RegisterListener -> {
- intermittentDaemon.await()
-
- registerListener(command.listener)
- }
- is Command.UnregisterListener -> unregisterListener(command.listenerId)
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Registration queue closed; stop registrator
- }
- }
-
- private fun registerListener(listener: Messenger) {
- synchronized(this) {
- val listenerId = newListenerId()
-
- listeners.put(listenerId, listener)
-
- val initialEvents =
- mutableListOf(
- Event.TunnelStateChange(connectionProxy.state),
- Event.AccountHistoryEvent(accountCache.onAccountHistoryChange.latestEvent),
- Event.SettingsUpdate(settingsListener.settings),
- Event.SplitTunnelingUpdate(splitTunneling.onChange.latestEvent),
- Event.CurrentVersion(appVersionInfoCache.currentVersion),
- Event.AppVersionInfo(appVersionInfoCache.appVersionInfo),
- Event.NewRelayList(relayListListener.relayList),
- Event.AuthToken(authTokenCache.authToken),
- Event.ListenerReady(messenger, listenerId)
- )
-
- if (vpnPermission.waitingForResponse) {
- initialEvents.add(Event.VpnPermissionRequest)
- }
-
- val didSuccessfullySendAllMessages =
- initialEvents.all { event ->
- listener.trySendEvent(event, SHOULD_LOG_DEAD_OBJECT_EXCEPTION)
- }
- if (didSuccessfullySendAllMessages.not()) {
- listeners.remove(listenerId)
- }
- }
- }
-
- private fun unregisterListener(listenerId: Int) {
- synchronized(this) { listeners.remove(listenerId) }
- }
-
- private fun newListenerId(): Int {
- val listenerId = listenerIdCounter
-
- listenerIdCounter += 1
-
- return listenerId
- }
-
- companion object {
- sealed class Command {
- data class RegisterListener(val listener: Messenger) : Command()
-
- data class UnregisterListener(val listenerId: Int) : Command()
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt
deleted file mode 100644
index 9422b6a94e..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SettingsListener.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.DnsOptions
-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.mullvadvpn.service.MullvadDaemon
-import net.mullvad.talpid.util.EventNotifier
-
-class SettingsListener(endpoint: ServiceEndpoint) {
- private sealed class Command {
- class SetAllowLan(val allow: Boolean) : Command()
-
- class SetAutoConnect(val autoConnect: Boolean) : Command()
-
- class SetWireGuardMtu(val mtu: Int?) : Command()
-
- class SetObfuscationSettings(val settings: ObfuscationSettings?) : Command()
-
- class SetQuantumResistant(val quantumResistant: QuantumResistantState) : Command()
- }
-
- private val commandChannel = spawnActor()
- private val daemon = endpoint.intermittentDaemon
-
- val dnsOptionsNotifier = EventNotifier<DnsOptions?>(null)
- val relaySettingsNotifier = EventNotifier<RelaySettings?>(null)
- val obfuscationSettingsNotifier = EventNotifier<ObfuscationSettings?>(null)
- val settingsNotifier = EventNotifier<Settings?>(null)
-
- var settings by settingsNotifier.notifiable()
- private set
-
- init {
- daemon.registerListener(this) { newDaemon ->
- if (newDaemon != null) {
- registerListener(newDaemon)
- fetchInitialSettings(newDaemon)
- }
- }
-
- settingsNotifier.subscribe(this) { settings ->
- endpoint.sendEvent(Event.SettingsUpdate(settings))
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.SetAllowLan::class) { request ->
- commandChannel.trySendBlocking(Command.SetAllowLan(request.allow))
- }
-
- registerHandler(Request.SetAutoConnect::class) { request ->
- commandChannel.trySendBlocking(Command.SetAutoConnect(request.autoConnect))
- }
-
- registerHandler(Request.SetWireGuardMtu::class) { request ->
- commandChannel.trySendBlocking(Command.SetWireGuardMtu(request.mtu))
- }
-
- registerHandler(Request.SetObfuscationSettings::class) { request ->
- commandChannel.trySendBlocking(Command.SetObfuscationSettings(request.settings))
- }
-
- registerHandler(Request.SetWireGuardQuantumResistant::class) { request ->
- commandChannel.trySendBlocking(
- Command.SetQuantumResistant(request.quantumResistant)
- )
- }
- }
- }
-
- fun onDestroy() {
- commandChannel.close()
- daemon.unregisterListener(this)
-
- dnsOptionsNotifier.unsubscribeAll()
- relaySettingsNotifier.unsubscribeAll()
- obfuscationSettingsNotifier.unsubscribeAll()
- settingsNotifier.unsubscribeAll()
- }
-
- fun subscribe(id: Any, listener: (Settings) -> Unit) {
- settingsNotifier.subscribe(id) { maybeSettings ->
- maybeSettings?.let { settings -> listener(settings) }
- }
- }
-
- fun unsubscribe(id: Any) {
- settingsNotifier.unsubscribe(id)
- }
-
- private fun registerListener(daemon: MullvadDaemon) {
- daemon.onSettingsChange.subscribe(this, ::handleNewSettings)
- }
-
- private fun fetchInitialSettings(daemon: MullvadDaemon) {
- synchronized(this) { handleNewSettings(daemon.getSettings()) }
- }
-
- private fun handleNewSettings(newSettings: Settings?) {
- if (newSettings != null) {
- synchronized(this) {
- if (settings?.tunnelOptions?.dnsOptions != newSettings.tunnelOptions.dnsOptions) {
- dnsOptionsNotifier.notify(newSettings.tunnelOptions.dnsOptions)
- }
-
- if (settings?.relaySettings != newSettings.relaySettings) {
- relaySettingsNotifier.notify(newSettings.relaySettings)
- }
-
- if (settings?.obfuscationSettings != newSettings.obfuscationSettings) {
- obfuscationSettingsNotifier.notify(newSettings.obfuscationSettings)
- }
-
- settings = newSettings
- }
- }
- }
-
- private fun spawnActor() =
- GlobalScope.actor<Command>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (command in channel) {
- when (command) {
- is Command.SetAllowLan -> daemon.await().setAllowLan(command.allow)
- is Command.SetAutoConnect ->
- daemon.await().setAutoConnect(command.autoConnect)
- is Command.SetWireGuardMtu -> daemon.await().setWireguardMtu(command.mtu)
- is Command.SetObfuscationSettings ->
- daemon.await().setObfuscationSettings(command.settings)
- is Command.SetQuantumResistant ->
- daemon.await().setQuantumResistant(command.quantumResistant)
- }
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Closed sender, so stop the actor
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
deleted file mode 100644
index 4fbe89c82b..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/SplitTunneling.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.service.persistence.SplitTunnelingPersistence
-import net.mullvad.talpid.util.EventNotifier
-
-class SplitTunneling(persistence: SplitTunnelingPersistence, endpoint: ServiceEndpoint) {
- private val excludedApps = persistence.excludedApps.toMutableSet()
-
- private var enabled by
- observable(persistence.enabled) { _, wasEnabled, isEnabled ->
- if (wasEnabled != isEnabled) {
- persistence.enabled = isEnabled
- update()
- }
- }
-
- val onChange =
- EventNotifier(
- if (enabled) {
- excludedApps.toList()
- } else {
- null
- }
- )
-
- init {
- onChange.subscribe(this) { excludedApps ->
- endpoint.sendEvent(Event.SplitTunnelingUpdate(excludedApps))
- }
-
- endpoint.dispatcher.apply {
- registerHandler(Request.IncludeApp::class) { request ->
- excludedApps.remove(request.packageName)
- update()
- }
-
- registerHandler(Request.ExcludeApp::class) { request ->
- excludedApps.add(request.packageName)
- update()
- }
-
- registerHandler(Request.SetEnableSplitTunneling::class) { request ->
- enabled = request.enable
- }
-
- registerHandler(Request.PersistExcludedApps::class) { _ ->
- persistence.excludedApps = excludedApps
- }
- }
- }
-
- fun onDestroy() {
- onChange.unsubscribeAll()
- }
-
- private fun update() {
- if (enabled) {
- onChange.notify(excludedApps.toList())
- } else {
- onChange.notify(null)
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt
deleted file mode 100644
index e7ecf5807d..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VoucherRedeemer.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.channels.actor
-import kotlinx.coroutines.channels.trySendBlocking
-import net.mullvad.mullvadvpn.lib.common.util.parseAsDateTime
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.model.VoucherSubmissionResult
-
-class VoucherRedeemer(
- private val endpoint: ServiceEndpoint,
- private val accountCache: AccountCache
-) {
- private val daemon
- get() = endpoint.intermittentDaemon
-
- private val voucherChannel = spawnActor()
-
- init {
- endpoint.dispatcher.registerHandler(Request.SubmitVoucher::class) { request ->
- voucherChannel.trySendBlocking(request.voucher)
- }
- }
-
- fun onDestroy() {
- voucherChannel.close()
- }
-
- private fun spawnActor() =
- GlobalScope.actor<String>(Dispatchers.Default, Channel.UNLIMITED) {
- try {
- for (voucher in channel) {
- val result = daemon.await().submitVoucher(voucher)
-
- // Let AccountCache know about the new expiry
- if (result is VoucherSubmissionResult.Ok) {
- val newExpiry = result.submission.newExpiry.parseAsDateTime()
- if (newExpiry != null) {
- accountCache.onAccountExpiryChange.notify(
- AccountExpiry.Available(newExpiry)
- )
- }
- }
- endpoint.sendEvent(Event.VoucherSubmissionResult(voucher, result))
- }
- } catch (exception: ClosedReceiveChannelException) {
- // Voucher channel was closed, stop the actor
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt
deleted file mode 100644
index 57fd6dc40c..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/VpnPermission.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package net.mullvad.mullvadvpn.service.endpoint
-
-import android.content.Context
-import android.content.Intent
-import android.net.VpnService
-import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.Request
-
-class VpnPermission(private val context: Context, private val endpoint: ServiceEndpoint) {
- private val isGranted = Intermittent<Boolean>()
-
- var waitingForResponse = false
- private set
-
- init {
- endpoint.dispatcher.registerHandler(Request.VpnPermissionResponse::class) { request ->
- waitingForResponse = false
- isGranted.spawnUpdate(request.isGranted)
- }
- }
-
- suspend fun request(): Boolean {
- val intent = VpnService.prepare(context)
-
- if (intent == null) {
- isGranted.update(true)
- } else {
- val activityIntent =
- Intent().apply {
- setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
- }
-
- isGranted.update(null)
- waitingForResponse = true
-
- context.startActivity(activityIntent)
- endpoint.sendEvent(Event.VpnPermissionRequest)
- }
-
- return isGranted.await()
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/migration/MigrateSplitTunneling.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/migration/MigrateSplitTunneling.kt
new file mode 100644
index 0000000000..486d2674b4
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/migration/MigrateSplitTunneling.kt
@@ -0,0 +1,51 @@
+package net.mullvad.mullvadvpn.service.migration
+
+import android.content.Context
+import java.io.File
+
+/**
+ * Migration for split tunneling apps, from Shared Preferences to Daemon.
+ *
+ * Previously apps where stored in Shared Preferences and injected from straight into the tunnel
+ * without the knowledge of the daemon. This migration happens in conjunction with the daemon.
+ *
+ * See: mullvad-daemon/src/migrations/v9.rs
+ */
+class MigrateSplitTunneling(private val context: Context) {
+ fun migrate() {
+ // Get old settings, if not found return
+ val enabled = getOldSettings(context) ?: return
+
+ // Migrate enable settings to file so that the daemon can read it
+ migrateSplitTunnelingEnabled(context, enabled)
+ }
+
+ private fun getOldSettings(context: Context): Boolean? {
+ // Get from shared preferences and appListFile
+ val appListFile = File(context.filesDir, SPLIT_TUNNELING_APPS_FILE)
+ val preferences = getSharedPreferences(context)
+
+ return if (appListFile.exists() && preferences.contains(KEY_ENABLED)) {
+ preferences.getBoolean(KEY_ENABLED, false)
+ } else {
+ null
+ }
+ }
+
+ private fun migrateSplitTunnelingEnabled(context: Context, enabled: Boolean) {
+ val enabledFile = File(context.filesDir, SPLIT_TUNNELING_ENABLED_FILE)
+ if (enabledFile.createNewFile()) {
+ enabledFile.writeText(enabled.toString())
+ }
+ }
+
+ private fun getSharedPreferences(context: Context) =
+ context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE)
+
+ companion object {
+ private const val SHARED_PREFERENCES = "split_tunnelling"
+ private const val KEY_ENABLED = "enabled"
+ private const val SPLIT_TUNNELING_APPS_FILE = "split-tunnelling.txt"
+ private const val SPLIT_TUNNELING_ENABLED_FILE = "split-tunnelling-enabled.txt"
+ }
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
deleted file mode 100644
index 634051b2b1..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/AccountExpiryNotification.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import android.annotation.SuppressLint
-import android.app.Notification
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.core.app.NotificationCompat
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.delay
-import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
-import net.mullvad.mullvadvpn.lib.common.util.Intermittent
-import net.mullvad.mullvadvpn.lib.common.util.JobTracker
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.isNotificationPermissionMissing
-import net.mullvad.mullvadvpn.model.AccountExpiry
-import net.mullvad.mullvadvpn.service.MullvadDaemon
-import net.mullvad.mullvadvpn.service.R
-import net.mullvad.mullvadvpn.service.constant.IS_PLAY_BUILD
-import net.mullvad.mullvadvpn.service.endpoint.AccountCache
-import org.joda.time.DateTime
-import org.joda.time.Duration
-
-class AccountExpiryNotification(
- val context: Context,
- val daemon: Intermittent<MullvadDaemon>,
- val accountCache: AccountCache
-) {
-
- private val jobTracker = JobTracker()
- private val resources = context.resources
-
- private val buyMoreTimeUrl = resources.getString(R.string.account_url)
-
- private val channel =
- NotificationChannel(
- context,
- "mullvad_account_time",
- NotificationCompat.VISIBILITY_PRIVATE,
- R.string.account_time_notification_channel_name,
- R.string.account_time_notification_channel_description,
- NotificationManager.IMPORTANCE_HIGH,
- true,
- true
- )
-
- var accountExpiry by
- observable<AccountExpiry>(AccountExpiry.Missing) { _, oldValue, newValue ->
- if (oldValue != newValue) {
- jobTracker.newUiJob("update") { update(newValue) }
- }
- }
-
- init {
- accountCache.onAccountExpiryChange.subscribe(this) { expiry -> accountExpiry = expiry }
- }
-
- fun onDestroy() {
- accountCache.onAccountExpiryChange.unsubscribe(this)
- }
-
- // Suppressing since the permission check is done by calling a common util in another module.
- @SuppressLint("MissingPermission")
- private suspend fun update(expiry: AccountExpiry) {
- val expiryDate = expiry.date()
- val durationUntilExpiry = expiryDate?.remainingTime()
-
- if (accountCache.isNewAccount.not() && durationUntilExpiry?.isCloseToExpiry() == true) {
- if (context.isNotificationPermissionMissing().not()) {
- val notification = build(expiryDate, durationUntilExpiry)
- channel.notificationManager.notify(NOTIFICATION_ID, notification)
- }
- jobTracker.newUiJob("scheduleUpdate") { scheduleUpdate() }
- } else {
- channel.notificationManager.cancel(NOTIFICATION_ID)
- jobTracker.cancelJob("scheduleUpdate")
- }
- }
-
- private fun DateTime.remainingTime(): Duration {
- return Duration(DateTime.now(), this)
- }
-
- private fun Duration.isCloseToExpiry(): Boolean {
- return isShorterThan(REMAINING_TIME_FOR_REMINDERS)
- }
-
- private suspend fun scheduleUpdate() {
- delay(TIME_BETWEEN_CHECKS)
- update(accountExpiry)
- }
-
- private suspend fun build(expiry: DateTime, remainingTime: Duration): Notification {
- val url =
- jobTracker.runOnBackground {
- Uri.parse("$buyMoreTimeUrl?token=${daemon.await().getWwwAuthToken()}")
- }
- val intent =
- if (IS_PLAY_BUILD) {
- Intent().apply {
- setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
- flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- action = Intent.ACTION_MAIN
- }
- } else {
- Intent(Intent.ACTION_VIEW, url)
- }
- val pendingIntent =
- PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
-
- return channel.buildNotification(pendingIntent, format(expiry, remainingTime))
- }
-
- private fun format(expiry: DateTime, remainingTime: Duration): String {
- if (remainingTime.isShorterThan(Duration.ZERO)) {
- return resources.getString(R.string.account_credit_has_expired)
- } else {
- val remainingTimeInfo = remainingTime.toPeriodTo(expiry)
-
- if (remainingTimeInfo.days >= 1) {
- return getRemainingText(
- R.plurals.account_credit_expires_in_days,
- remainingTime.standardDays.toInt()
- )
- } else if (remainingTimeInfo.hours >= 1) {
- return getRemainingText(
- R.plurals.account_credit_expires_in_hours,
- remainingTime.standardHours.toInt()
- )
- } else {
- return resources.getString(R.string.account_credit_expires_in_a_few_minutes)
- }
- }
- }
-
- private fun getRemainingText(pluralId: Int, quantity: Int): String {
- return resources.getQuantityString(pluralId, quantity, quantity)
- }
-
- companion object {
- const val NOTIFICATION_ID: Int = 2
- val REMAINING_TIME_FOR_REMINDERS = Duration.standardDays(2)
- const val TIME_BETWEEN_CHECKS: Long = 12 /* h */ * 60 /* min */ * 60 /* s */ * 1000 /* ms */
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt
new file mode 100644
index 0000000000..d65cb7255c
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ForegroundNotificationManager.kt
@@ -0,0 +1,80 @@
+package net.mullvad.mullvadvpn.service.notifications
+
+import android.app.Service
+import android.content.pm.ServiceInfo
+import android.net.VpnService
+import android.os.Build
+import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.lib.common.constant.TAG
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.lib.model.NotificationChannel
+import net.mullvad.mullvadvpn.lib.model.NotificationTunnelState
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
+import net.mullvad.mullvadvpn.service.MullvadVpnService
+import net.mullvad.mullvadvpn.service.notifications.tunnelstate.TunnelStateNotificationProvider
+import net.mullvad.mullvadvpn.service.notifications.tunnelstate.toNotification
+
+class ForegroundNotificationManager(
+ private val vpnService: MullvadVpnService,
+ private val tunnelStateNotificationProvider: TunnelStateNotificationProvider,
+ private val scope: CoroutineScope,
+) {
+ suspend fun start(foregroundProvider: ShouldBeOnForegroundProvider) {
+ scope.launch {
+ foregroundProvider.shouldBeOnForeground.collect {
+ if (it) {
+ Log.d(TAG, "Starting foreground")
+ notifyForeground(getTunnelStateNotificationOrDefault())
+ } else {
+ Log.d(TAG, "Stopping foreground")
+ vpnService.stopForeground(Service.STOP_FOREGROUND_DETACH)
+ }
+ }
+ }
+ }
+
+ private fun getTunnelStateNotificationOrDefault(): Notification.Tunnel {
+ val current = tunnelStateNotificationProvider.notifications.value
+
+ return if (current is NotificationUpdate.Notify) {
+ current.value
+ } else {
+ defaultNotification
+ }
+ }
+
+ private fun notifyForeground(tunnelStateNotification: Notification.Tunnel) {
+
+ val androidNotification = tunnelStateNotification.toNotification(vpnService)
+ if (VpnService.prepare(vpnService) != null) {
+ // Got connect/disconnect intent, but we don't have permission to go in foreground.
+ // tunnel state will return permission and we will eventually get stopped by system.
+ Log.d(TAG, "Did not start foreground: VPN permission not granted")
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ Log.d(TAG, "Starting foreground UPSIDE_DOWN_CAKE")
+ vpnService.startForeground(
+ tunnelStateNotificationProvider.notificationId.value,
+ androidNotification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED
+ )
+ } else {
+ vpnService.startForeground(
+ tunnelStateNotificationProvider.notificationId.value,
+ androidNotification,
+ )
+ }
+ }
+
+ private val defaultNotification =
+ Notification.Tunnel(
+ NotificationChannel.TunnelUpdates.id,
+ NotificationTunnelState.Disconnected(true),
+ emptyList(),
+ false
+ )
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt
deleted file mode 100644
index d6e904e6ca..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannel.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import android.app.Notification
-import android.app.PendingIntent
-import android.content.Context
-import androidx.core.app.NotificationChannelCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import net.mullvad.mullvadvpn.service.R
-
-class NotificationChannel(
- val context: Context,
- val id: String,
- val visibility: Int,
- name: Int,
- description: Int,
- importance: Int,
- isVibrationEnabled: Boolean,
- isBadgeEnabled: Boolean
-) {
- private val badgeColor by lazy { context.getColor(R.color.colorPrimary) }
-
- val notificationManager = NotificationManagerCompat.from(context)
-
- init {
- val channelName = context.getString(name)
- val channelDescription = context.getString(description)
-
- val channel =
- NotificationChannelCompat.Builder(id, importance)
- .setName(channelName)
- .setDescription(channelDescription)
- .setShowBadge(isBadgeEnabled)
- .setVibrationEnabled(isVibrationEnabled)
- .build()
-
- notificationManager.createNotificationChannel(channel)
- }
-
- fun buildNotification(
- intent: PendingIntent,
- title: String,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- return buildNotification(intent, title, emptyList(), deleteIntent, isOngoing)
- }
-
- fun buildNotification(
- intent: PendingIntent,
- title: Int,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- return buildNotification(intent, title, emptyList(), deleteIntent, isOngoing)
- }
-
- fun buildNotification(
- pendingIntent: PendingIntent,
- title: Int,
- actions: List<NotificationCompat.Action>,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- return buildNotification(
- pendingIntent,
- context.getString(title),
- actions,
- deleteIntent,
- isOngoing
- )
- }
-
- private fun buildNotification(
- pendingIntent: PendingIntent,
- title: String,
- actions: List<NotificationCompat.Action>,
- deleteIntent: PendingIntent? = null,
- isOngoing: Boolean = false
- ): Notification {
- val builder =
- NotificationCompat.Builder(context, id)
- .setSmallIcon(R.drawable.small_logo_black)
- .setColor(badgeColor)
- .setContentTitle(title)
- .setContentIntent(pendingIntent)
- .setVisibility(visibility)
- .setOngoing(isOngoing)
- for (action in actions) {
- builder.addAction(action)
- }
-
- deleteIntent?.let { intent -> builder.setDeleteIntent(intent) }
-
- return builder.build()
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannelFactory.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannelFactory.kt
new file mode 100644
index 0000000000..c7c9a67b43
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationChannelFactory.kt
@@ -0,0 +1,57 @@
+package net.mullvad.mullvadvpn.service.notifications
+
+import android.app.NotificationManager
+import android.content.res.Resources
+import androidx.core.app.NotificationChannelCompat
+import androidx.core.app.NotificationManagerCompat
+import net.mullvad.mullvadvpn.lib.common.R
+import net.mullvad.mullvadvpn.lib.model.NotificationChannel
+import net.mullvad.mullvadvpn.lib.model.NotificationChannelId
+
+class NotificationChannelFactory(
+ private val notificationManagerCompat: NotificationManagerCompat,
+ private val resources: Resources,
+ channels: List<NotificationChannel>
+) {
+ init {
+ channels.forEach { create(it) }
+ }
+
+ private fun create(channel: NotificationChannel): NotificationChannelId {
+ val androidChannel = channel.toAndroidNotificationChannel()
+ notificationManagerCompat.createNotificationChannel(androidChannel)
+ return channel.id
+ }
+
+ private fun NotificationChannel.toAndroidNotificationChannel(): NotificationChannelCompat =
+ when (this) {
+ NotificationChannel.AccountUpdates -> NotificationChannel.AccountUpdates.toChannel()
+ NotificationChannel.TunnelUpdates -> NotificationChannel.TunnelUpdates.toChannel()
+ }
+
+ private fun NotificationChannel.TunnelUpdates.toChannel(): NotificationChannelCompat =
+ NotificationChannelCompat.Builder(
+ id.value,
+ NotificationManager.IMPORTANCE_LOW,
+ )
+ .setName(resources.getString(R.string.foreground_notification_channel_name))
+ .setDescription(
+ resources.getString(R.string.foreground_notification_channel_description)
+ )
+ .setShowBadge(false)
+ .setVibrationEnabled(false)
+ .build()
+
+ private fun NotificationChannel.AccountUpdates.toChannel(): NotificationChannelCompat =
+ NotificationChannelCompat.Builder(
+ id.value,
+ NotificationManager.IMPORTANCE_HIGH,
+ )
+ .setName(resources.getString(R.string.account_time_notification_channel_name))
+ .setDescription(
+ resources.getString(R.string.account_time_notification_channel_description)
+ )
+ .setShowBadge(true)
+ .setVibrationEnabled(true)
+ .build()
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationManager.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationManager.kt
new file mode 100644
index 0000000000..74aff9cca0
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationManager.kt
@@ -0,0 +1,59 @@
+package net.mullvad.mullvadvpn.service.notifications
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationManagerCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
+import net.mullvad.mullvadvpn.service.notifications.accountexpiry.toNotification
+import net.mullvad.mullvadvpn.service.notifications.tunnelstate.toNotification
+
+class NotificationManager(
+ private val notificationManagerCompat: NotificationManagerCompat,
+ notificationProviders: List<NotificationProvider<Notification>>,
+ context: Context,
+ val scope: CoroutineScope,
+) {
+
+ init {
+ scope.launch {
+ notificationProviders
+ .map { it.notifications }
+ .merge()
+ .collect { notificationUpdate ->
+ when (notificationUpdate) {
+ is NotificationUpdate.Cancel ->
+ notificationManagerCompat.cancel(
+ notificationUpdate.notificationId.value
+ )
+ is NotificationUpdate.Notify -> {
+ val notification = notificationUpdate.value
+ val androidNotification = notification.toAndroidNotification(context)
+ if (
+ ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ notificationManagerCompat.notify(
+ notificationUpdate.notificationId.value,
+ androidNotification
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun Notification.toAndroidNotification(context: Context): android.app.Notification =
+ when (this) {
+ is Notification.Tunnel -> toNotification(context)
+ is Notification.AccountExpiry -> toNotification(context)
+ }
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationProvider.kt
new file mode 100644
index 0000000000..ecdde13d7a
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/NotificationProvider.kt
@@ -0,0 +1,8 @@
+package net.mullvad.mullvadvpn.service.notifications
+
+import kotlinx.coroutines.flow.Flow
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
+
+interface NotificationProvider<D> {
+ val notifications: Flow<NotificationUpdate<D>>
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ShouldBeOnForegroundProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ShouldBeOnForegroundProvider.kt
new file mode 100644
index 0000000000..90e533465c
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/ShouldBeOnForegroundProvider.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.service.notifications
+
+import kotlinx.coroutines.flow.StateFlow
+
+interface ShouldBeOnForegroundProvider {
+ val shouldBeOnForeground: StateFlow<Boolean>
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt
deleted file mode 100644
index 44a34589d9..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotification.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import android.annotation.SuppressLint
-import android.app.Notification
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import androidx.core.app.NotificationCompat
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.isNotificationPermissionMissing
-import net.mullvad.mullvadvpn.lib.common.util.getErrorNotificationResources
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.R
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-import net.mullvad.talpid.tunnel.ErrorStateCause
-
-class TunnelStateNotification(val context: Context) {
- private val channel =
- NotificationChannel(
- context,
- "vpn_tunnel_status",
- NotificationCompat.VISIBILITY_SECRET,
- R.string.foreground_notification_channel_name,
- R.string.foreground_notification_channel_description,
- NotificationManager.IMPORTANCE_MIN,
- false,
- false
- )
-
- private val notificationText: Int
- get() =
- when (val state = tunnelState) {
- is TunnelState.Disconnected -> R.string.unsecured
- is TunnelState.Connecting -> {
- if (reconnecting) {
- R.string.reconnecting
- } else {
- R.string.connecting
- }
- }
- is TunnelState.Connected -> R.string.secured
- is TunnelState.Disconnecting -> {
- when (state.actionAfterDisconnect) {
- ActionAfterDisconnect.Reconnect -> R.string.reconnecting
- else -> R.string.disconnecting
- }
- }
- is TunnelState.Error -> {
- if (state.isDeviceOffline()) {
- R.string.blocking_internet_device_offline
- } else {
- state.errorState.getErrorNotificationResources(context).titleResourceId
- }
- }
- }
-
- private fun TunnelState.isDeviceOffline(): Boolean {
- return (this as? TunnelState.Error)?.errorState?.cause is ErrorStateCause.IsOffline
- }
-
- private val shouldDisplayOngoingNotification: Boolean
- get() =
- when (tunnelState) {
- is TunnelState.Connected -> true
- is TunnelState.Disconnected,
- is TunnelState.Connecting,
- is TunnelState.Disconnecting,
- is TunnelState.Error -> false
- }
-
- private var reconnecting = false
- private var showingReconnecting = false
-
- var showAction by observable(false) { _, _, _ -> update() }
-
- var tunnelState by
- observable<TunnelState>(TunnelState.Disconnected()) { _, _, newState ->
- val isReconnecting = newState is TunnelState.Connecting && reconnecting
- val shouldBeginReconnecting =
- (newState as? TunnelState.Disconnecting)?.actionAfterDisconnect ==
- ActionAfterDisconnect.Reconnect
- reconnecting = isReconnecting || shouldBeginReconnecting
- update()
- }
-
- var visible by
- observable(true) { _, _, newValue ->
- if (newValue == true) {
- update()
- } else {
- channel.notificationManager.cancel(NOTIFICATION_ID)
- }
- }
-
- // Suppressing since the permission check is done by calling a common util in another module.
- @SuppressLint("MissingPermission")
- private fun update() {
- if (
- context.isNotificationPermissionMissing().not() &&
- visible &&
- (!reconnecting || !showingReconnecting)
- ) {
- channel.notificationManager.notify(NOTIFICATION_ID, build())
- }
- }
-
- fun build(): Notification {
- val intent =
- Intent().apply {
- setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
- flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
- action = Intent.ACTION_MAIN
- }
- val pendingIntent =
- PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
- val actions =
- if (showAction) {
- listOf(buildAction())
- } else {
- emptyList()
- }
-
- return channel.buildNotification(
- pendingIntent,
- notificationText,
- actions,
- isOngoing = shouldDisplayOngoingNotification
- )
- }
-
- private fun buildAction(): NotificationCompat.Action {
- val action = TunnelStateNotificationAction.from(tunnelState)
- val label = context.getString(action.text)
- val intent = Intent(action.key).setPackage(context.packageName)
- val pendingIntent =
- PendingIntent.getForegroundService(
- context,
- 1,
- intent,
- SdkUtils.getSupportedPendingIntentFlags()
- )
-
- return NotificationCompat.Action(action.icon, label, pendingIntent)
- }
-
- companion object {
- const val NOTIFICATION_ID: Int = 1
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt
deleted file mode 100644
index c836c765f6..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/TunnelStateNotificationAction.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package net.mullvad.mullvadvpn.service.notifications
-
-import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
-import net.mullvad.mullvadvpn.lib.common.constant.KEY_DISCONNECT_ACTION
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.R
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-
-enum class TunnelStateNotificationAction {
- Connect,
- Disconnect,
- Cancel,
- Dismiss;
-
- val text
- get() =
- when (this) {
- Connect -> R.string.connect
- Disconnect -> R.string.disconnect
- Cancel -> R.string.cancel
- Dismiss -> R.string.dismiss
- }
-
- val key
- get() =
- when (this) {
- Connect -> KEY_CONNECT_ACTION
- else -> KEY_DISCONNECT_ACTION
- }
-
- val icon
- get() =
- when (this) {
- Connect -> R.drawable.icon_notification_connect
- else -> R.drawable.icon_notification_disconnect
- }
-
- companion object {
- fun from(tunnelState: TunnelState) =
- when (tunnelState) {
- is TunnelState.Disconnected -> Connect
- is TunnelState.Connecting -> Cancel
- is TunnelState.Connected -> Disconnect
- is TunnelState.Disconnecting -> {
- when (tunnelState.actionAfterDisconnect) {
- ActionAfterDisconnect.Reconnect -> Cancel
- else -> Connect
- }
- }
- is TunnelState.Error -> {
- if (tunnelState.errorState.isBlocking) {
- Disconnect
- } else {
- Dismiss
- }
- }
- }
- }
-}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt
new file mode 100644
index 0000000000..5b5570470d
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryAndroidNotification.kt
@@ -0,0 +1,62 @@
+package net.mullvad.mullvadvpn.service.notifications.accountexpiry
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import androidx.core.app.NotificationCompat
+import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
+import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
+import net.mullvad.mullvadvpn.lib.common.util.createAccountUri
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.service.R
+import org.joda.time.Duration
+
+internal fun Notification.AccountExpiry.toNotification(context: Context) =
+ NotificationCompat.Builder(context, channelId.value)
+ .setContentIntent(contentIntent(context))
+ .setContentTitle(context.resources.contentTitle(durationUntilExpiry))
+ .setSmallIcon(R.drawable.small_logo_white)
+ .setOngoing(ongoing)
+ .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
+ .build()
+
+private fun Notification.AccountExpiry.contentIntent(context: Context): PendingIntent {
+
+ val intent =
+ if (websiteAuthToken == null) {
+ Intent().apply {
+ setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
+ flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ action = Intent.ACTION_MAIN
+ }
+ } else {
+ val uri = createAccountUri(context.getString(R.string.account_url), websiteAuthToken)
+ Intent(Intent.ACTION_VIEW, uri)
+ }
+ return PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
+}
+
+private fun Resources.contentTitle(remainingTime: Duration): String =
+ when {
+ remainingTime.isShorterThan(Duration.ZERO) -> {
+ getString(R.string.account_credit_has_expired)
+ }
+ remainingTime.standardDays >= 1 -> {
+ getRemainingText(
+ R.plurals.account_credit_expires_in_days,
+ remainingTime.standardDays.toInt()
+ )
+ }
+ remainingTime.standardHours >= 1 -> {
+ getRemainingText(
+ R.plurals.account_credit_expires_in_hours,
+ remainingTime.standardHours.toInt()
+ )
+ }
+ else -> getString(R.string.account_credit_expires_in_a_few_minutes)
+ }
+
+private fun Resources.getRemainingText(pluralId: Int, quantity: Int): String {
+ return getQuantityString(pluralId, quantity, quantity)
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt
new file mode 100644
index 0000000000..b1f1613890
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/accountexpiry/AccountExpiryNotificationProvider.kt
@@ -0,0 +1,64 @@
+package net.mullvad.mullvadvpn.service.notifications.accountexpiry
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.lib.model.NotificationChannelId
+import net.mullvad.mullvadvpn.lib.model.NotificationId
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
+import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
+import net.mullvad.mullvadvpn.service.constant.IS_PLAY_BUILD
+import net.mullvad.mullvadvpn.service.notifications.NotificationProvider
+import org.joda.time.DateTime
+import org.joda.time.Duration
+
+class AccountExpiryNotificationProvider(
+ channelId: NotificationChannelId,
+ accountRepository: AccountRepository,
+ deviceRepository: DeviceRepository,
+) : NotificationProvider<Notification.AccountExpiry> {
+ private val notificationId = NotificationId(3)
+
+ override val notifications: Flow<NotificationUpdate<Notification.AccountExpiry>> =
+ combine(
+ deviceRepository.deviceState,
+ accountRepository.accountData.filterNotNull(),
+ accountRepository.isNewAccount
+ ) { deviceState, accountData, isNewAccount ->
+ if (deviceState !is DeviceState.LoggedIn) {
+ return@combine NotificationUpdate.Cancel(notificationId)
+ }
+
+ val durationUntilExpiry = accountData.expiryDate.remainingTime()
+
+ val notification =
+ Notification.AccountExpiry(
+ channelId = channelId,
+ actions = emptyList(),
+ websiteAuthToken =
+ if (!IS_PLAY_BUILD) accountRepository.getWebsiteAuthToken() else null,
+ durationUntilExpiry = durationUntilExpiry,
+ )
+ if (!isNewAccount && durationUntilExpiry.isCloseToExpiry()) {
+ NotificationUpdate.Notify(notificationId, notification)
+ } else {
+ NotificationUpdate.Cancel(notificationId)
+ }
+ }
+ .filterNotNull()
+
+ private fun DateTime.remainingTime(): Duration {
+ return Duration(DateTime.now(), this)
+ }
+
+ private fun Duration.isCloseToExpiry(): Boolean {
+ return isShorterThan(REMAINING_TIME_FOR_REMINDERS)
+ }
+
+ companion object {
+ private val REMAINING_TIME_FOR_REMINDERS = Duration.standardDays(2)
+ }
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt
new file mode 100644
index 0000000000..74027ac940
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationAction.kt
@@ -0,0 +1,106 @@
+package net.mullvad.mullvadvpn.service.notifications.tunnelstate
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import androidx.core.app.NotificationCompat
+import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
+import net.mullvad.mullvadvpn.lib.common.constant.KEY_DISCONNECT_ACTION
+import net.mullvad.mullvadvpn.lib.common.constant.KEY_REQUEST_VPN_PERMISSION
+import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
+import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.lib.model.NotificationAction
+import net.mullvad.mullvadvpn.lib.model.NotificationTunnelState
+import net.mullvad.mullvadvpn.service.R
+
+internal fun Notification.Tunnel.toNotification(context: Context) =
+ NotificationCompat.Builder(context, channelId.value)
+ .setContentIntent(contentIntent(context))
+ .setContentTitle(context.getString(state.contentTitleResourceId()))
+ .setSmallIcon(R.drawable.small_logo_white)
+ .apply { actions.forEach { addAction(it.toCompatAction(context)) } }
+ .setOngoing(ongoing)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET)
+ .build()
+
+private fun Notification.Tunnel.contentIntent(context: Context): PendingIntent {
+ val intent =
+ Intent().apply {
+ setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
+ flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
+ action = Intent.ACTION_MAIN
+ }
+
+ return PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
+}
+
+private fun NotificationTunnelState.contentTitleResourceId(): Int =
+ when (this) {
+ NotificationTunnelState.Connected -> R.string.secured
+ NotificationTunnelState.Connecting -> R.string.connecting
+ is NotificationTunnelState.Disconnected -> {
+ if (this.hasVpnPermission) {
+ R.string.unsecured
+ } else {
+ R.string.unsecured_vpn_permission_error
+ }
+ }
+ NotificationTunnelState.Disconnecting -> R.string.disconnecting
+ NotificationTunnelState.Reconnecting -> R.string.reconnecting
+ NotificationTunnelState.Error.Blocking -> R.string.blocking_internet
+ is NotificationTunnelState.Error.Critical -> R.string.critical_error
+ NotificationTunnelState.Error.DeviceOffline -> R.string.blocking_internet_device_offline
+ NotificationTunnelState.Error.VpnPermissionDenied ->
+ R.string.vpn_permission_error_notification_title
+ NotificationTunnelState.Error.AlwaysOnVpn -> R.string.always_on_vpn_error_notification_title
+ }
+
+internal fun NotificationAction.Tunnel.toCompatAction(context: Context): NotificationCompat.Action {
+
+ val pendingIntent =
+ if (this is NotificationAction.Tunnel.RequestPermission) {
+ val intent =
+ Intent().apply {
+ setClassName(context.packageName, MAIN_ACTIVITY_CLASS)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ setAction(KEY_REQUEST_VPN_PERMISSION)
+ }
+
+ PendingIntent.getActivity(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
+ } else {
+ val intent = Intent(toKey()).setPackage(context.packageName)
+ PendingIntent.getService(context, 1, intent, SdkUtils.getSupportedPendingIntentFlags())
+ }
+
+ return NotificationCompat.Action(
+ toIconResource(),
+ context.getString(titleResource()),
+ pendingIntent
+ )
+}
+
+fun NotificationAction.Tunnel.titleResource() =
+ when (this) {
+ NotificationAction.Tunnel.Cancel -> R.string.cancel
+ NotificationAction.Tunnel.Connect,
+ NotificationAction.Tunnel.RequestPermission -> R.string.connect
+ NotificationAction.Tunnel.Disconnect -> R.string.disconnect
+ NotificationAction.Tunnel.Dismiss -> R.string.dismiss
+ }
+
+fun NotificationAction.Tunnel.toKey() =
+ when (this) {
+ NotificationAction.Tunnel.Connect -> KEY_CONNECT_ACTION
+ NotificationAction.Tunnel.RequestPermission -> KEY_REQUEST_VPN_PERMISSION
+ NotificationAction.Tunnel.Cancel,
+ NotificationAction.Tunnel.Disconnect,
+ NotificationAction.Tunnel.Dismiss -> KEY_DISCONNECT_ACTION
+ }
+
+fun NotificationAction.Tunnel.toIconResource() =
+ when (this) {
+ NotificationAction.Tunnel.Connect -> R.drawable.icon_notification_connect
+ else -> R.drawable.icon_notification_disconnect
+ }
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt
new file mode 100644
index 0000000000..7c1ac942b3
--- /dev/null
+++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt
@@ -0,0 +1,143 @@
+package net.mullvad.mullvadvpn.service.notifications.tunnelstate
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.DeviceState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
+import net.mullvad.mullvadvpn.lib.model.Notification
+import net.mullvad.mullvadvpn.lib.model.NotificationAction
+import net.mullvad.mullvadvpn.lib.model.NotificationChannelId
+import net.mullvad.mullvadvpn.lib.model.NotificationId
+import net.mullvad.mullvadvpn.lib.model.NotificationTunnelState
+import net.mullvad.mullvadvpn.lib.model.NotificationUpdate
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+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.service.notifications.NotificationProvider
+
+class TunnelStateNotificationProvider(
+ connectionProxy: ConnectionProxy,
+ vpnPermissionRepository: VpnPermissionRepository,
+ deviceRepository: DeviceRepository,
+ channelId: NotificationChannelId,
+ scope: CoroutineScope
+) : NotificationProvider<Notification.Tunnel> {
+ internal val notificationId = NotificationId(2)
+
+ override val notifications: StateFlow<NotificationUpdate<Notification.Tunnel>> =
+ combine(
+ connectionProxy.tunnelState,
+ connectionProxy.tunnelState.actionAfterDisconnect().distinctUntilChanged(),
+ deviceRepository.deviceState
+ ) { tunnelState: TunnelState, actionAfterDisconnect: ActionAfterDisconnect?, deviceState
+ ->
+ if (deviceState is DeviceState.LoggedOut) {
+ return@combine NotificationUpdate.Cancel(notificationId)
+ }
+ val notificationTunnelState =
+ tunnelState(
+ tunnelState,
+ actionAfterDisconnect,
+ vpnPermissionRepository.hasVpnPermission(),
+ vpnPermissionRepository.getAlwaysOnVpnAppName()
+ )
+
+ return@combine NotificationUpdate.Notify(
+ notificationId,
+ Notification.Tunnel(
+ channelId = channelId,
+ state = notificationTunnelState,
+ actions = listOfNotNull(notificationTunnelState.toAction()),
+ ongoing = notificationTunnelState is NotificationTunnelState.Connected
+ )
+ )
+ }
+ .stateIn(scope, SharingStarted.Eagerly, NotificationUpdate.Cancel(notificationId))
+
+ private fun tunnelState(
+ tunnelState: TunnelState,
+ actionAfterDisconnect: ActionAfterDisconnect?,
+ hasVpnPermission: Boolean,
+ alwaysOnVpnPermissionName: String?
+ ): NotificationTunnelState =
+ tunnelState.toNotificationTunnelState(
+ actionAfterDisconnect,
+ hasVpnPermission,
+ alwaysOnVpnPermissionName
+ )
+
+ private fun Flow<TunnelState>.actionAfterDisconnect(): Flow<ActionAfterDisconnect?> =
+ filterIsInstance<TunnelState.Disconnecting>()
+ .map<TunnelState.Disconnecting, ActionAfterDisconnect?> { it.actionAfterDisconnect }
+ .onStart { emit(null) }
+
+ private fun TunnelState.toNotificationTunnelState(
+ actionAfterDisconnect: ActionAfterDisconnect?,
+ hasVpnPermission: Boolean,
+ alwaysOnVpnPermissionName: String?
+ ) =
+ when (this) {
+ is TunnelState.Disconnected -> NotificationTunnelState.Disconnected(hasVpnPermission)
+ is TunnelState.Connecting -> {
+ if (actionAfterDisconnect == ActionAfterDisconnect.Reconnect) {
+ NotificationTunnelState.Reconnecting
+ } else {
+ NotificationTunnelState.Connecting
+ }
+ }
+ is TunnelState.Disconnecting -> {
+ if (actionAfterDisconnect == ActionAfterDisconnect.Reconnect) {
+ NotificationTunnelState.Reconnecting
+ } else {
+ NotificationTunnelState.Disconnecting
+ }
+ }
+ is TunnelState.Connected -> NotificationTunnelState.Connected
+ is TunnelState.Error -> toNotificationTunnelState(alwaysOnVpnPermissionName)
+ }
+
+ private fun TunnelState.Error.toNotificationTunnelState(
+ alwaysOnVpnPermissionName: String?
+ ): NotificationTunnelState.Error {
+ val cause = errorState.cause
+ return when {
+ cause is ErrorStateCause.IsOffline -> NotificationTunnelState.Error.DeviceOffline
+ cause is ErrorStateCause.InvalidDnsServers -> NotificationTunnelState.Error.Blocking
+ cause is ErrorStateCause.VpnPermissionDenied ->
+ alwaysOnVpnPermissionName?.let { NotificationTunnelState.Error.AlwaysOnVpn }
+ ?: NotificationTunnelState.Error.VpnPermissionDenied
+ errorState.isBlocking -> NotificationTunnelState.Error.Blocking
+ else -> NotificationTunnelState.Error.Critical
+ }
+ }
+
+ private fun NotificationTunnelState.toAction(): NotificationAction.Tunnel =
+ when (this) {
+ is NotificationTunnelState.Disconnected -> {
+ if (this.hasVpnPermission) {
+ NotificationAction.Tunnel.Connect
+ } else {
+ NotificationAction.Tunnel.RequestPermission
+ }
+ }
+ NotificationTunnelState.Disconnecting -> NotificationAction.Tunnel.Connect
+ NotificationTunnelState.Connected,
+ NotificationTunnelState.Error.Blocking -> NotificationAction.Tunnel.Disconnect
+ NotificationTunnelState.Connecting -> NotificationAction.Tunnel.Cancel
+ NotificationTunnelState.Reconnecting -> NotificationAction.Tunnel.Cancel
+ is NotificationTunnelState.Error.Critical,
+ NotificationTunnelState.Error.DeviceOffline,
+ NotificationTunnelState.Error.VpnPermissionDenied,
+ NotificationTunnelState.Error.AlwaysOnVpn -> NotificationAction.Tunnel.Dismiss
+ }
+}
diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt
deleted file mode 100644
index 055c9f8777..0000000000
--- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/persistence/SplitTunnelingPersistence.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.mullvad.mullvadvpn.service.persistence
-
-import android.content.Context
-import java.io.File
-import kotlin.properties.Delegates.observable
-
-// The spelling of the shared preferences location can't be changed to American English without
-// either having users lose their preferences on update or implementing some migration code.
-private const val SHARED_PREFERENCES = "split_tunnelling"
-private const val KEY_ENABLED = "enabled"
-
-class SplitTunnelingPersistence(context: Context) {
- // The spelling of the app list file name can't be changed to American English without either
- // having users lose their preferences on update or implementing some migration code.
- private val appListFile = File(context.filesDir, "split-tunnelling.txt")
- private val preferences = context.getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE)
-
- var enabled by
- observable(preferences.getBoolean(KEY_ENABLED, false)) { _, _, isEnabled ->
- preferences.edit().apply {
- putBoolean(KEY_ENABLED, isEnabled)
- apply()
- }
- }
-
- var excludedApps by
- observable(loadExcludedApps()) { _, _, excludedAppsSet ->
- appListFile.writeText(excludedAppsSet.joinToString(separator = "\n"))
- }
-
- private fun loadExcludedApps(): Set<String> {
- return if (appListFile.exists()) appListFile.readLines().toSet() else emptySet()
- }
-}
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index 30f77c2159..b03fc2538a 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -4,17 +4,20 @@ include(
":tile"
)
include(
+ ":lib:billing",
":lib:common",
+ ":lib:common-test",
+ ":lib:daemon-grpc",
":lib:endpoint",
+ ":lib:intent-provider",
":lib:ipc",
+ ":lib:map",
":lib:model",
+ ":lib:payment",
":lib:resource",
+ ":lib:shared",
":lib:talpid",
- ":lib:theme",
- ":lib:common-test",
- ":lib:billing",
- ":lib:payment",
- ":lib:map"
+ ":lib:theme"
)
include(
":test",
diff --git a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ArchitectureTest.kt b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ArchitectureTest.kt
index b6618713dd..117fb4bebc 100644
--- a/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ArchitectureTest.kt
+++ b/android/test/arch/src/test/kotlin/net/mullvad/mullvadvpn/test/arch/ArchitectureTest.kt
@@ -10,7 +10,7 @@ class ArchitectureTest {
@Test
fun `ensure model layer depends on nothing`() =
Konsist.scopeFromProduction().assertArchitecture {
- val model = Layer("Model", "net.mullvad.mullvadvpn.model..")
+ val model = Layer("Model", "net.mullvad.mullvadvpn.lib.model..")
model.dependsOnNothing()
}
diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/ForgetAllVpnAppsInSettingsTestRule.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/ForgetAllVpnAppsInSettingsTestRule.kt
index 0e5371fcc3..9457b7862e 100644
--- a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/ForgetAllVpnAppsInSettingsTestRule.kt
+++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/rule/ForgetAllVpnAppsInSettingsTestRule.kt
@@ -21,7 +21,7 @@ class ForgetAllVpnAppsInSettingsTestRule : BeforeTestExecutionCallback {
)
val vpnSettingsButtons =
device.findObjects(By.res(SETTINGS_PACKAGE, VPN_SETTINGS_BUTTON_ID))
- vpnSettingsButtons?.forEach { button ->
+ vpnSettingsButtons.forEach { button ->
button.click()
device.findObjectWithTimeout(By.text(FORGET_VPN_VPN_BUTTON_TEXT)).click()
device.findObjectByCaseInsensitiveText(FORGET_VPN_VPN_CONFIRM_BUTTON_TEXT).click()
diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
index b8bc5fa716..f25ca9a8b5 100644
--- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
+++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.test.e2e
import androidx.test.uiautomator.By
+import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.test.common.constant.CONNECTION_TIMEOUT
import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout
import net.mullvad.mullvadvpn.test.common.rule.ForgetAllVpnAppsInSettingsTestRule
diff --git a/android/tile/build.gradle.kts b/android/tile/build.gradle.kts
index 419ab58dc0..0b2aab45f5 100644
--- a/android/tile/build.gradle.kts
+++ b/android/tile/build.gradle.kts
@@ -26,11 +26,15 @@ android {
dependencies {
implementation(project(Dependencies.Mullvad.commonLib))
- implementation(project(Dependencies.Mullvad.ipcLib))
+ implementation(project(Dependencies.Mullvad.daemonGrpc))
implementation(project(Dependencies.Mullvad.modelLib))
implementation(project(Dependencies.Mullvad.resourceLib))
+ implementation(project(Dependencies.Mullvad.sharedLib))
implementation(project(Dependencies.Mullvad.talpidLib))
+ implementation(Dependencies.Koin.core)
+ implementation(Dependencies.Koin.android)
+
implementation(Dependencies.AndroidX.appcompat)
implementation(Dependencies.Kotlin.stdlib)
implementation(Dependencies.KotlinX.coroutinesAndroid)
diff --git a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt
index ae80bedef8..f796690f18 100644
--- a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt
+++ b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/MullvadTileService.kt
@@ -9,32 +9,40 @@ import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.util.Log
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
import net.mullvad.mullvadvpn.lib.common.constant.KEY_DISCONNECT_ACTION
import net.mullvad.mullvadvpn.lib.common.constant.MAIN_ACTIVITY_CLASS
+import net.mullvad.mullvadvpn.lib.common.constant.TAG
import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.setSubtitleIfSupported
-import net.mullvad.mullvadvpn.model.ServiceResult
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState
+import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
+import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import org.koin.android.ext.android.get
class MullvadTileService : TileService() {
- private var scope: CoroutineScope? = null
+ private var job: Job? = null
private lateinit var securedIcon: Icon
private lateinit var unsecuredIcon: Icon
+ private val connectionProxy = get<ConnectionProxy>()
+ private val managementService = get<ManagementService>()
+
override fun onCreate() {
securedIcon = Icon.createWithResource(this, R.drawable.small_logo_white)
unsecuredIcon = Icon.createWithResource(this, R.drawable.small_logo_black)
@@ -72,11 +80,11 @@ class MullvadTileService : TileService() {
}
override fun onStartListening() {
- scope = MainScope().apply { launchListenToTunnelState() }
+ job = MainScope().launch { launchListenToTunnelState() }
}
override fun onStopListening() {
- scope?.cancel()
+ job?.cancel()
}
@SuppressLint("StartActivityAndCollapseDeprecated")
@@ -84,7 +92,7 @@ class MullvadTileService : TileService() {
val isSetup = VpnService.prepare(applicationContext) == null
// TODO This logic should be more advanced, we should ensure user has an account setup etc.
if (!isSetup) {
- Log.d("MullvadTileService", "VPN service not setup, starting main activity")
+ Log.d(TAG, "TileService: VPN service not setup, starting main activity")
val intent =
Intent().apply {
@@ -98,7 +106,7 @@ class MullvadTileService : TileService() {
startActivityAndCollapseCompat(intent)
return
} else {
- Log.d("MullvadTileService", "VPN service is setup")
+ Log.d(TAG, "TileService: VPN service is setup")
}
val intent =
Intent().apply {
@@ -132,19 +140,23 @@ class MullvadTileService : TileService() {
}
@OptIn(FlowPreview::class)
- private fun CoroutineScope.launchListenToTunnelState() = launch {
- ServiceConnection(this@MullvadTileService, this)
- .tunnelState
- .debounce(300L)
+ private suspend fun launchListenToTunnelState() {
+ combine(
+ connectionProxy.tunnelState.onStart { emit(TunnelState.Disconnected(null)) },
+ managementService.connectionState
+ ) { tunnelState, connectionState ->
+ tunnelState to connectionState
+ }
+ .debounce(TUNNEL_STATE_DEBOUNCE_MS)
.map { (tunnelState, connectionState) -> mapToTileState(tunnelState, connectionState) }
.collect { updateTileState(it) }
}
private fun mapToTileState(
tunnelState: TunnelState,
- connectionState: ServiceResult.ConnectionState
+ connectionState: GrpcConnectivityState
): Int {
- return if (connectionState == ServiceResult.ConnectionState.CONNECTED) {
+ return if (connectionState == GrpcConnectivityState.Ready) {
when (tunnelState) {
is TunnelState.Disconnected -> Tile.STATE_INACTIVE
is TunnelState.Connecting -> Tile.STATE_ACTIVE
@@ -183,4 +195,8 @@ class MullvadTileService : TileService() {
updateTile()
}
}
+
+ companion object {
+ private const val TUNNEL_STATE_DEBOUNCE_MS = 300L
+ }
}
diff --git a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt b/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt
deleted file mode 100644
index 93218c66dc..0000000000
--- a/android/tile/src/main/kotlin/net/mullvad/mullvadvpn/tile/ServiceConnection.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package net.mullvad.mullvadvpn.tile
-
-import android.content.Context
-import android.content.Intent
-import android.os.IBinder
-import android.os.Looper
-import android.os.Messenger
-import kotlin.reflect.KClass
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.consumeAsFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS
-import net.mullvad.mullvadvpn.lib.common.util.DispatchingFlow
-import net.mullvad.mullvadvpn.lib.common.util.bindServiceFlow
-import net.mullvad.mullvadvpn.lib.common.util.dispatchTo
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.HandlerFlow
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.ServiceResult
-import net.mullvad.mullvadvpn.model.TunnelState
-
-@FlowPreview
-class ServiceConnection(context: Context, scope: CoroutineScope) {
- private val activeListeners = MutableStateFlow<Pair<Messenger, Int>?>(null)
- private val handler = HandlerFlow(Looper.getMainLooper(), Event.Companion::fromMessage)
- private val listener = Messenger(handler)
- private val listenerId = MutableStateFlow<Int?>(null)
-
- private lateinit var listenerRegistrations: StateFlow<Pair<Messenger, Int>?>
-
- lateinit var tunnelState: Flow<Pair<TunnelState, ServiceResult.ConnectionState>>
- private set
-
- private val serviceConnectionStateChannel =
- Channel<ServiceResult.ConnectionState>(Channel.RENDEZVOUS)
-
- init {
- val dispatcher =
- handler.filterNotNull().dispatchTo {
- listenerRegistrations =
- subscribeToState(Event.ListenerReady::class, scope) {
- Pair(connection, listenerId)
- }
-
- val tunnelStateEvents =
- subscribeToState(
- Event.TunnelStateChange::class,
- scope,
- TunnelState.Disconnected()
- ) {
- tunnelState
- }
-
- tunnelState =
- tunnelStateEvents.combine(serviceConnectionStateChannel.consumeAsFlow()) {
- tunnelState,
- serviceConnectionState ->
- tunnelState to serviceConnectionState
- }
- }
-
- scope.launch { connect(context) }
- scope.launch { dispatcher.collect() }
- scope.launch { unregisterOldListeners() }
- scope.launch { listenerRegistrations.collect { activeListeners.value = it } }
- }
-
- private suspend fun connect(context: Context) {
- val intent = Intent().apply { setClassName(context.packageName, VPN_SERVICE_CLASS) }
-
- context
- .bindServiceFlow(intent)
- .onStart { emit(ServiceResult.NOT_CONNECTED) }
- .onEach { result -> serviceConnectionStateChannel.send(result.connectionState) }
- .collect { result ->
- activeListeners.value = null
- result.binder?.let(::registerListener)
- }
- }
-
- private fun registerListener(binder: IBinder) {
- val request = Request.RegisterListener(listener)
- val messenger = Messenger(binder)
-
- messenger.send(request.message)
- }
-
- private suspend fun unregisterOldListeners() {
- var oldListener: Pair<Messenger, Int>? = null
-
- activeListeners
- .onCompletion { oldListener?.let(::unregisterListener) }
- .collect { newListener ->
- oldListener?.let(::unregisterListener)
- oldListener = newListener
- }
- }
-
- private fun unregisterListener(registration: Pair<Messenger, Int>) {
- val (messenger, listenerId) = registration
- val request = Request.UnregisterListener(listenerId)
-
- messenger.send(request.message)
- }
-
- private fun <V : Any, D> DispatchingFlow<in V>.subscribeToState(
- event: KClass<V>,
- scope: CoroutineScope,
- dataExtractor: suspend V.() -> D
- ) = subscribe(event).map(dataExtractor).stateIn(scope, SharingStarted.Lazily, null)
-
- private fun <V : Any, D> DispatchingFlow<in V>.subscribeToState(
- event: KClass<V>,
- scope: CoroutineScope,
- initialValue: D,
- dataExtractor: suspend V.() -> D
- ) = subscribe(event).map(dataExtractor).stateIn(scope, SharingStarted.Lazily, initialValue)
-}