From 38d384d6690c02ffcd0d5c55e278a9bfa083a583 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 28 Dec 2024 12:46:46 +0800 Subject: [PATCH] Add Wi-Fi --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 1 + .../com/bintianqi/owndroid/MainActivity.kt | 2 + .../com/bintianqi/owndroid/dpm/Network.kt | 276 +++++++++++++++++- .../com/bintianqi/owndroid/ui/Components.kt | 17 ++ app/src/main/res/drawable/wifi_add_fill0.xml | 9 + app/src/main/res/values-ru/strings.xml | 16 +- app/src/main/res/values-tr/strings.xml | 16 +- app/src/main/res/values-zh-rCN/strings.xml | 19 +- app/src/main/res/values/strings.xml | 16 +- 10 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable/wifi_add_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 41a93de..312b58b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,4 +86,5 @@ dependencies { implementation(libs.androidx.fragment) implementation(libs.hiddenApiBypass) implementation(libs.serialization) + implementation(kotlin("reflect")) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d7b8ee..99b7dcf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + = 30) { FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } } + FunctionItem(R.string.add_wifi, "", R.drawable.wifi_add_fill0) { navCtrl.navigate("AddWifi") } if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) { FunctionItem(R.string.min_wifi_security_level, "", R.drawable.wifi_password_fill0) { navCtrl.navigate("MinWifiSecurityLevel") } } @@ -202,6 +216,266 @@ fun NetworkOptions(navCtrl: NavHostController) { ) } +@Suppress("DEPRECATION") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddNetwork(navCtrl: NavHostController) { + val context = LocalContext.current + var resultDialog by remember { mutableStateOf(false) } + var createdNetworkId by remember { mutableIntStateOf(-1) } + var createNetworkResult by remember {mutableIntStateOf(0)} + var dropdownMenu by remember { mutableIntStateOf(0) } // 0: None, 1:Status, 2:Security, 3:MAC randomization, 4:Static IP, 5:Proxy + var networkId by remember { mutableStateOf("") } + var status by remember { mutableIntStateOf(WifiConfiguration.Status.ENABLED) } + var ssid by remember { mutableStateOf("") } + var hiddenSsid by remember { mutableStateOf(false) } + var securityType by remember { mutableIntStateOf(WifiConfiguration.SECURITY_TYPE_OPEN) } + var password by remember { mutableStateOf("") } + var macRandomizationSetting by remember { mutableIntStateOf(WifiConfiguration.RANDOMIZATION_AUTO) } + var useStaticIp by remember { mutableStateOf(false) } + var ipAddress by remember { mutableStateOf("") } + var gatewayAddress by remember { mutableStateOf("") } + var dnsServers by remember { mutableStateOf("") } + var useHttpProxy by remember { mutableStateOf(false) } + var httpProxyHost by remember { mutableStateOf("") } + var httpProxyPort by remember { mutableStateOf("") } + var httpProxyExclList by remember { mutableStateOf("") } + MyScaffold(R.string.add_wifi, 8.dp, navCtrl) { + OutlinedTextField( + value = networkId, onValueChange = { networkId = it }, label = { Text(stringResource(R.string.network_id)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + isError = networkId != "" && (try { networkId.toInt(); false } catch(_: NumberFormatException) { true }), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) + ) + ExposedDropdownMenuBox(dropdownMenu == 1, { dropdownMenu = if(it) 1 else 0 }) { + val statusText = when(status) { + WifiConfiguration.Status.CURRENT -> R.string.current + WifiConfiguration.Status.DISABLED -> R.string.disabled + WifiConfiguration.Status.ENABLED -> R.string.enabled + else -> R.string.place_holder + } + OutlinedTextField( + value = stringResource(statusText), onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.status)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 1) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 16.dp) + ) + ExposedDropdownMenu(dropdownMenu == 1, { dropdownMenu = 0 }) { + DropdownMenuItem( + text = { Text(stringResource(R.string.current)) }, + onClick = { + status = WifiConfiguration.Status.CURRENT + dropdownMenu = 0 + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.disabled)) }, + onClick = { + status = WifiConfiguration.Status.DISABLED + dropdownMenu = 0 + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.enabled)) }, + onClick = { + status = WifiConfiguration.Status.ENABLED + dropdownMenu = 0 + } + ) + } + } + OutlinedTextField( + value = ssid, onValueChange = { ssid = it }, label = { Text("SSID") }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + CheckBoxItem(R.string.hidden_ssid, hiddenSsid, { hiddenSsid = it }) + if(VERSION.SDK_INT >= 30) { + // TODO: more protocols + val securityTypeTextMap = mutableMapOf( + WifiConfiguration.SECURITY_TYPE_OPEN to stringResource(R.string.wifi_security_open), + WifiConfiguration.SECURITY_TYPE_PSK to "PSK" + ) + ExposedDropdownMenuBox(dropdownMenu == 2, { dropdownMenu = if(it) 2 else 0 }) { + OutlinedTextField( + value = securityTypeTextMap[securityType] ?: "", onValueChange = {}, label = { Text(stringResource(R.string.security)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 1) {} }, readOnly = true, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(vertical = 4.dp) + ) + ExposedDropdownMenu(dropdownMenu == 2, { dropdownMenu = 0 }) { + securityTypeTextMap.forEach { + DropdownMenuItem(text = { Text(it.value) }, onClick = { securityType = it.key; dropdownMenu = 0 }) + } + } + } + AnimatedVisibility(securityType == WifiConfiguration.SECURITY_TYPE_PSK) { + OutlinedTextField( + value = password, onValueChange = { password = it }, label = { Text(stringResource(R.string.password)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp) + ) + } + } + if(VERSION.SDK_INT >= 33) { + val macRandomizationSettingTextMap = mapOf( + WifiConfiguration.RANDOMIZATION_NONE to R.string.none, + WifiConfiguration.RANDOMIZATION_PERSISTENT to R.string.persistent, + WifiConfiguration.RANDOMIZATION_NON_PERSISTENT to R.string.non_persistent, + WifiConfiguration.RANDOMIZATION_AUTO to R.string.auto + ) + ExposedDropdownMenuBox(dropdownMenu == 3, { dropdownMenu = if(it) 3 else 0 }) { + OutlinedTextField( + value = stringResource(macRandomizationSettingTextMap[macRandomizationSetting] ?: R.string.place_holder), + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.mac_randomization)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 3) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 8.dp) + ) + ExposedDropdownMenu(dropdownMenu == 3, { dropdownMenu = 0 }) { + macRandomizationSettingTextMap.forEach { + DropdownMenuItem( + text = { Text(stringResource(it.value)) }, + onClick = { + macRandomizationSetting = it.key + dropdownMenu = 0 + } + ) + } + } + } + } + if(VERSION.SDK_INT >= 33) { + ExposedDropdownMenuBox(dropdownMenu == 4, { dropdownMenu = if(it) 4 else 0 }) { + OutlinedTextField( + value = if(useStaticIp) stringResource(R.string.static_str) else "DHCP", + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.ip_settings)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 4) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp) + ) + ExposedDropdownMenu(dropdownMenu == 4, { dropdownMenu = 0 }) { + DropdownMenuItem(text = { Text("DHCP") }, onClick = { useStaticIp = false; dropdownMenu = 0 }) + DropdownMenuItem(text = { Text(stringResource(R.string.static_str)) }, onClick = { useStaticIp = true; dropdownMenu = 0 }) + } + } + AnimatedVisibility(visible = useStaticIp, modifier = Modifier.padding(bottom = 8.dp)) { + Column { + OutlinedTextField( + value = ipAddress, onValueChange = { ipAddress = it }, + placeholder = { Text("192.168.1.2/24") }, label = { Text(stringResource(R.string.ip_address)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = gatewayAddress, onValueChange = { gatewayAddress = it }, + placeholder = { Text("192.168.1.1") }, label = { Text(stringResource(R.string.gateway_address)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = dnsServers, onValueChange = { dnsServers = it }, + label = { Text(stringResource(R.string.dns_servers)) }, minLines = 2, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + } + } + } + if(VERSION.SDK_INT >= 26) { + ExposedDropdownMenuBox(dropdownMenu == 5, { dropdownMenu = if(it) 5 else 0 }) { + OutlinedTextField( + value = if(useHttpProxy) "HTTP" else stringResource(R.string.none), + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.proxy)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 5) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp) + ) + ExposedDropdownMenu(dropdownMenu == 5, { dropdownMenu = 0 }) { + DropdownMenuItem(text = { Text(stringResource(R.string.none)) }, onClick = { useHttpProxy = false; dropdownMenu = 0 }) + DropdownMenuItem(text = { Text("HTTP") }, onClick = { useHttpProxy = true; dropdownMenu = 0 }) + } + } + AnimatedVisibility(visible = useHttpProxy, modifier = Modifier.padding(bottom = 8.dp)) { + Column { + OutlinedTextField( + value = httpProxyHost, onValueChange = { httpProxyHost = it }, label = { Text(stringResource(R.string.host)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = httpProxyPort, onValueChange = { httpProxyPort = it }, label = { Text(stringResource(R.string.port)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = httpProxyExclList, onValueChange = { httpProxyExclList = it }, label = { Text(stringResource(R.string.excluded_hosts)) }, + minLines = 2, placeholder = { Text("example.com\n*.example.com") }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + } + } + } + Button( + onClick = { + val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + try { + val config = WifiConfiguration() + if(networkId != "") config.networkId = networkId.toInt() + config.status = status + config.SSID = '"' + ssid + '"' + config.hiddenSSID = hiddenSsid + if(VERSION.SDK_INT >= 30) config.setSecurityParams(securityType) + if(securityType == WifiConfiguration.SECURITY_TYPE_PSK) config.preSharedKey = '"' + password + '"' + if(VERSION.SDK_INT >= 33) config.macRandomizationSetting = macRandomizationSetting + if(VERSION.SDK_INT >= 33 && useStaticIp) { + val ipConf = IpConfiguration.Builder() + val staticIpConf = StaticIpConfiguration.Builder() + val la: LinkAddress + val con = LinkAddress::class.constructors.find { it.parameters.size == 1 && it.parameters[0].type.jvmErasure == String::class } + la = con!!.call(ipAddress) + staticIpConf.setIpAddress(la) + staticIpConf.setGateway(InetAddress.getByName(gatewayAddress)) + staticIpConf.setDnsServers(dnsServers.lines().map { InetAddress.getByName(it) }) + ipConf.setStaticIpConfiguration(staticIpConf.build()) + config.setIpConfiguration(ipConf.build()) + } + if(VERSION.SDK_INT >= 26 && useHttpProxy) { + config.httpProxy = ProxyInfo.buildDirectProxy(httpProxyHost, httpProxyPort.toInt(), httpProxyExclList.lines()) + } + if(VERSION.SDK_INT >= 31) { + val result = wm.addNetworkPrivileged(config) + createdNetworkId = result.networkId + createNetworkResult = result.statusCode + } else { + createdNetworkId = wm.addNetwork(config) + } + resultDialog = true + } catch(e: Exception) { + e.printStackTrace() + AlertDialog.Builder(context) + .setTitle(R.string.error) + .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.cancel() } + .setMessage(e.message ?: "") + .show() + } + }, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.add)) + } + if(resultDialog) AlertDialog( + text = { + val statusText = when(createNetworkResult) { + WifiManager.AddNetworkResult.STATUS_SUCCESS -> R.string.success + //WifiManager.AddNetworkResult.STATUS_ADD_WIFI_CONFIG_FAILURE -> R.string.failed + WifiManager.AddNetworkResult.STATUS_INVALID_CONFIGURATION -> R.string.add_network_result_invalid_configuration + else -> R.string.failed + } + Text(stringResource(statusText) + "\n" + stringResource(R.string.network_id) + ": " + createdNetworkId) + }, + confirmButton = { + TextButton(onClick = { resultDialog = false }) { + Text(stringResource(R.string.confirm)) + } + }, + onDismissRequest = { resultDialog = false } + ) + } +} + @SuppressLint("NewApi") @Composable fun WifiSecurityLevel(navCtrl: NavHostController) { @@ -513,7 +787,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { OutlinedTextField( value = exclList, onValueChange = { exclList = it }, - label = { Text(stringResource(R.string.exclude_hosts)) }, + label = { Text(stringResource(R.string.excluded_hosts)) }, maxLines = 5, minLines = 2, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp) diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 0d8e8be..d83e433 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -4,6 +4,7 @@ import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,6 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme.colorScheme @@ -22,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -315,3 +318,17 @@ fun MyScaffold( } } } + +@Composable +fun UpOrDownTextFieldTrailingIconButton(active: Boolean, onClick: () -> Unit) { + val degrees by animateFloatAsState(if(active) 180F else 0F) + IconButton( + onClick = onClick, + modifier = Modifier.clip(RoundedCornerShape(50)) + ) { + Icon( + imageVector = Icons.Default.ArrowDropDown, contentDescription = null, + modifier = Modifier.rotate(degrees) + ) + } +} diff --git a/app/src/main/res/drawable/wifi_add_fill0.xml b/app/src/main/res/drawable/wifi_add_fill0.xml new file mode 100644 index 0000000..a815f40 --- /dev/null +++ b/app/src/main/res/drawable/wifi_add_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3aa2940..b8e39f2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -64,6 +64,7 @@ Unknown error Permission denied Error + Status @@ -215,6 +216,19 @@ Сеть MAC-адрес Wi-Fi + + Add Wi-Fi + Current + Hidden SSID + IP settings + Static + IP address + Gateway address + DNS servers + MAC randomization + Non persistent + Host + Invalid configuration Минимальный уровень безопасности Wi-Fi Открытая Блокировка ети, настроенной администратором @@ -235,7 +249,7 @@ Прямой прокси Указать порт Неверная конфигурация - Исключить хосты + Исключить хосты Network logging Размер файла журнала: %1$s Удалить журналы diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1e42e9c..0006a03 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -65,6 +65,7 @@ Unknown error Permission denied Error + Status Etkinleştirmek İçin Tıklayın @@ -216,6 +217,19 @@ Wi-Fi MAC adresi + + Add Wi-Fi + Current + Hidden SSID + IP settings + Static + IP address + Gateway address + DNS servers + MAC randomization + Non persistent + Host + Invalid configuration Minimum Wi-Fi güvenlik seviyesi Açık Yönetici tarafından yapılandırılmış ağı kilitle @@ -236,7 +250,7 @@ Direct proxy Specify port Invalid config - Exclude hosts + Excluded hosts Network logging Log file size: %1$s Delete logs diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a4ab158..6f6804e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -61,6 +61,7 @@ 未知错误 无权限 错误 + 状态 点击以激活 @@ -208,7 +209,19 @@ 网络 - Wi-Fi Mac地址 + Wi-Fi MAC地址 + 添加Wi-Fi + 当前 + 隐藏的SSID + IP设置 + 静态 + IP地址 + 网关地址 + DNS服务器 + MAC随机化 + 非持久 + 主机 + Invalid configuration 最低Wi-Fi安全等级 开放 锁定由管理员配置的网络 @@ -229,7 +242,7 @@ 直连代理 指定端口 无效配置 - 排除列表 + 排除的主机 网络日志 日志文件大小:%1$s 删除日志 @@ -534,7 +547,7 @@ 项目主页 外观 - 安全 + 安全性 锁定OwnDroid 使用生物识别 验证 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83de418..c8e4020 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ OwnDroid + Disabled Enabled Disable @@ -65,6 +66,7 @@ Permission denied API Error + Status Click to activate @@ -220,6 +222,18 @@ Network Wi-Fi Mac address + Add Wi-Fi + Current + Hidden SSID + IP settings + Static + IP address + Gateway address + DNS servers + MAC randomization + Non persistent + Host + Invalid configuration Minimum Wi-Fi security level Open Lockdown admin configured network @@ -240,7 +254,7 @@ Direct proxy Specify port Invalid config - Exclude hosts + Excluded hosts Network logging Log file size: %1$s Delete logs