diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 88b43cf..52b204d 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -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())) diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 4e1d6ad..d46766e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -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() } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 545b8b4..7fa9a45 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -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 { PermittedAccessibilityServicesScreen(pkgName) } composable { PermittedInputMethodsScreen(pkgName) } composable { KeepUninstalledPackagesScreen(pkgName) } - composable { 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(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) } - diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 8e5c021..996e4f4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -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)) - } } } diff --git a/app/src/main/res/drawable/code_fill0.xml b/app/src/main/res/drawable/code_fill0.xml new file mode 100644 index 0000000..c00ab1b --- /dev/null +++ b/app/src/main/res/drawable/code_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ad0372a..12c39f9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -406,8 +406,6 @@ Установить приложение для звонков по умолчанию Это приложение будет установлено в качестве приложения для звонков по умолчанию. Удалить приложение - Тихое удаление - Запросить удаление Установить приложение Choose an APK file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 9079adc..93fff2e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -414,8 +414,6 @@ Varsayılan Arama Uygulamasını Ayarla Bu uygulama varsayılan arama uygulaması olarak ayarlanacak. Uygulamayı Kaldır - Sessiz Kaldırma - Kaldırma İsteği Uygulama Yükle Bir APK Dosyası Seç Mevcut Uygulamayı Yükle diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8a25885..643423f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -394,8 +394,6 @@ 设为默认拨号应用 这个应用将被设为默认拨号应用 卸载应用 - 静默卸载 - 请求卸载 安装应用 选择一个APK文件 安装已存在的应用 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58ccda2..a3dc99d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -433,8 +433,6 @@ Set default dialer This app will be set as the default dialer application. Uninstall app - Silent uninstall - Request uninstall Install app Choose an APK file Install existing app