mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 19:15:58 +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.Network
|
||||||
import com.bintianqi.owndroid.dpm.NetworkLogging
|
import com.bintianqi.owndroid.dpm.NetworkLogging
|
||||||
import com.bintianqi.owndroid.dpm.NetworkOptions
|
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.OrgOwnedProfile
|
||||||
import com.bintianqi.owndroid.dpm.OverrideAPN
|
import com.bintianqi.owndroid.dpm.OverrideAPN
|
||||||
import com.bintianqi.owndroid.dpm.Password
|
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 = "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 = "NetworkStats") { NetworkStats(navCtrl, vm) }
|
||||||
|
composable(route = "NetworkStatsViewer") { NetworkStatsViewer(navCtrl, it.arguments!!) }
|
||||||
composable(route = "PrivateDNS") { PrivateDNS(navCtrl) }
|
composable(route = "PrivateDNS") { PrivateDNS(navCtrl) }
|
||||||
composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl, vm) }
|
composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl, vm) }
|
||||||
composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy(navCtrl) }
|
composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy(navCtrl) }
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ import android.app.admin.PreferentialNetworkServiceConfig
|
|||||||
import android.app.admin.WifiSsidPolicy
|
import android.app.admin.WifiSsidPolicy
|
||||||
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_ALLOWLIST
|
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_ALLOWLIST
|
||||||
import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_DENYLIST
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.net.IpConfiguration
|
import android.net.IpConfiguration
|
||||||
import android.net.LinkAddress
|
import android.net.LinkAddress
|
||||||
import android.net.ProxyInfo
|
import android.net.ProxyInfo
|
||||||
@@ -51,10 +54,13 @@ import android.widget.Toast
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.StringRes
|
||||||
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.background
|
||||||
import androidx.compose.foundation.clickable
|
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.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
|
||||||
@@ -80,6 +86,8 @@ 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.ButtonDefaults
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.DatePickerDialog
|
||||||
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
|
||||||
@@ -97,6 +105,7 @@ 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.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
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
|
||||||
@@ -121,12 +130,15 @@ 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.core.os.bundleOf
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
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
|
||||||
import com.bintianqi.owndroid.formatFileSize
|
import com.bintianqi.owndroid.formatFileSize
|
||||||
|
import com.bintianqi.owndroid.humanReadableDate
|
||||||
import com.bintianqi.owndroid.showOperationResultToast
|
import com.bintianqi.owndroid.showOperationResultToast
|
||||||
import com.bintianqi.owndroid.ui.CheckBoxItem
|
import com.bintianqi.owndroid.ui.CheckBoxItem
|
||||||
|
import com.bintianqi.owndroid.ui.ExpandExposedTextFieldIcon
|
||||||
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
|
||||||
@@ -134,12 +146,13 @@ import com.bintianqi.owndroid.ui.MyScaffold
|
|||||||
import com.bintianqi.owndroid.ui.NavIcon
|
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.writeClipBoard
|
import com.bintianqi.owndroid.writeClipBoard
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
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
|
||||||
@@ -158,6 +171,8 @@ fun Network(navCtrl:NavHostController) {
|
|||||||
if(VERSION.SDK_INT >= 30) {
|
if(VERSION.SDK_INT >= 30) {
|
||||||
FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") }
|
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) {
|
if(VERSION.SDK_INT >= 29 && deviceOwner) {
|
||||||
FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") }
|
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(
|
OutlinedTextField(
|
||||||
value = stringResource(statusText), onValueChange = {}, readOnly = true,
|
value = stringResource(statusText), onValueChange = {}, readOnly = true,
|
||||||
label = { Text(stringResource(R.string.status)) },
|
label = { Text(stringResource(R.string.status)) },
|
||||||
trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 1) {} },
|
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 1) },
|
||||||
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 }) {
|
||||||
@@ -530,7 +545,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo
|
|||||||
ExposedDropdownMenuBox(dropdownMenu == 2, { dropdownMenu = if(it) 2 else 0 }) {
|
ExposedDropdownMenuBox(dropdownMenu == 2, { dropdownMenu = if(it) 2 else 0 }) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = securityTypeTextMap[securityType] ?: "", onValueChange = {}, label = { Text(stringResource(R.string.security)) },
|
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)
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(vertical = 4.dp)
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(dropdownMenu == 2, { dropdownMenu = 0 }) {
|
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),
|
value = stringResource(macRandomizationSettingTextMap[macRandomizationSetting] ?: R.string.place_holder),
|
||||||
onValueChange = {}, readOnly = true,
|
onValueChange = {}, readOnly = true,
|
||||||
label = { Text(stringResource(R.string.mac_randomization)) },
|
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)
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 8.dp)
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(dropdownMenu == 3, { dropdownMenu = 0 }) {
|
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",
|
value = if(useStaticIp) stringResource(R.string.static_str) else "DHCP",
|
||||||
onValueChange = {}, readOnly = true,
|
onValueChange = {}, readOnly = true,
|
||||||
label = { Text(stringResource(R.string.ip_settings)) },
|
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)
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(dropdownMenu == 4, { dropdownMenu = 0 }) {
|
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),
|
value = if(useHttpProxy) "HTTP" else stringResource(R.string.none),
|
||||||
onValueChange = {}, readOnly = true,
|
onValueChange = {}, readOnly = true,
|
||||||
label = { Text(stringResource(R.string.proxy)) },
|
label = { Text(stringResource(R.string.proxy)) },
|
||||||
trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 5) {} },
|
trailingIcon = { ExpandExposedTextFieldIcon(dropdownMenu == 5) },
|
||||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
|
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp)
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(dropdownMenu == 5, { dropdownMenu = 0 }) {
|
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)
|
@RequiresApi(29)
|
||||||
@Composable
|
@Composable
|
||||||
fun PrivateDNS(navCtrl: NavHostController) {
|
fun PrivateDNS(navCtrl: NavHostController) {
|
||||||
@@ -1556,7 +2064,7 @@ fun OverrideAPN(navCtrl: NavHostController) {
|
|||||||
RadioButtonItem("GID", mvnoType == MVNO_TYPE_GID) { mvnoType = MVNO_TYPE_GID }
|
RadioButtonItem("GID", mvnoType == MVNO_TYPE_GID) { mvnoType = MVNO_TYPE_GID }
|
||||||
RadioButtonItem("ICCID", mvnoType == MVNO_TYPE_ICCID) { mvnoType = MVNO_TYPE_ICCID }
|
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(
|
TextField(
|
||||||
value = networkTypeBitmask,
|
value = networkTypeBitmask,
|
||||||
onValueChange = { networkTypeBitmask=it },
|
onValueChange = { networkTypeBitmask=it },
|
||||||
|
|||||||
@@ -314,15 +314,10 @@ fun MyScaffold(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpOrDownTextFieldTrailingIconButton(active: Boolean, onClick: () -> Unit) {
|
fun ExpandExposedTextFieldIcon(active: Boolean) {
|
||||||
val degrees by animateFloatAsState(if(active) 180F else 0F)
|
val degrees by animateFloatAsState(if(active) 180F else 0F)
|
||||||
IconButton(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = Modifier.clip(RoundedCornerShape(50))
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowDropDown, contentDescription = null,
|
imageVector = Icons.Default.ArrowDropDown, contentDescription = null,
|
||||||
modifier = Modifier.rotate(degrees)
|
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="wifi_ssid_policy">Политика SSID Wi-Fi</string>
|
||||||
<string name="ssid_list_is">Список SSID:</string>
|
<string name="ssid_list_is">Список SSID:</string>
|
||||||
<string name="already_exist">Уже существует</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="private_dns">Частный DNS</string>
|
||||||
<string name="dns_provide_hostname">Укажите имя хоста</string>
|
<string name="dns_provide_hostname">Укажите имя хоста</string>
|
||||||
<string name="host_not_serving_dns_tls">Хост не обслуживает DNS через TLS</string>
|
<string name="host_not_serving_dns_tls">Хост не обслуживает DNS через TLS</string>
|
||||||
@@ -299,7 +326,7 @@
|
|||||||
<string name="address">Адрес</string>
|
<string name="address">Адрес</string>
|
||||||
<string name="port">Порт</string>
|
<string name="port">Порт</string>
|
||||||
<string name="proxy">Прокси</string>
|
<string name="proxy">Прокси</string>
|
||||||
<string name="network_type">Тип сети</string>
|
<string name="apn_network_type">Тип сети</string>
|
||||||
<string name="persistent">Постоянный</string>
|
<string name="persistent">Постоянный</string>
|
||||||
<string name="protocol">Протокол</string>
|
<string name="protocol">Протокол</string>
|
||||||
<string name="roaming_protocol">Протокол роуминга</string>
|
<string name="roaming_protocol">Протокол роуминга</string>
|
||||||
|
|||||||
@@ -253,6 +253,33 @@
|
|||||||
<string name="wifi_ssid_policy">Wi-Fi SSID politikası</string>
|
<string name="wifi_ssid_policy">Wi-Fi SSID politikası</string>
|
||||||
<string name="ssid_list_is">SSID listesi:</string>
|
<string name="ssid_list_is">SSID listesi:</string>
|
||||||
<string name="already_exist">Zaten mevcut</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="private_dns">Özel DNS</string>
|
||||||
<string name="dns_provide_hostname">Ana bilgisayar adı sağlayın</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>
|
<string name="host_not_serving_dns_tls">Ana bilgisayar hizmet vermiyor</string>
|
||||||
@@ -300,7 +327,7 @@
|
|||||||
<string name="address">Adres</string>
|
<string name="address">Adres</string>
|
||||||
<string name="port">Port</string>
|
<string name="port">Port</string>
|
||||||
<string name="proxy">Proxy</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="persistent">Kalıcı</string>
|
||||||
<string name="protocol">Protokol</string>
|
<string name="protocol">Protokol</string>
|
||||||
<string name="roaming_protocol">Dolaşım 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="wifi_ssid_policy">Wi-Fi SSID策略</string>
|
||||||
<string name="ssid_list_is">SSID列表:</string>
|
<string name="ssid_list_is">SSID列表:</string>
|
||||||
<string name="already_exist">已经存在</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="private_dns">私人DNS</string>
|
||||||
<string name="dns_provide_hostname">指定主机名</string>
|
<string name="dns_provide_hostname">指定主机名</string>
|
||||||
<string name="host_not_serving_dns_tls">主机不支持</string>
|
<string name="host_not_serving_dns_tls">主机不支持</string>
|
||||||
@@ -291,7 +317,7 @@
|
|||||||
<string name="address">地址</string>
|
<string name="address">地址</string>
|
||||||
<string name="port">端口</string>
|
<string name="port">端口</string>
|
||||||
<string name="proxy">代理</string>
|
<string name="proxy">代理</string>
|
||||||
<string name="network_type">网络类型</string>
|
<string name="apn_network_type">网络类型</string>
|
||||||
<string name="persistent">持久的</string>
|
<string name="persistent">持久的</string>
|
||||||
<string name="protocol">协议</string>
|
<string name="protocol">协议</string>
|
||||||
<string name="roaming_protocol">漫游协议</string>
|
<string name="roaming_protocol">漫游协议</string>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="overview">Overview</string>
|
<string name="overview">Overview</string>
|
||||||
<string name="features">Features</string>
|
<string name="features">Features</string>
|
||||||
|
<string name="default_str">Default</string>
|
||||||
|
|
||||||
<!--Permissions-->
|
<!--Permissions-->
|
||||||
<string name="click_to_activate">Click to activate</string>
|
<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="wifi_ssid_policy">Wi-Fi SSID policy</string>
|
||||||
<string name="ssid_list_is">SSID list:</string>
|
<string name="ssid_list_is">SSID list:</string>
|
||||||
<string name="already_exist">Already exist</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="private_dns">Private DNS</string>
|
||||||
<string name="dns_provide_hostname">Provide hostname</string>
|
<string name="dns_provide_hostname">Provide hostname</string>
|
||||||
<string name="host_not_serving_dns_tls">Host not serving</string>
|
<string name="host_not_serving_dns_tls">Host not serving</string>
|
||||||
@@ -323,7 +352,7 @@
|
|||||||
<string name="address">Address</string>
|
<string name="address">Address</string>
|
||||||
<string name="port">Port</string>
|
<string name="port">Port</string>
|
||||||
<string name="proxy">Proxy</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="persistent">Persistent</string>
|
||||||
<string name="protocol">Protocol</string>
|
<string name="protocol">Protocol</string>
|
||||||
<string name="roaming_protocol">Roaming protocol</string>
|
<string name="roaming_protocol">Roaming protocol</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user