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 @@
Ağ
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