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.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) }
FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) } 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(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(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) }
FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) { FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) {
val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis())) val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis()))

View File

@@ -100,6 +100,9 @@ val Long.humanReadableDate: String
val Long.humanReadableDateTime: String val Long.humanReadableDateTime: String
get() = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(this)) 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) { fun Context.showOperationResultToast(success: Boolean) {
Toast.makeText(this, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() 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.R
import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.ErrorDialog
import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.FunctionItem
import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.ListItem
import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.NavIcon
@@ -157,7 +158,6 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) {
composable<PermittedAccessibilityServices> { PermittedAccessibilityServicesScreen(pkgName) } composable<PermittedAccessibilityServices> { PermittedAccessibilityServicesScreen(pkgName) }
composable<PermittedInputMethods> { PermittedInputMethodsScreen(pkgName) } composable<PermittedInputMethods> { PermittedInputMethodsScreen(pkgName) }
composable<KeepUninstalledPackages> { KeepUninstalledPackagesScreen(pkgName) } composable<KeepUninstalledPackages> { KeepUninstalledPackagesScreen(pkgName) }
composable<UninstallPackage> { UninstallPackageScreen(pkgName) }
} }
} }
} }
@@ -166,8 +166,9 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) {
@Composable @Composable
private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { 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 dialogStatus by remember { mutableIntStateOf(0) }
var errorMessage by remember { mutableStateOf("") }
val context = LocalContext.current val context = LocalContext.current
val dpm = context.getDPM() val dpm = context.getDPM()
val receiver = context.getReceiver() 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() Toast.makeText(context, R.string.choose_apk_file, Toast.LENGTH_SHORT).show()
chooseApks.launch(APK_MIME) chooseApks.launch(APK_MIME)
} }
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) { dialogStatus = 5 } if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) {
FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { onNavigate(UninstallPackage) } 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))) { if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) { FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) {
if(pkgName != "") dialogStatus = 3 if(pkgName != "") dialogStatus = 3
@@ -428,6 +433,27 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
}, },
onDismissRequest = { dialogStatus = 0 } 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() } LaunchedEffect(dialogStatus) { focusMgr.clearFocus() }
} }
@@ -706,7 +732,7 @@ private fun CrossProfileWidgetProvidersScreen(pkgName: String) {
@RequiresApi(34) @RequiresApi(34)
@Composable @Composable
private fun CredentialManagerPolicyScreen(pkgName: String) { // TODO: rename "manage" to "manager" private fun CredentialManagerPolicyScreen(pkgName: String) {
val context = LocalContext.current val context = LocalContext.current
val dpm = context.getDPM() val dpm = context.getDPM()
var policy: PackagePolicy? var policy: PackagePolicy?
@@ -940,19 +966,7 @@ private fun KeepUninstalledPackagesScreen(pkgName: String) {
} }
} }
@Serializable private object UninstallPackage private fun uninstallPackage(context: Context, packageName: String, onError: (String) -> Unit) {
@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() { val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999) val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
@@ -964,7 +978,7 @@ private fun UninstallPackageScreen(pkgName: String) {
if(statusExtra == PackageInstaller.STATUS_SUCCESS) { if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
context.showOperationResultToast(true) context.showOperationResultToast(true)
} else { } else {
errorMessage = parsePackageInstallerMessage(context, intent) onError(parsePackageInstallerMessage(context, intent))
} }
} }
} }
@@ -981,33 +995,5 @@ private fun UninstallPackageScreen(pkgName: String) {
} else { } else {
PendingIntent.getBroadcast(context, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender PendingIntent.getBroadcast(context, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
} }
context.getPackageInstaller().uninstall(pkgName, pi) context.getPackageInstaller().uninstall(packageName, 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))
}
}
}
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 }
)
}

View File

@@ -88,6 +88,7 @@ import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow 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.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.ChoosePackageContract
import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.HorizontalPadding
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SharedPrefs import com.bintianqi.owndroid.SharedPrefs
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.formatFileSize
import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.humanReadableDate
import com.bintianqi.owndroid.humanReadableDateTime import com.bintianqi.owndroid.humanReadableDateTime
@@ -153,6 +156,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.InetAddress import java.net.InetAddress
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
@Serializable object Network @Serializable object Network
@@ -917,7 +923,8 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta
MyScaffold(R.string.network_stats, onNavigateUp) { MyScaffold(R.string.network_stats, onNavigateUp) {
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
activeTextField == NetworkStatsActiveTextField.Type, 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( val typeTextMap = mapOf(
1 to R.string.summary, 1 to R.string.summary,
@@ -927,7 +934,7 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta
value = stringResource(typeTextMap[queryType]!!), onValueChange = {}, readOnly = true, value = stringResource(typeTextMap[queryType]!!), onValueChange = {}, readOnly = true,
label = { Text(stringResource(R.string.type)) }, label = { Text(stringResource(R.string.type)) },
trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.Type) }, trailingIcon = { ExpandExposedTextFieldIcon(activeTextField == NetworkStatsActiveTextField.Type) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp) modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth()
) )
ExposedDropdownMenu( ExposedDropdownMenu(
activeTextField == NetworkStatsActiveTextField.Type, { activeTextField = NetworkStatsActiveTextField.None } activeTextField == NetworkStatsActiveTextField.Type, { activeTextField = NetworkStatsActiveTextField.None }
@@ -1185,7 +1192,7 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta
Button( Button(
onClick = { onClick = {
querying = true querying = true
coroutine.launch { coroutine.launch(Dispatchers.IO) {
val buckets = try { val buckets = try {
@Suppress("NewApi") if(queryType == 1) { @Suppress("NewApi") if(queryType == 1) {
if(target == NetworkStatsTarget.Device) if(target == NetworkStatsTarget.Device)
@@ -1287,30 +1294,51 @@ data class NetworkStatsViewer(
fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit) { fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit) {
var index by remember { mutableIntStateOf(0) } var index by remember { mutableIntStateOf(0) }
val size = nsv.stats.size 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( if(size > 1) Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp) modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 8.dp)
) { ) {
IconButton( IconButton(
onClick = { index -= 1 }, onClick = {
coroutine.launch {
ps.animateScrollToPage(index - 1)
}
},
enabled = index > 0 enabled = index > 0
) { ) {
Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft, contentDescription = null)
} }
Text("${index + 1} / $size", modifier = Modifier.padding(horizontal = 8.dp)) Text("${index + 1} / $size", modifier = Modifier.padding(horizontal = 8.dp))
IconButton( IconButton(
onClick = { index += 1 }, onClick = {
coroutine.launch {
ps.animateScrollToPage(index + 1)
}
},
enabled = index < size - 1 enabled = index < size - 1
) { ) {
Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, contentDescription = null) Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, contentDescription = null)
} }
} }
val data = nsv.stats[index] 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( Text(
data.startTime.humanReadableDateTime + " ~ " + data.endTime.humanReadableDateTime, formatDate("yyyy/MM/dd", data.startTime) + "\n" + formatDate("HH:mm:ss", data.startTime),
modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp) 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 val txBytes = data.txBytes
Text(stringResource(R.string.transmitted), style = typography.titleLarge) Text(stringResource(R.string.transmitted), style = typography.titleLarge)
Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) { Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) {
@@ -1362,6 +1390,8 @@ fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit)
} }
} }
} }
}
}
@Serializable object PrivateDns @Serializable object PrivateDns

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="set_default_dialer">Установить приложение для звонков по умолчанию</string>
<string name="app_will_be_default_dialer">Это приложение будет установлено в качестве приложения для звонков по умолчанию.</string> <string name="app_will_be_default_dialer">Это приложение будет установлено в качестве приложения для звонков по умолчанию.</string>
<string name="uninstall_app">Удалить приложение</string> <string name="uninstall_app">Удалить приложение</string>
<string name="silent_uninstall">Тихое удаление</string>
<string name="request_uninstall">Запросить удаление</string>
<string name="install_app">Установить приложение</string> <string name="install_app">Установить приложение</string>
<!--TODO: 2 strings--> <!--TODO: 2 strings-->
<string name="choose_apk_file">Choose an APK file</string> <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="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="app_will_be_default_dialer">Bu uygulama varsayılan arama uygulaması olarak ayarlanacak.</string>
<string name="uninstall_app">Uygulamayı Kaldır</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="install_app">Uygulama Yükle</string>
<string name="choose_apk_file">Bir APK Dosyası Seç</string> <string name="choose_apk_file">Bir APK Dosyası Seç</string>
<string name="install_existing_app">Mevcut Uygulamayı Yükle</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="set_default_dialer">设为默认拨号应用</string>
<string name="app_will_be_default_dialer">这个应用将被设为默认拨号应用</string> <string name="app_will_be_default_dialer">这个应用将被设为默认拨号应用</string>
<string name="uninstall_app">卸载应用</string> <string name="uninstall_app">卸载应用</string>
<string name="silent_uninstall">静默卸载</string>
<string name="request_uninstall">请求卸载</string>
<string name="install_app">安装应用</string> <string name="install_app">安装应用</string>
<string name="choose_apk_file">选择一个APK文件</string> <string name="choose_apk_file">选择一个APK文件</string>
<string name="install_existing_app">安装已存在的应用</string> <string name="install_existing_app">安装已存在的应用</string>

View File

@@ -433,8 +433,6 @@
<string name="set_default_dialer">Set default dialer</string> <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="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="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="install_app">Install app</string>
<string name="choose_apk_file">Choose an APK file</string> <string name="choose_apk_file">Choose an APK file</string>
<string name="install_existing_app">Install existing app</string> <string name="install_existing_app">Install existing app</string>