diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fe21232..fd8a58e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -80,6 +80,8 @@ gradle.taskGraph.whenReady {
dependencies {
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.accompanist.drawablepainter)
implementation(libs.accompanist.permissions)
implementation(libs.androidx.material3)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7212e08..447ebb0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,18 +42,16 @@
android:theme="@style/Theme.Transparent">
+ android:launchMode="singleInstance">
-
-
-
+
+
@@ -75,11 +73,6 @@
-
-
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
new file mode 100644
index 0000000..62c6b2a
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
@@ -0,0 +1,305 @@
+package com.bintianqi.owndroid
+
+import android.app.Application
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInstaller
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Clear
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExtendedFloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewModelScope
+import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage
+import com.bintianqi.owndroid.ui.RadioButtonItem
+import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.net.URLDecoder
+
+class AppInstallerActivity:ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+ val myVm by viewModels()
+ val vm by viewModels()
+ vm.initialize(intent)
+ setContent {
+ OwnDroidTheme(myVm) {
+ val installing by vm.installing.collectAsStateWithLifecycle()
+ val sessionMode by vm.sessionMode.collectAsStateWithLifecycle()
+ val packages by vm.packages.collectAsStateWithLifecycle()
+ val writtenPackages by vm.writtenPackages.collectAsStateWithLifecycle()
+ val writingPackage by vm.writingPackage.collectAsStateWithLifecycle()
+ val result by vm.result.collectAsStateWithLifecycle()
+ AppInstaller(
+ installing, sessionMode, { vm.sessionMode.value = it },
+ packages, { uri -> vm.packages.update { it.minus(uri) } },
+ { uris -> vm.packages.update { it.plus(uris) } },
+ vm::startInstallationProcess, writtenPackages, writingPackage,
+ result, { vm.result.value = null }
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Composable
+private fun AppInstaller(
+ installing: Boolean = false,
+ sessionMode: Int = PackageInstaller.SessionParams.MODE_INHERIT_EXISTING,
+ onSessionModeChoose: (Int) -> Unit = {},
+ packages: Set = setOf(Uri.parse("https://example.com")),
+ onPackageRemove: (Uri) -> Unit = {},
+ onPackageChoose: (List) -> Unit = {},
+ onFabPressed: () -> Unit = {},
+ writtenPackages: Set = setOf(Uri.parse("https://example.com")),
+ writingPackage: Uri? = null,
+ result: Intent? = null,
+ onResultDialogClose: () -> Unit = {}
+) {
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(R.string.app_installer)) }
+ )
+ },
+ floatingActionButton = {
+ if(packages.isNotEmpty()) ExtendedFloatingActionButton(
+ text = { Text(stringResource(R.string.start)) },
+ icon = {
+ if(installing) CircularProgressIndicator(modifier = Modifier.size(24.dp))
+ else Icon(Icons.Default.PlayArrow, null)
+ },
+ onClick = onFabPressed,
+ expanded = !installing
+ )
+ }
+ ) { paddingValues ->
+ Column(modifier = Modifier.padding(paddingValues)) {
+ SessionMode(sessionMode, onSessionModeChoose)
+ Packages(installing, packages, onPackageRemove, onPackageChoose, writtenPackages, writingPackage)
+ ResultDialog(result, onResultDialogClose)
+ }
+ }
+}
+
+@Composable
+private fun SessionMode(mode: Int, onChoose: (Int) -> Unit) {
+ Text(
+ stringResource(R.string.mode), modifier = Modifier.padding(top = 10.dp, start = 8.dp),
+ style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary
+ )
+ RadioButtonItem(R.string.full_install, mode == PackageInstaller.SessionParams.MODE_FULL_INSTALL) {
+ onChoose(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+ }
+ RadioButtonItem(R.string.inherit_existing, mode == PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) {
+ onChoose(PackageInstaller.SessionParams.MODE_INHERIT_EXISTING)
+ }
+}
+
+@Composable
+private fun Packages(
+ installing: Boolean,
+ packages: Set, onRemove: (Uri) -> Unit, onChoose: (List) -> Unit,
+ writtenPackages: Set, writingPackage: Uri?
+) {
+ val chooseSplitPackage = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents(), onChoose)
+ Text(
+ stringResource(R.string.packages), modifier = Modifier.padding(start = 8.dp, top = 10.dp, bottom = 4.dp),
+ style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary
+ )
+ packages.forEach {
+ PackageItem(
+ it, installing,
+ { onRemove(it) }, it in writtenPackages, it == writingPackage
+ )
+ }
+ if(!installing) Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable {
+ chooseSplitPackage.launch(APK_MIME)
+ }.padding(vertical = 12.dp)
+ ) {
+ Icon(Icons.Default.Add, null, modifier = Modifier.padding(horizontal = 10.dp))
+ Text(stringResource(R.string.add_packages), style = MaterialTheme.typography.titleMedium)
+ }
+}
+
+
+@Composable
+private fun PackageItem(uri: Uri, installing: Boolean, onRemove: () -> Unit, isWritten: Boolean, isWriting: Boolean) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 6.dp, bottom = 6.dp).heightIn(min = 40.dp)
+ ) {
+ Text(
+ URLDecoder.decode(URLDecoder.decode(uri.path ?: uri.toString())),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.fillMaxWidth(0.85F)
+ )
+ if(!installing) IconButton(onRemove) {
+ Icon(Icons.Default.Clear, contentDescription = stringResource(R.string.remove))
+ }
+ if(isWritten) Icon(Icons.Default.Check, null, Modifier.padding(end = 8.dp), MaterialTheme.colorScheme.secondary)
+ if(isWriting) CircularProgressIndicator(Modifier.padding(end = 8.dp).size(24.dp))
+ }
+}
+
+@Composable
+private fun ResultDialog(result: Intent?, onDialogClose: () -> Unit) {
+ if(result != null) {
+ val status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
+ AlertDialog(
+ title = {
+ val text = if(status == PackageInstaller.STATUS_SUCCESS) R.string.success else R.string.failure
+ Text(stringResource(text))
+ },
+ text = {
+ val context = LocalContext.current
+ Text(parsePackageInstallerMessage(context, result))
+ },
+ confirmButton = {
+ TextButton(onDialogClose) {
+ Text(stringResource(R.string.confirm))
+ }
+ },
+ onDismissRequest = onDialogClose
+ )
+ }
+}
+
+class AppInstallerViewModel(application: Application): AndroidViewModel(application) {
+ fun initialize(intent: Intent) {
+ intent.data?.let { uri -> packages.update { it + uri } }
+ intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri -> packages.update { it + uri } }
+ intent.getParcelableArrayExtra(Intent.EXTRA_STREAM)?.forEach { uri -> packages.update { it + (uri as Uri) } }
+ intent.clipData?.let { clipData ->
+ for(i in 0..(clipData.itemCount - 1)) {
+ packages.update { it + clipData.getItemAt(i).uri }
+ }
+ }
+ }
+ val installing = MutableStateFlow(false)
+ val result = MutableStateFlow(null)
+ val packages = MutableStateFlow(setOf())
+
+ val sessionMode = MutableStateFlow(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
+
+ val writtenPackages = MutableStateFlow(setOf())
+ val writingPackage = MutableStateFlow(null)
+ fun startInstallationProcess() {
+ if(installing.value) return
+ installing.value = true
+ viewModelScope.launch(Dispatchers.IO) {
+ val context = getApplication()
+ val packageInstaller = context.packageManager.packageInstaller
+ val sessionId = packageInstaller.createSession(PackageInstaller.SessionParams(sessionMode.value))
+ val session = packageInstaller.openSession(sessionId)
+ try {
+ packages.value.forEach { splitPackageUri ->
+ withContext(Dispatchers.Main) { writingPackage.value = splitPackageUri }
+ session.openWrite(splitPackageUri.hashCode().toString(), 0, -1).use { splitPackageOut ->
+ context.contentResolver.openInputStream(splitPackageUri)!!.use { splitPackageIn ->
+ splitPackageIn.copyTo(splitPackageOut)
+ }
+ session.fsync(splitPackageOut)
+ }
+ withContext(Dispatchers.Main) { writtenPackages.update { it.plus(splitPackageUri) } }
+ }
+ withContext(Dispatchers.Main) { writingPackage.value = null }
+ } catch(e: Exception) {
+ e.printStackTrace()
+ session.abandon()
+ return@launch
+ }
+ 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 {
+ result.value = intent
+ writtenPackages.value = setOf()
+ if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
+ packages.value = setOf()
+ }
+ installing.value = false
+ context.unregisterReceiver(this)
+ }
+ }
+ }
+ ContextCompat.registerReceiver(
+ context, receiver, IntentFilter(ACTION), null,
+ null, ContextCompat.RECEIVER_EXPORTED
+ )
+ val pi = if(Build.VERSION.SDK_INT >= 34) {
+ PendingIntent.getBroadcast(
+ context, sessionId, Intent(ACTION),
+ PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
+ ).intentSender
+ } else {
+ PendingIntent.getBroadcast(context, sessionId, Intent(ACTION), PendingIntent.FLAG_MUTABLE).intentSender
+ }
+ session.commit(pi)
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ viewModelScope.cancel()
+ }
+ companion object {
+ const val ACTION = "com.bintianqi.owndroid.action.PACKAGE_INSTALLER_SESSION_STATUS_CHANGED"
+ }
+}
diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt
deleted file mode 100644
index 86ecea4..0000000
--- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.bintianqi.owndroid
-
-import android.content.Intent
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.viewModels
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.LinearProgressIndicator
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.DialogProperties
-import androidx.core.view.WindowCompat
-import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.lifecycleScope
-import com.bintianqi.owndroid.dpm.installPackage
-import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
-import com.github.fishb1.apkinfo.ApkInfo
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.FileInputStream
-
-class InstallAppActivity: FragmentActivity() {
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
- this.intent = intent
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- WindowCompat.setDecorFitsSystemWindows(window, false)
- val context = applicationContext
- window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
- val uri = this.intent.data!!
- var apkInfoText by mutableStateOf(context.getString(R.string.parsing_apk_info))
- var status by mutableStateOf("parsing")
- this.lifecycleScope.launch(Dispatchers.IO) {
- val fd = applicationContext.contentResolver.openFileDescriptor(uri, "r")
- val apkInfo = ApkInfo.fromInputStream(
- FileInputStream(fd?.fileDescriptor)
- )
- fd?.close()
- withContext(Dispatchers.Main) {
- status = "pending"
- apkInfoText = "${context.getString(R.string.package_name)}: ${apkInfo.packageName}\n"
- apkInfoText += "${context.getString(R.string.version_name)}: ${apkInfo.versionName}\n"
- apkInfoText += "${context.getString(R.string.version_code)}: ${apkInfo.versionCode}"
- }
- }
- val vm by viewModels()
- if(!vm.initialized) vm.initialize(applicationContext)
- setContent {
- OwnDroidTheme(vm) {
- AlertDialog(
- properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
- title = {
- Text(stringResource(R.string.install_app))
- },
- onDismissRequest = {
- if(status != "installing") finish()
- },
- text = {
- Column {
- AnimatedVisibility(status != "pending") {
- LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
- }
- Text(text = apkInfoText, modifier = Modifier.padding(top = 4.dp))
- }
- },
- dismissButton = {
- TextButton(
- onClick = { finish() },
- enabled = status != "installing"
- ) {
- Text(stringResource(R.string.cancel))
- }
- },
- confirmButton = {
- TextButton(
- onClick = {
- status = "installing"
- intent.data?.let {
- uriToStream(applicationContext, it) { stream -> installPackage(applicationContext, stream) }
- }
- },
- enabled = status != "installing"
- ) {
- Text(stringResource(R.string.install))
- }
- },
- modifier = Modifier.fillMaxWidth()
- )
- }
- val installDone by installAppDone.collectAsState()
- LaunchedEffect(installDone) {
- if(installDone) {
- installAppDone.value = false
- finish()
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
index 10b697d..36e6304 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
@@ -137,7 +137,6 @@ import com.bintianqi.owndroid.dpm.isDeviceAdmin
import com.bintianqi.owndroid.dpm.isDeviceOwner
import com.bintianqi.owndroid.dpm.isProfileOwner
import com.bintianqi.owndroid.dpm.setDefaultAffiliationID
-import com.bintianqi.owndroid.dpm.toggleInstallAppActivity
import com.bintianqi.owndroid.ui.Animations
import com.bintianqi.owndroid.ui.MyScaffold
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
@@ -160,9 +159,7 @@ class MainActivity : FragmentActivity() {
if (VERSION.SDK_INT >= 28) HiddenApiBypass.setHiddenApiExemptions("")
val locale = context.resources?.configuration?.locale
zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA
- toggleInstallAppActivity()
val vm by viewModels()
- if(!vm.initialized) vm.initialize(context)
lifecycleScope.launch { delay(5000); setDefaultAffiliationID(context) }
setContent {
OwnDroidTheme(vm) {
@@ -462,8 +459,6 @@ private fun DhizukuErrorDialog() {
val context = LocalContext.current
val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)
LaunchedEffect(Unit) {
- context.toggleInstallAppActivity()
- delay(200)
sharedPref.edit().putBoolean("dhizuku", false).apply()
}
AlertDialog(
diff --git a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
index c9e9f14..a67a4f3 100644
--- a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt
@@ -28,7 +28,6 @@ class ManageSpaceActivity: FragmentActivity() {
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
val authenticate = sharedPref.getBoolean("auth", false)
val vm by viewModels()
- if(!vm.initialized) vm.initialize(applicationContext)
fun clearStorage() {
filesDir.deleteRecursively()
cacheDir.deleteRecursively()
diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
index 8fbe61c..c5c93fb 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
@@ -1,22 +1,22 @@
package com.bintianqi.owndroid
+import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
-import androidx.lifecycle.ViewModel
+import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
-class MyViewModel: ViewModel() {
+class MyViewModel(application: Application): AndroidViewModel(application) {
val theme = MutableStateFlow(ThemeSettings())
val installedPackages = mutableListOf()
val selectedPackage = MutableStateFlow("")
val userRestrictions = MutableStateFlow(Bundle())
- var initialized = false
- fun initialize(context: Context) {
- val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE)
+ init {
+ val sharedPrefs = application.getSharedPreferences("data", Context.MODE_PRIVATE)
theme.value = ThemeSettings(
materialYou = sharedPrefs.getBoolean("material_you", Build.VERSION.SDK_INT >= 31),
darkTheme = if(sharedPrefs.contains("dark_theme")) sharedPrefs.getBoolean("dark_theme", false) else null,
diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
index c0d8e4c..321e2d9 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
@@ -3,24 +3,11 @@ package com.bintianqi.owndroid
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DeviceAdminReceiver
-import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageInstaller.EXTRA_STATUS
-import android.content.pm.PackageInstaller.STATUS_FAILURE
-import android.content.pm.PackageInstaller.STATUS_FAILURE_ABORTED
-import android.content.pm.PackageInstaller.STATUS_FAILURE_BLOCKED
-import android.content.pm.PackageInstaller.STATUS_FAILURE_CONFLICT
-import android.content.pm.PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
-import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID
-import android.content.pm.PackageInstaller.STATUS_FAILURE_STORAGE
-import android.content.pm.PackageInstaller.STATUS_FAILURE_TIMEOUT
-import android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION
-import android.content.pm.PackageInstaller.STATUS_SUCCESS
import android.os.Build.VERSION
import android.os.PersistableBundle
-import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import com.bintianqi.owndroid.dpm.handleNetworkLogs
@@ -28,10 +15,8 @@ import com.bintianqi.owndroid.dpm.isDeviceAdmin
import com.bintianqi.owndroid.dpm.isDeviceOwner
import com.bintianqi.owndroid.dpm.isProfileOwner
import com.bintianqi.owndroid.dpm.processSecurityLogs
-import com.bintianqi.owndroid.dpm.toggleInstallAppActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class Receiver : DeviceAdminReceiver() {
@@ -48,7 +33,6 @@ class Receiver : DeviceAdminReceiver() {
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
- context.toggleInstallAppActivity()
if(context.isDeviceAdmin || context.isProfileOwner || context.isDeviceOwner){
Toast.makeText(context, context.getString(R.string.onEnabled), Toast.LENGTH_SHORT).show()
}
@@ -56,7 +40,6 @@ class Receiver : DeviceAdminReceiver() {
override fun onDisabled(context: Context, intent: Intent) {
super.onDisabled(context, intent)
- context.toggleInstallAppActivity()
Toast.makeText(context, R.string.onDisabled, Toast.LENGTH_SHORT).show()
}
@@ -93,7 +76,6 @@ class Receiver : DeviceAdminReceiver() {
super.onTransferOwnershipComplete(context, bundle)
val sp = context.getSharedPreferences("data", Context.MODE_PRIVATE)
sp.edit().putBoolean("dhizuku", false).apply()
- context.toggleInstallAppActivity()
}
override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) {
@@ -116,29 +98,3 @@ class Receiver : DeviceAdminReceiver() {
nm.cancel(1)
}
}
-
-val installAppDone = MutableStateFlow(false)
-
-class PackageInstallerReceiver: BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val toastText = when(intent.getIntExtra(EXTRA_STATUS, 999)){
- STATUS_PENDING_USER_ACTION -> R.string.status_pending_action
- STATUS_SUCCESS -> R.string.success
- STATUS_FAILURE -> R.string.failed
- STATUS_FAILURE_BLOCKED -> R.string.status_fail_blocked
- STATUS_FAILURE_ABORTED -> R.string.status_fail_aborted
- STATUS_FAILURE_INVALID -> R.string.status_fail_invalid
- STATUS_FAILURE_CONFLICT -> R.string.status_fail_conflict
- STATUS_FAILURE_STORAGE -> R.string.status_fail_storage
- STATUS_FAILURE_INCOMPATIBLE -> R.string.status_fail_incompatible
- STATUS_FAILURE_TIMEOUT -> R.string.status_fail_timeout
- else -> 999
- }
- Log.e("OwnDroid", intent.getIntExtra(EXTRA_STATUS, 999).toString())
- installAppDone.value = true
- if(toastText != 999){
- val text = context.getString(R.string.app_installer_status) + context.getString(toastText)
- Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
- }
- }
-}
diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt
index 9f0e7e5..3fd33c1 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt
@@ -31,9 +31,9 @@ fun uriToStream(
operation: (stream: InputStream)->Unit
){
try {
- val stream = context.contentResolver.openInputStream(uri)
- if(stream != null) { operation(stream) }
- stream?.close()
+ context.contentResolver.openInputStream(uri)?.use {
+ operation(it)
+ }
}
catch(_: FileNotFoundException) { Toast.makeText(context, R.string.file_not_exist, Toast.LENGTH_SHORT).show() }
catch(_: IOException) { Toast.makeText(context, R.string.io_exception, Toast.LENGTH_SHORT).show() }
@@ -91,3 +91,5 @@ fun Context.showOperationResultToast(success: Boolean) {
fun getContext(): Context {
return Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null) as Context
}
+
+const val APK_MIME = "application/vnd.android.package-archive"
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 39e0c40..d0385b3 100644
--- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt
@@ -1,5 +1,6 @@
package com.bintianqi.owndroid.dpm
+import android.app.AlertDialog
import android.app.PendingIntent
import android.app.admin.DevicePolicyManager
import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT
@@ -9,8 +10,11 @@ import android.app.admin.PackagePolicy
import android.app.admin.PackagePolicy.PACKAGE_POLICY_ALLOWLIST
import android.app.admin.PackagePolicy.PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM
import android.app.admin.PackagePolicy.PACKAGE_POLICY_BLOCKLIST
+import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInstaller
import android.content.pm.PackageManager.NameNotFoundException
import android.net.Uri
import android.os.Build.VERSION
@@ -73,15 +77,17 @@ 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.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
-import com.bintianqi.owndroid.InstallAppActivity
+import com.bintianqi.owndroid.APK_MIME
+import com.bintianqi.owndroid.AppInstallerActivity
+import com.bintianqi.owndroid.AppInstallerViewModel
import com.bintianqi.owndroid.MyViewModel
-import com.bintianqi.owndroid.PackageInstallerReceiver
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.Animations
@@ -153,7 +159,6 @@ fun ApplicationManage(navCtrl:NavHostController, vm: MyViewModel) {
composable(route = "Accessibility") { PermittedAccessibility(pkgName) }
composable(route = "IME") { PermittedIME(pkgName) }
composable(route = "KeepUninstalled") { KeepUninstalledApp(pkgName) }
- composable(route = "InstallApp") { InstallApp() }
composable(route = "UninstallApp") { UninstallApp(pkgName) }
}
}
@@ -253,7 +258,14 @@ private fun Home(navCtrl:NavHostController, pkgName: String) {
if(pkgName != "") dialogStatus = 2
}
}
- FunctionItem(title = R.string.install_app, icon = R.drawable.install_mobile_fill0) { navCtrl.navigate("InstallApp") }
+ val chooseApks = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) {
+ val intent = Intent(context, AppInstallerActivity::class.java)
+ intent.putExtra(Intent.EXTRA_STREAM, it.toTypedArray())
+ startActivity(context, intent, null)
+ }
+ FunctionItem(title = R.string.install_app, icon = R.drawable.install_mobile_fill0) {
+ chooseApks.launch(APK_MIME)
+ }
FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") }
if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) {
FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) {
@@ -876,10 +888,39 @@ private fun UninstallApp(pkgName: String) {
Column(modifier = Modifier.fillMaxWidth()) {
Button(
onClick = {
- val intent = Intent(context, PackageInstallerReceiver::class.java)
- val intentSender = PendingIntent.getBroadcast(context, 8, intent, PendingIntent.FLAG_IMMUTABLE).intentSender
- val pkgInstaller = context.getPI()
- pkgInstaller.uninstall(pkgName, intentSender)
+ 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 {
+ AlertDialog.Builder(context)
+ .setTitle(R.string.failure)
+ .setMessage(parsePackageInstallerMessage(context, intent))
+ .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() }
+ .show()
+ }
+ }
+ }
+ }
+ 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()
@@ -901,57 +942,3 @@ private fun UninstallApp(pkgName: String) {
}
}
-@Composable
-private fun InstallApp() {
- val context = LocalContext.current
- val focusMgr = LocalFocusManager.current
- val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE)
- var apkFileUri by remember { mutableStateOf(null) }
- val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- result.data.also { if(it != null) apkFileUri = it.data }
- }
- Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) {
- Spacer(Modifier.padding(vertical = 10.dp))
- Text(text = stringResource(R.string.install_app), style = typography.headlineLarge)
- Spacer(Modifier.padding(vertical = 5.dp))
- Button(
- onClick = {
- focusMgr.clearFocus()
- val installApkIntent = Intent(Intent.ACTION_GET_CONTENT)
- installApkIntent.setType("application/vnd.android.package-archive")
- installApkIntent.addCategory(Intent.CATEGORY_OPENABLE)
- getFileLauncher.launch(installApkIntent)
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(stringResource(R.string.select_apk))
- }
- AnimatedVisibility(apkFileUri != null) {
- Spacer(Modifier.padding(vertical = 3.dp))
- Column(modifier = Modifier.fillMaxWidth()) {
- Button(
- onClick = {
- val intent = Intent(context, InstallAppActivity::class.java)
- intent.data = apkFileUri
- context.startActivity(intent)
- },
- enabled = !sharedPrefs.getBoolean("dhizuku", false) && context.isDeviceOwner,
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(stringResource(R.string.silent_install))
- }
- Button(
- onClick = {
- val intent = Intent(Intent.ACTION_INSTALL_PACKAGE)
- intent.setData(apkFileUri)
- intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- context.startActivity(intent)
- },
- modifier = Modifier.fillMaxWidth()
- ) {
- Text(stringResource(R.string.request_install))
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt
index 87934a9..9206013 100644
--- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt
@@ -2,7 +2,6 @@ package com.bintianqi.owndroid.dpm
import android.Manifest
import android.annotation.SuppressLint
-import android.app.PendingIntent
import android.app.admin.ConnectEvent
import android.app.admin.DevicePolicyManager
import android.app.admin.DnsEvent
@@ -15,7 +14,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.IPackageInstaller
import android.content.pm.PackageInstaller
-import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.UserManager
import android.util.Log
@@ -23,8 +21,6 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
-import com.bintianqi.owndroid.InstallAppActivity
-import com.bintianqi.owndroid.PackageInstallerReceiver
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.Receiver
import com.bintianqi.owndroid.backToHomeStateFlow
@@ -40,8 +36,6 @@ import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
-import java.io.IOException
-import java.io.InputStream
import java.io.OutputStream
lateinit var addDeviceAdmin: ActivityResultLauncher
@@ -84,24 +78,6 @@ fun DevicePolicyManager.isOrgProfile(receiver: ComponentName): Boolean {
return VERSION.SDK_INT >= 30 && this.isProfileOwnerApp("com.bintianqi.owndroid") && isManagedProfile(receiver) && isOrganizationOwnedDeviceWithManagedProfile
}
-@Throws(IOException::class)
-fun installPackage(context: Context, inputStream: InputStream) {
- val packageInstaller = context.packageManager.packageInstaller
- val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
- val sessionId = packageInstaller.createSession(params)
- val session = packageInstaller.openSession(sessionId)
- val out = session.openWrite("COSU", 0, -1)
- val buffer = ByteArray(65536)
- var c: Int
- while(inputStream.read(buffer).also{c = it}!=-1) { out.write(buffer, 0, c) }
- session.fsync(out)
- inputStream.close()
- out.close()
- val intent = Intent(context, PackageInstallerReceiver::class.java)
- val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, PendingIntent.FLAG_IMMUTABLE).intentSender
- session.commit(pendingIntent)
-}
-
@SuppressLint("PrivateApi")
private fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? {
try {
@@ -142,7 +118,7 @@ private fun binderWrapperPackageInstaller(appContext: Context): PackageInstaller
return null
}
-fun Context.getPI(): PackageInstaller {
+fun Context.getPackageInstaller(): PackageInstaller {
val sharedPref = this.getSharedPreferences("data", Context.MODE_PRIVATE)
if(sharedPref.getBoolean("dhizuku", false)) {
if (!dhizukuPermissionGranted()) {
@@ -246,16 +222,6 @@ fun Context.resetDevicePolicy() {
dpm.setRecommendedGlobalProxy(receiver, null)
}
-fun Context.toggleInstallAppActivity() {
- val sharedPrefs = getSharedPreferences("data", Context.MODE_PRIVATE)
- val disable = sharedPrefs.getBoolean("dhizuku", false) || !isDeviceOwner
- packageManager?.setComponentEnabledSetting(
- ComponentName(this, InstallAppActivity::class.java),
- if (disable) PackageManager.COMPONENT_ENABLED_STATE_DISABLED else PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
- PackageManager.DONT_KILL_APP
- )
-}
-
data class PermissionItem(
val permission: String,
@StringRes val label: Int,
@@ -608,3 +574,29 @@ fun dhizukuPermissionGranted() =
false
}
+fun parsePackageInstallerMessage(context: Context, result: Intent): String {
+ val status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
+ val statusMessage = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
+ val otherPackageName = result.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
+ return when(status) {
+ PackageInstaller.STATUS_FAILURE_BLOCKED ->
+ context.getString(
+ R.string.status_failure_blocked,
+ otherPackageName ?: context.getString(R.string.unknown)
+ )
+ PackageInstaller.STATUS_FAILURE_ABORTED ->
+ context.getString(R.string.status_failure_aborted)
+ PackageInstaller.STATUS_FAILURE_INVALID ->
+ context.getString(R.string.status_failure_invalid)
+ PackageInstaller.STATUS_FAILURE_CONFLICT ->
+ context.getString(R.string.status_failure_conflict, otherPackageName ?: "???")
+ PackageInstaller.STATUS_FAILURE_STORAGE ->
+ context.getString(R.string.status_failure_storage) +
+ result.getStringExtra(PackageInstaller.EXTRA_STORAGE_PATH).let { if(it == null) "" else "\n$it" }
+ PackageInstaller.STATUS_FAILURE_INCOMPATIBLE ->
+ context.getString(R.string.status_failure_incompatible)
+ PackageInstaller.STATUS_FAILURE_TIMEOUT ->
+ context.getString(R.string.timeout)
+ else -> ""
+ } + statusMessage.let { if(it == null) "" else "\n$it" }
+}
diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt
index 8a20be4..ecd7c5f 100644
--- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt
@@ -256,7 +256,6 @@ private fun toggleDhizukuMode(status: Boolean, context: Context) {
if(grantResult == PackageManager.PERMISSION_GRANTED) {
sharedPref.edit().putBoolean("dhizuku", true).apply()
Dhizuku.init(context)
- context.toggleInstallAppActivity()
backToHomeStateFlow.value = true
} else {
dhizukuErrorStatus.value = 2
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index f059ae7..e1b23d6 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -417,21 +417,6 @@
Тихое удаление
Запросить удаление
Установить приложение
- Выберите APK...
- Тихая установка
- Запросить установку
- Поиск
-
- Установщик приложений:
- Ожидание действия пользователя
- Ошибка: заблокировано
- Ошибка: прервано
- Ошибка: неверный APK
- Ошибка: конфликт
- Ошибка: недостаточно места
- Ошибка: несовместимо
- Ошибка: истекло время ожидания
-
Ограничения пользователя
@@ -668,9 +653,19 @@
Доступ к датчикам тела в фоновом режиме
Распознавание активности
- Версия
- Код версии
- Анализ информации APK...
+
+ App installer
+ Mode
+ Full install
+ Inherit existing
+ Packages
+ Add package(s)
+ The operation was blocked by: %1$s
+ The operation was aborted.
+ The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.
+ The operation failed because it conflicts with %1$s. For example, an existing permission, incompatible certificates, etc. You can uninstall %1$s to fix the issue.
+ The operation failed because of storage issues. For example, the device may be running low on space, or external media may be unavailable. You may try to help free space or insert different external media.
+ The operation failed because it is fundamentally incompatible with this device. For example, the app may require a hardware feature that doesn\'t exist, it may be missing native code for the ABIs supported by the device, or it requires a newer SDK version, etc.
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index eea53f8..be39434 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -417,22 +417,8 @@
Sessiz kaldırma
Kaldırma isteği
Uygulamayı yükle
- APK seç...
- Sessiz yükleme
- Yükleme isteği
Search
-
- Uygulama yükleyici:
- Bekleyen kullanıcı işlemi
- Başarısız: engellendi
- Başarısız: iptal edildi
- Başarısız: geçersiz APK
- Başarısız: çakışma
- Başarısız: boş alan yok
- Başarısız: uyumsuz
- Başarısız: zaman aşımı
-
Kullanıcı kısıtlaması
Profil sahibi sınırlı işlev kullanabilir
@@ -664,9 +650,19 @@
Arka planda vücut sensörlerine eriş
Aktivite tanıma
- Version name
- Version code
- Parsing APK info...
+
+ App installer
+ Mode
+ Full install
+ Inherit existing
+ Packages
+ Add package(s)
+ The operation was blocked by: %1$s
+ The operation was aborted.
+ The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.
+ The operation failed because it conflicts with %1$s. For example, an existing permission, incompatible certificates, etc. You can uninstall %1$s to fix the issue.
+ The operation failed because of storage issues. For example, the device may be running low on space, or external media may be unavailable. You may try to help free space or insert different external media.
+ The operation failed because it is fundamentally incompatible with this device. For example, the app may require a hardware feature that doesn\'t exist, it may be missing native code for the ABIs supported by the device, or it requires a newer SDK version, etc.
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 83e5e91..31e517c 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -402,22 +402,9 @@
静默卸载
请求卸载
安装应用
- 选择APK...
- 静默安装
- 请求安装
启用系统应用
重新启用一个默认被禁用的系统应用
搜索
-
- 应用安装器:
- 等待用户操作
- 被阻止
- 被打断
- 无效APK
- 冲突
- 空间不足
- 不兼容
- 超时
用户限制
@@ -649,9 +636,18 @@
后台使用身体传感器
查看使用情况
- 版本名
- 版本号
- 解析APK信息中...
+ App安装器
+ 模式
+ 完整安装
+ 继承已有
+ 软件包
+ 添加包
+ 操作被 %1$s 阻止。
+ 操作被打断。
+ 操作失败,因为有一个或多个APK无效。例如,它们可能被篡改,损坏,签名错误,不匹配等。
+ 操作失败,因为它与 %1$s 冲突。例如,存在的权限,不完整的证书等。你可以卸载 %1$s 以解决这个问题。
+ 由于存储问题,操作失败。例如,设备空间不足,或外部媒体不可用。你可以尝试清理空间或插入不同的外部媒体。
+ 操作失败,因为它与设备不兼容。例如,这个app可能需要不存在的硬件功能,它有可能缺少受此设备支持的ABI的本地代码,或它需要高于此设备的SDK版本。
Dhizuku可以分享Device owner权限给其余应用
指示设备是否除了密钥证明之外还支持设备标识符证明
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ba3902e..6f402bc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7,6 +7,7 @@
Disable
Enable
Success
+ Failure
Failed
Add
Remove
@@ -71,6 +72,7 @@
Overview
Features
Default
+ Timeout
Click to activate
@@ -441,20 +443,7 @@
Silent uninstall
Request uninstall
Install app
- Select APK...
- Silent install
- Request install
Search
-
- App installer:
- Pending user action
- Fail: blocked
- Fail: aborted
- Fail: invalid APK
- Fail: conflict
- Fail: no space
- Fail: incompatible
- Fail: timeout
User restriction
@@ -687,9 +676,18 @@
Access body sensors in background
Activity recognition
- Version name
- Version code
- Parsing APK info...
+ App installer
+ Mode
+ Full install
+ Inherit existing
+ Packages
+ Add package(s)
+ The operation was blocked by: %1$s
+ The operation was aborted.
+ The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.
+ The operation failed because it conflicts with %1$s. For example, an existing permission, incompatible certificates, etc. You can uninstall %1$s to fix the issue.
+ The operation failed because of storage issues. For example, the device may be running low on space, or external media may be unavailable. You may try to help free space or insert different external media.
+ The operation failed because it is fundamentally incompatible with this device. For example, the app may require a hardware feature that doesn\'t exist, it may be missing native code for the ABIs supported by the device, or it requires a newer SDK version, etc.
Dhizuku is a tool that can share Device owner permissions to other application.
Indicates if the device supports attestation of device identifiers in addition to key attestation.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 13626cb..f1304aa 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,8 +2,8 @@
agp = "8.8.0"
kotlin = "2.0.21"
-navigation-compose = "2.8.5"
-composeBom = "2025.01.00"
+navigation-compose = "2.8.6"
+composeBom = "2025.01.01"
accompanist-drawablepainter = "0.35.0-alpha"
accompanist-permissions = "0.37.0"
shizuku = "13.1.5"
@@ -15,6 +15,8 @@ serialization = "1.7.3"
[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-activity-compose = { module = "androidx.activity:activity-compose" }
androidx-material3 = { module = "androidx.compose.material3:material3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }