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

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