Query network stats

This commit is contained in:
BinTianqi
2025-01-19 13:14:23 +08:00
parent 426311ecfe
commit 2dc760dbe3
8 changed files with 646 additions and 21 deletions

View File

@@ -88,6 +88,8 @@ import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
import com.bintianqi.owndroid.dpm.Network
import com.bintianqi.owndroid.dpm.NetworkLogging
import com.bintianqi.owndroid.dpm.NetworkOptions
import com.bintianqi.owndroid.dpm.NetworkStats
import com.bintianqi.owndroid.dpm.NetworkStatsViewer
import com.bintianqi.owndroid.dpm.OrgOwnedProfile
import com.bintianqi.owndroid.dpm.OverrideAPN
import com.bintianqi.owndroid.dpm.Password
@@ -245,6 +247,8 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
composable(route = "UpdateNetwork") { UpdateNetwork(it.arguments!!, navCtrl) }
composable(route = "MinWifiSecurityLevel") { WifiSecurityLevel(navCtrl) }
composable(route = "WifiSsidPolicy") { WifiSsidPolicy(navCtrl) }
composable(route = "NetworkStats") { NetworkStats(navCtrl, vm) }
composable(route = "NetworkStatsViewer") { NetworkStatsViewer(navCtrl, it.arguments!!) }
composable(route = "PrivateDNS") { PrivateDNS(navCtrl) }
composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl, vm) }
composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy(navCtrl) }

View File

@@ -17,9 +17,12 @@ import android.app.admin.PreferentialNetworkServiceConfig
import android.app.admin.WifiSsidPolicy
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_ALLOWLIST
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_DENYLIST
import android.app.usage.NetworkStats
import android.app.usage.NetworkStatsManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.net.ConnectivityManager
import android.net.IpConfiguration
import android.net.LinkAddress
import android.net.ProxyInfo
@@ -51,10 +54,13 @@ import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -80,6 +86,8 @@ import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
@@ -97,6 +105,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -121,12 +130,15 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
import com.bintianqi.owndroid.ui.ExpandExposedTextFieldIcon
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.InfoCard
import com.bintianqi.owndroid.ui.ListItem
@@ -134,12 +146,13 @@ import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.RadioButtonItem
import com.bintianqi.owndroid.ui.SwitchItem
import com.bintianqi.owndroid.ui.UpOrDownTextFieldTrailingIconButton
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.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetAddress
import kotlin.math.max
import kotlin.reflect.jvm.jvmErasure
@@ -158,6 +171,8 @@ fun Network(navCtrl:NavHostController) {
if(VERSION.SDK_INT >= 30) {
FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") }
}
if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner))
FunctionItem(R.string.network_stats, icon = R.drawable.query_stats_fill0) { navCtrl.navigate("NetworkStats") }
if(VERSION.SDK_INT >= 29 && deviceOwner) {
FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") }
}
@@ -496,7 +511,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo
OutlinedTextField(
value = stringResource(statusText), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.status)) },
trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 1) {} },
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 1) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 16.dp)
)
ExposedDropdownMenu(dropdownMenu == 1, { dropdownMenu = 0 }) {
@@ -530,7 +545,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo
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,
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 1) }, readOnly = true,
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(vertical = 4.dp)
)
ExposedDropdownMenu(dropdownMenu == 2, { dropdownMenu = 0 }) {
@@ -558,7 +573,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo
value = stringResource(macRandomizationSettingTextMap[macRandomizationSetting] ?: R.string.place_holder),
onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.mac_randomization)) },
trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 3) {} },
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 3) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 8.dp)
)
ExposedDropdownMenu(dropdownMenu == 3, { dropdownMenu = 0 }) {
@@ -580,7 +595,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo
value = if(useStaticIp) stringResource(R.string.static_str) else "DHCP",
onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.ip_settings)) },
trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 4) {} },
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 4) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(dropdownMenu == 4, { dropdownMenu = 0 }) {
@@ -614,7 +629,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo
value = if(useHttpProxy) "HTTP" else stringResource(R.string.none),
onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.proxy)) },
trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 5) {} },
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 5) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(dropdownMenu == 5, { dropdownMenu = 0 }) {
@@ -817,6 +832,499 @@ fun WifiSsidPolicy(navCtrl: NavHostController) {
}
}
private enum class NetworkStatsActiveTextField { None, Type, Target, NetworkType, SubscriberId, StartTime, EndTime, Uid, Tag, State }
@Suppress("DEPRECATION")
private enum class NetworkType(val type: Int, @StringRes val strRes: Int) {
Mobile(ConnectivityManager.TYPE_MOBILE, R.string.mobile),
Wifi(ConnectivityManager.TYPE_WIFI, R.string.wifi),
Bluetooth(ConnectivityManager.TYPE_BLUETOOTH, R.string.bluetooth),
Ethernet(ConnectivityManager.TYPE_ETHERNET, R.string.ethernet),
Vpn(ConnectivityManager.TYPE_VPN, R.string.vpn),
}
private enum class NetworkStatsTarget(@StringRes val strRes: Int, val minApi: Int) {
Device(R.string.device, 23), User(R.string.user, 23),
Uid(R.string.uid, 23), UidTag(R.string.uid_tag, 24), UidTagState(R.string.uid_tag_state, 28)
}
@RequiresApi(23)
private enum class NetworkStatsUID(val uid: Int, @StringRes val strRes: Int) {
All(NetworkStats.Bucket.UID_ALL, R.string.all),
Removed(NetworkStats.Bucket.UID_REMOVED, R.string.uninstalled),
Tethering(NetworkStats.Bucket.UID_TETHERING, R.string.tethering)
}
@RequiresApi(23)
fun NetworkStats.toBucketList(): List<NetworkStats.Bucket> {
val list = mutableListOf<NetworkStats.Bucket>()
while(hasNextBucket()) {
val bucket = NetworkStats.Bucket()
if(getNextBucket(bucket)) list += bucket
}
close()
return list
}
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(23)
@Composable
fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) {
val context = LocalContext.current
val deviceOwner = context.isDeviceOwner
val nsm = context.getSystemService(NetworkStatsManager::class.java)
val coroutine = rememberCoroutineScope()
var activeTextField by remember { mutableStateOf(NetworkStatsActiveTextField.None) } //0:None, 1:Network type, 2:Start time, 3:End time
var queryType by rememberSaveable { mutableIntStateOf(1) } //1:Summary, 2:Details
var target by rememberSaveable { mutableStateOf(NetworkStatsTarget.Device) }
var networkType by rememberSaveable { mutableStateOf(NetworkType.Mobile) }
var subscriberId by rememberSaveable { mutableStateOf<String?>(null) }
var startTime by rememberSaveable { mutableLongStateOf(System.currentTimeMillis() - 7*24*60*60*1000) }
var endTime by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) }
var uid by rememberSaveable { mutableIntStateOf(NetworkStats.Bucket.UID_ALL) }
var tag by rememberSaveable { mutableIntStateOf(NetworkStats.Bucket.TAG_NONE) }
var state by rememberSaveable { mutableIntStateOf(NetworkStats.Bucket.STATE_ALL) }
val startTimeTextFieldInteractionSource = remember { MutableInteractionSource() }
val endTimeTextFieldInteractionSource = remember { MutableInteractionSource() }
if(startTimeTextFieldInteractionSource.collectIsPressedAsState().value) activeTextField = NetworkStatsActiveTextField.StartTime
if(endTimeTextFieldInteractionSource.collectIsPressedAsState().value) activeTextField = NetworkStatsActiveTextField.EndTime
MyScaffold(R.string.network_stats, 8.dp, navCtrl) {
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.Type,
{ activeTextField = if(it) NetworkStatsActiveTextField.Type else NetworkStatsActiveTextField.Type }
) {
val typeTextMap = mapOf(
1 to R.string.summary,
2 to R.string.details
)
OutlinedTextField(
value = stringResource(typeTextMap[queryType]!!), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.type)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.Type) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.Type, { activeTextField = NetworkStatsActiveTextField.None }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.summary)) },
onClick = {
queryType = 1
target = NetworkStatsTarget.Device
activeTextField = NetworkStatsActiveTextField.None
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.details)) },
onClick = {
queryType = 2
target = NetworkStatsTarget.Uid
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.Target,
{ activeTextField = if(it) NetworkStatsActiveTextField.Target else NetworkStatsActiveTextField.None }
) {
OutlinedTextField(
value = stringResource(target.strRes), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.target)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.Target) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.Target, { activeTextField = NetworkStatsActiveTextField.None }
) {
NetworkStatsTarget.entries.forEach {
if(
VERSION.SDK_INT >= it.minApi &&
(deviceOwner || it != NetworkStatsTarget.Device) &&
((queryType == 1 && (it == NetworkStatsTarget.Device || it == NetworkStatsTarget.User)) ||
(queryType == 2 && (it == NetworkStatsTarget.Uid || it == NetworkStatsTarget.UidTag || it == NetworkStatsTarget.UidTagState)))
) DropdownMenuItem(
text = { Text(stringResource(it.strRes)) },
onClick = {
target = it
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
}
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.NetworkType,
{ activeTextField = if(it) NetworkStatsActiveTextField.NetworkType else NetworkStatsActiveTextField.None }
) {
OutlinedTextField(
value = stringResource(networkType.strRes), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.network_type)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.NetworkType) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.NetworkType, { activeTextField = NetworkStatsActiveTextField.None }
) {
NetworkType.entries.forEach {
DropdownMenuItem(
text = { Text(stringResource(it.strRes)) },
onClick = {
networkType = it
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
}
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.SubscriberId,
{ activeTextField = if(it) NetworkStatsActiveTextField.SubscriberId else NetworkStatsActiveTextField.None }
) {
var readOnly by rememberSaveable { mutableStateOf(true) }
OutlinedTextField(
value = subscriberId ?: "null", onValueChange = { if(!readOnly) subscriberId = it }, readOnly = readOnly,
label = { Text(stringResource(R.string.subscriber_id)) },
isError = !readOnly && subscriberId.isNullOrBlank(),
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.SubscriberId) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.SubscriberId, { activeTextField = NetworkStatsActiveTextField.None }
) {
DropdownMenuItem(
text = { Text("null") },
onClick = {
readOnly = true
subscriberId = null
activeTextField = NetworkStatsActiveTextField.None
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.input)) },
onClick = {
readOnly = false
subscriberId = ""
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
OutlinedTextField(
value = startTime.let { if(it == -1L) "" else it.humanReadableDate }, onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.start_time)) },
interactionSource = startTimeTextFieldInteractionSource,
isError = startTime >= endTime,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
)
OutlinedTextField(
value = endTime.humanReadableDate, onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.end_time)) },
interactionSource = endTimeTextFieldInteractionSource,
isError = startTime >= endTime,
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp)
)
if(target == NetworkStatsTarget.Uid || target == NetworkStatsTarget.UidTag || target == NetworkStatsTarget.UidTagState)
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.Uid,
{ activeTextField = if(it) NetworkStatsActiveTextField.Uid else NetworkStatsActiveTextField.None }
) {
var uidText by rememberSaveable { mutableStateOf(context.getString(NetworkStatsUID.All.strRes)) }
var readOnly by rememberSaveable { mutableStateOf(true) }
if(!readOnly && uidText.toIntOrNull() != null) uid = uidText.toInt()
if(VERSION.SDK_INT >= 24) {
val selectedPackage by vm.selectedPackage.collectAsStateWithLifecycle()
if(readOnly && selectedPackage != "") {
try {
uid = context.packageManager.getPackageUid(selectedPackage, 0)
uidText = "$selectedPackage ($uid)"
} catch(_: NameNotFoundException) {
context.showOperationResultToast(false)
}
}
}
OutlinedTextField(
value = uidText, onValueChange = { if(!readOnly) uidText = it }, readOnly = readOnly,
label = { Text(stringResource(R.string.uid)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.Uid) },
isError = !readOnly && uidText.toIntOrNull() == null,
modifier = Modifier
.menuAnchor(if(readOnly) MenuAnchorType.PrimaryNotEditable else MenuAnchorType.PrimaryEditable)
.fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.Uid, { activeTextField = NetworkStatsActiveTextField.None }
) {
NetworkStatsUID.entries.forEach {
DropdownMenuItem(
text = { Text(stringResource(it.strRes)) },
onClick = {
uid = it.uid
readOnly = true
uidText = context.getString(it.strRes)
activeTextField = NetworkStatsActiveTextField.None
}
)
}
if(VERSION.SDK_INT >= 24) DropdownMenuItem(
text = { Text(stringResource(R.string.choose_an_app)) },
onClick = {
readOnly = true
navCtrl.navigate("PackageSelector")
activeTextField = NetworkStatsActiveTextField.None
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.input)) },
onClick = {
readOnly = false
uidText = ""
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
if(VERSION.SDK_INT >= 24 && (target == NetworkStatsTarget.UidTag || target == NetworkStatsTarget.UidTagState))
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.Tag,
{ activeTextField == if(it) NetworkStatsActiveTextField.Tag else NetworkStatsActiveTextField.None }
) {
var tagText by rememberSaveable { mutableStateOf(context.getString(R.string.all)) }
var readOnly by rememberSaveable { mutableStateOf(true) }
if(!readOnly && tagText.toIntOrNull() != null) tag = tagText.toInt()
OutlinedTextField(
value = tagText, onValueChange = { if(!readOnly) tagText = it }, readOnly = readOnly,
label = { Text(stringResource(R.string.uid)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.Tag) },
isError = !readOnly && tagText.toIntOrNull() == null,
modifier = Modifier
.menuAnchor(if(readOnly) MenuAnchorType.PrimaryNotEditable else MenuAnchorType.PrimaryEditable)
.fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.Tag, { activeTextField = NetworkStatsActiveTextField.None }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.all)) },
onClick = {
tag = NetworkStats.Bucket.TAG_NONE
tagText = context.getString(R.string.all)
readOnly = true
activeTextField = NetworkStatsActiveTextField.None
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.input)) },
onClick = {
tagText = ""
readOnly = false
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
if(VERSION.SDK_INT >= 28 && target == NetworkStatsTarget.UidTagState)
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.State,
{ activeTextField = if(it) NetworkStatsActiveTextField.State else NetworkStatsActiveTextField.None }
) {
val textMap = mapOf(
NetworkStats.Bucket.STATE_ALL to R.string.all,
NetworkStats.Bucket.STATE_DEFAULT to R.string.default_str,
NetworkStats.Bucket.STATE_FOREGROUND to R.string.foreground
)
OutlinedTextField(
value = stringResource(textMap[state]!!), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.uid)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.State) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.State, { activeTextField = NetworkStatsActiveTextField.None }
) {
textMap.forEach {
DropdownMenuItem(
text = { Text(stringResource(it.value)) },
onClick = {
state = it.key
activeTextField = NetworkStatsActiveTextField.None
}
)
}
}
}
var querying by rememberSaveable { mutableStateOf(false) }
Button(
onClick = {
querying = true
coroutine.launch {
val buckets = try {
if(queryType == 1) {
if(target == NetworkStatsTarget.Device)
listOf(nsm.querySummaryForDevice(networkType.type, subscriberId, startTime, endTime))
else listOf(nsm.querySummaryForUser(networkType.type, subscriberId, startTime, endTime))
} else {
if(target == NetworkStatsTarget.Uid)
nsm.queryDetailsForUid(networkType.type, subscriberId, startTime, endTime, uid).toBucketList()
else if(target == NetworkStatsTarget.UidTag)
nsm.queryDetailsForUidTag(networkType.type, subscriberId, startTime, endTime, uid, tag).toBucketList()
else nsm.queryDetailsForUidTagState(networkType.type, subscriberId, startTime, endTime, uid, tag, state).toBucketList()
}
} catch(e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
querying = false
AlertDialog.Builder(context)
.setTitle(R.string.error)
.setMessage(e.message ?: "")
.setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() }
.show()
}
return@launch
}
if(buckets.isEmpty()) {
withContext(Dispatchers.Main) {
querying = false
context.showOperationResultToast(false)
}
} else {
val bundle = Bundle()
bundle.putInt("size", buckets.size)
buckets.forEachIndexed { index, bucket ->
val subBundle = bundleOf(
"rx_bytes" to bucket.rxBytes,
"rx_packets" to bucket.rxPackets,
"tx_bytes" to bucket.txBytes,
"tx_packets" to bucket.txPackets,
"uid" to bucket.uid,
"state" to bucket.state,
"start_time" to bucket.startTimeStamp,
"end_time" to bucket.endTimeStamp
)
if(VERSION.SDK_INT >= 24) {
subBundle.putInt("tag", bucket.tag)
subBundle.putInt("roaming", bucket.roaming)
}
if(VERSION.SDK_INT >= 26) subBundle.putInt("metered", bucket.metered)
bundle.putBundle(index.toString(), subBundle)
}
withContext(Dispatchers.Main) {
querying = false
val nodeId = navCtrl.graph.findNode("NetworkStatsViewer")?.id
if(nodeId != null) navCtrl.navigate(nodeId, bundle)
}
}
}
},
enabled = !querying,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
) {
Text(stringResource(R.string.query))
}
if(activeTextField == NetworkStatsActiveTextField.StartTime || activeTextField == NetworkStatsActiveTextField.EndTime) {
val datePickerState = rememberDatePickerState(if(activeTextField == NetworkStatsActiveTextField.StartTime) startTime else endTime)
DatePickerDialog(
onDismissRequest = { activeTextField = NetworkStatsActiveTextField.None },
dismissButton = {
TextButton(onClick = { activeTextField = NetworkStatsActiveTextField.None }) {
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(
onClick = {
if(activeTextField == NetworkStatsActiveTextField.StartTime) startTime = datePickerState.selectedDateMillis!!
else endTime = datePickerState.selectedDateMillis!!
activeTextField = NetworkStatsActiveTextField.None
},
enabled = datePickerState.selectedDateMillis != null
) {
Text(stringResource(R.string.confirm))
}
}
) {
DatePicker(datePickerState)
}
}
}
}
@RequiresApi(23)
@Composable
fun NetworkStatsViewer(navCtrl: NavHostController, navArgs: Bundle) {
var index by remember { mutableIntStateOf(0) }
val size = navArgs.getInt("size", 1)
MyScaffold(R.string.place_holder, 8.dp, navCtrl, false) {
if(size > 1) Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp)
) {
IconButton(
onClick = { index -= 1 },
enabled = index > 0
) {
Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft, contentDescription = null)
}
Text("${index + 1} / $size", modifier = Modifier.padding(horizontal = 8.dp))
IconButton(
onClick = { index += 1 },
enabled = index < size - 1
) {
Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, contentDescription = null)
}
}
val data = navArgs.getBundle(index.toString())!!
Text(
data.getLong("start_time").humanReadableDate + " ~ " + data.getLong("end_time").humanReadableDate,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp)
)
val txBytes = data.getLong("tx_bytes")
Text(stringResource(R.string.transmitted), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) {
Text("$txBytes bytes")
Text(formatFileSize(txBytes))
Text(data.getLong("tx_packets").toString() + " packets")
}
val rxBytes = data.getLong("rx_bytes")
Text(stringResource(R.string.received), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) {
Text("$rxBytes bytes")
Text(formatFileSize(rxBytes))
Text(data.getLong("rx_packets").toString() + " packets")
}
Row(verticalAlignment = Alignment.CenterVertically) {
val textMap = mapOf(
NetworkStats.Bucket.STATE_ALL to R.string.all,
NetworkStats.Bucket.STATE_DEFAULT to R.string.default_str,
NetworkStats.Bucket.STATE_FOREGROUND to R.string.foreground
)
Text(stringResource(R.string.state), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(textMap[data.getInt("state")] ?: R.string.unknown))
}
if(VERSION.SDK_INT >= 24) {
Row(verticalAlignment = Alignment.CenterVertically) {
val tag = data.getInt("tag")
Text(stringResource(R.string.tag), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(if(tag == NetworkStats.Bucket.TAG_NONE) stringResource(R.string.all) else tag.toString())
}
Row(verticalAlignment = Alignment.CenterVertically) {
val textMap = mapOf(
NetworkStats.Bucket.ROAMING_ALL to R.string.all,
NetworkStats.Bucket.ROAMING_YES to R.string.yes,
NetworkStats.Bucket.ROAMING_NO to R.string.no
)
Text(stringResource(R.string.roaming), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(textMap[data.getInt("roaming")] ?: R.string.unknown))
}
}
if(VERSION.SDK_INT >= 26) Row(verticalAlignment = Alignment.CenterVertically) {
val textMap = mapOf(
NetworkStats.Bucket.METERED_ALL to R.string.all,
NetworkStats.Bucket.METERED_YES to R.string.yes,
NetworkStats.Bucket.METERED_NO to R.string.no
)
Text(stringResource(R.string.metered), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(textMap[data.getInt("metered")] ?: R.string.unknown))
}
}
}
@RequiresApi(29)
@Composable
fun PrivateDNS(navCtrl: NavHostController) {
@@ -1556,7 +2064,7 @@ fun OverrideAPN(navCtrl: NavHostController) {
RadioButtonItem("GID", mvnoType == MVNO_TYPE_GID) { mvnoType = MVNO_TYPE_GID }
RadioButtonItem("ICCID", mvnoType == MVNO_TYPE_ICCID) { mvnoType = MVNO_TYPE_ICCID }
Text(text = stringResource(R.string.network_type), style = typography.titleLarge)
Text(text = stringResource(R.string.apn_network_type), style = typography.titleLarge)
TextField(
value = networkTypeBitmask,
onValueChange = { networkTypeBitmask=it },

View File

@@ -314,15 +314,10 @@ fun MyScaffold(
}
@Composable
fun UpOrDownTextFieldTrailingIconButton(active: Boolean, onClick: () -> Unit) {
fun ExpandExposedTextFieldIcon(active: Boolean) {
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)
)
}
}

View File

@@ -0,0 +1,9 @@
<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="m105,561 l-65,-47 200,-320 120,140 160,-260 120,180 135,-214 65,47 -198,314 -119,-179 -152,247 -121,-141 -145,233ZM580,720q42,0 71,-29t29,-71q0,-42 -29,-71t-71,-29q-42,0 -71,29t-29,71q0,42 29,71t71,29ZM784,880 L676,772q-21,14 -45.5,21t-50.5,7q-75,0 -127.5,-52.5T400,620q0,-75 52.5,-127.5T580,440q75,0 127.5,52.5T760,620q0,26 -7,50.5T732,716l108,108 -56,56Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -252,6 +252,33 @@
<string name="wifi_ssid_policy">Политика SSID Wi-Fi</string>
<string name="ssid_list_is">Список SSID:</string>
<string name="already_exist">Уже существует</string>
<!--TODO: The following 26 strings-->
<string name="network_stats">Network stats</string>
<string name="type">Type</string>
<string name="summary">Summary</string>
<string name="details">Details</string>
<string name="target">Target</string>
<string name="device">Device</string>
<string name="user">User</string>
<string name="uid_tag">UID tag</string>
<string name="uid_tag_state">UID tag state</string>
<string name="network_type">Network type</string>
<string name="mobile">Mobile</string>
<string name="ethernet">Ethernet</string>
<string name="subscriber_id">Subscriber ID</string>
<string name="all">All</string>
<string name="uninstalled">Uninstalled</string>
<string name="tethering">Tethering</string>
<string name="choose_an_app" tools:ignore="TypographyEllipsis">Choose an app...</string>
<string name="input">Input</string>
<string name="query">Query</string>
<string name="transmitted">Transmitted</string>
<string name="received">Received</string>
<string name="state">State</string>
<string name="foreground">Foreground</string>
<string name="tag">Tag</string>
<string name="roaming">Roaming</string>
<string name="metered">Metered</string>
<string name="private_dns">Частный DNS</string>
<string name="dns_provide_hostname">Укажите имя хоста</string>
<string name="host_not_serving_dns_tls">Хост не обслуживает DNS через TLS</string>
@@ -299,7 +326,7 @@
<string name="address">Адрес</string>
<string name="port">Порт</string>
<string name="proxy">Прокси</string>
<string name="network_type">Тип сети</string>
<string name="apn_network_type">Тип сети</string>
<string name="persistent">Постоянный</string>
<string name="protocol">Протокол</string>
<string name="roaming_protocol">Протокол роуминга</string>

View File

@@ -253,6 +253,33 @@
<string name="wifi_ssid_policy">Wi-Fi SSID politikası</string>
<string name="ssid_list_is">SSID listesi:</string>
<string name="already_exist">Zaten mevcut</string>
<!--TODO: The following 26 strings-->
<string name="network_stats">Network stats</string>
<string name="type">Type</string>
<string name="summary">Summary</string>
<string name="details">Details</string>
<string name="target">Target</string>
<string name="device">Device</string>
<string name="user">User</string>
<string name="uid_tag">UID tag</string>
<string name="uid_tag_state">UID tag state</string>
<string name="network_type">Network type</string>
<string name="mobile">Mobile</string>
<string name="ethernet">Ethernet</string>
<string name="subscriber_id">Subscriber ID</string>
<string name="all">All</string>
<string name="uninstalled">Uninstalled</string>
<string name="tethering">Tethering</string>
<string name="choose_an_app" tools:ignore="TypographyEllipsis">Choose an app...</string>
<string name="input">Input</string>
<string name="query">Query</string>
<string name="transmitted">Transmitted</string>
<string name="received">Received</string>
<string name="state">State</string>
<string name="foreground">Foreground</string>
<string name="tag">Tag</string>
<string name="roaming">Roaming</string>
<string name="metered">Metered</string>
<string name="private_dns">Özel DNS</string>
<string name="dns_provide_hostname">Ana bilgisayar adı sağlayın</string>
<string name="host_not_serving_dns_tls">Ana bilgisayar hizmet vermiyor</string>
@@ -300,7 +327,7 @@
<string name="address">Adres</string>
<string name="port">Port</string>
<string name="proxy">Proxy</string>
<string name="network_type">Ağ türü</string>
<string name="apn_network_type">Ağ türü</string>
<string name="persistent">Kalıcı</string>
<string name="protocol">Protokol</string>
<string name="roaming_protocol">Dolaşım protokolü</string>

View File

@@ -244,6 +244,32 @@
<string name="wifi_ssid_policy">Wi-Fi SSID策略</string>
<string name="ssid_list_is">SSID列表</string>
<string name="already_exist">已经存在</string>
<string name="network_stats">Network stats</string>
<string name="type">类型</string>
<string name="summary">摘要</string>
<string name="details">详情</string>
<string name="target">目标</string>
<string name="device">设备</string>
<string name="user">用户</string>
<string name="uid_tag">UID 标签</string>
<string name="uid_tag_state">UID 标签 状态</string>
<string name="network_type">网络类型</string>
<string name="mobile">移动</string>
<string name="ethernet">以太网</string>
<string name="subscriber_id">订阅者ID</string>
<string name="all">全部</string>
<string name="uninstalled">已卸载</string>
<string name="tethering">热点</string>
<string name="choose_an_app" tools:ignore="TypographyEllipsis">选择一个app...</string>
<string name="input">输入</string>
<string name="query">查询</string>
<string name="transmitted">发送</string>
<string name="received">接收</string>
<string name="state">状态</string>
<string name="foreground">前台</string>
<string name="tag">标签</string>
<string name="roaming">漫游</string>
<string name="metered">按量计费</string>
<string name="private_dns">私人DNS</string>
<string name="dns_provide_hostname">指定主机名</string>
<string name="host_not_serving_dns_tls">主机不支持</string>
@@ -291,7 +317,7 @@
<string name="address">地址</string>
<string name="port">端口</string>
<string name="proxy">代理</string>
<string name="network_type">网络类型</string>
<string name="apn_network_type">网络类型</string>
<string name="persistent">持久的</string>
<string name="protocol">协议</string>
<string name="roaming_protocol">漫游协议</string>

View File

@@ -70,6 +70,7 @@
<string name="edit">Edit</string>
<string name="overview">Overview</string>
<string name="features">Features</string>
<string name="default_str">Default</string>
<!--Permissions-->
<string name="click_to_activate">Click to activate</string>
@@ -276,6 +277,34 @@
<string name="wifi_ssid_policy">Wi-Fi SSID policy</string>
<string name="ssid_list_is">SSID list:</string>
<string name="already_exist">Already exist</string>
<string name="network_stats">Network stats</string>
<string name="type">Type</string>
<string name="summary">Summary</string>
<string name="details">Details</string>
<string name="target">Target</string>
<string name="device">Device</string>
<string name="user">User</string>
<string name="uid" translatable="false">UID</string>
<string name="uid_tag">UID tag</string>
<string name="uid_tag_state">UID tag state</string>
<string name="network_type">Network type</string>
<string name="mobile">Mobile</string>
<string name="ethernet">Ethernet</string>
<string name="vpn" translatable="false">VPN</string>
<string name="subscriber_id">Subscriber ID</string>
<string name="all">All</string>
<string name="uninstalled">Uninstalled</string>
<string name="tethering">Tethering</string>
<string name="choose_an_app" tools:ignore="TypographyEllipsis">Choose an app...</string>
<string name="input">Input</string>
<string name="query">Query</string>
<string name="transmitted">Transmitted</string>
<string name="received">Received</string>
<string name="state">State</string>
<string name="foreground">Foreground</string>
<string name="tag">Tag</string>
<string name="roaming">Roaming</string>
<string name="metered">Metered</string>
<string name="private_dns">Private DNS</string>
<string name="dns_provide_hostname">Provide hostname</string>
<string name="host_not_serving_dns_tls">Host not serving</string>
@@ -323,7 +352,7 @@
<string name="address">Address</string>
<string name="port">Port</string>
<string name="proxy">Proxy</string>
<string name="network_type">Network type</string>
<string name="apn_network_type">Network type</string>
<string name="persistent">Persistent</string>
<string name="protocol">Protocol</string>
<string name="roaming_protocol">Roaming protocol</string>