diff options
| author | David Göransson <david.goransson@mullvad.net> | 2026-04-10 16:15:00 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2026-04-10 16:15:00 +0200 |
| commit | d01078bcca29284594258d09bf2468a1b18fff68 (patch) | |
| tree | 876bbb48ab44fb5a99e12d22c0205a677b7cdf76 | |
| parent | 37d615047677f2174af610174f219746a6dc6c34 (diff) | |
| parent | 6b8f70247f4072fd3404fcdab331726506996e46 (diff) | |
| download | mullvadvpn-d01078bcca29284594258d09bf2468a1b18fff68.tar.xz mullvadvpn-d01078bcca29284594258d09bf2468a1b18fff68.zip | |
Merge branch 'add-search-to-split-tunneling-screen-droid-1732'
89 files changed, 743 insertions, 366 deletions
diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 70c4f48f15..7d5278a6a5 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -26,6 +26,7 @@ Line wrap the file at 100 chars. Th - Add Ukrainian as a new language in the app. - Add in-app language selector for Android 13 and up. - Add reconnect action to tunnel status notification. +- Add search for Split Tunneling ### Changed - Drop support for Android 8/8.1 (Android 9/API level 28 or later is now required). 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 index 942ac4c269..cb8c9de228 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/AppModule.kt @@ -68,8 +68,7 @@ val appModule = module { single { ScheduleNotificationAlarmUseCase(androidContext(), get()) } single { AccountExpiryNotificationActionUseCase(get(), get()) } // TODO Move these back to UiModule when fixDisableBug is removed - single<String>(named(SELF_PACKAGE_NAME)) { androidContext().packageName } - single { AppObfuscationRepository(get(), get(named(SELF_PACKAGE_NAME))) } + single { AppObfuscationRepository(get(), get()) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { single { LanguageRepository(androidContext()) } } 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 0f63892780..150e16656e 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 @@ -56,11 +56,14 @@ import net.mullvad.mullvadvpn.feature.serveripoverride.impl.reset.ResetServerIpO import net.mullvad.mullvadvpn.feature.settings.impl.SettingsViewModel import net.mullvad.mullvadvpn.feature.splittunneling.impl.SplitTunnelingViewModel import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider +import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.SplitTunnelingUseCase +import net.mullvad.mullvadvpn.feature.splittunneling.impl.search.SearchSplitTunnelingViewModel import net.mullvad.mullvadvpn.feature.vpnsettings.impl.VpnSettingsViewModel import net.mullvad.mullvadvpn.feature.vpnsettings.impl.dns.DnsDialogViewModel import net.mullvad.mullvadvpn.feature.vpnsettings.impl.mtu.MtuDialogViewModel import net.mullvad.mullvadvpn.lib.common.constant.BillingTypes import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.model.RelayListType import net.mullvad.mullvadvpn.lib.payment.PaymentProvider import net.mullvad.mullvadvpn.lib.repository.ApiAccessRepository @@ -127,11 +130,8 @@ val uiModule = module { ComponentName(androidContext(), AutoStartVpnBootCompletedReceiver::class.java) } - viewModel { params -> - SplitTunnelingViewModel(isModal = params.get(), get(), get(), Dispatchers.Default) - } - - single { ApplicationsProvider(get(), get(named(SELF_PACKAGE_NAME))) } + single { PackageName(androidContext().packageName) } + single { ApplicationsProvider(get(), get()) } scope<MainActivity> { scoped { ServiceConnectionManager(androidContext()) } } single { InetAddressValidator.getInstance() } single { androidContext().assets } @@ -155,6 +155,7 @@ val uiModule = module { single { RelayListFilterRepository(get()) } single { VoucherRepository(get(), get()) } single { SplitTunnelingRepository(get()) } + single { SplitTunnelingUseCase(get(), get(), get()) } single { ApiAccessRepository(get()) } single { NewDeviceRepository() } single { SplashCompleteRepository() } @@ -266,7 +267,7 @@ val uiModule = module { resources = get(), isPlayBuild = IS_PLAY_BUILD, isFdroidBuild = IS_FDROID_BUILD, - packageName = get(named(SELF_PACKAGE_NAME)), + self = get(), ) } viewModel { @@ -286,7 +287,7 @@ val uiModule = module { resources = get(), isPlayBuild = IS_PLAY_BUILD, isFdroidBuild = IS_FDROID_BUILD, - packageName = get(named(SELF_PACKAGE_NAME)), + self = get(), ) } viewModel { params -> DeviceListViewModel(accountNumber = params.get(), get()) } @@ -419,13 +420,17 @@ val uiModule = module { viewModel { LanguageViewModel(get()) } } viewModel { AutoConnectAndLockdownModeViewModel(isPlayBuild = IS_PLAY_BUILD) } + viewModel { params -> + SplitTunnelingViewModel(isModal = params.get(), get(), get(), get(), Dispatchers.Default) + } + + viewModel { SearchSplitTunnelingViewModel(get(), get(), Dispatchers.Default) } // This view model must be single so we correctly attach lifecycle and share it with activity single { MullvadAppViewModel(get(), get()) } } const val APP_PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.app_preferences" -const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" const val KERMIT_FILE_LOG_DIR_NAME = "android_app_logs" private const val BOOT_COMPLETED_RECEIVER_COMPONENT_NAME = "BOOT_COMPLETED_RECEIVER_COMPONENT_NAME" diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountScreen.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountScreen.kt index 794f7ada8c..2ae962c589 100644 --- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountScreen.kt +++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountScreen.kt @@ -55,8 +55,8 @@ import net.mullvad.mullvadvpn.feature.login.api.LoginNavKey import net.mullvad.mullvadvpn.feature.managedevices.api.ManageDevicesNavKey import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.util.toExpiryDateString -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.designsystem.NegativeOutlinedButton import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryTextButton import net.mullvad.mullvadvpn.lib.ui.tag.MANAGE_DEVICES_BUTTON_TEST_TAG diff --git a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt index cb853f5bc5..622564bcfd 100644 --- a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt +++ b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/AntiCensorshipSettingsScreen.kt @@ -29,10 +29,10 @@ import net.mullvad.mullvadvpn.feature.anticensorship.api.SelectPortNavKey import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.PortType -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.lib.ui.component.annotatedStringResource +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.InfoListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.ObfuscationModeListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.SelectableListItem diff --git a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt index 3ea03b0984..01bb6ec73b 100644 --- a/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt +++ b/android/lib/feature/anticensorship/impl/src/main/java/net/mullvad/mullvadvpn/feature/anticensorship/impl/selectport/SelectPortScreen.kt @@ -23,8 +23,8 @@ import net.mullvad.mullvadvpn.feature.anticensorship.api.SelectPortNavKey import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Port -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.CustomPortListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.InfoListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.SelectableListItem diff --git a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/detail/ApiAccessMethodDetailsScreen.kt b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/detail/ApiAccessMethodDetailsScreen.kt index 60aea74567..439cc534f4 100644 --- a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/detail/ApiAccessMethodDetailsScreen.kt +++ b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/detail/ApiAccessMethodDetailsScreen.kt @@ -48,8 +48,8 @@ import net.mullvad.mullvadvpn.feature.apiaccess.impl.component.TestMethodButton import net.mullvad.mullvadvpn.feature.apiaccess.impl.util.toDisplayName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethod import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.NavigationListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.SwitchListItem import net.mullvad.mullvadvpn.lib.ui.component.text.ListItemInfo diff --git a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/edit/EditApiAccessMethodScreen.kt b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/edit/EditApiAccessMethodScreen.kt index b8bc7f83d2..59888c0528 100644 --- a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/edit/EditApiAccessMethodScreen.kt +++ b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/edit/EditApiAccessMethodScreen.kt @@ -64,8 +64,8 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.Cipher import net.mullvad.mullvadvpn.lib.model.InvalidDataError -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithSmallTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.lib.ui.component.textfield.ErrorSupportingText import net.mullvad.mullvadvpn.lib.ui.component.textfield.mullvadDarkTextFieldColors diff --git a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt index d06ca73d7d..403a8e57a2 100644 --- a/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt +++ b/android/lib/feature/apiaccess/impl/src/main/java/net/mullvad/mullvadvpn/feature/apiaccess/impl/screen/list/ApiAccessListScreen.kt @@ -29,8 +29,8 @@ import net.mullvad.mullvadvpn.feature.apiaccess.api.ApiAccessMethodInfoNavKey import net.mullvad.mullvadvpn.feature.apiaccess.api.EditApiAccessMethodNavKey import net.mullvad.mullvadvpn.feature.apiaccess.impl.util.toDisplayName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodSetting -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.NavigationListItem import net.mullvad.mullvadvpn.lib.ui.component.positionForIndex import net.mullvad.mullvadvpn.lib.ui.component.text.ScreenDescription diff --git a/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt b/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt index ed9304b45d..a2436b5582 100644 --- a/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt +++ b/android/lib/feature/appearance/impl/src/main/java/net/mullvad/mullvadvpn/feature/appearance/impl/AppearanceScreen.kt @@ -14,8 +14,8 @@ import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.appicon.api.AppIconNavKey import net.mullvad.mullvadvpn.feature.language.api.LanguageNavKey -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.NavigationListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.Position import net.mullvad.mullvadvpn.lib.ui.resource.R diff --git a/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/AppIconScreen.kt b/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/AppIconScreen.kt index e002cf15dd..02083c362d 100644 --- a/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/AppIconScreen.kt +++ b/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/AppIconScreen.kt @@ -35,10 +35,10 @@ import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.appicon.impl.obfuscation.AppObfuscation import net.mullvad.mullvadvpn.lib.common.Lc -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.SPACE_CHAR import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.lib.ui.component.annotatedStringResource +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.griditem.AppIconAndTitleGridItem import net.mullvad.mullvadvpn.lib.ui.component.text.ScreenDescription import net.mullvad.mullvadvpn.lib.ui.designsystem.ListHeader diff --git a/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/obfuscation/AppObfuscationRepository.kt b/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/obfuscation/AppObfuscationRepository.kt index e4a7abe68f..e146f5d551 100644 --- a/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/obfuscation/AppObfuscationRepository.kt +++ b/android/lib/feature/appicon/impl/src/main/java/net/mullvad/mullvadvpn/feature/appicon/impl/obfuscation/AppObfuscationRepository.kt @@ -10,11 +10,12 @@ import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED import android.content.pm.PackageManager.DONT_KILL_APP import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.ui.resource.R class AppObfuscationRepository( private val packageManager: PackageManager, - private val packageName: String, + private val self: PackageName, ) { private val _currentAppObfuscation = MutableStateFlow(getObfuscation()) val currentAppObfuscation: StateFlow<AppObfuscation> = _currentAppObfuscation @@ -54,7 +55,7 @@ class AppObfuscationRepository( else -> error("Unknown component enabled setting") } - private fun AppObfuscation.toComponentName() = ComponentName(packageName, className) + private fun AppObfuscation.toComponentName() = ComponentName(self.value, className) } /** diff --git a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt index 5414d35ebd..fbca756979 100644 --- a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt +++ b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoScreen.kt @@ -28,8 +28,8 @@ import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.appinfo.api.ChangelogNavKey import net.mullvad.mullvadvpn.lib.common.Lc -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.ExternalLinkListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.NavigationListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge diff --git a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt index d5a59a2d56..7c39a1d9f8 100644 --- a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt +++ b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/AppInfoViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.repository.AppVersionInfoRepository import net.mullvad.mullvadvpn.lib.ui.resource.R @@ -22,7 +23,7 @@ class AppInfoViewModel( private val resources: Resources, private val isPlayBuild: Boolean, private val isFdroidBuild: Boolean, - private val packageName: String, + private val self: PackageName, ) : ViewModel() { private val _uiSideEffect = Channel<AppInfoSideEffect>() @@ -41,7 +42,7 @@ class AppInfoViewModel( val sideEffect = if (isPlayBuild || isFdroidBuild) { AppInfoSideEffect.OpenUri( - uri = resources.getString(R.string.market_uri, packageName).toUri(), + uri = resources.getString(R.string.market_uri, self.value).toUri(), errorMessage = resources.getString(R.string.uri_market_app_not_found), ) } else { diff --git a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/changelog/ChangelogScreen.kt b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/changelog/ChangelogScreen.kt index c755ea86ca..7bf1a2619e 100644 --- a/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/changelog/ChangelogScreen.kt +++ b/android/lib/feature/appinfo/impl/src/main/java/net/mullvad/mullvadvpn/feature/appinfo/impl/changelog/ChangelogScreen.kt @@ -25,9 +25,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.appinfo.api.ChangelogNavKey -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.lib.ui.resource.R import net.mullvad.mullvadvpn.lib.ui.theme.AppTheme diff --git a/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt b/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt index 568e41ef2d..db9f9df50b 100644 --- a/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt +++ b/android/lib/feature/autoconnect/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/autoconnect/impl/AutoConnectAndLockdownModeScreen.kt @@ -62,8 +62,8 @@ import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.autoconnect.impl.PAGES.Companion.annotatedTopText import net.mullvad.mullvadvpn.lib.common.util.appendHideNavOnPlayBuild import net.mullvad.mullvadvpn.lib.common.util.openVpnSettings -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.toAnnotatedString import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryButton import net.mullvad.mullvadvpn.lib.ui.resource.R diff --git a/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlist/EditCustomListScreen.kt b/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlist/EditCustomListScreen.kt index cb81e94942..34caf67719 100644 --- a/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlist/EditCustomListScreen.kt +++ b/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlist/EditCustomListScreen.kt @@ -37,8 +37,8 @@ import net.mullvad.mullvadvpn.feature.customlist.api.EditCustomListNameNavKey import net.mullvad.mullvadvpn.feature.customlist.api.EditCustomListNavResult import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.EditCustomListListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.lib.ui.designsystem.Position diff --git a/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlocations/CustomListLocationsScreen.kt b/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlocations/CustomListLocationsScreen.kt index 089a81f84c..18d2c66156 100644 --- a/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlocations/CustomListLocationsScreen.kt +++ b/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/editlocations/CustomListLocationsScreen.kt @@ -39,8 +39,8 @@ import net.mullvad.mullvadvpn.feature.customlist.api.EditCustomListNavResult import net.mullvad.mullvadvpn.feature.customlist.impl.screen.lists.ContentType import net.mullvad.mullvadvpn.lib.common.Lce import net.mullvad.mullvadvpn.lib.model.RelayItem -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithSmallTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge @@ -239,7 +239,7 @@ private fun LazyListScope.content( @Composable private fun LocationsEmptyText(searchTerm: String) { Text( - text = stringResource(R.string.search_location_empty_text, searchTerm), + text = stringResource(R.string.search_no_matches_for_text, searchTerm), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/lists/CustomListsScreen.kt b/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/lists/CustomListsScreen.kt index 83a74c9bba..e8e770a4eb 100644 --- a/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/lists/CustomListsScreen.kt +++ b/android/lib/feature/customlist/impl/src/main/java/net/mullvad/mullvadvpn/feature/customlist/impl/screen/lists/CustomListsScreen.kt @@ -36,8 +36,8 @@ import net.mullvad.mullvadvpn.feature.customlist.api.EditCustomListNavKey import net.mullvad.mullvadvpn.feature.customlist.api.EditCustomListNavResult import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.communication.CustomListActionResultData -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.NavigationListItem import net.mullvad.mullvadvpn.lib.ui.component.positionForIndex import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge diff --git a/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt b/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt index fc5cb4cdc5..d698b26f09 100644 --- a/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt +++ b/android/lib/feature/daita/impl/src/main/java/net/mullvad/mullvadvpn/feature/daita/impl/DaitaScreen.kt @@ -40,9 +40,9 @@ import net.mullvad.mullvadvpn.feature.daita.api.DaitaDirectOnlyConfirmedNavResul import net.mullvad.mullvadvpn.feature.daita.api.DaitaDirectOnlyInfoNavKey import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.FeatureIndicator -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.SwitchListItem import net.mullvad.mullvadvpn.lib.ui.component.text.ScreenDescription import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge diff --git a/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/DeleteAccountScreen.kt b/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/DeleteAccountScreen.kt index d2ca63c9ca..9700b41b27 100644 --- a/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/DeleteAccountScreen.kt +++ b/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/DeleteAccountScreen.kt @@ -30,9 +30,9 @@ import androidx.lifecycle.compose.dropUnlessResumed import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.deleteaccount.api.DeleteAccountConfirmationNavKey import net.mullvad.mullvadvpn.lib.ui.component.CheckboxConfirmation -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.lib.ui.component.annotatedStringResource +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryButton import net.mullvad.mullvadvpn.lib.ui.resource.R import net.mullvad.mullvadvpn.lib.ui.theme.AppTheme diff --git a/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountcomplete/DeleteAccountCompleteScreen.kt b/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountcomplete/DeleteAccountCompleteScreen.kt index 65ecd72cdb..009c46e8bb 100644 --- a/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountcomplete/DeleteAccountCompleteScreen.kt +++ b/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountcomplete/DeleteAccountCompleteScreen.kt @@ -25,8 +25,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.login.api.LoginNavKey -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithSmallTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryButton import net.mullvad.mullvadvpn.lib.ui.resource.R import net.mullvad.mullvadvpn.lib.ui.theme.AppTheme diff --git a/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt b/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt index bbe93a88de..b8c9b91c21 100644 --- a/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt +++ b/android/lib/feature/deleteaccount/impl/src/main/java/net/mullvad/mullvadvpn/feature/deleteaccount/impl/deleteaccountconfirmation/DeleteAccountConfirmationScreen.kt @@ -62,9 +62,9 @@ import net.mullvad.mullvadvpn.common.compose.showSnackbarImmediately import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.deleteaccount.api.DeleteAccountCompleteNavKey import net.mullvad.mullvadvpn.lib.model.DeleteAccountError -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.lib.ui.component.annotatedStringResource +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.textfield.mullvadDarkTextFieldColors import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorSmall import net.mullvad.mullvadvpn.lib.ui.designsystem.NegativeButton diff --git a/android/lib/feature/filter/impl/src/main/java/net/mullvad/mullvadvpn/feature/filter/impl/FilterScreen.kt b/android/lib/feature/filter/impl/src/main/java/net/mullvad/mullvadvpn/feature/filter/impl/FilterScreen.kt index 5d6e99e6b1..bfcb600ea7 100644 --- a/android/lib/feature/filter/impl/src/main/java/net/mullvad/mullvadvpn/feature/filter/impl/FilterScreen.kt +++ b/android/lib/feature/filter/impl/src/main/java/net/mullvad/mullvadvpn/feature/filter/impl/FilterScreen.kt @@ -37,8 +37,8 @@ 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.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithSmallTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.CheckableListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.ExpandableListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.SelectableListItem diff --git a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt index 5972119456..eae192ccad 100644 --- a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt +++ b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModel.kt @@ -27,6 +27,7 @@ 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.DisconnectReason +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.model.PrepareError import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken @@ -61,7 +62,7 @@ class ConnectViewModel( private val resources: Resources, private val isPlayBuild: Boolean, private val isFdroidBuild: Boolean, - private val packageName: String, + private val self: PackageName, ) : ViewModel() { private val _uiSideEffect = Channel<UiSideEffect>() @@ -200,7 +201,7 @@ class ConnectViewModel( val sideEffect = if (isPlayBuild || isFdroidBuild) { UiSideEffect.OpenUri( - uri = resources.getString(R.string.market_uri, packageName).toUri(), + uri = resources.getString(R.string.market_uri, self.value).toUri(), errorMessage = resources.getString(R.string.uri_market_app_not_found), ) } else { diff --git a/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt b/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt index 732341cbbd..141d2e6695 100644 --- a/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt +++ b/android/lib/feature/home/impl/src/test/kotlin/net/mullvad/mullvadvpn/feature/home/impl/connect/ConnectViewModelTest.kt @@ -26,6 +26,7 @@ import net.mullvad.mullvadvpn.lib.model.DisconnectReason import net.mullvad.mullvadvpn.lib.model.ErrorState import net.mullvad.mullvadvpn.lib.model.GeoIpLocation import net.mullvad.mullvadvpn.lib.model.InAppNotification +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken @@ -126,7 +127,7 @@ class ConnectViewModelTest { resources = mockk(), isPlayBuild = false, isFdroidBuild = false, - packageName = "net.mullvad.mullvadvpn", + self = PackageName("net.mullvad.mullvadvpn"), ) } diff --git a/android/lib/feature/language/impl/src/main/java/net/mullvad/mullvadvpn/feature/language/impl/LanguageScreen.kt b/android/lib/feature/language/impl/src/main/java/net/mullvad/mullvadvpn/feature/language/impl/LanguageScreen.kt index 872a2500e6..f8334a986f 100644 --- a/android/lib/feature/language/impl/src/main/java/net/mullvad/mullvadvpn/feature/language/impl/LanguageScreen.kt +++ b/android/lib/feature/language/impl/src/main/java/net/mullvad/mullvadvpn/feature/language/impl/LanguageScreen.kt @@ -19,8 +19,8 @@ import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.toLc -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.SelectableListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.lib.ui.designsystem.Position diff --git a/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/RelayListContent.kt b/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/RelayListContent.kt index ed13c57fbd..53538d45df 100644 --- a/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/RelayListContent.kt +++ b/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/RelayListContent.kt @@ -106,7 +106,7 @@ fun LazyListScope.relayListContent( @Composable private fun LocationsEmptyText(searchTerm: String) { Text( - text = stringResource(R.string.search_location_empty_text, searchTerm), + text = stringResource(R.string.search_no_matches_for_text, searchTerm), style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/SelectLocationScreen.kt b/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/SelectLocationScreen.kt index 27c562f87b..0ec1e6f18b 100644 --- a/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/SelectLocationScreen.kt +++ b/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/SelectLocationScreen.kt @@ -104,6 +104,7 @@ import net.mullvad.mullvadvpn.lib.model.RelayListType import net.mullvad.mullvadvpn.lib.ui.component.MultihopSelector import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithSmallTopBar import net.mullvad.mullvadvpn.lib.ui.component.Singlehop +import net.mullvad.mullvadvpn.lib.ui.component.button.SearchButton import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.lib.ui.icon.DeleteHistory import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_MENU_BUTTON_TEST_TAG @@ -111,7 +112,6 @@ import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_SCREEN_TEST_TAG import net.mullvad.mullvadvpn.lib.ui.theme.AppTheme import net.mullvad.mullvadvpn.lib.ui.theme.Dimens import net.mullvad.mullvadvpn.lib.ui.theme.color.AlphaDisabled -import net.mullvad.mullvadvpn.lib.ui.theme.color.AlphaVisible import net.mullvad.mullvadvpn.lib.usecase.FilterChip import org.koin.androidx.compose.koinViewModel @@ -417,19 +417,10 @@ fun SelectLocationScreen( actions = { if (isTv()) { val isSearchButtonEnabled = state.contentOrNull()?.isSearchButtonEnabled == true - IconButton( + SearchButton( enabled = isSearchButtonEnabled, onClick = { state.contentOrNull()?.let { onSearchClick(it.relayListType) } }, - ) { - Icon( - imageVector = Icons.Rounded.Search, - contentDescription = stringResource(id = R.string.search), - tint = - MaterialTheme.colorScheme.onSurface.copy( - alpha = if (isSearchButtonEnabled) AlphaVisible else AlphaDisabled - ), - ) - } + ) } val filterButtonEnabled = state.contentOrNull()?.isFilterButtonEnabled == true val recentsCurrentlyEnabled = state.contentOrNull()?.isRecentsEnabled == true diff --git a/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/search/SearchLocationScreen.kt b/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/search/SearchLocationScreen.kt index 7e60f86d9b..28b6fcc6ab 100644 --- a/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/search/SearchLocationScreen.kt +++ b/android/lib/feature/location/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/location/impl/search/SearchLocationScreen.kt @@ -3,22 +3,13 @@ package net.mullvad.mullvadvpn.feature.location.impl.search import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -61,8 +52,8 @@ import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.lib.model.RelayItemId import net.mullvad.mullvadvpn.lib.model.RelayListType +import net.mullvad.mullvadvpn.lib.ui.component.MullvadSearchBar import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar -import net.mullvad.mullvadvpn.lib.ui.component.textfield.mullvadDarkTextFieldColors import net.mullvad.mullvadvpn.lib.ui.designsystem.ListHeader import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadSnackbar @@ -197,7 +188,6 @@ fun SearchLocation(relayListType: RelayListType, navigator: Navigator) { } @Suppress("LongMethod", "LongParameterList") -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchLocationScreen( state: Lce<Unit, SearchLocationUiState, Unit>, @@ -222,7 +212,7 @@ fun SearchLocationScreen( Column(modifier = Modifier.padding(it)) { val focusRequester = remember { FocusRequester() } LaunchedEffect(state is Lce.Content) { focusRequester.requestFocus() } - SearchBar( + MullvadSearchBar( modifier = Modifier.focusRequester(focusRequester), searchTerm = state.contentOrNull()?.searchTerm ?: "", enabled = state is Lce.Content, @@ -296,55 +286,6 @@ fun SearchLocationScreen( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SearchBar( - searchTerm: String, - enabled: Boolean, - onSearchInputChanged: (String) -> Unit, - hideKeyboard: () -> Unit, - onGoBack: () -> Unit, - modifier: Modifier = Modifier, -) { - SearchBarDefaults.InputField( - modifier = modifier.height(Dimens.searchFieldHeightExpanded).fillMaxWidth(), - query = searchTerm, - enabled = enabled, - onQueryChange = onSearchInputChanged, - onSearch = { hideKeyboard() }, - expanded = true, - onExpandedChange = {}, - leadingIcon = { - IconButton(onClick = onGoBack) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = stringResource(R.string.back), - ) - } - }, - trailingIcon = { - if (searchTerm.isNotEmpty()) { - IconButton(onClick = { onSearchInputChanged("") }) { - Icon( - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(R.string.clear_input), - ) - } - } - }, - placeholder = { Text(text = stringResource(id = R.string.search_placeholder)) }, - colors = - mullvadDarkTextFieldColors() - .copy( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - errorContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surface, - disabledLeadingIconColor = MaterialTheme.colorScheme.onSurface, - ), - ) -} - private fun LazyListScope.filterRow( filters: List<FilterChip>, onRemoveOwnershipFilter: () -> Unit, diff --git a/android/lib/feature/managedevices/impl/src/main/java/net/mullvad/mullvadvpn/feature/managedevices/impl/ManageDevicesScreen.kt b/android/lib/feature/managedevices/impl/src/main/java/net/mullvad/mullvadvpn/feature/managedevices/impl/ManageDevicesScreen.kt index a4d25c6925..20e04cec2a 100644 --- a/android/lib/feature/managedevices/impl/src/main/java/net/mullvad/mullvadvpn/feature/managedevices/impl/ManageDevicesScreen.kt +++ b/android/lib/feature/managedevices/impl/src/main/java/net/mullvad/mullvadvpn/feature/managedevices/impl/ManageDevicesScreen.kt @@ -34,8 +34,8 @@ import net.mullvad.mullvadvpn.lib.common.Lce import net.mullvad.mullvadvpn.lib.model.AccountNumber import net.mullvad.mullvadvpn.lib.model.Device import net.mullvad.mullvadvpn.lib.model.GetDeviceListError -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.DeviceListItem import net.mullvad.mullvadvpn.lib.ui.component.positionForIndex import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorMedium diff --git a/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt b/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt index 478eb23c11..2058dd324d 100644 --- a/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt +++ b/android/lib/feature/multihop/impl/src/main/java/net/mullvad/mullvadvpn/feature/multihop/impl/MultihopScreen.kt @@ -27,9 +27,9 @@ import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.FeatureIndicator -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.SwitchListItem import net.mullvad.mullvadvpn.lib.ui.component.text.ScreenDescription import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge diff --git a/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt b/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt index 999512b802..7758bc6c1d 100644 --- a/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt +++ b/android/lib/feature/notification/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/notification/impl/NotificationSettingsScreen.kt @@ -27,8 +27,8 @@ import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.util.openAppInfoNotificationSettings -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.SwitchListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryButton diff --git a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt index 898b134e1f..3bdb92d3ce 100644 --- a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt +++ b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/ReportProblemScreen.kt @@ -57,8 +57,8 @@ import net.mullvad.mullvadvpn.feature.problemreport.api.ViewLogsNavKey import net.mullvad.mullvadvpn.lib.common.util.appendHideNavOnPlayBuild import net.mullvad.mullvadvpn.lib.ui.component.CheckboxConfirmation import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevron -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.textfield.mullvadDarkTextFieldColors import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryButton diff --git a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/viewlogs/ViewLogsScreen.kt b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/viewlogs/ViewLogsScreen.kt index 2b46145f76..59b196f225 100644 --- a/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/viewlogs/ViewLogsScreen.kt +++ b/android/lib/feature/problemreport/impl/src/main/java/net/mullvad/mullvadvpn/feature/problemreport/impl/viewlogs/ViewLogsScreen.kt @@ -47,7 +47,7 @@ import net.mullvad.mullvadvpn.core.Navigator import net.mullvad.mullvadvpn.feature.problemreport.impl.provider.getLogsShareIntent import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.ui.component.MullvadMediumTopBar -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorMedium import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadSnackbar diff --git a/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt b/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt index f79027809d..e81ae089e3 100644 --- a/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt +++ b/android/lib/feature/serveripoverride/impl/src/main/java/net/mullvad/mullvadvpn/feature/serveripoverride/impl/ServerIpOverridesScreen.kt @@ -56,9 +56,9 @@ import net.mullvad.mullvadvpn.feature.serveripoverride.api.ServerIpOverrideNavKe import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.FeatureIndicator import net.mullvad.mullvadvpn.lib.model.SettingsPatchError -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.ServerIpOverridesListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadSnackbar import net.mullvad.mullvadvpn.lib.ui.designsystem.PrimaryButton diff --git a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt index dca81319f8..992562d841 100644 --- a/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt +++ b/android/lib/feature/settings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/settings/impl/SettingsScreen.kt @@ -41,8 +41,8 @@ import net.mullvad.mullvadvpn.feature.splittunneling.api.SplitTunnelingNavKey import net.mullvad.mullvadvpn.feature.vpnsettings.api.VpnSettingsNavKey import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.util.appendHideNavOnPlayBuild -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.ExternalLinkListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.NavigationListItem import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge diff --git a/android/lib/feature/splittunneling/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/api/SearchSplitTunnelingNavKey.kt b/android/lib/feature/splittunneling/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/api/SearchSplitTunnelingNavKey.kt new file mode 100644 index 0000000000..b491525cc4 --- /dev/null +++ b/android/lib/feature/splittunneling/api/src/main/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/api/SearchSplitTunnelingNavKey.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.feature.splittunneling.api + +import kotlinx.parcelize.Parcelize +import net.mullvad.mullvadvpn.core.NavKey2 + +@Parcelize data object SearchSplitTunnelingNavKey : NavKey2 diff --git a/android/lib/feature/splittunneling/impl/src/androidTest/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt b/android/lib/feature/splittunneling/impl/src/androidTest/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt index f33686f074..776305c756 100644 --- a/android/lib/feature/splittunneling/impl/src/androidTest/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt +++ b/android/lib/feature/splittunneling/impl/src/androidTest/kotlin/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt @@ -12,6 +12,7 @@ import io.mockk.verify import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.toLc +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.screen.test.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.screen.test.setContentWithTheme import org.junit.jupiter.api.AfterEach @@ -37,10 +38,11 @@ class SplitTunnelingScreenTest { state: Lc<Loading, SplitTunnelingUiState>, onEnableSplitTunneling: (Boolean) -> Unit = {}, onShowSystemAppsClick: (show: Boolean) -> Unit = {}, - onExcludeAppClick: (packageName: String) -> Unit = {}, - onIncludeAppClick: (packageName: String) -> Unit = {}, + onExcludeAppClick: (packageName: PackageName) -> Unit = {}, + onIncludeAppClick: (packageName: PackageName) -> Unit = {}, onBackClick: () -> Unit = {}, - onResolveIcon: (String) -> Drawable? = { null }, + onResolveIcon: (PackageName) -> Drawable? = { null }, + navigateToSearch: () -> Unit = {}, ) { setContentWithTheme { SplitTunnelingScreen( @@ -51,6 +53,7 @@ class SplitTunnelingScreenTest { onIncludeAppClick = onIncludeAppClick, onBackClick = onBackClick, onResolveIcon = onResolveIcon, + navigateToSearch = navigateToSearch, ) } } @@ -58,7 +61,7 @@ class SplitTunnelingScreenTest { @Test fun testLoadingState() = composeExtension.use { // Arrange - initScreen(state = Lc.Loading(Loading(enabled = true))) + initScreen(state = Lc.Loading(Loading())) // Assert onNodeWithText(TITLE).assertExists() @@ -129,7 +132,7 @@ class SplitTunnelingScreenTest { AppData(packageName = EXCLUDED_APP_PACKAGE_NAME, iconRes = 0, name = EXCLUDED_APP_NAME) val includedApp = AppData(packageName = INCLUDED_APP_PACKAGE_NAME, iconRes = 0, name = INCLUDED_APP_NAME) - val mockedClickHandler: (String) -> Unit = mockk(relaxed = true) + val mockedClickHandler: (PackageName) -> Unit = mockk(relaxed = true) initScreen( state = SplitTunnelingUiState( @@ -156,7 +159,7 @@ class SplitTunnelingScreenTest { AppData(packageName = EXCLUDED_APP_PACKAGE_NAME, iconRes = 0, name = EXCLUDED_APP_NAME) val includedApp = AppData(packageName = INCLUDED_APP_PACKAGE_NAME, iconRes = 0, name = INCLUDED_APP_NAME) - val mockedClickHandler: (String) -> Unit = mockk(relaxed = true) + val mockedClickHandler: (PackageName) -> Unit = mockk(relaxed = true) initScreen( state = SplitTunnelingUiState( @@ -204,9 +207,9 @@ class SplitTunnelingScreenTest { } companion object { - private const val EXCLUDED_APP_PACKAGE_NAME = "excluded-pkg" + private val EXCLUDED_APP_PACKAGE_NAME = PackageName("excluded-pkg") private const val EXCLUDED_APP_NAME = "Excluded Name" - private const val INCLUDED_APP_PACKAGE_NAME = "included-pkg" + private val INCLUDED_APP_PACKAGE_NAME = PackageName("included-pkg") private const val INCLUDED_APP_NAME = "Included Name" private const val TITLE = "Split tunneling" private const val DESCRIPTION = diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt index f647531047..62b87e60f6 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt @@ -40,14 +40,17 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.common.compose.unlessIsDetail import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.feature.splittunneling.api.SearchSplitTunnelingNavKey import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData import net.mullvad.mullvadvpn.feature.splittunneling.impl.extensions.hasValidSize import net.mullvad.mullvadvpn.feature.splittunneling.impl.extensions.isBelowMaxByteSize import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.model.FeatureIndicator -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.ui.component.ScaffoldWithMediumTopBar +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.SearchButton import net.mullvad.mullvadvpn.lib.ui.component.listitem.IconState import net.mullvad.mullvadvpn.lib.ui.component.listitem.SplitTunnelingListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.SwitchListItem @@ -76,6 +79,7 @@ private fun PreviewSplitTunnelingScreen( onExcludeAppClick = {}, onIncludeAppClick = {}, onBackClick = {}, + navigateToSearch = {}, onResolveIcon = { null }, ) } @@ -104,6 +108,7 @@ fun SharedTransitionScope.SplitTunneling( onExcludeAppClick = viewModel::onExcludeAppClick, onIncludeAppClick = viewModel::onIncludeAppClick, onBackClick = dropUnlessResumed { navigator.goBack() }, + navigateToSearch = dropUnlessResumed { navigator.navigate(SearchSplitTunnelingNavKey) }, onResolveIcon = { packageName -> packageManager.getApplicationIconOrNull(packageName) }, ) } @@ -113,10 +118,11 @@ fun SplitTunnelingScreen( state: Lc<Loading, SplitTunnelingUiState>, onEnableSplitTunneling: (Boolean) -> Unit, onShowSystemAppsClick: (show: Boolean) -> Unit, - onExcludeAppClick: (packageName: String) -> Unit, - onIncludeAppClick: (packageName: String) -> Unit, + onExcludeAppClick: (packageName: PackageName) -> Unit, + onIncludeAppClick: (packageName: PackageName) -> Unit, onBackClick: () -> Unit, - onResolveIcon: (String) -> Drawable?, + onResolveIcon: (PackageName) -> Drawable?, + navigateToSearch: () -> Unit, modifier: Modifier = Modifier, ) { val focusManager = LocalFocusManager.current @@ -131,6 +137,7 @@ fun SplitTunnelingScreen( unlessIsDetail { NavigateBackIconButton(onNavigateBack = onBackClick) } } }, + actions = { SearchButton(onClick = navigateToSearch, enabled = state.enabled()) }, ) { modifier, lazyListState: LazyListState -> LazyColumn( modifier = @@ -141,20 +148,25 @@ fun SplitTunnelingScreen( state = lazyListState, ) { description() - enabledToggle( - enabled = state.enabled(), - onEnableSplitTunneling = onEnableSplitTunneling, - ) when (state) { is Lc.Loading -> { spacer() loading() } is Lc.Content -> { + enabledToggle( + enabled = state.value.enabled, + onEnableSplitTunneling = onEnableSplitTunneling, + ) + item { HorizontalDivider(color = Color.Transparent) } + systemAppsToggle( + showSystemApps = state.value.showSystemApps, + onShowSystemAppsClick = onShowSystemAppsClick, + enabled = state.value.enabled, + ) appList( state = state.value, focusManager = focusManager, - onShowSystemAppsClick = onShowSystemAppsClick, onExcludeAppClick = onExcludeAppClick, onIncludeAppClick = onIncludeAppClick, onResolveIcon = onResolveIcon, @@ -174,6 +186,7 @@ private fun LazyListScope.enabledToggle( title = stringResource(id = R.string.enable), isToggled = enabled, onCellClicked = onEnableSplitTunneling, + position = Position.Top, ) } } @@ -200,10 +213,9 @@ private fun LazyListScope.loading() { private fun LazyListScope.appList( state: SplitTunnelingUiState, focusManager: FocusManager, - onShowSystemAppsClick: (show: Boolean) -> Unit, - onExcludeAppClick: (packageName: String) -> Unit, - onIncludeAppClick: (packageName: String) -> Unit, - onResolveIcon: (String) -> Drawable?, + onExcludeAppClick: (packageName: PackageName) -> Unit, + onIncludeAppClick: (packageName: PackageName) -> Unit, + onResolveIcon: (PackageName) -> Drawable?, ) { if (state.excludedApps.isNotEmpty()) { headerItem( @@ -221,11 +233,6 @@ private fun LazyListScope.appList( ) } spacer() - systemAppsToggle( - showSystemApps = state.showSystemApps, - onShowSystemAppsClick = onShowSystemAppsClick, - enabled = state.enabled, - ) headerItem( key = SplitTunnelingContentKey.INCLUDED_APPLICATIONS, textId = R.string.all_applications, @@ -242,17 +249,17 @@ private fun LazyListScope.appList( spacer() } -private fun LazyListScope.appItems( +internal fun LazyListScope.appItems( apps: List<AppData>, focusManager: FocusManager, - onAppClick: (String) -> Unit, - onResolveIcon: (String) -> Drawable?, + onAppClick: (PackageName) -> Unit, + onResolveIcon: (PackageName) -> Drawable?, enabled: Boolean, excluded: Boolean, ) { itemsIndexedWithDivider( items = apps, - key = { _, listItem -> listItem.packageName }, + key = { _, listItem -> listItem.packageName.value }, contentType = { _, _ -> ContentType.ITEM }, ) { index, listItem -> val packageName = listItem.packageName @@ -303,7 +310,7 @@ private fun LazyListScope.appItems( } } -private fun LazyListScope.headerItem(key: String, textId: Int, enabled: Boolean) { +internal fun LazyListScope.headerItem(key: String, textId: Int, enabled: Boolean) { itemWithDivider(key = key, contentType = ContentType.HEADER) { ListHeader( modifier = @@ -320,7 +327,7 @@ private fun LazyListScope.headerItem(key: String, textId: Int, enabled: Boolean) } } -private fun LazyListScope.systemAppsToggle( +internal fun LazyListScope.systemAppsToggle( showSystemApps: Boolean, onShowSystemAppsClick: (show: Boolean) -> Unit, enabled: Boolean, @@ -341,7 +348,7 @@ private fun LazyListScope.systemAppsToggle( } else { AlphaDisabled }, - position = Position.Single, + position = Position.Bottom, ) } } @@ -354,19 +361,19 @@ private fun LazyListScope.spacer() { private fun Lc<Loading, SplitTunnelingUiState>.isModal(): Boolean = when (this) { - is Lc.Loading -> this.value.isModal - is Lc.Content -> this.value.isModal + is Lc.Loading -> value.isModal + is Lc.Content -> value.isModal } private fun Lc<Loading, SplitTunnelingUiState>.enabled(): Boolean = when (this) { - is Lc.Loading -> this.value.enabled - is Lc.Content -> this.value.enabled + is Lc.Loading -> false + is Lc.Content -> value.enabled } -fun PackageManager.getApplicationIconOrNull(packageName: String): Drawable? = +fun PackageManager.getApplicationIconOrNull(packageName: PackageName): Drawable? = try { - getApplicationIcon(packageName) + getApplicationIcon(packageName.value) } catch (e: PackageManager.NameNotFoundException) { // Name not found is thrown if the application is not installed null diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt index 7bb091ea1c..7913fab030 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.feature.splittunneling.impl import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData -data class Loading(val enabled: Boolean = false, val isModal: Boolean = false) +data class Loading(val isModal: Boolean = false) data class SplitTunnelingUiState( val enabled: Boolean = false, diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt index 94524e1909..88781a12b8 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.toLc +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.ui.resource.R class SplitTunnelingUiStatePreviewParameterProvider : @@ -24,16 +25,24 @@ class SplitTunnelingUiStatePreviewParameterProvider : showSystemApps = false, ) .toLc(), - Lc.Loading(Loading(enabled = true)), + Lc.Loading(Loading()), ) } private val excludedApps = listOf( - AppData(packageName = "my.package.a", name = "TitleA", iconRes = R.drawable.icon_android), - AppData(packageName = "my.package.b", name = "TitleB", iconRes = R.drawable.icon_android), AppData( - packageName = "my.package.c", + packageName = PackageName("my.package.a"), + name = "TitleA", + iconRes = R.drawable.icon_android, + ), + AppData( + packageName = PackageName("my.package.b"), + name = "TitleB", + iconRes = R.drawable.icon_android, + ), + AppData( + packageName = PackageName("my.package.c"), name = "TitleC (System app)", iconRes = R.drawable.icon_android, isSystemApp = true, @@ -41,9 +50,13 @@ private val excludedApps = ) private val includedApps = listOf( - AppData(packageName = "my.package.d", name = "TitleD", iconRes = R.drawable.icon_android), AppData( - packageName = "my.package.e", + packageName = PackageName("my.package.d"), + name = "TitleD", + iconRes = R.drawable.icon_android, + ), + AppData( + packageName = PackageName("my.package.e"), name = "TitleE (System app)", iconRes = R.drawable.icon_android, isSystemApp = true, diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt index 96c0971d58..4db3b65138 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt @@ -3,95 +3,66 @@ package net.mullvad.mullvadvpn.feature.splittunneling.impl import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData -import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider +import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.SplitTunnelingUseCase import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT -import net.mullvad.mullvadvpn.lib.common.toLc -import net.mullvad.mullvadvpn.lib.model.AppId +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.repository.SplitTunnelingRepository +import net.mullvad.mullvadvpn.lib.repository.UserPreferencesRepository class SplitTunnelingViewModel( isModal: Boolean, - private val appsProvider: ApplicationsProvider, private val splitTunnelingRepository: SplitTunnelingRepository, + private val userPreferencesRepository: UserPreferencesRepository, + splitTunnelingUseCase: SplitTunnelingUseCase, private val dispatcher: CoroutineDispatcher, ) : ViewModel() { - private val allApps = MutableStateFlow<List<AppData>?>(null) - private val showSystemApps = MutableStateFlow(false) - val uiState: StateFlow<Lc<Loading, SplitTunnelingUiState>> = combine( - splitTunnelingRepository.excludedApps, + splitTunnelingUseCase(), splitTunnelingRepository.splitTunnelingEnabled, - allApps, - showSystemApps, - ) { excludedApps, enabled, allApps, showSystemApps -> - if (allApps == null) { - return@combine Lc.Loading(Loading(enabled = enabled, isModal = isModal)) - } - - val (excludedApps, includedApps) = - allApps.partition { appData -> - if (enabled) { - excludedApps.contains(AppId(appData.packageName)) - } else { - false - } - } - - SplitTunnelingUiState( + userPreferencesRepository.showSystemAppsSplitTunneling(), + ) { splitApps, enabled, showSystemApps -> + Lc.Content( + SplitTunnelingUiState( enabled = enabled, - excludedApps = excludedApps, - includedApps = - if (showSystemApps) includedApps - else includedApps.filter { appData -> !appData.isSystemApp }, + excludedApps = splitApps.excludedApps, + includedApps = splitApps.includedApps, showSystemApps = showSystemApps, isModal = isModal, ) - .toLc() + ) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT), - Lc.Loading(Loading(enabled = false, isModal = isModal)), + Lc.Loading(Loading(isModal = isModal)), ) - init { - viewModelScope.launch(dispatcher) { fetchApps() } - } - fun onEnableSplitTunneling(isEnabled: Boolean) { viewModelScope.launch(dispatcher) { splitTunnelingRepository.enableSplitTunneling(isEnabled) } } - fun onIncludeAppClick(packageName: String) { - viewModelScope.launch(dispatcher) { - splitTunnelingRepository.includeApp(AppId(packageName)) - } + fun onIncludeAppClick(packageName: PackageName) { + viewModelScope.launch(dispatcher) { splitTunnelingRepository.includeApp(packageName) } } - fun onExcludeAppClick(packageName: String) { - viewModelScope.launch(dispatcher) { - splitTunnelingRepository.excludeApp(AppId(packageName)) - } + fun onExcludeAppClick(packageName: PackageName) { + viewModelScope.launch(dispatcher) { splitTunnelingRepository.excludeApp(packageName) } } fun onShowSystemAppsClick(show: Boolean) { - viewModelScope.launch(dispatcher) { showSystemApps.emit(show) } - } - - private suspend fun fetchApps() { - appsProvider.apps().let { appsList -> allApps.emit(appsList) } + viewModelScope.launch(dispatcher) { + userPreferencesRepository.setShowSystemAppsSplitTunneling(show) + } } } diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt index 9c199567bf..6fc59bab58 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.feature.splittunneling.impl.applist +import net.mullvad.mullvadvpn.lib.model.PackageName + data class AppData( - val packageName: String, + val packageName: PackageName, val iconRes: Int, val name: String, val isSystemApp: Boolean = false, diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt index c913a865c2..0421eab563 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt @@ -3,10 +3,11 @@ package net.mullvad.mullvadvpn.feature.splittunneling.impl.applist import android.Manifest import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import net.mullvad.mullvadvpn.lib.model.PackageName class ApplicationsProvider( private val packageManager: PackageManager, - private val thisPackageName: String, + private val self: PackageName, ) { private val applicationFilterPredicate: (ApplicationInfo) -> Boolean = { appInfo -> hasInternetPermission(appInfo.packageName) && !isSelfApplication(appInfo.packageName) @@ -21,7 +22,7 @@ class ApplicationsProvider( .filter(applicationFilterPredicate) .map { info -> AppData( - info.packageName, + PackageName(info.packageName), info.icon, info.loadLabel(packageManager).toString(), !isLaunchable(info.packageName), @@ -42,6 +43,6 @@ class ApplicationsProvider( } private fun isSelfApplication(packageName: String): Boolean { - return packageName == thisPackageName + return packageName == self.value } } diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/SplitTunnelingUseCase.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/SplitTunnelingUseCase.kt new file mode 100644 index 0000000000..ce4a2e2de1 --- /dev/null +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/SplitTunnelingUseCase.kt @@ -0,0 +1,44 @@ +package net.mullvad.mullvadvpn.feature.splittunneling.impl.applist + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import net.mullvad.mullvadvpn.lib.model.PackageName +import net.mullvad.mullvadvpn.lib.repository.SplitTunnelingRepository +import net.mullvad.mullvadvpn.lib.repository.UserPreferencesRepository + +class SplitTunnelingUseCase( + private val splitTunnelingRepository: SplitTunnelingRepository, + private val applicationsProvider: ApplicationsProvider, + private val preferencesRepository: UserPreferencesRepository, +) { + operator fun invoke(): Flow<SplitApps> = + combine( + flowOf(applicationsProvider.apps()), + splitTunnelingRepository.excludedApps, + splitTunnelingRepository.splitTunnelingEnabled, + preferencesRepository.showSystemAppsSplitTunneling(), + ) { allApps, exclusions, splitTunnelingEnabled, showSystemApps -> + val exclusions = if (splitTunnelingEnabled) exclusions else emptySet() + SplitApps( + allApps = + if (showSystemApps) allApps + else allApps.filter { !it.isSystemApp || it.packageName in exclusions }, + exclusions = exclusions, + ) + } +} + +data class SplitApps(private val allApps: List<AppData>, private val exclusions: Set<PackageName>) { + val includedApps: List<AppData> + val excludedApps: List<AppData> + + init { + allApps + .partition { appData -> exclusions.contains(appData.packageName) } + .also { (exclusions, inclusions) -> + includedApps = inclusions + excludedApps = exclusions + } + } +} diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SearchSplitTunnelingEntryProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SearchSplitTunnelingEntryProvider.kt new file mode 100644 index 0000000000..0d2016bc65 --- /dev/null +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SearchSplitTunnelingEntryProvider.kt @@ -0,0 +1,17 @@ +package net.mullvad.mullvadvpn.feature.splittunneling.impl.navigation + +import androidx.navigation3.runtime.EntryProviderScope +import net.mullvad.mullvadvpn.core.NavKey2 +import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.core.animation.slideInHorizontalTransition +import net.mullvad.mullvadvpn.core.scene.ListDetailSceneStrategy +import net.mullvad.mullvadvpn.feature.splittunneling.api.SearchSplitTunnelingNavKey +import net.mullvad.mullvadvpn.feature.splittunneling.impl.search.SearchSplitTunnelingScreen + +fun EntryProviderScope<NavKey2>.searchSplitTunnelingEntry(navigator: Navigator) { + entry<SearchSplitTunnelingNavKey>( + metadata = ListDetailSceneStrategy.detailPane() + slideInHorizontalTransition() + ) { _ -> + SearchSplitTunnelingScreen(navigator = navigator) + } +} diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt index f6594bc21e..ed5128ba70 100644 --- a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/navigation/SplitTunnelingEntryProvider.kt @@ -20,4 +20,5 @@ fun EntryProviderScope<NavKey2>.splitTunnelingEntry(navigator: Navigator) { animatedVisibilityScope = LocalNavAnimatedContentScope.current, ) } + searchSplitTunnelingEntry(navigator) } diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingScreen.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingScreen.kt new file mode 100644 index 0000000000..13c7cef23d --- /dev/null +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingScreen.kt @@ -0,0 +1,203 @@ +package net.mullvad.mullvadvpn.feature.splittunneling.impl.search + +import android.graphics.drawable.Drawable +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed +import net.mullvad.mullvadvpn.core.Navigator +import net.mullvad.mullvadvpn.feature.splittunneling.impl.CommonContentKey +import net.mullvad.mullvadvpn.feature.splittunneling.impl.ContentType +import net.mullvad.mullvadvpn.feature.splittunneling.impl.SplitTunnelingContentKey +import net.mullvad.mullvadvpn.feature.splittunneling.impl.appItems +import net.mullvad.mullvadvpn.feature.splittunneling.impl.getApplicationIconOrNull +import net.mullvad.mullvadvpn.feature.splittunneling.impl.headerItem +import net.mullvad.mullvadvpn.lib.common.Lc +import net.mullvad.mullvadvpn.lib.model.PackageName +import net.mullvad.mullvadvpn.lib.ui.component.MullvadSearchBar +import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorLarge +import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadSnackbar +import net.mullvad.mullvadvpn.lib.ui.resource.R +import net.mullvad.mullvadvpn.lib.ui.theme.Dimens +import net.mullvad.mullvadvpn.lib.ui.theme.color.AlphaScrollbar +import org.koin.androidx.compose.koinViewModel + +@Composable +fun SearchSplitTunnelingScreen(navigator: Navigator) { + val viewModel = koinViewModel<SearchSplitTunnelingViewModel>() + val state by viewModel.uiState.collectAsStateWithLifecycle() + + SearchSplitTunnelingScreen( + state = state, + onSearchInputChanged = viewModel::onSearchInputChanged, + onExcludeAppClick = viewModel::onExcludeAppClick, + onIncludeAppClick = viewModel::onIncludeAppClick, + onGoBack = dropUnlessResumed { navigator.goBack() }, + ) +} + +@Composable +fun SearchSplitTunnelingScreen( + state: Lc<Unit, SearchSplitTunnelingUiState>, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + onSearchInputChanged: (String) -> Unit, + onExcludeAppClick: (packageName: PackageName) -> Unit, + onIncludeAppClick: (packageName: PackageName) -> Unit, + onGoBack: () -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + Scaffold( + snackbarHost = { + SnackbarHost( + snackbarHostState, + snackbar = { snackbarData -> MullvadSnackbar(snackbarData = snackbarData) }, + ) + } + ) { + val focusManager = LocalFocusManager.current + Column(modifier = Modifier.fillMaxSize().padding(it).consumeWindowInsets(it).imePadding()) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(state is Lc.Content) { focusRequester.requestFocus() } + MullvadSearchBar( + modifier = Modifier.focusRequester(focusRequester), + searchTerm = state.contentOrNull()?.searchTerm ?: "", + enabled = state is Lc.Content, + onSearchInputChanged = onSearchInputChanged, + hideKeyboard = { keyboardController?.hide() }, + onGoBack = onGoBack, + ) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface) + val lazyListState = rememberLazyListState() + val context = LocalContext.current + val packageManager = remember(context) { context.packageManager } + LazyColumn( + modifier = + Modifier.fillMaxSize() + .padding(horizontal = Dimens.mediumPadding) + .background(color = MaterialTheme.colorScheme.surface) + .drawVerticalScrollbar( + lazyListState, + MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar), + ), + state = lazyListState, + ) { + when (state) { + is Lc.Loading -> { + spacer() + loading() + } + is Lc.Content -> { + appList( + state = state.value, + focusManager = focusManager, + onExcludeAppClick = onExcludeAppClick, + onIncludeAppClick = onIncludeAppClick, + onResolveIcon = { packageName -> + packageManager.getApplicationIconOrNull(packageName) + }, + ) + } + } + } + } + } +} + +private fun LazyListScope.loading() { + item(key = CommonContentKey.PROGRESS, contentType = ContentType.PROGRESS) { + MullvadCircularProgressIndicatorLarge() + } +} + +private fun LazyListScope.spacer() { + item(contentType = ContentType.SPACER) { + Spacer(modifier = Modifier.animateItem().height(Dimens.cellVerticalSpacing)) + } +} + +private fun LazyListScope.appList( + state: SearchSplitTunnelingUiState, + focusManager: FocusManager, + onExcludeAppClick: (packageName: PackageName) -> Unit, + onIncludeAppClick: (packageName: PackageName) -> Unit, + onResolveIcon: (PackageName) -> Drawable?, +) { + if (state.includedApps.isEmpty() && state.excludedApps.isEmpty()) { + item { NoAppsMatchingSearch(state.searchTerm) } + } + if (state.excludedApps.isNotEmpty()) { + headerItem( + key = SplitTunnelingContentKey.EXCLUDED_APPLICATIONS, + textId = R.string.exclude_applications, + enabled = true, + ) + appItems( + apps = state.excludedApps, + focusManager = focusManager, + onAppClick = onIncludeAppClick, + onResolveIcon = onResolveIcon, + enabled = true, + excluded = true, + ) + spacer() + } + if (state.includedApps.isNotEmpty()) { + headerItem( + key = SplitTunnelingContentKey.INCLUDED_APPLICATIONS, + textId = R.string.all_applications, + enabled = true, + ) + appItems( + apps = state.includedApps, + focusManager = focusManager, + onAppClick = onExcludeAppClick, + onResolveIcon = onResolveIcon, + enabled = true, + excluded = false, + ) + spacer() + } +} + +@Composable +private fun NoAppsMatchingSearch(searchTerm: String) { + Text( + text = stringResource(R.string.search_no_matches_for_text, searchTerm), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(Dimens.cellVerticalSpacing), + ) +} diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingUiState.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingUiState.kt new file mode 100644 index 0000000000..ba119f777b --- /dev/null +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingUiState.kt @@ -0,0 +1,9 @@ +package net.mullvad.mullvadvpn.feature.splittunneling.impl.search + +import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData + +data class SearchSplitTunnelingUiState( + val searchTerm: String, + val excludedApps: List<AppData> = emptyList(), + val includedApps: List<AppData> = emptyList(), +) diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingViewModel.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingViewModel.kt new file mode 100644 index 0000000000..112724fdb9 --- /dev/null +++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/search/SearchSplitTunnelingViewModel.kt @@ -0,0 +1,63 @@ +package net.mullvad.mullvadvpn.feature.splittunneling.impl.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.SplitTunnelingUseCase +import net.mullvad.mullvadvpn.lib.common.Lc +import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT +import net.mullvad.mullvadvpn.lib.model.PackageName +import net.mullvad.mullvadvpn.lib.repository.SplitTunnelingRepository + +class SearchSplitTunnelingViewModel( + splitTunnelingUseCase: SplitTunnelingUseCase, + private val splitTunnelingRepository: SplitTunnelingRepository, + private val dispatcher: CoroutineDispatcher, +) : ViewModel() { + private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM) + + val uiState: StateFlow<Lc<Unit, SearchSplitTunnelingUiState>> = + combine(splitTunnelingUseCase(), _searchTerm) { splitApps, searchTerm -> + Lc.Content( + SearchSplitTunnelingUiState( + searchTerm = searchTerm, + excludedApps = + splitApps.excludedApps.filter { + it.name.contains(searchTerm, ignoreCase = true) + }, + includedApps = + splitApps.includedApps.filter { + it.name.contains(searchTerm, ignoreCase = true) + }, + ) + ) + } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT), + Lc.Loading(Unit), + ) + + fun onSearchInputChanged(searchTerm: String) { + viewModelScope.launch { _searchTerm.emit(searchTerm) } + } + + fun onIncludeAppClick(packageName: PackageName) { + viewModelScope.launch(dispatcher) { splitTunnelingRepository.includeApp(packageName) } + } + + fun onExcludeAppClick(packageName: PackageName) { + viewModelScope.launch(dispatcher) { splitTunnelingRepository.excludeApp(packageName) } + } + + companion object { + private const val EMPTY_SEARCH_TERM = "" + } +} diff --git a/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt index 09ddf5170e..8df0b133bf 100644 --- a/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt +++ b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt @@ -19,10 +19,12 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider +import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.SplitTunnelingUseCase import net.mullvad.mullvadvpn.lib.common.Lc import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule -import net.mullvad.mullvadvpn.lib.model.AppId +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.repository.SplitTunnelingRepository +import net.mullvad.mullvadvpn.lib.repository.UserPreferencesRepository import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -36,15 +38,19 @@ class SplitTunnelingViewModelTest { private val mockedApplicationsProvider = mockk<ApplicationsProvider>() private val mockedSplitTunnelingRepository = mockk<SplitTunnelingRepository>() + private val mockedUserPreferencesRepository = mockk<UserPreferencesRepository>() private lateinit var testSubject: SplitTunnelingViewModel - private val excludedApps: MutableStateFlow<Set<AppId>> = MutableStateFlow(emptySet()) + private val excludedApps: MutableStateFlow<Set<PackageName>> = MutableStateFlow(emptySet()) private val enabled: MutableStateFlow<Boolean> = MutableStateFlow(true) + private val showSystemApps: MutableStateFlow<Boolean> = MutableStateFlow(false) @BeforeEach fun setup() { every { mockedSplitTunnelingRepository.splitTunnelingEnabled } returns enabled every { mockedSplitTunnelingRepository.excludedApps } returns excludedApps + every { mockedUserPreferencesRepository.showSystemAppsSplitTunneling() } returns + showSystemApps } @AfterEach @@ -58,7 +64,7 @@ class SplitTunnelingViewModelTest { initTestSubject(emptyList()) val actualState: Lc<Loading, SplitTunnelingUiState> = testSubject.uiState.value - val initialExpectedState = Lc.Loading(Loading(enabled = false)) + val initialExpectedState = Lc.Loading(Loading()) assertIs<Lc.Loading<Loading>>(actualState) assertEquals(initialExpectedState, actualState) @@ -85,11 +91,11 @@ class SplitTunnelingViewModelTest { @Test 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") + val appExcluded = AppData(PackageName("test.excluded"), 0, "testName1") + val appNotExcluded = AppData(PackageName("test.not.excluded"), 0, "testName2") initTestSubject(listOf(appExcluded, appNotExcluded)) - excludedApps.value = setOf(AppId(appExcluded.packageName)) + excludedApps.value = setOf(appExcluded.packageName) val expectedState = SplitTunnelingUiState( @@ -108,10 +114,10 @@ class SplitTunnelingViewModelTest { @Test fun `include app should work`() = runTest { - val app = AppData("test", 0, "testName") + val app = AppData(PackageName("test"), 0, "testName") initTestSubject(listOf(app)) - excludedApps.value = setOf(AppId(app.packageName)) + excludedApps.value = setOf(app.packageName) val expectedStateBeforeAction = SplitTunnelingUiState( @@ -127,8 +133,7 @@ class SplitTunnelingViewModelTest { includedApps = listOf(app), showSystemApps = false, ) - coEvery { mockedSplitTunnelingRepository.includeApp(AppId(app.packageName)) } returns - Unit.right() + coEvery { mockedSplitTunnelingRepository.includeApp(app.packageName) } returns Unit.right() testSubject.uiState.test { val beforeAction = awaitItem() @@ -140,13 +145,13 @@ class SplitTunnelingViewModelTest { assertIs<Lc.Content<SplitTunnelingUiState>>(afterAction) assertEquals(expectedStateAfterAction, afterAction.value) - coVerify { mockedSplitTunnelingRepository.includeApp(AppId(app.packageName)) } + coVerify { mockedSplitTunnelingRepository.includeApp(app.packageName) } } } @Test fun `onExcludeApp should result in new uiState with app excluded`() = runTest { - val app = AppData("test", 0, "testName") + val app = AppData(PackageName("test"), 0, "testName") initTestSubject(listOf(app)) @@ -166,20 +171,19 @@ class SplitTunnelingViewModelTest { showSystemApps = false, ) - coEvery { mockedSplitTunnelingRepository.excludeApp(AppId(app.packageName)) } returns - Unit.right() + coEvery { mockedSplitTunnelingRepository.excludeApp(app.packageName) } returns Unit.right() testSubject.uiState.test { val beforeAction = awaitItem() assertIs<Lc.Content<SplitTunnelingUiState>>(beforeAction) assertEquals(expectedStateBeforeAction, beforeAction.value) testSubject.onExcludeAppClick(app.packageName) - excludedApps.value = setOf(AppId(app.packageName)) + excludedApps.value = setOf(app.packageName) val afterAction = awaitItem() assertIs<Lc.Content<SplitTunnelingUiState>>(afterAction) assertEquals(expectedStateAfterAction, afterAction.value) - coVerify { mockedSplitTunnelingRepository.excludeApp(AppId(app.packageName)) } + coVerify { mockedSplitTunnelingRepository.excludeApp(app.packageName) } } } @@ -202,8 +206,13 @@ class SplitTunnelingViewModelTest { testSubject = SplitTunnelingViewModel( isModal = false, - mockedApplicationsProvider, mockedSplitTunnelingRepository, + mockedUserPreferencesRepository, + SplitTunnelingUseCase( + mockedSplitTunnelingRepository, + mockedApplicationsProvider, + mockedUserPreferencesRepository, + ), UnconfinedTestDispatcher(), ) } diff --git a/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt index 2ffbb8f34a..ebe071994c 100644 --- a/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt +++ b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt @@ -10,13 +10,15 @@ import io.mockk.unmockkAll import io.mockk.verifyAll import kotlin.test.assertEquals import net.mullvad.mullvadvpn.lib.common.test.assertLists +import net.mullvad.mullvadvpn.lib.model.PackageName import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test class ApplicationsProviderTest { private val mockedPackageManager = mockk<PackageManager>() - private val selfPackageName = "self_package_name" - private val testSubject = ApplicationsProvider(mockedPackageManager, selfPackageName) + private val self = PackageName("self_package_name") + private val testSubject = ApplicationsProvider(mockedPackageManager, self) + private val internet = Manifest.permission.INTERNET @AfterEach @@ -27,50 +29,33 @@ class ApplicationsProviderTest { @SuppressLint("UseCheckPermission") @Test fun `fetch all apps should work`() { - val launchWithInternetPackageName = "launch_with_internet_package_name" - val launchWithoutInternetPackageName = "launch_without_internet_package_name" - val nonLaunchWithInternetPackageName = "non_launch_with_internet_package_name" - val nonLaunchWithoutInternetPackageName = "non_launch_without_internet_package_name" - val leanbackLaunchWithInternetPackageName = "leanback_launch_with_internet_package_name" - val leanbackLaunchWithoutInternetPackageName = - "leanback_launch_without_internet_package_name" + val launchWithInternet = PackageName("launch_with_internet_package_name") + val launchWithoutInternet = PackageName("launch_without_internet_package_name") + val nonLaunchWithInternet = PackageName("non_launch_with_internet_package_name") + val nonLaunchWithoutInternet = PackageName("non_launch_without_internet_package_name") + val leanbackLaunchWithInternet = PackageName("leanback_launch_with_internet_package_name") + val leanbackLaunchWithoutInternet = + PackageName("leanback_launch_without_internet_package_name") every { mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA) } returns listOf( - createApplicationInfo( - launchWithInternetPackageName, - launch = true, - internet = true, - ), - createApplicationInfo(launchWithoutInternetPackageName, launch = true), - createApplicationInfo(nonLaunchWithInternetPackageName, internet = true), - createApplicationInfo(nonLaunchWithoutInternetPackageName), - createApplicationInfo( - leanbackLaunchWithInternetPackageName, - leanback = true, - internet = true, - ), - createApplicationInfo(leanbackLaunchWithoutInternetPackageName, leanback = true), - createApplicationInfo(selfPackageName, internet = true, launch = true), + createApplicationInfo(launchWithInternet, launch = true, internet = true), + createApplicationInfo(launchWithoutInternet, launch = true), + createApplicationInfo(nonLaunchWithInternet, internet = true), + createApplicationInfo(nonLaunchWithoutInternet), + createApplicationInfo(leanbackLaunchWithInternet, leanback = true, internet = true), + createApplicationInfo(leanbackLaunchWithoutInternet, leanback = true), + createApplicationInfo(self, internet = true, launch = true), ) val result = testSubject.apps() val expected = listOf( - AppData(launchWithInternetPackageName, 0, launchWithInternetPackageName), - AppData( - nonLaunchWithInternetPackageName, - 0, - nonLaunchWithInternetPackageName, - true, - ), - AppData( - leanbackLaunchWithInternetPackageName, - 0, - leanbackLaunchWithInternetPackageName, - ), + AppData(launchWithInternet, 0, launchWithInternet.value), + AppData(nonLaunchWithInternet, 0, nonLaunchWithInternet.value, true), + AppData(leanbackLaunchWithInternet, 0, leanbackLaunchWithInternet.value), ) assertLists(expected, result) @@ -80,51 +65,46 @@ class ApplicationsProviderTest { // Ensure checkPermission was invoked on all packages listOf( - launchWithInternetPackageName, - launchWithoutInternetPackageName, - nonLaunchWithInternetPackageName, - nonLaunchWithoutInternetPackageName, - leanbackLaunchWithInternetPackageName, - leanbackLaunchWithoutInternetPackageName, - selfPackageName, + launchWithInternet, + launchWithoutInternet, + nonLaunchWithInternet, + nonLaunchWithoutInternet, + leanbackLaunchWithInternet, + leanbackLaunchWithoutInternet, + self, ) .forEach { packageName -> - mockedPackageManager.checkPermission(internet, packageName) + mockedPackageManager.checkPermission(internet, packageName.value) } - listOf( - launchWithInternetPackageName, - nonLaunchWithInternetPackageName, - leanbackLaunchWithInternetPackageName, - ) - .forEach { packageName -> - mockedPackageManager.getLaunchIntentForPackage(packageName) - } + listOf(launchWithInternet, nonLaunchWithInternet, leanbackLaunchWithInternet).forEach { + packageName -> + mockedPackageManager.getLaunchIntentForPackage(packageName.value) + } - listOf(nonLaunchWithInternetPackageName, leanbackLaunchWithInternetPackageName) - .forEach { packageName -> - mockedPackageManager.getLeanbackLaunchIntentForPackage(packageName) - } + listOf(nonLaunchWithInternet, leanbackLaunchWithInternet).forEach { packageName -> + mockedPackageManager.getLeanbackLaunchIntentForPackage(packageName.value) + } } } @SuppressLint("UseCheckPermission") @Test fun `apps should be returned in descending order`() { - val packageNames = listOf("b", "d", "c", "a", "e") + val packageNames = listOf("b", "d", "c", "a", "e").map { PackageName(it) } every { mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA) } returns packageNames.map { createApplicationInfo(it, launch = true, internet = true) } val actual = testSubject.apps() - val expected = packageNames.sorted().map { AppData(it, 0, it) } + val expected = packageNames.sortedBy { it.value }.map { AppData(it, 0, it.value) } assertEquals(expected, actual) } private fun createApplicationInfo( - packageName: String, + packageName: PackageName, launch: Boolean = false, leanback: Boolean = false, internet: Boolean = false, @@ -132,19 +112,19 @@ class ApplicationsProviderTest { ): ApplicationInfo { val mockApplicationInfo = mockk<ApplicationInfo>() - mockApplicationInfo.packageName = packageName + mockApplicationInfo.packageName = packageName.value mockApplicationInfo.icon = 0 - every { mockApplicationInfo.loadLabel(mockedPackageManager) } returns packageName + every { mockApplicationInfo.loadLabel(mockedPackageManager) } returns packageName.value - every { mockedPackageManager.getLaunchIntentForPackage(packageName) } returns + every { mockedPackageManager.getLaunchIntentForPackage(packageName.value) } returns if (launch || systemApp) mockk() else null - every { mockedPackageManager.getLeanbackLaunchIntentForPackage(packageName) } returns + every { mockedPackageManager.getLeanbackLaunchIntentForPackage(packageName.value) } returns if (leanback || systemApp) mockk() else null every { - mockedPackageManager.checkPermission(Manifest.permission.INTERNET, packageName) + mockedPackageManager.checkPermission(Manifest.permission.INTERNET, packageName.value) } returns if (internet) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED diff --git a/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt b/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt index dec9b9e077..5a22c15410 100644 --- a/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt +++ b/android/lib/feature/vpnsettings/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/vpnsettings/impl/VpnSettingsScreen.kt @@ -81,9 +81,9 @@ import net.mullvad.mullvadvpn.lib.model.IpVersion import net.mullvad.mullvadvpn.lib.model.Mtu import net.mullvad.mullvadvpn.lib.ui.component.DividerButton import net.mullvad.mullvadvpn.lib.ui.component.MullvadMediumTopBar -import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton -import net.mullvad.mullvadvpn.lib.ui.component.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.SPACE_CHAR +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateBackIconButton +import net.mullvad.mullvadvpn.lib.ui.component.button.NavigateCloseIconButton import net.mullvad.mullvadvpn.lib.ui.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.lib.ui.component.listitem.DnsListItem import net.mullvad.mullvadvpn.lib.ui.component.listitem.ExpandableListItem diff --git a/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/ManagementService.kt b/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/ManagementService.kt index 05266556f7..11d106db76 100644 --- a/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/ManagementService.kt +++ b/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/ManagementService.kt @@ -54,7 +54,6 @@ import net.mullvad.mullvadvpn.lib.model.AddSplitTunnelingAppError import net.mullvad.mullvadvpn.lib.model.ApiAccessMethod import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodSetting -import net.mullvad.mullvadvpn.lib.model.AppId import net.mullvad.mullvadvpn.lib.model.AppVersionInfo as ModelAppVersionInfo import net.mullvad.mullvadvpn.lib.model.ClearAccountHistoryError import net.mullvad.mullvadvpn.lib.model.ClearAllOverridesError @@ -93,6 +92,7 @@ import net.mullvad.mullvadvpn.lib.model.NewAccessMethodSetting import net.mullvad.mullvadvpn.lib.model.ObfuscationMode import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings import net.mullvad.mullvadvpn.lib.model.Ownership as ModelOwnership +import net.mullvad.mullvadvpn.lib.model.PackageName import net.mullvad.mullvadvpn.lib.model.PlayExternalObfuscatedAccountId import net.mullvad.mullvadvpn.lib.model.PlayPurchase import net.mullvad.mullvadvpn.lib.model.PlayPurchaseInitError @@ -808,13 +808,15 @@ class ManagementService( .mapLeft { PlayPurchaseVerifyError.OtherError } .mapEmpty() - suspend fun addSplitTunnelingApp(app: AppId): Either<AddSplitTunnelingAppError, Unit> = + suspend fun addSplitTunnelingApp(app: PackageName): Either<AddSplitTunnelingAppError, Unit> = Either.catch { grpc.addSplitTunnelApp(StringValue.of(app.value)) } .onLeft { Logger.e("Add split tunneling app error") } .mapLeft(AddSplitTunnelingAppError::Unknown) .mapEmpty() - suspend fun removeSplitTunnelingApp(app: AppId): Either<RemoveSplitTunnelingAppError, Unit> = + suspend fun removeSplitTunnelingApp( + app: PackageName + ): Either<RemoveSplitTunnelingAppError, Unit> = Either.catch { grpc.removeSplitTunnelApp(StringValue.of(app.value)) } .onLeft { Logger.e("Remove split tunneling app error") } .mapLeft(RemoveSplitTunnelingAppError::Unknown) diff --git a/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/mapper/ToDomain.kt b/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/mapper/ToDomain.kt index 96b9cab684..97a28d9605 100644 --- a/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/mapper/ToDomain.kt +++ b/android/lib/grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/grpc/mapper/ToDomain.kt @@ -23,7 +23,6 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethod import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodSetting -import net.mullvad.mullvadvpn.lib.model.AppId import net.mullvad.mullvadvpn.lib.model.AppVersionInfo import net.mullvad.mullvadvpn.lib.model.AuthFailedError import net.mullvad.mullvadvpn.lib.model.Cipher @@ -54,6 +53,7 @@ import net.mullvad.mullvadvpn.lib.model.ObfuscationMode 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.PackageName import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError import net.mullvad.mullvadvpn.lib.model.PlayExternalObfuscatedAccountId import net.mullvad.mullvadvpn.lib.model.Port @@ -667,7 +667,7 @@ internal fun ManagementInterface.VoucherSubmission.toDomain(): RedeemVoucherSucc internal fun ManagementInterface.SplitTunnelSettings.toDomain(): SplitTunnelSettings = SplitTunnelSettings( enabled = enableExclusions, - excludedApps = appsList.map { AppId(it) }.toSet(), + excludedApps = appsList.map { PackageName(it) }.toSet(), ) internal fun ManagementInterface.PlayExternalObfuscatedAccountId.toDomain(): 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 deleted file mode 100644 index 0663b530a1..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt +++ /dev/null @@ -1,3 +0,0 @@ -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/PackageName.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PackageName.kt new file mode 100644 index 0000000000..4541c0645a --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PackageName.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline value class PackageName(val value: String) 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 index a937d53bae..b74b79393d 100644 --- 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 @@ -1,3 +1,3 @@ package net.mullvad.mullvadvpn.lib.model -data class SplitTunnelSettings(val enabled: Boolean, val excludedApps: Set<AppId>) +data class SplitTunnelSettings(val enabled: Boolean, val excludedApps: Set<PackageName>) diff --git a/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/SplitTunnelingRepository.kt b/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/SplitTunnelingRepository.kt index 36442dff85..ed7cc8c47d 100644 --- a/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/SplitTunnelingRepository.kt +++ b/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/SplitTunnelingRepository.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import net.mullvad.mullvadvpn.lib.grpc.ManagementService -import net.mullvad.mullvadvpn.lib.model.AppId +import net.mullvad.mullvadvpn.lib.model.PackageName class SplitTunnelingRepository( private val managementService: ManagementService, @@ -26,7 +26,7 @@ class SplitTunnelingRepository( suspend fun enableSplitTunneling(enabled: Boolean) = managementService.setSplitTunnelingState(enabled) - suspend fun excludeApp(app: AppId) = managementService.addSplitTunnelingApp(app) + suspend fun excludeApp(app: PackageName) = managementService.addSplitTunnelingApp(app) - suspend fun includeApp(app: AppId) = managementService.removeSplitTunnelingApp(app) + suspend fun includeApp(app: PackageName) = managementService.removeSplitTunnelingApp(app) } diff --git a/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/UserPreferencesRepository.kt b/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/UserPreferencesRepository.kt index 0e851ea3bb..16866d88f8 100644 --- a/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/UserPreferencesRepository.kt +++ b/android/lib/repository/src/main/kotlin/net/mullvad/mullvadvpn/lib/repository/UserPreferencesRepository.kt @@ -64,4 +64,13 @@ class UserPreferencesRepository( userPreferencesStore.updateData { prefs -> prefs.toBuilder().setShowAndroid16ConnectWarning(show).build() } + + fun showSystemAppsSplitTunneling(): Flow<Boolean> = + userPreferencesStore.data.map { it.showSystemAppsSplitTunneling } + + suspend fun setShowSystemAppsSplitTunneling(show: Boolean) { + userPreferencesStore.updateData { prefs -> + prefs.toBuilder().setShowSystemAppsSplitTunneling(show).build() + } + } } diff --git a/android/lib/repository/src/main/proto/user_prefs.proto b/android/lib/repository/src/main/proto/user_prefs.proto index 36db3f2ad3..2d7f21fcd9 100644 --- a/android/lib/repository/src/main/proto/user_prefs.proto +++ b/android/lib/repository/src/main/proto/user_prefs.proto @@ -9,4 +9,5 @@ message UserPreferences { int64 account_expiry_unix_time_seconds = 3; bool show_android_16_connect_warning = 4; bool show_location_in_system_notification = 5; + bool show_system_apps_split_tunneling = 6; } diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/MullvadSearchBar.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/MullvadSearchBar.kt new file mode 100644 index 0000000000..bafe11a38c --- /dev/null +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/MullvadSearchBar.kt @@ -0,0 +1,67 @@ +package net.mullvad.mullvadvpn.lib.ui.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import net.mullvad.mullvadvpn.lib.ui.component.textfield.mullvadDarkTextFieldColors +import net.mullvad.mullvadvpn.lib.ui.theme.Dimens + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MullvadSearchBar( + searchTerm: String, + enabled: Boolean, + onSearchInputChanged: (String) -> Unit, + hideKeyboard: () -> Unit, + onGoBack: () -> Unit, + modifier: Modifier = Modifier, +) { + SearchBarDefaults.InputField( + modifier = modifier.height(Dimens.searchFieldHeightExpanded).fillMaxWidth(), + query = searchTerm, + enabled = enabled, + onQueryChange = onSearchInputChanged, + onSearch = { hideKeyboard() }, + expanded = true, + onExpandedChange = {}, + leadingIcon = { + IconButton(onClick = onGoBack) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.back), + ) + } + }, + trailingIcon = { + if (searchTerm.isNotEmpty()) { + IconButton(onClick = { onSearchInputChanged("") }) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.clear_input), + ) + } + } + }, + placeholder = { Text(text = stringResource(id = R.string.search_placeholder)) }, + colors = + mullvadDarkTextFieldColors() + .copy( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + errorContainerColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.surface, + disabledLeadingIconColor = MaterialTheme.colorScheme.onSurface, + ), + ) +} diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NavigateButton.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/NavigateBackButton.kt index 36c54281bb..d299bb57f9 100644 --- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NavigateButton.kt +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/NavigateBackButton.kt @@ -1,14 +1,13 @@ -package net.mullvad.mullvadvpn.lib.ui.component +package net.mullvad.mullvadvpn.lib.ui.component.button import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.ArrowDownward -import androidx.compose.material.icons.rounded.Close import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import net.mullvad.mullvadvpn.lib.ui.component.R @Composable fun NavigateBackIconButton( @@ -23,23 +22,3 @@ fun NavigateBackIconButton( ) } } - -@Composable -fun NavigateBackDownIconButton(onNavigateBack: () -> Unit) { - IconButton(onClick = onNavigateBack) { - Icon( - imageVector = Icons.Rounded.ArrowDownward, - contentDescription = stringResource(id = R.string.back), - ) - } -} - -@Composable -fun NavigateCloseIconButton(onNavigateClose: () -> Unit) { - IconButton(onClick = onNavigateClose) { - Icon( - imageVector = Icons.Rounded.Close, - contentDescription = stringResource(id = R.string.close), - ) - } -} diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/NavigateCloseButton.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/NavigateCloseButton.kt new file mode 100644 index 0000000000..39d372949b --- /dev/null +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/NavigateCloseButton.kt @@ -0,0 +1,19 @@ +package net.mullvad.mullvadvpn.lib.ui.component.button + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import net.mullvad.mullvadvpn.lib.ui.component.R + +@Composable +fun NavigateCloseIconButton(onNavigateClose: () -> Unit) { + IconButton(onClick = onNavigateClose) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(id = R.string.close), + ) + } +} diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/SearchButton.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/SearchButton.kt new file mode 100644 index 0000000000..bdfce90c72 --- /dev/null +++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/button/SearchButton.kt @@ -0,0 +1,30 @@ +package net.mullvad.mullvadvpn.lib.ui.component.button + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.mullvad.mullvadvpn.lib.ui.component.preview.PreviewColumn +import net.mullvad.mullvadvpn.lib.ui.resource.R + +@Preview +@Composable +private fun PreviewSearchButton() { + PreviewColumn { + SearchButton(onClick = {}) + SearchButton(onClick = {}, enabled = false) + } +} + +@Composable +fun SearchButton(onClick: () -> Unit, enabled: Boolean = true) { + IconButton(onClick = onClick, enabled = enabled) { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = stringResource(id = R.string.search), + ) + } +} diff --git a/android/lib/ui/resource/src/main/res/values-ar/strings.xml b/android/lib/ui/resource/src/main/res/values-ar/strings.xml index 92fcc979db..ae6c8515d5 100644 --- a/android/lib/ui/resource/src/main/res/values-ar/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-ar/strings.xml @@ -397,7 +397,7 @@ <string name="retry">إعادة المحاولة</string> <string name="save">حفظ</string> <string name="search">بحث</string> - <string name="search_location_empty_text">لم يتم العثور على نتائج للبحث عن \"%1$s\"، يُرجى تجربة بحث مختلف</string> + <string name="search_no_matches_for_text">لم يتم العثور على نتائج للبحث عن \"%1$s\"، يُرجى تجربة بحث مختلف</string> <string name="search_placeholder">ابحث عن...</string> <string name="select_location">حدِّد الموقع</string> <string name="send">إرسال</string> diff --git a/android/lib/ui/resource/src/main/res/values-da/strings.xml b/android/lib/ui/resource/src/main/res/values-da/strings.xml index 580f8336c0..368b68af32 100644 --- a/android/lib/ui/resource/src/main/res/values-da/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-da/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Prøv igen</string> <string name="save">Gem</string> <string name="search">Søg</string> - <string name="search_location_empty_text">Intet resultat for \"%1$s\". Prøv en anden søgning</string> + <string name="search_no_matches_for_text">Intet resultat for \"%1$s\". Prøv en anden søgning</string> <string name="search_placeholder">Søg efter...</string> <string name="select_location">Vælg placering</string> <string name="send">Send</string> diff --git a/android/lib/ui/resource/src/main/res/values-de/strings.xml b/android/lib/ui/resource/src/main/res/values-de/strings.xml index 33e2c155f0..0d7f019219 100644 --- a/android/lib/ui/resource/src/main/res/values-de/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-de/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Erneut versuchen</string> <string name="save">Speichern</string> <string name="search">Suche</string> - <string name="search_location_empty_text">Kein Ergebnis für „%1$s“, bitte versuchen Sie einen anderen Suchbegriff</string> + <string name="search_no_matches_for_text">Kein Ergebnis für „%1$s“, bitte versuchen Sie einen anderen Suchbegriff</string> <string name="search_placeholder">Suchen nach …</string> <string name="select_location">Ort auswählen</string> <string name="send">Senden</string> diff --git a/android/lib/ui/resource/src/main/res/values-es/strings.xml b/android/lib/ui/resource/src/main/res/values-es/strings.xml index c61cba908f..0b1fe3a96f 100644 --- a/android/lib/ui/resource/src/main/res/values-es/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-es/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Reintentar</string> <string name="save">Guardar</string> <string name="search">Buscar</string> - <string name="search_location_empty_text">No hay resultados para «%1$s», intente una búsqueda diferente</string> + <string name="search_no_matches_for_text">No hay resultados para «%1$s», intente una búsqueda diferente</string> <string name="search_placeholder">Buscar...</string> <string name="select_location">Seleccionar ubicación</string> <string name="send">Enviar</string> diff --git a/android/lib/ui/resource/src/main/res/values-fa/strings.xml b/android/lib/ui/resource/src/main/res/values-fa/strings.xml index bf9bc9a706..c3f9d97fe0 100644 --- a/android/lib/ui/resource/src/main/res/values-fa/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-fa/strings.xml @@ -397,7 +397,7 @@ <string name="retry">تلاش دوباره</string> <string name="save">ذخیره</string> <string name="search">جستجو</string> - <string name="search_location_empty_text">هیچ نتیجهای برای «%1$s» یافت نشد، لطفاً جستجوی دیگری انجام دهید</string> + <string name="search_no_matches_for_text">هیچ نتیجهای برای «%1$s» یافت نشد، لطفاً جستجوی دیگری انجام دهید</string> <string name="search_placeholder">جستوجو برای...</string> <string name="select_location">انتخاب مکان</string> <string name="send">ارسال</string> diff --git a/android/lib/ui/resource/src/main/res/values-fi/strings.xml b/android/lib/ui/resource/src/main/res/values-fi/strings.xml index 4d487b4453..4d7485644b 100644 --- a/android/lib/ui/resource/src/main/res/values-fi/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-fi/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Yritä uudelleen</string> <string name="save">Tallenna</string> <string name="search">Haku</string> - <string name="search_location_empty_text">Haulle \"%1$s\" ei löytynyt tuloksia. Kokeile toista hakua.</string> + <string name="search_no_matches_for_text">Haulle \"%1$s\" ei löytynyt tuloksia. Kokeile toista hakua.</string> <string name="search_placeholder">Hae...</string> <string name="select_location">Valitse sijainti</string> <string name="send">Lähetä</string> diff --git a/android/lib/ui/resource/src/main/res/values-fr/strings.xml b/android/lib/ui/resource/src/main/res/values-fr/strings.xml index b728e6d952..2490986f08 100644 --- a/android/lib/ui/resource/src/main/res/values-fr/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-fr/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Réessayer</string> <string name="save">Enregistrer</string> <string name="search">Recherche</string> - <string name="search_location_empty_text">Aucun résultat pour « %1$s ». Veuillez essayer une autre recherche</string> + <string name="search_no_matches_for_text">Aucun résultat pour « %1$s ». Veuillez essayer une autre recherche</string> <string name="search_placeholder">Rechercher...</string> <string name="select_location">Sélectionner une localisation</string> <string name="send">Envoyer</string> diff --git a/android/lib/ui/resource/src/main/res/values-it/strings.xml b/android/lib/ui/resource/src/main/res/values-it/strings.xml index 75f5caa769..aa0df0a0eb 100644 --- a/android/lib/ui/resource/src/main/res/values-it/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-it/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Riprova</string> <string name="save">Salva</string> <string name="search">Cerca</string> - <string name="search_location_empty_text">Nessun risultato per \"%1$s\", prova una ricerca diversa</string> + <string name="search_no_matches_for_text">Nessun risultato per \"%1$s\", prova una ricerca diversa</string> <string name="search_placeholder">Cerca...</string> <string name="select_location">Seleziona posizione</string> <string name="send">Invia</string> diff --git a/android/lib/ui/resource/src/main/res/values-ja/strings.xml b/android/lib/ui/resource/src/main/res/values-ja/strings.xml index 5e88cd394d..988aa51dec 100644 --- a/android/lib/ui/resource/src/main/res/values-ja/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-ja/strings.xml @@ -397,7 +397,7 @@ <string name="retry">再試行</string> <string name="save">保存</string> <string name="search">検索</string> - <string name="search_location_empty_text">「%1$s」の結果はありません。別の検索をお試しください。</string> + <string name="search_no_matches_for_text">「%1$s」の結果はありません。別の検索をお試しください。</string> <string name="search_placeholder">検索...</string> <string name="select_location">場所を選択する</string> <string name="send">送信</string> diff --git a/android/lib/ui/resource/src/main/res/values-ko/strings.xml b/android/lib/ui/resource/src/main/res/values-ko/strings.xml index 6b3737bb30..7e007cfc18 100644 --- a/android/lib/ui/resource/src/main/res/values-ko/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-ko/strings.xml @@ -397,7 +397,7 @@ <string name="retry">다시 시도</string> <string name="save">저장</string> <string name="search">검색</string> - <string name="search_location_empty_text">\"%1$s\" 검색 결과가 없습니다. 다른 검색어를 시도하세요</string> + <string name="search_no_matches_for_text">\"%1$s\" 검색 결과가 없습니다. 다른 검색어를 시도하세요</string> <string name="search_placeholder">검색...</string> <string name="select_location">위치 선택</string> <string name="send">전송</string> diff --git a/android/lib/ui/resource/src/main/res/values-my/strings.xml b/android/lib/ui/resource/src/main/res/values-my/strings.xml index e8e92bf08c..5a5a47f366 100644 --- a/android/lib/ui/resource/src/main/res/values-my/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-my/strings.xml @@ -397,7 +397,7 @@ <string name="retry">ပြန်ကြိုးစားကြည့်ရန်</string> <string name="save">သိမ်းမည်</string> <string name="search">ရှာဖွေမှု</string> - <string name="search_location_empty_text">\"%1$s\" အတွက် ရလဒ်မရှိပါ၊ အခြားရှာဖွေမှုတစ်ခုကို စမ်းလုပ်ကြည့်ပါ</string> + <string name="search_no_matches_for_text">\"%1$s\" အတွက် ရလဒ်မရှိပါ၊ အခြားရှာဖွေမှုတစ်ခုကို စမ်းလုပ်ကြည့်ပါ</string> <string name="search_placeholder">ရှာရန်...</string> <string name="select_location">တည်နေရာ ရွေးရန်</string> <string name="send">ပို့ရန်</string> diff --git a/android/lib/ui/resource/src/main/res/values-nb/strings.xml b/android/lib/ui/resource/src/main/res/values-nb/strings.xml index d892d8d15e..07417d0ffd 100644 --- a/android/lib/ui/resource/src/main/res/values-nb/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-nb/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Prøv på nytt</string> <string name="save">Lagre</string> <string name="search">Søk</string> - <string name="search_location_empty_text">Ingen resultater for «%1$s». Prøv et annet søk</string> + <string name="search_no_matches_for_text">Ingen resultater for «%1$s». Prøv et annet søk</string> <string name="search_placeholder">Søk etter ...</string> <string name="select_location">Velg plassering</string> <string name="send">Send</string> diff --git a/android/lib/ui/resource/src/main/res/values-nl/strings.xml b/android/lib/ui/resource/src/main/res/values-nl/strings.xml index 2df3aed49f..5e6af40ddd 100644 --- a/android/lib/ui/resource/src/main/res/values-nl/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-nl/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Opnieuw proberen</string> <string name="save">Opslaan</string> <string name="search">Zoeken</string> - <string name="search_location_empty_text">Geen resultaat voor \"%1$s\", probeer een andere zoekopdracht</string> + <string name="search_no_matches_for_text">Geen resultaat voor \"%1$s\", probeer een andere zoekopdracht</string> <string name="search_placeholder">Zoeken naar...</string> <string name="select_location">Locatie selecteren</string> <string name="send">Verzenden</string> diff --git a/android/lib/ui/resource/src/main/res/values-pl/strings.xml b/android/lib/ui/resource/src/main/res/values-pl/strings.xml index 724382395d..24b13337e0 100644 --- a/android/lib/ui/resource/src/main/res/values-pl/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-pl/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Ponów próbę</string> <string name="save">Zapisz</string> <string name="search">Wyszukaj</string> - <string name="search_location_empty_text">Brak wyników dla hasła „%1$s”, spróbuj innego wyszukiwania</string> + <string name="search_no_matches_for_text">Brak wyników dla hasła „%1$s”, spróbuj innego wyszukiwania</string> <string name="search_placeholder">Wyszukaj...</string> <string name="select_location">Wybierz lokalizację</string> <string name="send">Wyślij</string> diff --git a/android/lib/ui/resource/src/main/res/values-pt/strings.xml b/android/lib/ui/resource/src/main/res/values-pt/strings.xml index 6c13e9ad8d..d98dfb6704 100644 --- a/android/lib/ui/resource/src/main/res/values-pt/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-pt/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Tentar novamente</string> <string name="save">Guardar</string> <string name="search">Pesquisar</string> - <string name="search_location_empty_text">Sem resultados para \"%1$s\". Experimente uma pesquisa diferente</string> + <string name="search_no_matches_for_text">Sem resultados para \"%1$s\". Experimente uma pesquisa diferente</string> <string name="search_placeholder">Pesquisar por...</string> <string name="select_location">Selecionar localização</string> <string name="send">Enviar</string> diff --git a/android/lib/ui/resource/src/main/res/values-ru/strings.xml b/android/lib/ui/resource/src/main/res/values-ru/strings.xml index 4b5a2a5906..982fb4269d 100644 --- a/android/lib/ui/resource/src/main/res/values-ru/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-ru/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Повторить</string> <string name="save">Сохранить</string> <string name="search">Поиск</string> - <string name="search_location_empty_text">По запросу «%1$s» ничего не найдено — попробуйте другой запрос</string> + <string name="search_no_matches_for_text">По запросу «%1$s» ничего не найдено — попробуйте другой запрос</string> <string name="search_placeholder">Поиск...</string> <string name="select_location">Выбор местоположения</string> <string name="send">Отправить</string> diff --git a/android/lib/ui/resource/src/main/res/values-sv/strings.xml b/android/lib/ui/resource/src/main/res/values-sv/strings.xml index 169f4674fb..7ac9964506 100644 --- a/android/lib/ui/resource/src/main/res/values-sv/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-sv/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Försök igen</string> <string name="save">Spara</string> <string name="search">Sök</string> - <string name="search_location_empty_text">Inga resultat hittades för \"%1$s\", försök med en annan sökning</string> + <string name="search_no_matches_for_text">Inga resultat hittades för \"%1$s\", försök med en annan sökning</string> <string name="search_placeholder">Sök efter …</string> <string name="select_location">Välj plats</string> <string name="send">Skicka</string> diff --git a/android/lib/ui/resource/src/main/res/values-th/strings.xml b/android/lib/ui/resource/src/main/res/values-th/strings.xml index 129e20250d..c6265a168b 100644 --- a/android/lib/ui/resource/src/main/res/values-th/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-th/strings.xml @@ -397,7 +397,7 @@ <string name="retry">ลองอีกครั้ง</string> <string name="save">บันทึก</string> <string name="search">ค้นหา</string> - <string name="search_location_empty_text">ไม่พบผลลัพธ์สำหรับ \"%1$s\" โปรดลองใช้คำค้นหาอื่น</string> + <string name="search_no_matches_for_text">ไม่พบผลลัพธ์สำหรับ \"%1$s\" โปรดลองใช้คำค้นหาอื่น</string> <string name="search_placeholder">ค้นหา…</string> <string name="select_location">เลือกตำแหน่งที่ตั้ง</string> <string name="send">ส่ง</string> diff --git a/android/lib/ui/resource/src/main/res/values-tr/strings.xml b/android/lib/ui/resource/src/main/res/values-tr/strings.xml index 566c86860b..6cfc70f63e 100644 --- a/android/lib/ui/resource/src/main/res/values-tr/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-tr/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Tekrar dene</string> <string name="save">Kaydet</string> <string name="search">Ara</string> - <string name="search_location_empty_text">\"%1$s\" için sonuç yok, lütfen farklı bir arama yapın</string> + <string name="search_no_matches_for_text">\"%1$s\" için sonuç yok, lütfen farklı bir arama yapın</string> <string name="search_placeholder">Ara...</string> <string name="select_location">Konum seçin</string> <string name="send">Gönder</string> diff --git a/android/lib/ui/resource/src/main/res/values-uk/strings.xml b/android/lib/ui/resource/src/main/res/values-uk/strings.xml index 3b31f6271f..4852076682 100644 --- a/android/lib/ui/resource/src/main/res/values-uk/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-uk/strings.xml @@ -397,7 +397,7 @@ <string name="retry">Повторити</string> <string name="save">Зберегти</string> <string name="search">Пошук</string> - <string name="search_location_empty_text">За запитом «%1$s» нічого не знайдено, спробуйте інший запит</string> + <string name="search_no_matches_for_text">За запитом «%1$s» нічого не знайдено, спробуйте інший запит</string> <string name="search_placeholder">Пошук…</string> <string name="select_location">Вибір розташування</string> <string name="send">Надіслати</string> diff --git a/android/lib/ui/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/ui/resource/src/main/res/values-zh-rCN/strings.xml index 1205e4c7a3..3201f42b6b 100644 --- a/android/lib/ui/resource/src/main/res/values-zh-rCN/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-zh-rCN/strings.xml @@ -397,7 +397,7 @@ <string name="retry">重试</string> <string name="save">保存</string> <string name="search">搜索</string> - <string name="search_location_empty_text">“%1$s”无结果,请尝试其他搜索</string> + <string name="search_no_matches_for_text">“%1$s”无结果,请尝试其他搜索</string> <string name="search_placeholder">搜索…</string> <string name="select_location">选择位置</string> <string name="send">发送</string> diff --git a/android/lib/ui/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/ui/resource/src/main/res/values-zh-rTW/strings.xml index 73a5a11f8f..395cbd358a 100644 --- a/android/lib/ui/resource/src/main/res/values-zh-rTW/strings.xml +++ b/android/lib/ui/resource/src/main/res/values-zh-rTW/strings.xml @@ -397,7 +397,7 @@ <string name="retry">重試</string> <string name="save">儲存</string> <string name="search">搜尋</string> - <string name="search_location_empty_text">找不到「%1$s」的結果,請使改用其他搜尋條件。</string> + <string name="search_no_matches_for_text">找不到「%1$s」的結果,請使改用其他搜尋條件。</string> <string name="search_placeholder">搜尋…</string> <string name="select_location">選擇位置</string> <string name="send">傳送</string> diff --git a/android/lib/ui/resource/src/main/res/values/strings.xml b/android/lib/ui/resource/src/main/res/values/strings.xml index 82ab85978e..4d92f6c27b 100644 --- a/android/lib/ui/resource/src/main/res/values/strings.xml +++ b/android/lib/ui/resource/src/main/res/values/strings.xml @@ -209,7 +209,7 @@ <string name="on">On</string> <string name="wireguard_port_title">WireGuard port</string> <string name="search_placeholder">Search for...</string> - <string name="search_location_empty_text">No result for \"%s\", please try a different search</string> + <string name="search_no_matches_for_text">No result for \"%s\", please try a different search</string> <string name="wireguard_custon_port_title">Custom</string> <string name="port">Port</string> <string name="custom_port_dialog_submit">Set port</string> |
