diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/ClipBoard.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/ClipBoard.kt new file mode 100644 index 00000000..02622994 --- /dev/null +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/common/functions/ClipBoard.kt @@ -0,0 +1,42 @@ +package com.zaneschepke.wireguardautotunnel.ui.common.functions + +import android.content.ClipData +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.Clipboard +import androidx.compose.ui.platform.LocalClipboard +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ClipboardHelper( + private val clipboard: Clipboard, + private val coroutineScope: CoroutineScope, + private val dispatcher: CoroutineDispatcher = Dispatchers.Main, +) { + fun copy(text: String, label: String = "") { + coroutineScope.launch(dispatcher) { + val clipData = ClipData.newPlainText(label, text) + clipboard.setClipEntry(ClipEntry(clipData)) + } + } + + fun paste(onResult: (String?) -> Unit) { + coroutineScope.launch(dispatcher) { + val entry = clipboard.getClipEntry() + val text = entry?.clipData?.getItemAt(0)?.text?.toString() + onResult(text) + } + } +} + +@Composable +fun rememberClipboardHelper( + coroutineScope: CoroutineScope = rememberCoroutineScope() +): ClipboardHelper { + val clipboard = LocalClipboard.current + return remember(clipboard, coroutineScope) { ClipboardHelper(clipboard, coroutineScope) } +} diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt index 7ebb80cf..30b29950 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/autotunnel/components/NetworkTunnelingItems.kt @@ -1,7 +1,7 @@ package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AirplanemodeActive +import androidx.compose.material.icons.outlined.PublicOff import androidx.compose.material.icons.outlined.SettingsEthernet import androidx.compose.material.icons.outlined.SignalCellular4Bar import androidx.compose.material3.MaterialTheme @@ -95,7 +95,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List Boolean, ): List { val context = LocalContext.current - val clipboard = LocalClipboardManager.current + val clipboardHelper = rememberClipboardHelper() val baseItems = listOf( @@ -71,29 +70,41 @@ fun WifiTunnelingItems( ) }, description = { - val wifiName by + val wifiInfo by remember(uiState.networkStatus) { derivedStateOf { (uiState.networkStatus as? NetworkStatus.Connected) ?.takeIf { it.wifiConnected } - ?.wifiSsid + .let { Pair(it?.wifiSsid, it?.securityType) } } } - Text( - text = - wifiName?.let { stringResource(R.string.wifi_name_template, it) } - ?: stringResource(R.string.inactive), - style = - MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.outline - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = - Modifier.clickable { - wifiName?.let { clipboard.setText(AnnotatedString(it)) } - }, - ) + val (wifiName, securityType) = wifiInfo + Column { + Text( + text = + wifiName?.let { stringResource(R.string.wifi_name_template, it) } + ?: stringResource(R.string.inactive), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = + Modifier.clickable { wifiName?.let { clipboardHelper.copy(it) } }, + ) + securityType?.let { + Text( + text = stringResource(R.string.security_template, it.name), + style = + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.outline + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } }, onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) }, ), diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt index 578861ab..74583f62 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/MainScreen.kt @@ -7,12 +7,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.Route import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog +import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ExportTunnelsBottomSheet @@ -29,7 +29,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) { val navController = LocalNavController.current - val clipboard = LocalClipboardManager.current + val clipboard = rememberClipboardHelper() var showUrlImportDialog by remember { mutableStateOf(false) } @@ -90,8 +90,9 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap requestPermissionLauncher.launch(android.Manifest.permission.CAMERA) }, onClipboardClick = { - clipboard.getText()?.text?.let { - viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it)) + clipboard.paste { result -> + if (result != null) + viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(result)) } }, onManualImportClick = { diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt index e86d1f42..41dfcbff 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/main/config/components/InterfaceFields.kt @@ -16,16 +16,15 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox +import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy @Composable @@ -38,7 +37,7 @@ fun InterfaceFields( onInterfaceChange: (InterfaceProxy) -> Unit, ) { val keyboardController = LocalSoftwareKeyboardController.current - val clipboardManager = LocalClipboardManager.current + val clipboardManager = rememberClipboardHelper() val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }) val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) @@ -88,9 +87,7 @@ fun InterfaceFields( modifier = Modifier.fillMaxWidth(), singleLine = true, trailingIcon = { - IconButton( - onClick = { clipboardManager.setText(AnnotatedString(interfaceState.publicKey)) } - ) { + IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) { Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key)) } }, diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt index 91a2d281..9d47cac9 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/advanced/components/RemoteControlItem.kt @@ -8,20 +8,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import com.zaneschepke.wireguardautotunnel.R import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem +import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent @Composable fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem { - val clipboardManager = LocalClipboardManager.current + val clipboardManager = rememberClipboardHelper() return SelectionItem( leadingIcon = Icons.Filled.SmartToy, @@ -42,8 +41,7 @@ fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIt ), maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = - Modifier.clickable { clipboardManager.setText(AnnotatedString(key)) }, + modifier = Modifier.clickable { clipboardManager.copy(key) }, ) } } diff --git a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/logs/components/LogItem.kt b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/logs/components/LogItem.kt index 5c12ca0f..3d8b8f89 100644 --- a/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/logs/components/LogItem.kt +++ b/app/src/main/java/com/zaneschepke/wireguardautotunnel/ui/screens/settings/logs/components/LogItem.kt @@ -11,17 +11,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.zaneschepke.logcatter.model.LogMessage +import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel @Composable fun LogItem(log: LogMessage) { - val clipboardManager = LocalClipboardManager.current + val clipboardManager = rememberClipboardHelper() val fontSize = 10.sp Row( @@ -32,7 +31,7 @@ fun LogItem(log: LogMessage) { .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = { clipboardManager.setText(AnnotatedString(log.toString())) }, + onClick = { clipboardManager.copy(log.toString()) }, ), ) { Text(text = log.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 014192c2..ab34551b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -221,6 +221,7 @@ Active: %1$s Key: %1$s Version: %1$s + Security: %1$s Flavor: %1$s config error dns resolution error diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt index 4b16a0ca..df02bdcc 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/AndroidNetworkMonitor.kt @@ -46,13 +46,18 @@ class AndroidNetworkMonitor( private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO @get:Synchronized @set:Synchronized var currentSsid: String? = null + @get:Synchronized @set:Synchronized var securityType: WifiSecurityType? = null @get:Synchronized @set:Synchronized var wifiConnected = false // Track active Wi-Fi networks and last active network ID private val activeNetworks = Collections.synchronizedSet(mutableSetOf()) - data class WifiState(val connected: Boolean = false, val ssid: String? = null) + data class WifiState( + val connected: Boolean = false, + val ssid: String? = null, + val securityType: WifiSecurityType? = null, + ) data class TransportState(val connected: Boolean = false) @@ -76,15 +81,15 @@ class AndroidNetworkMonitor( suspend fun handleUnknownWifi() { val newSsid = getWifiSsid() + val securityType = wifiManager?.getCurrentSecurityType() // Only update if new SSID is valid; preserve existing valid SSID otherwise if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) { currentSsid = newSsid - trySend(WifiState(connected = wifiConnected, ssid = currentSsid)) + trySend(WifiState(wifiConnected, currentSsid, securityType)) } else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) { currentSsid = newSsid - trySend(WifiState(connected = wifiConnected, ssid = currentSsid)) + trySend(WifiState(wifiConnected, currentSsid, securityType)) } - Timber.d("handleUnknownWifi: currentSsid=$currentSsid") } val locationPermissionReceiver = @@ -146,8 +151,15 @@ class AndroidNetworkMonitor( activeNetworks.add(network) launch { currentSsid = getWifiSsid() + securityType = wifiManager?.getCurrentSecurityType() wifiConnected = true - trySend(WifiState(connected = true, ssid = currentSsid)) + trySend( + WifiState( + connected = true, + ssid = currentSsid, + securityType = securityType, + ) + ) } } @@ -160,7 +172,7 @@ class AndroidNetworkMonitor( ) currentSsid = null wifiConnected = false - trySend(WifiState(connected = false, ssid = null)) + trySend(WifiState(connected = false, ssid = null, securityType = null)) } else { Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring") } @@ -241,6 +253,7 @@ class AndroidNetworkMonitor( if (hasAnyConnection) { NetworkStatus.Connected( wifiSsid = wifi.ssid, + securityType = wifi.securityType, wifiConnected = wifi.connected, cellularConnected = cellular.connected, ethernetConnected = ethernet.connected, diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt index d93445c9..3085b36b 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/Extensions.kt @@ -1,5 +1,7 @@ package com.zaneschepke.networkmonitor +import android.net.wifi.WifiManager +import android.os.Build import com.wireguard.android.util.RootShell fun RootShell.getCurrentWifiName(): String? { @@ -10,3 +12,12 @@ fun RootShell.getCurrentWifiName(): String? { ) return response.firstOrNull() } + +@Suppress("DEPRECATION") +fun WifiManager.getCurrentSecurityType(): WifiSecurityType? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + WifiSecurityType.from(connectionInfo.currentSecurityType) + } else { + null + } +} diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt index fa7ab27c..b0b9dae9 100644 --- a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/NetworkStatus.kt @@ -9,6 +9,7 @@ sealed class NetworkStatus { data class Connected( val wifiSsid: String? = null, + val securityType: WifiSecurityType? = null, override val wifiConnected: Boolean = false, override val ethernetConnected: Boolean = false, override val cellularConnected: Boolean = false, diff --git a/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/WifiSecurityType.kt b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/WifiSecurityType.kt new file mode 100644 index 00000000..a4e36c35 --- /dev/null +++ b/networkmonitor/src/main/java/com/zaneschepke/networkmonitor/WifiSecurityType.kt @@ -0,0 +1,38 @@ +package com.zaneschepke.networkmonitor + +import android.net.wifi.WifiInfo + +enum class WifiSecurityType { + UNKNOWN, + OPEN, + WEP, + WPA2, // WPA and WPA2 + WPA3, // WPA3-Personal (SAE) + OWE, + WAPI, // All WAPI_PSK and WAPI_CERT + EAP, // All EAP (covers both WPA3 and others) + PASSPOINT, // All Passpoint versions + DPP; + + companion object { + fun from(securityType: Int): WifiSecurityType { + return when (securityType) { + WifiInfo.SECURITY_TYPE_OPEN -> OPEN + WifiInfo.SECURITY_TYPE_WEP -> WEP + WifiInfo.SECURITY_TYPE_PSK -> WPA2 + WifiInfo.SECURITY_TYPE_EAP -> EAP + WifiInfo.SECURITY_TYPE_SAE -> WPA3 + WifiInfo.SECURITY_TYPE_OWE -> OWE + WifiInfo.SECURITY_TYPE_WAPI_PSK, + WifiInfo.SECURITY_TYPE_WAPI_CERT -> WAPI + WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE -> EAP + WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT -> EAP + WifiInfo.SECURITY_TYPE_PASSPOINT_R1_R2, + WifiInfo.SECURITY_TYPE_PASSPOINT_R3 -> PASSPOINT + WifiInfo.SECURITY_TYPE_DPP -> DPP + WifiInfo.SECURITY_TYPE_UNKNOWN -> UNKNOWN + else -> UNKNOWN + } + } + } +}