mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 19:15:58 +00:00
New app installer
Update dependencies
This commit is contained in:
305
app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
Normal file
305
app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user