Remove Request uninstall feature

Scroll between pages in Network stats viewer
This commit is contained in:
BinTianqi
2025-03-15 17:54:12 +08:00
parent 3c0696faa3
commit 52a29331be
9 changed files with 158 additions and 138 deletions

View File

@@ -67,7 +67,7 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) }
FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) }
FunctionItem(R.string.app_lock, icon = R.drawable.lock_fill0) { onNavigate(AppLockSettings) }
FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { onNavigate(ApiSettings) }
FunctionItem(title = R.string.api, icon = R.drawable.code_fill0) { onNavigate(ApiSettings) }
FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) }
FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) {
val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis()))

View File

@@ -100,6 +100,9 @@ val Long.humanReadableDate: String
val Long.humanReadableDateTime: String
get() = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(this))
fun formatDate(pattern: String, value: Long): String
= SimpleDateFormat(pattern, Locale.getDefault()).format(Date(value))
fun Context.showOperationResultToast(success: Boolean) {
Toast.makeText(this, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show()
}

View File

@@ -90,6 +90,7 @@ import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.ErrorDialog
import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.ListItem
import com.bintianqi.owndroid.ui.NavIcon
@@ -157,7 +158,6 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) {
composable<PermittedAccessibilityServices> { PermittedAccessibilityServicesScreen(pkgName) }
composable<PermittedInputMethods> { PermittedInputMethodsScreen(pkgName) }
composable<KeepUninstalledPackages> { KeepUninstalledPackagesScreen(pkgName) }
composable<UninstallPackage> { UninstallPackageScreen(pkgName) }
}
}
}
@@ -166,8 +166,9 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) {
@Composable
private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
/** 1:Enable system app, 2:Clear app storage, 3:Set default dialer, 4:App control, 5:Install existing app */
/** 1:Enable system app, 2:Clear app storage, 3:Set default dialer, 4:App control, 5:Install existing app, 6:Uninstall confirmation */
var dialogStatus by remember { mutableIntStateOf(0) }
var errorMessage by remember { mutableStateOf("") }
val context = LocalContext.current
val dpm = context.getDPM()
val receiver = context.getReceiver()
@@ -271,8 +272,12 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
Toast.makeText(context, R.string.choose_apk_file, Toast.LENGTH_SHORT).show()
chooseApks.launch(APK_MIME)
}
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) { dialogStatus = 5 }
FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { onNavigate(UninstallPackage) }
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) {
if(pkgName.isNotBlank()) dialogStatus = 5
}
FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) {
if(pkgName.isNotBlank()) dialogStatus = 6
}
if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) {
if(pkgName != "") dialogStatus = 3
@@ -428,6 +433,27 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
},
onDismissRequest = { dialogStatus = 0 }
)
if(dialogStatus == 6) AlertDialog(
title = { Text(stringResource(R.string.uninstall)) },
text = { Text(pkgName) },
onDismissRequest = { dialogStatus = 0 },
confirmButton = {
TextButton(
onClick = { uninstallPackage(context, pkgName) { errorMessage = it } },
colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error)
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton({
dialogStatus = 0
}) {
Text(stringResource(R.string.cancel))
}
}
)
ErrorDialog(errorMessage) { errorMessage = "" }
LaunchedEffect(dialogStatus) { focusMgr.clearFocus() }
}
@@ -706,7 +732,7 @@ private fun CrossProfileWidgetProvidersScreen(pkgName: String) {
@RequiresApi(34)
@Composable
private fun CredentialManagerPolicyScreen(pkgName: String) { // TODO: rename "manage" to "manager"
private fun CredentialManagerPolicyScreen(pkgName: String) {
val context = LocalContext.current
val dpm = context.getDPM()
var policy: PackagePolicy?
@@ -940,74 +966,34 @@ private fun KeepUninstalledPackagesScreen(pkgName: String) {
}
}
@Serializable private object UninstallPackage
@Composable
private fun UninstallPackageScreen(pkgName: String) {
val context = LocalContext.current
var errorMessage by remember { mutableStateOf<String?>(null) }
Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.uninstall_app), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
Column(modifier = Modifier.fillMaxWidth()) {
Button(
onClick = {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
if(statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
@SuppressWarnings("UnsafeIntentLaunch")
context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
} else {
context.unregisterReceiver(this)
if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
context.showOperationResultToast(true)
} else {
errorMessage = parsePackageInstallerMessage(context, intent)
}
}
}
}
ContextCompat.registerReceiver(
context, receiver, IntentFilter(AppInstallerViewModel.ACTION), null,
null, ContextCompat.RECEIVER_EXPORTED
)
val pi = if(VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
context, 0, Intent(AppInstallerViewModel.ACTION),
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
PendingIntent.getBroadcast(context, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
}
context.getPackageInstaller().uninstall(pkgName, pi)
},
enabled = pkgName != "",
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.silent_uninstall))
}
Button(
onClick = {
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE)
intent.setData(Uri.parse("package:$pkgName"))
context.startActivity(intent)
},
enabled = pkgName != "",
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.request_uninstall))
private fun uninstallPackage(context: Context, packageName: String, onError: (String) -> Unit) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
if(statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
@SuppressWarnings("UnsafeIntentLaunch")
context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
} else {
context.unregisterReceiver(this)
if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
context.showOperationResultToast(true)
} else {
onError(parsePackageInstallerMessage(context, intent))
}
}
}
}
if(errorMessage != null) AlertDialog(
title = { Text(stringResource(R.string.failure)) },
text = { Text(errorMessage!!) },
confirmButton = {
TextButton({ errorMessage = null }) { Text(stringResource(R.string.confirm)) }
},
onDismissRequest = { errorMessage = null }
ContextCompat.registerReceiver(
context, receiver, IntentFilter(AppInstallerViewModel.ACTION), null,
null, ContextCompat.RECEIVER_EXPORTED
)
val pi = if(VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
context, 0, Intent(AppInstallerViewModel.ACTION),
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
PendingIntent.getBroadcast(context, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
}
context.getPackageInstaller().uninstall(packageName, pi)
}

View File

@@ -88,6 +88,7 @@ import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
@@ -122,12 +123,14 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import com.bintianqi.owndroid.ChoosePackageContract
import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SharedPrefs
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.humanReadableDateTime
@@ -153,6 +156,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import java.net.InetAddress
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.reflect.jvm.jvmErasure
@Serializable object Network
@@ -917,7 +923,8 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta
MyScaffold(R.string.network_stats, onNavigateUp) {
ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.Type,
{ activeTextField = if(it) NetworkStatsActiveTextField.Type else NetworkStatsActiveTextField.Type }
{ activeTextField = if(it) NetworkStatsActiveTextField.Type else NetworkStatsActiveTextField.Type },
Modifier.padding(top = 8.dp, bottom = 4.dp)
) {
val typeTextMap = mapOf(
1 to R.string.summary,
@@ -927,7 +934,7 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta
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)
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth()
)
ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.Type, { activeTextField = NetworkStatsActiveTextField.None }
@@ -1185,7 +1192,7 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta
Button(
onClick = {
querying = true
coroutine.launch {
coroutine.launch(Dispatchers.IO) {
val buckets = try {
@Suppress("NewApi") if(queryType == 1) {
if(target == NetworkStatsTarget.Device)
@@ -1287,79 +1294,102 @@ data class NetworkStatsViewer(
fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit) {
var index by remember { mutableIntStateOf(0) }
val size = nsv.stats.size
MySmallTitleScaffold(R.string.network_stats, onNavigateUp) {
val ps = rememberPagerState { size }
index = ps.currentPage
val coroutine = rememberCoroutineScope()
MySmallTitleScaffold(R.string.network_stats, onNavigateUp, 0.dp) {
if(size > 1) Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp)
modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 8.dp)
) {
IconButton(
onClick = { index -= 1 },
onClick = {
coroutine.launch {
ps.animateScrollToPage(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 },
onClick = {
coroutine.launch {
ps.animateScrollToPage(index + 1)
}
},
enabled = index < size - 1
) {
Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, contentDescription = null)
}
}
val data = nsv.stats[index]
Text(
data.startTime.humanReadableDateTime + " ~ " + data.endTime.humanReadableDateTime,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp)
)
val txBytes = data.txBytes
Text(stringResource(R.string.transmitted), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) {
Text("$txBytes bytes (${formatFileSize(txBytes)})")
Text(data.txPackets.toString() + " packets")
}
val rxBytes = data.rxBytes
Text(stringResource(R.string.received), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) {
Text("$rxBytes bytes (${formatFileSize(rxBytes)})")
Text(data.rxPackets.toString() + " packets")
}
Row(verticalAlignment = Alignment.CenterVertically) {
val text = when(data.state) {
NetworkStats.Bucket.STATE_ALL -> R.string.all
NetworkStats.Bucket.STATE_DEFAULT -> R.string.default_str
NetworkStats.Bucket.STATE_FOREGROUND -> R.string.foreground
else -> R.string.unknown
}
Text(stringResource(R.string.state), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(text))
}
if(VERSION.SDK_INT >= 24) {
Row(verticalAlignment = Alignment.CenterVertically) {
val tag = data.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 text = when(data.roaming) {
NetworkStats.Bucket.ROAMING_ALL -> R.string.all
NetworkStats.Bucket.ROAMING_YES -> R.string.yes
NetworkStats.Bucket.ROAMING_NO -> R.string.no
else -> R.string.unknown
HorizontalPager(ps, Modifier.padding(top = 8.dp)) { page ->
val data = nsv.stats[page]
Column(Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)) {
Row(Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
SimpleDateFormat("", Locale.getDefault()).format(Date(data.startTime))
Text(
formatDate("yyyy/MM/dd", data.startTime) + "\n" + formatDate("HH:mm:ss", data.startTime),
textAlign = TextAlign.Center
)
Text("~", Modifier.padding(horizontal = 8.dp))
Text(
formatDate("yyyy/MM/dd", data.endTime) + "\n" + formatDate("HH:mm:ss", data.endTime),
textAlign = TextAlign.Center
)
}
val txBytes = data.txBytes
Text(stringResource(R.string.transmitted), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) {
Text("$txBytes bytes (${formatFileSize(txBytes)})")
Text(data.txPackets.toString() + " packets")
}
val rxBytes = data.rxBytes
Text(stringResource(R.string.received), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) {
Text("$rxBytes bytes (${formatFileSize(rxBytes)})")
Text(data.rxPackets.toString() + " packets")
}
Row(verticalAlignment = Alignment.CenterVertically) {
val text = when(data.state) {
NetworkStats.Bucket.STATE_ALL -> R.string.all
NetworkStats.Bucket.STATE_DEFAULT -> R.string.default_str
NetworkStats.Bucket.STATE_FOREGROUND -> R.string.foreground
else -> R.string.unknown
}
Text(stringResource(R.string.state), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(text))
}
if(VERSION.SDK_INT >= 24) {
Row(verticalAlignment = Alignment.CenterVertically) {
val tag = data.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 text = when(data.roaming) {
NetworkStats.Bucket.ROAMING_ALL -> R.string.all
NetworkStats.Bucket.ROAMING_YES -> R.string.yes
NetworkStats.Bucket.ROAMING_NO -> R.string.no
else -> R.string.unknown
}
Text(stringResource(R.string.roaming), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(text))
}
}
if(VERSION.SDK_INT >= 26) Row(verticalAlignment = Alignment.CenterVertically) {
val text = when(data.metered) {
NetworkStats.Bucket.METERED_ALL -> R.string.all
NetworkStats.Bucket.METERED_YES -> R.string.yes
NetworkStats.Bucket.METERED_NO -> R.string.no
else -> R.string.unknown
}
Text(stringResource(R.string.metered), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(text))
}
Text(stringResource(R.string.roaming), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(text))
}
}
if(VERSION.SDK_INT >= 26) Row(verticalAlignment = Alignment.CenterVertically) {
val text = when(data.metered) {
NetworkStats.Bucket.METERED_ALL -> R.string.all
NetworkStats.Bucket.METERED_YES -> R.string.yes
NetworkStats.Bucket.METERED_NO -> R.string.no
else -> R.string.unknown
}
Text(stringResource(R.string.metered), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp))
Text(stringResource(text))
}
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M320,720 L80,480l240,-240 57,57 -184,184 183,183 -56,56ZM640,720 L583,663 767,479 584,296 640,240 880,480 640,720Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -406,8 +406,6 @@
<string name="set_default_dialer">Установить приложение для звонков по умолчанию</string>
<string name="app_will_be_default_dialer">Это приложение будет установлено в качестве приложения для звонков по умолчанию.</string>
<string name="uninstall_app">Удалить приложение</string>
<string name="silent_uninstall">Тихое удаление</string>
<string name="request_uninstall">Запросить удаление</string>
<string name="install_app">Установить приложение</string>
<!--TODO: 2 strings-->
<string name="choose_apk_file">Choose an APK file</string>

View File

@@ -414,8 +414,6 @@
<string name="set_default_dialer">Varsayılan Arama Uygulamasını Ayarla</string>
<string name="app_will_be_default_dialer">Bu uygulama varsayılan arama uygulaması olarak ayarlanacak.</string>
<string name="uninstall_app">Uygulamayı Kaldır</string>
<string name="silent_uninstall">Sessiz Kaldırma</string>
<string name="request_uninstall">Kaldırma İsteği</string>
<string name="install_app">Uygulama Yükle</string>
<string name="choose_apk_file">Bir APK Dosyası Seç</string>
<string name="install_existing_app">Mevcut Uygulamayı Yükle</string>

View File

@@ -394,8 +394,6 @@
<string name="set_default_dialer">设为默认拨号应用</string>
<string name="app_will_be_default_dialer">这个应用将被设为默认拨号应用</string>
<string name="uninstall_app">卸载应用</string>
<string name="silent_uninstall">静默卸载</string>
<string name="request_uninstall">请求卸载</string>
<string name="install_app">安装应用</string>
<string name="choose_apk_file">选择一个APK文件</string>
<string name="install_existing_app">安装已存在的应用</string>

View File

@@ -433,8 +433,6 @@
<string name="set_default_dialer">Set default dialer</string>
<string name="app_will_be_default_dialer">This app will be set as the default dialer application.</string>
<string name="uninstall_app">Uninstall app</string>
<string name="silent_uninstall">Silent uninstall</string>
<string name="request_uninstall">Request uninstall</string>
<string name="install_app">Install app</string>
<string name="choose_apk_file">Choose an APK file</string>
<string name="install_existing_app">Install existing app</string>