Some Wi-Fi operations

Enable/disable Wi-Fi
Disconnect/reconnect Wi-Fi
View, edit or delete saved networks
This commit is contained in:
BinTianqi
2024-12-28 21:17:23 +08:00
parent 38d384d669
commit 4250d47683
11 changed files with 351 additions and 79 deletions

View File

@@ -77,6 +77,7 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.accompanist.drawablepainter) implementation(libs.accompanist.drawablepainter)
implementation(libs.accompanist.permissions)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.shizuku.provider) implementation(libs.shizuku.provider)

View File

@@ -20,6 +20,8 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.shared,rikka.shizuku.aidl"/> <uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.shared,rikka.shizuku.aidl"/>
<application <application

View File

@@ -59,7 +59,6 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.bintianqi.owndroid.dpm.AddNetwork
import com.bintianqi.owndroid.dpm.AffiliationID import com.bintianqi.owndroid.dpm.AffiliationID
import com.bintianqi.owndroid.dpm.AlwaysOnVPNPackage import com.bintianqi.owndroid.dpm.AlwaysOnVPNPackage
import com.bintianqi.owndroid.dpm.ApplicationManage import com.bintianqi.owndroid.dpm.ApplicationManage
@@ -111,12 +110,14 @@ import com.bintianqi.owndroid.dpm.SystemManage
import com.bintianqi.owndroid.dpm.SystemOptions import com.bintianqi.owndroid.dpm.SystemOptions
import com.bintianqi.owndroid.dpm.SystemUpdatePolicy import com.bintianqi.owndroid.dpm.SystemUpdatePolicy
import com.bintianqi.owndroid.dpm.TransferOwnership import com.bintianqi.owndroid.dpm.TransferOwnership
import com.bintianqi.owndroid.dpm.UpdateNetwork
import com.bintianqi.owndroid.dpm.UserOperation import com.bintianqi.owndroid.dpm.UserOperation
import com.bintianqi.owndroid.dpm.UserOptions import com.bintianqi.owndroid.dpm.UserOptions
import com.bintianqi.owndroid.dpm.UserRestriction import com.bintianqi.owndroid.dpm.UserRestriction
import com.bintianqi.owndroid.dpm.UserRestrictionItem import com.bintianqi.owndroid.dpm.UserRestrictionItem
import com.bintianqi.owndroid.dpm.UserSessionMessage import com.bintianqi.owndroid.dpm.UserSessionMessage
import com.bintianqi.owndroid.dpm.Users import com.bintianqi.owndroid.dpm.Users
import com.bintianqi.owndroid.dpm.Wifi
import com.bintianqi.owndroid.dpm.WifiAuthKeypair import com.bintianqi.owndroid.dpm.WifiAuthKeypair
import com.bintianqi.owndroid.dpm.WifiSecurityLevel import com.bintianqi.owndroid.dpm.WifiSecurityLevel
import com.bintianqi.owndroid.dpm.WifiSsidPolicy import com.bintianqi.owndroid.dpm.WifiSsidPolicy
@@ -236,8 +237,9 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
composable(route = "WipeData") { WipeData(navCtrl) } composable(route = "WipeData") { WipeData(navCtrl) }
composable(route = "Network") { Network(navCtrl) } composable(route = "Network") { Network(navCtrl) }
composable(route = "Wifi") { Wifi(navCtrl) }
composable(route = "NetworkOptions") { NetworkOptions(navCtrl) } composable(route = "NetworkOptions") { NetworkOptions(navCtrl) }
composable(route = "AddWifi") { AddNetwork(navCtrl) } composable(route = "UpdateNetwork") { UpdateNetwork(it.arguments!!, navCtrl) }
composable(route = "MinWifiSecurityLevel") { WifiSecurityLevel(navCtrl) } composable(route = "MinWifiSecurityLevel") { WifiSecurityLevel(navCtrl) }
composable(route = "WifiSsidPolicy") { WifiSsidPolicy(navCtrl) } composable(route = "WifiSsidPolicy") { WifiSsidPolicy(navCtrl) }
composable(route = "PrivateDNS") { PrivateDNS(navCtrl) } composable(route = "PrivateDNS") { PrivateDNS(navCtrl) }

View File

@@ -122,3 +122,7 @@ fun parseTimestamp(timestamp: Long): String {
val Long.humanReadableDate: String val Long.humanReadableDate: String
get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this)) get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this))
fun Context.showOperationResultToast(success: Boolean) {
Toast.makeText(this, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
}

View File

@@ -1,5 +1,6 @@
package com.bintianqi.owndroid.dpm package com.bintianqi.owndroid.dpm
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OFF import android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OFF
@@ -29,6 +30,7 @@ import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.net.wifi.WifiSsid import android.net.wifi.WifiSsid
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Bundle
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
import android.telephony.data.ApnSetting.AUTH_TYPE_CHAP import android.telephony.data.ApnSetting.AUTH_TYPE_CHAP
@@ -47,38 +49,54 @@ import android.telephony.data.ApnSetting.PROTOCOL_NON_IP
import android.telephony.data.ApnSetting.PROTOCOL_PPP import android.telephony.data.ApnSetting.PROTOCOL_PPP
import android.telephony.data.ApnSetting.PROTOCOL_UNSTRUCTURED import android.telephony.data.ApnSetting.PROTOCOL_UNSTRUCTURED
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -88,6 +106,7 @@ import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -101,6 +120,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
@@ -108,15 +128,21 @@ import com.bintianqi.owndroid.exportFile
import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.exportFilePath
import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.InfoCard
import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.ListItem
import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.RadioButtonItem
import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.ui.UpOrDownTextFieldTrailingIconButton import com.bintianqi.owndroid.ui.UpOrDownTextFieldTrailingIconButton
import com.bintianqi.owndroid.writeClipBoard import com.bintianqi.owndroid.writeClipBoard
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.coroutines.launch
import java.net.InetAddress import java.net.InetAddress
import kotlin.math.max import kotlin.math.max
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
@@ -130,25 +156,15 @@ fun Network(navCtrl:NavHostController) {
val profileOwner = context.isProfileOwner val profileOwner = context.isProfileOwner
val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)
val dhizuku = sharedPref.getBoolean("dhizuku", false) val dhizuku = sharedPref.getBoolean("dhizuku", false)
var wifiMacDialog by remember { mutableStateOf(false) }
MyScaffold(R.string.network, 0.dp, navCtrl) { MyScaffold(R.string.network, 0.dp, navCtrl) {
if(VERSION.SDK_INT >= 24 && (deviceOwner || dpm.isOrgProfile(receiver))) { if(!dhizuku) FunctionItem(R.string.wifi, "", R.drawable.wifi_fill0) { navCtrl.navigate("Wifi") }
FunctionItem(R.string.wifi_mac_address, "", R.drawable.wifi_fill0) { wifiMacDialog = true }
}
if(VERSION.SDK_INT >= 30) { if(VERSION.SDK_INT >= 30) {
FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } 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") }
}
if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(R.string.wifi_ssid_policy, "", R.drawable.wifi_fill0) { navCtrl.navigate("WifiSsidPolicy") }
}
if(VERSION.SDK_INT >= 29 && deviceOwner) { if(VERSION.SDK_INT >= 29 && deviceOwner) {
FunctionItem(R.string.private_dns, "", R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } FunctionItem(R.string.private_dns, "", R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") }
} }
if(VERSION.SDK_INT >= 24 && (deviceOwner || profileOwner)) { if(VERSION.SDK_INT >= 24) {
FunctionItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } FunctionItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") }
} }
if(deviceOwner) { if(deviceOwner) {
@@ -157,39 +173,16 @@ fun Network(navCtrl:NavHostController) {
if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) {
FunctionItem(R.string.network_logging, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } FunctionItem(R.string.network_logging, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") }
} }
if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { if(VERSION.SDK_INT >= 31) {
FunctionItem(R.string.wifi_auth_keypair, "", R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } FunctionItem(R.string.wifi_auth_keypair, "", R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") }
} }
if(VERSION.SDK_INT >= 33 && (deviceOwner || profileOwner)) { if(VERSION.SDK_INT >= 33) {
FunctionItem(R.string.preferential_network_service, "", R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } FunctionItem(R.string.preferential_network_service, "", R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") }
} }
if(VERSION.SDK_INT >= 28 && deviceOwner) { if(VERSION.SDK_INT >= 28 && deviceOwner) {
FunctionItem(R.string.override_apn_settings, "", R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") } FunctionItem(R.string.override_apn_settings, "", R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") }
} }
} }
if(wifiMacDialog && VERSION.SDK_INT >= 24) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
AlertDialog(
onDismissRequest = { wifiMacDialog = false },
confirmButton = { TextButton(onClick = { wifiMacDialog = false }) { Text(stringResource(R.string.confirm)) } },
title = { Text(stringResource(R.string.wifi_mac_address)) },
text = {
val mac = dpm.getWifiMacAddress(receiver)
OutlinedTextField(
value = mac ?: stringResource(R.string.none),
onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxWidth(), textStyle = typography.bodyLarge,
trailingIcon = {
if(mac != null) IconButton(onClick = { writeClipBoard(context, mac) }) {
Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy))
}
}
)
},
modifier = Modifier.fillMaxWidth()
)
}
} }
@Composable @Composable
@@ -216,16 +209,265 @@ fun NetworkOptions(navCtrl: NavHostController) {
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Wifi(navCtrl: NavHostController) {
val context = LocalContext.current
val coroutine = rememberCoroutineScope()
val pagerState = rememberPagerState { 3 }
var tabIndex by rememberSaveable { mutableIntStateOf(0) }
tabIndex = pagerState.currentPage
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.wifi)) },
navigationIcon = { NavIcon { navCtrl.navigateUp() } }
)
}
) { paddingValues ->
var wifiMacDialog by remember { mutableStateOf(false) }
Column(
modifier = Modifier.fillMaxSize().padding(paddingValues)
) {
TabRow(tabIndex) {
Tab(
selected = tabIndex == 0, onClick = { tabIndex = 0; coroutine.launch { pagerState.animateScrollToPage(tabIndex) } },
text = { Text(stringResource(R.string.overview)) }
)
Tab(
selected = tabIndex == 1, onClick = { tabIndex = 1; coroutine.launch { pagerState.animateScrollToPage(tabIndex) } },
text = { Text(stringResource(R.string.saved_networks)) }
)
Tab(
selected = tabIndex == 2, onClick = { tabIndex = 2; coroutine.launch { pagerState.animateScrollToPage(tabIndex) } },
text = { Text(stringResource(R.string.add_network)) }
)
}
HorizontalPager(state = pagerState, verticalAlignment = Alignment.Top) { page ->
if(page == 0) {
val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val deviceOwner = context.isDeviceOwner
val orgProfileOwner = context.getDPM().isOrgProfile(context.getReceiver())
@Suppress("DEPRECATION") Column(
modifier = Modifier.fillMaxSize().padding(top = 12.dp)
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = { context.showOperationResultToast(wm.setWifiEnabled(true)) },
modifier = Modifier.padding(end = 8.dp)
) {
Text(stringResource(R.string.enable))
}
Button(onClick = { context.showOperationResultToast(wm.setWifiEnabled(false)) }) {
Text(stringResource(R.string.disable))
}
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
) {
Button(
onClick = { context.showOperationResultToast(wm.disconnect()) },
modifier = Modifier.padding(end = 8.dp)
) {
Text(stringResource(R.string.disconnect))
}
Button(onClick = { context.showOperationResultToast(wm.reconnect()) }) {
Text(stringResource(R.string.reconnect))
}
}
if(VERSION.SDK_INT >= 24 && (deviceOwner || orgProfileOwner)) {
FunctionItem(R.string.wifi_mac_address, "", null) { wifiMacDialog = true }
}
if(VERSION.SDK_INT >= 33 && (deviceOwner || orgProfileOwner)) {
FunctionItem(R.string.min_wifi_security_level, "", null) { navCtrl.navigate("MinWifiSecurityLevel") }
FunctionItem(R.string.wifi_ssid_policy, "", null) { navCtrl.navigate("WifiSsidPolicy") }
}
}
} else if(page == 1) {
SavedNetworks(navCtrl)
} else {
AddNetwork()
}
}
}
if(wifiMacDialog && VERSION.SDK_INT >= 24) {
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
AlertDialog(
onDismissRequest = { wifiMacDialog = false },
confirmButton = { TextButton(onClick = { wifiMacDialog = false }) { Text(stringResource(R.string.confirm)) } },
text = {
val mac = dpm.getWifiMacAddress(receiver)
OutlinedTextField(
value = mac ?: stringResource(R.string.none), label = { Text(stringResource(R.string.wifi_mac_address)) },
onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxWidth(), textStyle = typography.bodyLarge,
trailingIcon = {
if(mac != null) IconButton(onClick = { writeClipBoard(context, mac) }) {
Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy))
}
}
)
},
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Suppress("DEPRECATION")
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun SavedNetworks(navCtrl: NavHostController) {
val context = LocalContext.current
val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val configuredNetworks = remember { mutableStateListOf<WifiConfiguration>() }
var networkDetailsDialog by remember { mutableIntStateOf(-1) } // -1:Hidden, 0+:Index of configuredNetworks
fun refresh() {
configuredNetworks.clear()
wm.configuredNetworks.forEach { network ->
if(configuredNetworks.none { it.networkId == network.networkId }) configuredNetworks += network
}
}
LaunchedEffect(Unit) { refresh() }
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 8.dp, end = 8.dp, bottom = 60.dp)
) {
val locationPermission = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
val requestPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {
if(it) refresh()
}
if(!locationPermission.status.isGranted) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.clip(RoundedCornerShape(15))
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable { requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) }
) {
Icon(
imageVector = Icons.Outlined.LocationOn, contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(start = 8.dp, end = 4.dp))
Text(
text = stringResource(R.string.request_location_permission_description),
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(8.dp)
)
}
}
configuredNetworks.forEachIndexed { index, network ->
Row(
horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(start = 8.dp, top = 8.dp)
) {
Text(text = network.SSID.removeSurrounding("\""), style = typography.titleLarge)
IconButton(onClick = { networkDetailsDialog = index }) {
Icon(painter = painterResource(R.drawable.more_horiz_fill0), contentDescription = null)
}
}
}
}
if(networkDetailsDialog != -1) AlertDialog(
text = {
val network = configuredNetworks[networkDetailsDialog]
val statusText = when(network.status) {
WifiConfiguration.Status.CURRENT -> R.string.current
WifiConfiguration.Status.DISABLED -> R.string.disabled
WifiConfiguration.Status.ENABLED -> R.string.enabled
else -> R.string.place_holder
}
Column {
Text(stringResource(R.string.network_id) + ": " + network.networkId.toString())
SelectionContainer {
Text("SSID: " + network.SSID)
if(network.BSSID != null) Text("BSSID: " + network.BSSID)
}
Text(stringResource(R.string.status) + ": " + stringResource(statusText))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(top = 12.dp)
) {
Button(
onClick = {
val success = wm.enableNetwork(network.networkId, false)
Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
networkDetailsDialog = -1
refresh()
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.enable))
}
Button(
onClick = {
val success = wm.disableNetwork(network.networkId)
Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
networkDetailsDialog = -1
refresh()
},
modifier = Modifier.fillMaxWidth(0.96F)
) {
Text(stringResource(R.string.disable))
}
}
Button(
onClick = {
networkDetailsDialog = -1
val dest = navCtrl.graph.findNode("UpdateNetwork")
if(dest != null)
navCtrl.navigate(dest.id, bundleOf("wifi_configuration" to network))
},
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.edit))
}
TextButton(
onClick = {
val success = wm.removeNetwork(network.networkId)
Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
networkDetailsDialog = -1
refresh()
},
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.remove))
}
}
},
confirmButton = {
TextButton(onClick = { networkDetailsDialog = -1 }) {
Text(stringResource(R.string.confirm))
}
},
onDismissRequest = { networkDetailsDialog = -1 }
)
}
@Composable
fun UpdateNetwork(arguments: Bundle, navCtrl: NavHostController) {
MyScaffold(R.string.update_network, 0.dp, navCtrl, false) {
AddNetwork(arguments.getParcelable("wifi_configuration"), navCtrl)
}
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AddNetwork(navCtrl: NavHostController) { private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostController? = null) {
val context = LocalContext.current val context = LocalContext.current
var resultDialog by remember { mutableStateOf(false) } var resultDialog by remember { mutableStateOf(false) }
var createdNetworkId by remember { mutableIntStateOf(-1) } var createdNetworkId by remember { mutableIntStateOf(-1) }
var createNetworkResult by remember {mutableIntStateOf(0)} 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 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 status by remember { mutableIntStateOf(WifiConfiguration.Status.ENABLED) }
var ssid by remember { mutableStateOf("") } var ssid by remember { mutableStateOf("") }
var hiddenSsid by remember { mutableStateOf(false) } var hiddenSsid by remember { mutableStateOf(false) }
@@ -240,16 +482,19 @@ fun AddNetwork(navCtrl: NavHostController) {
var httpProxyHost by remember { mutableStateOf("") } var httpProxyHost by remember { mutableStateOf("") }
var httpProxyPort by remember { mutableStateOf("") } var httpProxyPort by remember { mutableStateOf("") }
var httpProxyExclList by remember { mutableStateOf("") } var httpProxyExclList by remember { mutableStateOf("") }
MyScaffold(R.string.add_wifi, 8.dp, navCtrl) { LaunchedEffect(Unit) {
OutlinedTextField( if(wifiConfig != null) {
value = networkId, onValueChange = { networkId = it }, label = { Text(stringResource(R.string.network_id)) }, status = wifiConfig.status
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), if(wifiConfig.status == WifiConfiguration.Status.CURRENT) status = WifiConfiguration.Status.ENABLED
isError = networkId != "" && (try { networkId.toInt(); false } catch(_: NumberFormatException) { true }), ssid = wifiConfig.SSID.removeSurrounding("\"")
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) }
) }
Column(
modifier = (if(wifiConfig == null) Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 60.dp) else Modifier)
.padding(start = 8.dp, end = 8.dp, top = 12.dp)
) {
ExposedDropdownMenuBox(dropdownMenu == 1, { dropdownMenu = if(it) 1 else 0 }) { ExposedDropdownMenuBox(dropdownMenu == 1, { dropdownMenu = if(it) 1 else 0 }) {
val statusText = when(status) { val statusText = when(status) {
WifiConfiguration.Status.CURRENT -> R.string.current
WifiConfiguration.Status.DISABLED -> R.string.disabled WifiConfiguration.Status.DISABLED -> R.string.disabled
WifiConfiguration.Status.ENABLED -> R.string.enabled WifiConfiguration.Status.ENABLED -> R.string.enabled
else -> R.string.place_holder else -> R.string.place_holder
@@ -261,13 +506,6 @@ fun AddNetwork(navCtrl: NavHostController) {
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 16.dp) modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 16.dp)
) )
ExposedDropdownMenu(dropdownMenu == 1, { dropdownMenu = 0 }) { ExposedDropdownMenu(dropdownMenu == 1, { dropdownMenu = 0 }) {
DropdownMenuItem(
text = { Text(stringResource(R.string.current)) },
onClick = {
status = WifiConfiguration.Status.CURRENT
dropdownMenu = 0
}
)
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(R.string.disabled)) }, text = { Text(stringResource(R.string.disabled)) },
onClick = { onClick = {
@@ -413,7 +651,6 @@ fun AddNetwork(navCtrl: NavHostController) {
val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
try { try {
val config = WifiConfiguration() val config = WifiConfiguration()
if(networkId != "") config.networkId = networkId.toInt()
config.status = status config.status = status
config.SSID = '"' + ssid + '"' config.SSID = '"' + ssid + '"'
config.hiddenSSID = hiddenSsid config.hiddenSSID = hiddenSsid
@@ -435,6 +672,10 @@ fun AddNetwork(navCtrl: NavHostController) {
if(VERSION.SDK_INT >= 26 && useHttpProxy) { if(VERSION.SDK_INT >= 26 && useHttpProxy) {
config.httpProxy = ProxyInfo.buildDirectProxy(httpProxyHost, httpProxyPort.toInt(), httpProxyExclList.lines()) config.httpProxy = ProxyInfo.buildDirectProxy(httpProxyHost, httpProxyPort.toInt(), httpProxyExclList.lines())
} }
if(wifiConfig != null) {
config.networkId = wifiConfig.networkId
createdNetworkId = wm.updateNetwork(config)
} else {
if(VERSION.SDK_INT >= 31) { if(VERSION.SDK_INT >= 31) {
val result = wm.addNetworkPrivileged(config) val result = wm.addNetworkPrivileged(config)
createdNetworkId = result.networkId createdNetworkId = result.networkId
@@ -442,6 +683,7 @@ fun AddNetwork(navCtrl: NavHostController) {
} else { } else {
createdNetworkId = wm.addNetwork(config) createdNetworkId = wm.addNetwork(config)
} }
}
resultDialog = true resultDialog = true
} catch(e: Exception) { } catch(e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -454,7 +696,7 @@ fun AddNetwork(navCtrl: NavHostController) {
}, },
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
) { ) {
Text(stringResource(R.string.add)) Text(stringResource(if(wifiConfig != null) R.string.update else R.string.add))
} }
if(resultDialog) AlertDialog( if(resultDialog) AlertDialog(
text = { text = {
@@ -467,7 +709,12 @@ fun AddNetwork(navCtrl: NavHostController) {
Text(stringResource(statusText) + "\n" + stringResource(R.string.network_id) + ": " + createdNetworkId) Text(stringResource(statusText) + "\n" + stringResource(R.string.network_id) + ": " + createdNetworkId)
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { resultDialog = false }) { TextButton(
onClick = {
resultDialog = false
if(createdNetworkId != -1) navCtrl?.navigateUp()
}
) {
Text(stringResource(R.string.confirm)) Text(stringResource(R.string.confirm))
} }
}, },

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M720,840v-120L600,720v-80h120v-120h80v120h120v80L800,720v120h-80ZM480,840L0,360q95,-97 219.5,-148.5T480,160q136,0 260.5,51.5T960,360L822,497q-14,-14 -28,-28.5T766,440l78,-78q-79,-60 -172,-91t-192,-31q-99,0 -192,31t-172,91l364,364 40,-40 28.5,28.5L577,743l-97,97ZM480,483Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -217,7 +217,12 @@
<string name="network">Сеть</string> <string name="network">Сеть</string>
<string name="wifi_mac_address">MAC-адрес Wi-Fi</string> <string name="wifi_mac_address">MAC-адрес Wi-Fi</string>
<!--TODO--> <!--TODO-->
<string name="add_wifi">Add Wi-Fi</string> <string name="disconnect">Disconnect</string>
<string name="reconnect">Reconnect</string>
<string name="saved_networks">Saved networks</string>
<string name="request_location_permission_description">This app need location permission to get saved networks, your geographic location will not be read.\nClick to request the permission.</string>
<string name="add_network">Add network</string>
<string name="update_network">Update network</string>
<string name="current">Current</string> <string name="current">Current</string>
<string name="hidden_ssid">Hidden SSID</string> <string name="hidden_ssid">Hidden SSID</string>
<string name="ip_settings">IP settings</string> <string name="ip_settings">IP settings</string>

View File

@@ -218,7 +218,12 @@
<string name="network"></string> <string name="network"></string>
<string name="wifi_mac_address">Wi-Fi MAC adresi</string> <string name="wifi_mac_address">Wi-Fi MAC adresi</string>
<!--TODO--> <!--TODO-->
<string name="add_wifi">Add Wi-Fi</string> <string name="disconnect">Disconnect</string>
<string name="reconnect">Reconnect</string>
<string name="saved_networks">Saved networks</string>
<string name="request_location_permission_description">This app need location permission to get saved networks, your geographic location will not be read.\nClick to request the permission.</string>
<string name="add_network">Add network</string>
<string name="update_network">Update network</string>
<string name="current">Current</string> <string name="current">Current</string>
<string name="hidden_ssid">Hidden SSID</string> <string name="hidden_ssid">Hidden SSID</string>
<string name="ip_settings">IP settings</string> <string name="ip_settings">IP settings</string>

View File

@@ -210,7 +210,12 @@
<!--Network--> <!--Network-->
<string name="network">网络</string> <string name="network">网络</string>
<string name="wifi_mac_address">Wi-Fi MAC地址</string> <string name="wifi_mac_address">Wi-Fi MAC地址</string>
<string name="add_wifi">添加Wi-Fi</string> <string name="disconnect">断开连接</string>
<string name="reconnect">重新连接</string>
<string name="saved_networks">已保存的网络</string>
<string name="request_location_permission_description">此app需要定位权限以获取已保存的网络你的地理位置将不会被读取。点击以请求权限</string>
<string name="add_network">添加网络</string>
<string name="update_network">更新网络</string>
<string name="current">当前</string> <string name="current">当前</string>
<string name="hidden_ssid">隐藏的SSID</string> <string name="hidden_ssid">隐藏的SSID</string>
<string name="ip_settings">IP设置</string> <string name="ip_settings">IP设置</string>

View File

@@ -67,6 +67,8 @@
<string name="api" translatable="false">API</string> <string name="api" translatable="false">API</string>
<string name="error">Error</string> <string name="error">Error</string>
<string name="status">Status</string> <string name="status">Status</string>
<string name="edit">Edit</string>
<string name="overview">Overview</string>
<!--Permissions--> <!--Permissions-->
<string name="click_to_activate">Click to activate</string> <string name="click_to_activate">Click to activate</string>
@@ -221,8 +223,14 @@
<!--Network--> <!--Network-->
<string name="network">Network</string> <string name="network">Network</string>
<string name="wifi_mac_address">Wi-Fi Mac address</string> <string name="wifi_mac_address">Wi-Fi MAC address</string>
<string name="add_wifi">Add Wi-Fi</string> <string name="wifi" translatable="false">Wi-Fi</string>
<string name="disconnect">Disconnect</string>
<string name="reconnect">Reconnect</string>
<string name="saved_networks">Saved networks</string>
<string name="request_location_permission_description">This app need location permission to get saved networks, your geographic location will not be read.\nClick to request the permission.</string>
<string name="add_network">Add network</string>
<string name="update_network">Update network</string>
<string name="current">Current</string> <string name="current">Current</string>
<string name="hidden_ssid">Hidden SSID</string> <string name="hidden_ssid">Hidden SSID</string>
<string name="ip_settings">IP settings</string> <string name="ip_settings">IP settings</string>

View File

@@ -5,6 +5,7 @@ kotlin = "2.0.21"
navigation-compose = "2.8.5" navigation-compose = "2.8.5"
composeBom = "2024.12.01" composeBom = "2024.12.01"
accompanist-drawablepainter = "0.35.0-alpha" accompanist-drawablepainter = "0.35.0-alpha"
accompanist-permissions = "0.37.0"
shizuku = "13.1.5" shizuku = "13.1.5"
biometric = "1.2.0-alpha05" biometric = "1.2.0-alpha05"
fragment = "1.8.5" fragment = "1.8.5"
@@ -19,6 +20,7 @@ androidx-material3 = { module = "androidx.compose.material3:material3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" } accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" }
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist-permissions" }
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }