New app installer

Update dependencies
This commit is contained in:
BinTianqi
2025-02-05 21:17:46 +08:00
parent 4640f6d1f2
commit 5726bbca14
17 changed files with 454 additions and 356 deletions

View File

@@ -42,18 +42,16 @@
android:theme="@style/Theme.Transparent">
</activity>
<activity
android:name=".InstallAppActivity"
android:name=".AppInstallerActivity"
android:label="@string/app_installer"
android:exported="true"
android:windowSoftInputMode="adjustResize|stateHidden"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
android:theme="@style/Theme.Transparent">
android:launchMode="singleInstance">
<intent-filter>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.INSTALL_PACKAGE"/>
<data android:scheme="content"/>
<data android:scheme="file"/>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<data android:mimeType="application/vnd.android.package-archive"/>
</intent-filter>
</activity>
@@ -75,11 +73,6 @@
<action android:name="android.app.action.DEVICE_ADMIN_DISABLED"/>
</intent-filter>
</receiver>
<receiver
android:name=".PackageInstallerReceiver"
android:description="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN">
</receiver>
<receiver
android:name=".ApiReceiver"
android:exported="true">

View File

@@ -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<MyViewModel>()
val vm by viewModels<AppInstallerViewModel>()
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<Uri> = setOf(Uri.parse("https://example.com")),
onPackageRemove: (Uri) -> Unit = {},
onPackageChoose: (List<Uri>) -> Unit = {},
onFabPressed: () -> Unit = {},
writtenPackages: Set<Uri> = 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<Uri>, onRemove: (Uri) -> Unit, onChoose: (List<Uri>) -> Unit,
writtenPackages: Set<Uri>, 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<Uri>(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<Intent?>(null)
val packages = MutableStateFlow(setOf<Uri>())
val sessionMode = MutableStateFlow(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val writtenPackages = MutableStateFlow(setOf<Uri>())
val writingPackage = MutableStateFlow<Uri?>(null)
fun startInstallationProcess() {
if(installing.value) return
installing.value = true
viewModelScope.launch(Dispatchers.IO) {
val context = getApplication<Application>()
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"
}
}

View File

@@ -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<MyViewModel>()
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()
}
}
}
}
}

View File

@@ -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<MyViewModel>()
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(

View File

@@ -28,7 +28,6 @@ class ManageSpaceActivity: FragmentActivity() {
val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE)
val authenticate = sharedPref.getBoolean("auth", false)
val vm by viewModels<MyViewModel>()
if(!vm.initialized) vm.initialize(applicationContext)
fun clearStorage() {
filesDir.deleteRecursively()
cacheDir.deleteRecursively()

View File

@@ -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<PackageInfo>()
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,

View File

@@ -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()
}
}
}

View File

@@ -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"

View File

@@ -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<Uri?>(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))
}
}
}
}
}

View File

@@ -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<Intent>
@@ -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" }
}

View File

@@ -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

View File

@@ -417,21 +417,6 @@
<string name="silent_uninstall">Тихое удаление</string>
<string name="request_uninstall">Запросить удаление</string>
<string name="install_app">Установить приложение</string>
<string name="select_apk" tools:ignore="TypographyEllipsis">Выберите APK...</string>
<string name="silent_install">Тихая установка</string>
<string name="request_install">Запросить установку</string>
<string name="search">Поиск</string>
<!--Статус сеанса установки приложения-->
<string name="app_installer_status">Установщик приложений:</string>
<string name="status_pending_action">Ожидание действия пользователя</string>
<string name="status_fail_blocked">Ошибка: заблокировано</string>
<string name="status_fail_aborted">Ошибка: прервано</string>
<string name="status_fail_invalid">Ошибка: неверный APK</string>
<string name="status_fail_conflict">Ошибка: конфликт</string>
<string name="status_fail_storage">Ошибка: недостаточно места</string>
<string name="status_fail_incompatible">Ошибка: несовместимо</string>
<string name="status_fail_timeout">Ошибка: истекло время ожидания</string>
<!--Ограничения пользователя-->
<string name="user_restriction">Ограничения пользователя</string>
@@ -668,9 +653,19 @@
<string name="permission_BODY_SENSORS_BACKGROUND">Доступ к датчикам тела в фоновом режиме</string>
<string name="permission_ACTIVITY_RECOGNITION">Распознавание активности</string>
<string name="version_name">Версия</string>
<string name="version_code">Код версии</string>
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">Анализ информации APK...</string>
<!--TODO-->
<string name="app_installer">App installer</string>
<string name="mode">Mode</string>
<string name="full_install">Full install</string>
<string name="inherit_existing">Inherit existing</string>
<string name="packages">Packages</string>
<string name="add_packages">Add package(s)</string>
<string name="status_failure_blocked">The operation was blocked by: %1$s</string>
<string name="status_failure_aborted">The operation was aborted.</string>
<string name="status_failure_invalid">The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.</string>
<string name="status_failure_conflict">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.</string>
<string name="status_failure_storage">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.</string>
<string name="status_failure_incompatible">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.</string>
<!--TODO: Translate strings start with "info_"-->
</resources>

View File

@@ -417,22 +417,8 @@
<string name="silent_uninstall">Sessiz kaldırma</string>
<string name="request_uninstall">Kaldırma isteği</string>
<string name="install_app">Uygulamayı yükle</string>
<string name="select_apk" tools:ignore="TypographyEllipsis">APK seç...</string>
<string name="silent_install">Sessiz yükleme</string>
<string name="request_install">Yükleme isteği</string>
<string name="search">Search</string> <!--TODO-->
<!--App install session status-->
<string name="app_installer_status">Uygulama yükleyici:</string>
<string name="status_pending_action">Bekleyen kullanıcı işlemi</string>
<string name="status_fail_blocked">Başarısız: engellendi</string>
<string name="status_fail_aborted">Başarısız: iptal edildi</string>
<string name="status_fail_invalid">Başarısız: geçersiz APK</string>
<string name="status_fail_conflict">Başarısız: çakışma</string>
<string name="status_fail_storage">Başarısız: boş alan yok</string>
<string name="status_fail_incompatible">Başarısız: uyumsuz</string>
<string name="status_fail_timeout">Başarısız: zaman aşımı</string>
<!--UserRestriction-->
<string name="user_restriction">Kullanıcı kısıtlaması</string>
<string name="profile_owner_is_restricted">Profil sahibi sınırlı işlev kullanabilir</string>
@@ -664,9 +650,19 @@
<string name="permission_BODY_SENSORS_BACKGROUND">Arka planda vücut sensörlerine eriş</string>
<string name="permission_ACTIVITY_RECOGNITION">Aktivite tanıma</string>
<string name="version_name">Version name</string> <!--TODO-->
<string name="version_code">Version code</string> <!--TODO-->
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">Parsing APK info...</string>
<!--TODO-->
<string name="app_installer">App installer</string>
<string name="mode">Mode</string>
<string name="full_install">Full install</string>
<string name="inherit_existing">Inherit existing</string>
<string name="packages">Packages</string>
<string name="add_packages">Add package(s)</string>
<string name="status_failure_blocked">The operation was blocked by: %1$s</string>
<string name="status_failure_aborted">The operation was aborted.</string>
<string name="status_failure_invalid">The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.</string>
<string name="status_failure_conflict">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.</string>
<string name="status_failure_storage">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.</string>
<string name="status_failure_incompatible">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.</string>
<!--TODO: Translate strings start with "info_"-->
</resources>

View File

@@ -402,22 +402,9 @@
<string name="silent_uninstall">静默卸载</string>
<string name="request_uninstall">请求卸载</string>
<string name="install_app">安装应用</string>
<string name="select_apk" tools:ignore="TypographyEllipsis">选择APK...</string>
<string name="silent_install">静默安装</string>
<string name="request_install">请求安装</string>
<string name="enable_system_app">启用系统应用</string>
<string name="enable_system_app_desc">重新启用一个默认被禁用的系统应用</string>
<string name="search">搜索</string>
<!--App install session status-->
<string name="app_installer_status">应用安装器:</string>
<string name="status_pending_action">等待用户操作</string>
<string name="status_fail_blocked">被阻止</string>
<string name="status_fail_aborted">被打断</string>
<string name="status_fail_invalid">无效APK</string>
<string name="status_fail_conflict">冲突</string>
<string name="status_fail_storage">空间不足</string>
<string name="status_fail_incompatible">不兼容</string>
<string name="status_fail_timeout">超时</string>
<!--UserRestriction-->
<string name="user_restriction">用户限制</string>
@@ -649,9 +636,18 @@
<string name="permission_BODY_SENSORS_BACKGROUND">后台使用身体传感器</string>
<string name="permission_ACTIVITY_RECOGNITION">查看使用情况</string>
<string name="version_name">版本名</string>
<string name="version_code">版本号</string>
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">解析APK信息中...</string>
<string name="app_installer">App安装器</string>
<string name="mode">模式</string>
<string name="full_install">完整安装</string>
<string name="inherit_existing">继承已有</string>
<string name="packages">软件包</string>
<string name="add_packages">添加包</string>
<string name="status_failure_blocked">操作被 %1$s 阻止。</string>
<string name="status_failure_aborted">操作被打断。</string>
<string name="status_failure_invalid">操作失败因为有一个或多个APK无效。例如它们可能被篡改损坏签名错误不匹配等。</string>
<string name="status_failure_conflict">操作失败,因为它与 %1$s 冲突。例如,存在的权限,不完整的证书等。你可以卸载 %1$s 以解决这个问题。</string>
<string name="status_failure_storage">由于存储问题,操作失败。例如,设备空间不足,或外部媒体不可用。你可以尝试清理空间或插入不同的外部媒体。</string>
<string name="status_failure_incompatible">操作失败因为它与设备不兼容。例如这个app可能需要不存在的硬件功能它有可能缺少受此设备支持的ABI的本地代码或它需要高于此设备的SDK版本。</string>
<string name="info_dhizuku">Dhizuku可以分享Device owner权限给其余应用</string>
<string name="info_device_id_attestation">指示设备是否除了密钥证明之外还支持设备标识符证明</string>

View File

@@ -7,6 +7,7 @@
<string name="disable">Disable</string>
<string name="enable">Enable</string>
<string name="success">Success</string>
<string name="failure">Failure</string>
<string name="failed">Failed</string>
<string name="add">Add</string>
<string name="remove">Remove</string>
@@ -71,6 +72,7 @@
<string name="overview">Overview</string>
<string name="features">Features</string>
<string name="default_str">Default</string>
<string name="timeout">Timeout</string>
<!--Permissions-->
<string name="click_to_activate">Click to activate</string>
@@ -441,20 +443,7 @@
<string name="silent_uninstall">Silent uninstall</string>
<string name="request_uninstall">Request uninstall</string>
<string name="install_app">Install app</string>
<string name="select_apk" tools:ignore="TypographyEllipsis">Select APK...</string>
<string name="silent_install">Silent install</string>
<string name="request_install">Request install</string>
<string name="search">Search</string>
<!--App install session status-->
<string name="app_installer_status">App installer:</string>
<string name="status_pending_action">Pending user action</string>
<string name="status_fail_blocked">Fail: blocked</string>
<string name="status_fail_aborted">Fail: aborted</string>
<string name="status_fail_invalid">Fail: invalid APK</string>
<string name="status_fail_conflict">Fail: conflict</string>
<string name="status_fail_storage">Fail: no space</string>
<string name="status_fail_incompatible">Fail: incompatible</string>
<string name="status_fail_timeout">Fail: timeout</string>
<!--UserRestriction-->
<string name="user_restriction">User restriction</string>
@@ -687,9 +676,18 @@
<string name="permission_BODY_SENSORS_BACKGROUND">Access body sensors in background</string>
<string name="permission_ACTIVITY_RECOGNITION">Activity recognition</string>
<string name="version_name">Version name</string>
<string name="version_code">Version code</string>
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">Parsing APK info...</string>
<string name="app_installer">App installer</string>
<string name="mode">Mode</string>
<string name="full_install">Full install</string>
<string name="inherit_existing">Inherit existing</string>
<string name="packages">Packages</string>
<string name="add_packages">Add package(s)</string>
<string name="status_failure_blocked">The operation was blocked by: %1$s</string>
<string name="status_failure_aborted">The operation was aborted.</string>
<string name="status_failure_invalid">The operation failed because one or more of the APKs was invalid. For example, they might be malformed, corrupt, incorrectly signed, mismatched, etc.</string>
<string name="status_failure_conflict">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.</string>
<string name="status_failure_storage">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.</string>
<string name="status_failure_incompatible">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.</string>
<string name="info_dhizuku">Dhizuku is a tool that can share Device owner permissions to other application.</string>
<string name="info_device_id_attestation">Indicates if the device supports attestation of device identifiers in addition to key attestation.</string>