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:
@@ -80,6 +80,8 @@ gradle.taskGraph.whenReady {
|
||||
dependencies {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
implementation(libs.accompanist.drawablepainter)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.androidx.material3)
|
||||
|
||||
@@ -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">
|
||||
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
agp = "8.8.0"
|
||||
kotlin = "2.0.21"
|
||||
|
||||
navigation-compose = "2.8.5"
|
||||
composeBom = "2025.01.00"
|
||||
navigation-compose = "2.8.6"
|
||||
composeBom = "2025.01.01"
|
||||
accompanist-drawablepainter = "0.35.0-alpha"
|
||||
accompanist-permissions = "0.37.0"
|
||||
shizuku = "13.1.5"
|
||||
@@ -15,6 +15,8 @@ serialization = "1.7.3"
|
||||
|
||||
[libraries]
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose" }
|
||||
androidx-material3 = { module = "androidx.compose.material3:material3" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
|
||||
|
||||
Reference in New Issue
Block a user