From fbcd071c7b4b5c5138c600b1e919dd1c45cdaa1d Mon Sep 17 00:00:00 2001 From: davfsa Date: Thu, 13 Mar 2025 01:09:48 +0100 Subject: [PATCH 1/3] Add ability to select included/excluded apps from App Split Tunneling Updates tailscale/tailscale#14660 Signed-off-by: davfsa --- .../src/main/java/com/tailscale/ipn/App.kt | 44 ++++--- .../main/java/com/tailscale/ipn/IPNService.kt | 60 +++++++--- .../java/com/tailscale/ipn/ui/view/Buttons.kt | 24 ++++ .../ipn/ui/view/SplitTunnelAppPickerView.kt | 107 +++++++++++++++--- .../SplitTunnelAppPickerViewModel.kt | 43 +++++-- android/src/main/res/values/strings.xml | 7 ++ 6 files changed, 230 insertions(+), 55 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index a449bf0adb..0369b94857 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -377,7 +377,13 @@ open class UninitializedApp : Application() { // the VPN (i.e. we're logged in and machine is authorized). private const val ABLE_TO_START_VPN_KEY = "ableToStartVPN" - private const val DISALLOWED_APPS_KEY = "disallowedApps" + // The value is 'disallowedApps' as it used to represent + // only disallowed applications. This has been changed + // and allowing/disallowing is based on ALLOW_SELECTED_APPS_KEY + // + // The value is kept the same to not reset everyone's configuration + private const val SELECTED_APPS_KEY = "disallowedApps" + private const val ALLOW_SELECTED_APPS_KEY = "allowSelectedApps" // File for shared preferences that are not encrypted. private const val UNENCRYPTED_PREFERENCES = "unencrypted" @@ -535,7 +541,7 @@ open class UninitializedApp : Application() { return builder.build() } - fun addUserDisallowedPackageName(packageName: String) { + fun addUserSelectedPackage(packageName: String) { if (packageName.isEmpty()) { TSLog.e(TAG, "addUserDisallowedPackageName called with empty packageName") return @@ -544,13 +550,13 @@ open class UninitializedApp : Application() { getUnencryptedPrefs() .edit() .putStringSet( - DISALLOWED_APPS_KEY, disallowedPackageNames().toMutableSet().union(setOf(packageName))) + SELECTED_APPS_KEY, selectedPackageNames().toMutableSet().union(setOf(packageName))) .apply() this.restartVPN() } - fun removeUserDisallowedPackageName(packageName: String) { + fun removeUserSelectedPackage(packageName: String) { if (packageName.isEmpty()) { TSLog.e(TAG, "removeUserDisallowedPackageName called with empty packageName") return @@ -559,23 +565,29 @@ open class UninitializedApp : Application() { getUnencryptedPrefs() .edit() .putStringSet( - DISALLOWED_APPS_KEY, - disallowedPackageNames().toMutableSet().subtract(setOf(packageName))) + SELECTED_APPS_KEY, selectedPackageNames().toMutableSet().subtract(setOf(packageName))) .apply() this.restartVPN() } - fun disallowedPackageNames(): List { - val mdmDisallowed = - MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList() - if (mdmDisallowed.isNotEmpty()) { - TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed") - return builtInDisallowedPackageNames + mdmDisallowed - } - val userDisallowed = - getUnencryptedPrefs().getStringSet(DISALLOWED_APPS_KEY, emptySet())?.toList() ?: emptyList() - return builtInDisallowedPackageNames + userDisallowed + fun switchUserSelectedPackages() { + getUnencryptedPrefs() + .edit() + .putBoolean(ALLOW_SELECTED_APPS_KEY, !allowSelectedPackages()) + .apply() + getUnencryptedPrefs().edit().putStringSet(SELECTED_APPS_KEY, setOf()).apply() + + this.restartVPN() + } + + fun selectedPackageNames(): List { + return getUnencryptedPrefs().getStringSet(SELECTED_APPS_KEY, emptySet())?.toList() + ?: emptyList() + } + + fun allowSelectedPackages(): Boolean { + return getUnencryptedPrefs().getBoolean(ALLOW_SELECTED_APPS_KEY, false) } fun getAppScopedViewModel(): VpnViewModel { diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index 920d08d7d7..a941f93ef8 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -132,6 +132,14 @@ open class IPNService : VpnService(), libtailscale.IPNService { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } + private fun allowApp(b: Builder, name: String) { + try { + b.addAllowedApplication(name) + } catch (e: PackageManager.NameNotFoundException) { + TSLog.d(TAG, "Failed to add allowed application: $e") + } + } + private fun disallowApp(b: Builder, name: String) { try { b.addDisallowedApplication(name) @@ -151,23 +159,49 @@ open class IPNService : VpnService(), libtailscale.IPNService { } b.setUnderlyingNetworks(null) // Use all available networks. - val includedPackages: List = + val mdmAllowed = MDMSettings.includedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList() - if (includedPackages.isNotEmpty()) { - // If an admin defined a list of packages that are exclusively allowed to be used via - // Tailscale, - // then only allow those apps. - for (packageName in includedPackages) { + val mdmDisallowed = + MDMSettings.excludedPackages.flow.value.value?.split(",")?.map { it.trim() } ?: emptyList() + + var packagesList: List + var allowPackages: Boolean + if (mdmAllowed.isNotEmpty()) { + // An admin defined a list of packages that are exclusively allowed to be used via + // Tailscale, so only allow those. + packagesList = mdmAllowed + allowPackages = true + TSLog.d(TAG, "Included application packages were set via MDM: $mdmAllowed") + } else if (mdmDisallowed.isNotEmpty()) { + // An admin defined a list of packages that are excluded from accessing Tailscale, + // so ignore user definitions and only exclude those + packagesList = mdmDisallowed + allowPackages = false + TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed") + } else { + // Otherwise, prevent user manually disallowed apps from getting their traffic + DNS routed + // via Tailscale + packagesList = UninitializedApp.get().selectedPackageNames() + allowPackages = UninitializedApp.get().allowSelectedPackages() + TSLog.d(TAG, "Application packages were set by user: $packagesList") + } + + if (allowPackages) { + // There always needs to be at least one allowed application for the VPN service to filter the + // traffic so add our own application by default to fulfill that requirement + packagesList += "com.tailscale.ipn" + + for (packageName in packagesList) { TSLog.d(TAG, "Including app: $packageName") - b.addAllowedApplication(packageName) + allowApp(b, packageName) } } else { - // Otherwise, prevent certain apps from getting their traffic + DNS routed via Tailscale: - // - any app that the user manually disallowed in the GUI - // - any app that we disallowed via hard-coding - for (disallowedPackageName in UninitializedApp.get().disallowedPackageNames()) { - TSLog.d(TAG, "Disallowing app: $disallowedPackageName") - disallowApp(b, disallowedPackageName) + // Make sure to also exclude hard-coded apps that are known to cause issues + packagesList += UninitializedApp.get().builtInDisallowedPackageNames + + for (packageName in packagesList) { + TSLog.d(TAG, "Disallowing app: $packageName") + disallowApp(b, packageName) } } diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt b/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt index 5f7113f9d5..73e74bdeaa 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/Buttons.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import com.tailscale.ipn.ui.theme.link +import com.tailscale.ipn.ui.theme.secondaryButton +import com.tailscale.ipn.ui.theme.warningButton @Composable fun PrimaryActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) { @@ -26,6 +28,28 @@ fun PrimaryActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> content = content) } +@Composable +fun WarningActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) { + Button( + onClick = onClick, + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.fillMaxWidth(), + content = content, + colors = MaterialTheme.colorScheme.warningButton, + ) +} + +@Composable +fun DismissActionButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) { + Button( + onClick = onClick, + contentPadding = PaddingValues(vertical = 12.dp), + modifier = Modifier.fillMaxWidth(), + content = content, + colors = MaterialTheme.colorScheme.secondaryButton, + ) +} + @Composable fun OpenURLButton(title: String, url: String) { val handler = LocalUriHandler.current diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt index 27b18c5dac..f7033f2b4d 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SplitTunnelAppPickerView.kt @@ -4,12 +4,20 @@ package com.tailscale.ipn.ui.view import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -27,6 +35,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.tailscale.ipn.App import com.tailscale.ipn.R import com.tailscale.ipn.ui.util.Lists +import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.viewModel.SplitTunnelAppPickerViewModel @Composable @@ -35,23 +44,39 @@ fun SplitTunnelAppPickerView( model: SplitTunnelAppPickerViewModel = viewModel() ) { val installedApps by model.installedApps.collectAsState() - val excludedPackageNames by model.excludedPackageNames.collectAsState() + val selectedPackageNames by model.selectedPackageNames.collectAsState() + val allowSelected by model.allowSelected.collectAsState() val builtInDisallowedPackageNames: List = App.get().builtInDisallowedPackageNames val mdmIncludedPackages by model.mdmIncludedPackages.collectAsState() val mdmExcludedPackages by model.mdmExcludedPackages.collectAsState() + val showHeaderMenu by model.showHeaderMenu.collectAsState() + val showSwitchDialog by model.showSwitchDialog.collectAsState() - Scaffold(topBar = { Header(titleRes = R.string.split_tunneling, onBack = backToSettings) }) { - innerPadding -> - LazyColumn(modifier = Modifier.padding(innerPadding)) { - item(key = "header") { - ListItem( - headlineContent = { - Text( - stringResource( - R.string - .selected_apps_will_access_the_internet_directly_without_using_tailscale)) + if (showSwitchDialog) { + SwitchAlertDialog( + onConfirm = { + model.showSwitchDialog.set(false) + model.performSelectionSwitch() + }, + onDismiss = { model.showSwitchDialog.set(false) }) + } + + Scaffold( + topBar = { + Header( + titleRes = R.string.split_tunneling, + onBack = backToSettings, + actions = { + Row { + FusMenu(viewModel = model, onSwitchClick = { model.showSwitchDialog.set(true) }) + IconButton(onClick = { model.showHeaderMenu.set(!showHeaderMenu) }) { + Icon(Icons.Default.MoreVert, "menu") + } + } }) - } + }, + ) { innerPadding -> + LazyColumn(modifier = Modifier.padding(innerPadding)) { if (mdmExcludedPackages.value?.isNotEmpty() == true) { item("mdmExcludedNotice") { ListItem( @@ -67,9 +92,22 @@ fun SplitTunnelAppPickerView( }) } } else { + item("header") { + ListItem( + headlineContent = { + Text( + stringResource( + if (allowSelected) R.string.selected_apps_will_access_tailscale + else + R.string + .selected_apps_will_access_the_internet_directly_without_using_tailscale)) + }) + } item("resolversHeader") { Lists.SectionDivider( - stringResource(R.string.count_excluded_apps, excludedPackageNames.count())) + stringResource( + if (allowSelected) R.string.count_included_apps else R.string.count_excluded_apps, + selectedPackageNames.count())) } items(installedApps) { app -> ListItem( @@ -93,13 +131,13 @@ fun SplitTunnelAppPickerView( }, trailingContent = { Checkbox( - checked = excludedPackageNames.contains(app.packageName), + checked = selectedPackageNames.contains(app.packageName), enabled = !builtInDisallowedPackageNames.contains(app.packageName), onCheckedChange = { checked -> if (checked) { - model.exclude(packageName = app.packageName) + model.select(packageName = app.packageName) } else { - model.unexclude(packageName = app.packageName) + model.deselect(packageName = app.packageName) } }) }) @@ -109,3 +147,40 @@ fun SplitTunnelAppPickerView( } } } + +@Composable +fun FusMenu(viewModel: SplitTunnelAppPickerViewModel, onSwitchClick: (() -> Unit)) { + val expanded by viewModel.showHeaderMenu.collectAsState() + val allowSelected by viewModel.allowSelected.collectAsState() + + DropdownMenu( + expanded = expanded, + onDismissRequest = { viewModel.showHeaderMenu.set(false) }, + modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer)) { + MenuItem( + onClick = { + viewModel.showHeaderMenu.set(false) + onSwitchClick() + }, + text = + stringResource( + if (allowSelected) R.string.switch_to_select_to_exclude + else R.string.switch_to_select_to_include)) + } +} + +@Composable +fun SwitchAlertDialog(onConfirm: (() -> Unit), onDismiss: (() -> Unit)) { + AlertDialog( + title = { Text(text = stringResource(R.string.switch_warning_dialog_title)) }, + text = { Text(text = stringResource(R.string.switch_warning_dialog_description)) }, + onDismissRequest = onDismiss, + confirmButton = { + WarningActionButton(onClick = onConfirm) { + Text(text = stringResource(R.string.confirm_switch)) + } + }, + dismissButton = { + DismissActionButton(onClick = onDismiss) { Text(text = stringResource(R.string.cancel)) } + }) +} diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt index d00efb6759..89fe839286 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/SplitTunnelAppPickerViewModel.kt @@ -15,30 +15,53 @@ import kotlinx.coroutines.flow.StateFlow class SplitTunnelAppPickerViewModel : ViewModel() { val installedAppsManager = InstalledAppsManager(packageManager = App.get().packageManager) - val excludedPackageNames: StateFlow> = MutableStateFlow(listOf()) + val installedApps: StateFlow> = MutableStateFlow(listOf()) + val selectedPackageNames: StateFlow> = MutableStateFlow(listOf()) + + val allowSelected: StateFlow = MutableStateFlow(false) + val showHeaderMenu: StateFlow = MutableStateFlow(false) + val showSwitchDialog: StateFlow = MutableStateFlow(false) + val mdmExcludedPackages: StateFlow> = MDMSettings.excludedPackages.flow val mdmIncludedPackages: StateFlow> = MDMSettings.includedPackages.flow init { installedApps.set(installedAppsManager.fetchInstalledApps()) - excludedPackageNames.set( + initSelectedPackageNames() + } + + private fun initSelectedPackageNames() { + allowSelected.set(App.get().allowSelectedPackages()) + selectedPackageNames.set( App.get() - .disallowedPackageNames() + .selectedPackageNames() + .let { + if (!allowSelected.value) { + it.union(App.get().builtInDisallowedPackageNames) + } else { + it + } + } .intersect(installedApps.value.map { it.packageName }.toSet()) .toList()) } - fun exclude(packageName: String) { - if (excludedPackageNames.value.contains(packageName)) { + fun performSelectionSwitch() { + App.get().switchUserSelectedPackages() + initSelectedPackageNames() + } + + fun select(packageName: String) { + if (selectedPackageNames.value.contains(packageName)) { return } - excludedPackageNames.set(excludedPackageNames.value + packageName) - App.get().addUserDisallowedPackageName(packageName) + selectedPackageNames.set(selectedPackageNames.value + packageName) + App.get().addUserSelectedPackage(packageName) } - fun unexclude(packageName: String) { - excludedPackageNames.set(excludedPackageNames.value - packageName) - App.get().removeUserDisallowedPackageName(packageName) + fun deselect(packageName: String) { + selectedPackageNames.set(selectedPackageNames.value - packageName) + App.get().removeUserSelectedPackage(packageName) } } diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index c1c539ac0d..15bbad3391 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -282,9 +282,16 @@ App split tunneling Exclude certain apps from using Tailscale Apps selected here will access the Internet directly, without using Tailscale. + Only apps selected here will be allowed to access Tailscale. Excluded apps (%1$s) + Included apps (%1$s) Certain apps are not routed via Tailscale on this device. This setting is managed by your organization and cannot be changed by you. For more information, contact your network administrator. Only specific apps are routed via Tailscale on this device. This setting is managed by your organization and cannot be changed by you. For more information, contact your network administrator. + Switch to select included + Switch to select excluded + Selected apps will be reset + By switching filters, all your previously selected applications will be reset. Please ensure that this is intended. + Switch Specifies a list of apps that will be excluded from Tailscale routes and DNS even when Tailscale is running. All other apps will use Tailscale. Specifies a list of apps that will always use Tailscale routes and DNS when Tailscale is running. All other apps won\'t use Tailscale if this value is non-empty. Included packages From 7f4cccf018a1308d62cf1305cd3bf7535997050b Mon Sep 17 00:00:00 2001 From: davfsa Date: Tue, 1 Apr 2025 01:26:39 +0200 Subject: [PATCH 2/3] Update app split tunneling description string Signed-off-by: davfsa --- android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt | 2 +- android/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt index 52609f7932..bb50293201 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/SettingsView.kt @@ -93,7 +93,7 @@ fun SettingsView( Lists.ItemDivider() Setting.Text( R.string.split_tunneling, - subtitle = stringResource(R.string.exclude_certain_apps_from_using_tailscale), + subtitle = stringResource(R.string.filter_apps_allowed_to_access_tailscale), onClick = settingsNav.onNavigateToSplitTunneling ) diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 15bbad3391..0df332c3af 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -280,7 +280,7 @@ An unknown error occurred. Please try again. Request timed out. Make sure that \'%1$s\' is online. App split tunneling - Exclude certain apps from using Tailscale + Filter what apps are allowed to access Tailscale Apps selected here will access the Internet directly, without using Tailscale. Only apps selected here will be allowed to access Tailscale. Excluded apps (%1$s) From 6c255476debd5e8c5e964df17c2ddc34c126fbe3 Mon Sep 17 00:00:00 2001 From: davfsa Date: Mon, 21 Apr 2025 16:53:30 +0200 Subject: [PATCH 3/3] Address review comments Signed-off-by: davfsa --- android/src/main/java/com/tailscale/ipn/App.kt | 4 ++-- android/src/main/java/com/tailscale/ipn/IPNService.kt | 8 ++++---- .../com/tailscale/ipn/ui/util/InstalledAppsManager.kt | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index 0369b94857..db85569640 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -543,7 +543,7 @@ open class UninitializedApp : Application() { fun addUserSelectedPackage(packageName: String) { if (packageName.isEmpty()) { - TSLog.e(TAG, "addUserDisallowedPackageName called with empty packageName") + TSLog.e(TAG, "addUserSelectedPackage called with empty packageName") return } @@ -558,7 +558,7 @@ open class UninitializedApp : Application() { fun removeUserSelectedPackage(packageName: String) { if (packageName.isEmpty()) { - TSLog.e(TAG, "removeUserDisallowedPackageName called with empty packageName") + TSLog.e(TAG, "removeUserSelectedPackage called with empty packageName") return } diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index a941f93ef8..7d751299da 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -136,7 +136,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { try { b.addAllowedApplication(name) } catch (e: PackageManager.NameNotFoundException) { - TSLog.d(TAG, "Failed to add allowed application: $e") + TSLog.e(TAG, "Failed to add allowed application: $e") } } @@ -144,7 +144,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { try { b.addDisallowedApplication(name) } catch (e: PackageManager.NameNotFoundException) { - TSLog.d(TAG, "Failed to add disallowed application: $e") + TSLog.e(TAG, "Failed to add disallowed application: $e") } } @@ -179,7 +179,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { allowPackages = false TSLog.d(TAG, "Excluded application packages were set via MDM: $mdmDisallowed") } else { - // Otherwise, prevent user manually disallowed apps from getting their traffic + DNS routed + // Otherwise, prevent user manually disallowed apps from getting their traffic + DNS routed // via Tailscale packagesList = UninitializedApp.get().selectedPackageNames() allowPackages = UninitializedApp.get().allowSelectedPackages() @@ -189,7 +189,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { if (allowPackages) { // There always needs to be at least one allowed application for the VPN service to filter the // traffic so add our own application by default to fulfill that requirement - packagesList += "com.tailscale.ipn" + packagesList += BuildConfig.APPLICATION_ID for (packageName in packagesList) { TSLog.d(TAG, "Including app: $packageName") diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/InstalledAppsManager.kt b/android/src/main/java/com/tailscale/ipn/ui/util/InstalledAppsManager.kt index 8abb3b6220..f1bea2bbb5 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/InstalledAppsManager.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/InstalledAppsManager.kt @@ -6,6 +6,7 @@ package com.tailscale.ipn.ui.util import android.Manifest import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import com.tailscale.ipn.BuildConfig data class InstalledApp(val name: String, val packageName: String) @@ -26,7 +27,7 @@ class InstalledAppsManager( } private val appIsIncluded: (ApplicationInfo) -> Boolean = { app -> - app.packageName != "com.tailscale.ipn" && + app.packageName != BuildConfig.APPLICATION_ID && // Only show apps that can access the Internet packageManager.checkPermission(Manifest.permission.INTERNET, app.packageName) == PackageManager.PERMISSION_GRANTED