From 9c723903987c33bdf9659d5f5a2064d6fd7bc242 Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Fri, 20 Sep 2024 23:23:27 -0400 Subject: [PATCH] fix: split tunneling selection Minor improvements to split tunneling selection Add check to cleanup any uninstalled apps from included/excluded packages Closes #236 --- .../service/tunnel/WireGuardTunnel.kt | 1 - .../ui/screens/config/ConfigScreen.kt | 174 +-------------- .../ui/screens/config/ConfigViewModel.kt | 38 +++- .../components/ApplicationSelectionDialog.kt | 199 ++++++++++++++++++ 4 files changed, 245 insertions(+), 167 deletions(-) create mode 100644 app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/components/ApplicationSelectionDialog.kt diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt index faa6208..3aa191c 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/service/tunnel/WireGuardTunnel.kt @@ -8,7 +8,6 @@ import com.zaneschepke.wireguardautotunnel.data.repository.AppDataRepository import com.zaneschepke.wireguardautotunnel.module.ApplicationScope import com.zaneschepke.wireguardautotunnel.module.IoDispatcher import com.zaneschepke.wireguardautotunnel.module.Kernel -import com.zaneschepke.wireguardautotunnel.module.Userspace import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.AmneziaStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.WireGuardStatistics diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt index f981f6b..01a908e 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigScreen.kt @@ -1,36 +1,27 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.config import android.annotation.SuppressLint -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Android import androidx.compose.material.icons.rounded.ContentCopy import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Save -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -39,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -64,23 +54,19 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.accompanist.drawablepainter.DrawablePainter import com.zaneschepke.wireguardautotunnel.R -import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationToggle import com.zaneschepke.wireguardautotunnel.ui.common.prompt.AuthorizationPrompt import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController import com.zaneschepke.wireguardautotunnel.ui.common.text.SectionTitle +import com.zaneschepke.wireguardautotunnel.ui.screens.config.components.ApplicationSelectionDialog import com.zaneschepke.wireguardautotunnel.ui.screens.main.ConfigType import com.zaneschepke.wireguardautotunnel.util.Constants import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv import kotlinx.coroutines.delay @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") -@OptIn( - ExperimentalMaterial3Api::class, -) @Composable fun ConfigScreen(tunnelId: Int, viewModel: ConfigViewModel, focusRequester: FocusRequester) { val context = LocalContext.current @@ -106,6 +92,11 @@ fun ConfigScreen(tunnelId: Int, viewModel: ConfigViewModel, focusRequester: Focu } } + LaunchedEffect(Unit) { + delay(2_000L) + viewModel.cleanUpUninstalledApps() + } + val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) @@ -154,156 +145,9 @@ fun ConfigScreen(tunnelId: Int, viewModel: ConfigViewModel, focusRequester: Focu ) } - if (showApplicationsDialog) { - val sortedPackages = - remember(uiState.packages) { - uiState.packages.sortedBy { viewModel.getPackageLabel(it) } - } - BasicAlertDialog(onDismissRequest = { showApplicationsDialog = false }) { - Surface( - tonalElevation = 2.dp, - shadowElevation = 2.dp, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surface, - modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), - ) { - Column( - modifier = - Modifier - .fillMaxWidth(), - ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(stringResource(id = R.string.tunnel_all)) - Switch( - checked = uiState.isAllApplicationsEnabled, - onCheckedChange = { viewModel.onAllApplicationsChange(it) }, - ) - } - if (!uiState.isAllApplicationsEnabled) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(stringResource(id = R.string.include)) - Checkbox( - checked = uiState.include, - onCheckedChange = { - viewModel.onIncludeChange(!uiState.include) - }, - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text(stringResource(id = R.string.exclude)) - Checkbox( - checked = !uiState.include, - onCheckedChange = { - viewModel.onIncludeChange(!uiState.include) - }, - ) - } - } - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp, vertical = 7.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - SearchBar(viewModel::emitQueriedPackages) - } - Spacer(Modifier.padding(5.dp)) - LazyColumn( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxHeight(4 / 5f), - ) { - items(sortedPackages, key = { it.packageName }) { pack -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = - Modifier - .fillMaxSize() - .padding(5.dp), - ) { - Row(modifier = Modifier.fillMaxWidth(fillMaxWidth)) { - val drawable = - pack.applicationInfo?.loadIcon(context.packageManager) - if (drawable != null) { - Image( - painter = DrawablePainter(drawable), - stringResource(id = R.string.icon), - modifier = Modifier.size(50.dp, 50.dp), - ) - } else { - val icon = Icons.Rounded.Android - Icon( - icon, - icon.name, - modifier = Modifier.size(50.dp, 50.dp), - ) - } - Text( - viewModel.getPackageLabel(pack), - modifier = Modifier.padding(5.dp), - ) - } - Checkbox( - modifier = Modifier.fillMaxSize(), - checked = - ( - uiState.checkedPackageNames.contains( - pack.packageName, - ) - ), - onCheckedChange = { - if (it) { - viewModel.onAddCheckedPackage(pack.packageName) - } else { - viewModel.onRemoveCheckedPackage(pack.packageName) - } - }, - ) - } - } - } - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .fillMaxSize() - .padding(top = 5.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton(onClick = { showApplicationsDialog = false }) { - Text(stringResource(R.string.done)) - } - } - } - } + if(showApplicationsDialog) { + ApplicationSelectionDialog(viewModel, uiState) { + showApplicationsDialog = false } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt index 995d0ae..f37b6c6 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/ConfigViewModel.kt @@ -53,7 +53,10 @@ constructor( private val _uiState = MutableStateFlow(ConfigUiState()) val uiState = _uiState.onStart { appDataRepository.tunnels.getById(id)?.let { - _uiState.value = ConfigUiState.from(it) + val packages = getQueriedPackages() + _uiState.value = ConfigUiState.from(it).copy( + packages = packages, + ) } }.stateIn( viewModelScope + ioDispatcher, @@ -73,6 +76,33 @@ constructor( } } + fun cleanUpUninstalledApps() = viewModelScope.launch(ioDispatcher) { + uiState.value.tunnel?.let { + val config = it.toAmConfig() + val packages = getQueriedPackages() + val packageSet = packages.map { pack -> pack.packageName }.toSet() + val includedApps = config.`interface`.includedApplications.toMutableList() + val excludedApps = config.`interface`.excludedApplications.toMutableList() + if (includedApps.isEmpty() && excludedApps.isEmpty()) return@launch + if (includedApps.retainAll(packageSet) || excludedApps.retainAll(packageSet)) { + Timber.i("Removing split tunnel package name that no longer exists on the device") + _uiState.update { state -> + state.copy( + checkedPackageNames = if (_uiState.value.include) includedApps else excludedApps, + ) + } + val wgQuick = buildConfig().toWgQuickString(true) + val amQuick = buildAmConfig().toAwgQuickString(true) + saveConfig( + it.copy( + amQuick = amQuick, + wgQuick = wgQuick, + ), + ) + } + } + } + fun onAddCheckedPackage(packageName: String) { _uiState.update { it.copy( @@ -95,6 +125,12 @@ constructor( } } + private fun getQueriedPackages(query: String = ""): List { + return getAllInternetCapablePackages().filter { + getPackageLabel(it).lowercase().contains(query.lowercase()) + } + } + fun getPackageLabel(packageInfo: PackageInfo): String { return packageInfo.applicationInfo?.loadLabel(packageManager).toString() } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/components/ApplicationSelectionDialog.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/components/ApplicationSelectionDialog.kt new file mode 100644 index 0000000..020c716 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/config/components/ApplicationSelectionDialog.kt @@ -0,0 +1,199 @@ +package com.zaneschepke.wireguardautotunnel.ui.screens.config.components + +import android.content.pm.PackageInfo +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Android +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.drawablepainter.DrawablePainter +import com.zaneschepke.wireguardautotunnel.R +import com.zaneschepke.wireguardautotunnel.ui.common.SearchBar +import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigUiState +import com.zaneschepke.wireguardautotunnel.ui.screens.config.ConfigViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ApplicationSelectionDialog(viewModel: ConfigViewModel, uiState: ConfigUiState, onDismiss: () -> Unit) { + val context = LocalContext.current + val licenseComparator = compareBy { viewModel.getPackageLabel(it) } + + val sortedPackages = remember(uiState.packages, licenseComparator) { + uiState.packages.sortedWith(licenseComparator) + } + BasicAlertDialog( + onDismissRequest = { onDismiss() }, + ) { + Surface( + tonalElevation = 2.dp, + shadowElevation = 2.dp, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surface, + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(if (uiState.isAllApplicationsEnabled) 1 / 5f else 4 / 5f), + ) { + Column( + modifier = + Modifier + .fillMaxWidth(), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.tunnel_all)) + Switch( + checked = uiState.isAllApplicationsEnabled, + onCheckedChange = viewModel::onAllApplicationsChange, + ) + } + if (!uiState.isAllApplicationsEnabled) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.include)) + Checkbox( + checked = uiState.include, + onCheckedChange = { + viewModel.onIncludeChange(!uiState.include) + }, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(stringResource(id = R.string.exclude)) + Checkbox( + checked = !uiState.include, + onCheckedChange = { + viewModel.onIncludeChange(!uiState.include) + }, + ) + } + } + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 7.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + SearchBar(viewModel::emitQueriedPackages) + } + Spacer(Modifier.padding(5.dp)) + LazyColumn( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxHeight(19 / 22f), + ) { + items(sortedPackages, key = { it.packageName }) { pack -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier + .fillMaxSize() + .padding(5.dp).padding(end = 25.dp), + ) { + Row(modifier = Modifier.fillMaxWidth().padding(start = 5.dp)) { + val drawable = + pack.applicationInfo?.loadIcon(context.packageManager) + val iconSize = 35.dp + if (drawable != null) { + Image( + painter = DrawablePainter(drawable), + stringResource(id = R.string.icon), + modifier = Modifier.size(iconSize), + ) + } else { + val icon = Icons.Rounded.Android + Icon( + icon, + icon.name, + modifier = Modifier.size(iconSize), + ) + } + Text( + viewModel.getPackageLabel(pack), + modifier = Modifier.padding(5.dp), + ) + } + Checkbox( + modifier = Modifier.fillMaxSize(), + checked = + ( + uiState.checkedPackageNames.contains( + pack.packageName, + ) + ), + onCheckedChange = { + if (it) { + viewModel.onAddCheckedPackage(pack.packageName) + } else { + viewModel.onRemoveCheckedPackage(pack.packageName) + } + }, + ) + } + } + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .padding(top = 5.dp), + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { onDismiss() }) { + Text(stringResource(R.string.done)) + } + } + } + } + } +}