mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
Query network stats
This commit is contained in:
@@ -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) }
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown, contentDescription = null,
|
||||
modifier = Modifier.rotate(degrees)
|
||||
)
|
||||
}
|
||||
|
||||
9
app/src/main/res/drawable/query_stats_fill0.xml
Normal file
9
app/src/main/res/drawable/query_stats_fill0.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user