More options in app installer

Add download links to READMEs
This commit is contained in:
BinTianqi
2025-02-15 21:03:32 +08:00
parent 7995bfbdfe
commit d8044ee8ab
11 changed files with 204 additions and 52 deletions

View File

@@ -6,6 +6,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Build
@@ -17,14 +18,21 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.biometric.BiometricPrompt
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
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.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
@@ -38,11 +46,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
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.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -55,7 +69,8 @@ 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.FullWidthCheckBoxItem
import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
@@ -76,13 +91,13 @@ class AppInstallerActivity:FragmentActivity() {
val theme by myVm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) {
val installing by vm.installing.collectAsStateWithLifecycle()
val sessionMode by vm.sessionMode.collectAsStateWithLifecycle()
val options by vm.options.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 },
installing, options, { if(!installing) vm.options.value = it },
packages, { uri -> vm.packages.update { it.minus(uri) } },
{ uris -> vm.packages.update { it.plus(uris) } },
{ vm.startInstallationProcess(this) }, writtenPackages, writingPackage,
@@ -98,8 +113,8 @@ class AppInstallerActivity:FragmentActivity() {
@Composable
private fun AppInstaller(
installing: Boolean = false,
sessionMode: Int = PackageInstaller.SessionParams.MODE_INHERIT_EXISTING,
onSessionModeChoose: (Int) -> Unit = {},
options: SessionParamsOptions = SessionParamsOptions(),
onOptionsChange: (SessionParamsOptions) -> Unit = {},
packages: Set<Uri> = setOf(Uri.parse("https://example.com")),
onPackageRemove: (Uri) -> Unit = {},
onPackageChoose: (List<Uri>) -> Unit = {},
@@ -109,6 +124,7 @@ private fun AppInstaller(
result: Intent? = null,
onResultDialogClose: () -> Unit = {}
) {
val coroutine = rememberCoroutineScope()
Scaffold(
topBar = {
TopAppBar(
@@ -127,53 +143,65 @@ private fun AppInstaller(
)
}
) { paddingValues ->
var tab by remember { mutableIntStateOf(0) }
val pagerState = rememberPagerState { 2 }
val scrollState = rememberScrollState()
Column(modifier = Modifier.padding(paddingValues)) {
SessionMode(sessionMode, onSessionModeChoose)
Packages(installing, packages, onPackageRemove, onPackageChoose, writtenPackages, writingPackage)
TabRow(tab) {
Tab(
tab == 0,
onClick = {
tab = 0
coroutine.launch { scrollState.animateScrollTo(0) }
coroutine.launch { pagerState.animateScrollToPage(0) }
},
text = { Text(stringResource(R.string.packages)) }
)
Tab(
tab == 1,
onClick = {
tab = 1
coroutine.launch { scrollState.animateScrollTo(0) }
coroutine.launch { pagerState.animateScrollToPage(1) }
},
text = { Text(stringResource(R.string.options)) }
)
}
HorizontalPager(pagerState) { page ->
Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(top = 8.dp)) {
if(page == 0) Packages(installing, packages, onPackageRemove, onPackageChoose, writtenPackages, writingPackage)
else Options(options, onOptionsChange)
}
}
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(
private fun ColumnScope.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)
AnimatedVisibility(!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)
}
}
}
@@ -198,6 +226,50 @@ private fun PackageItem(uri: Uri, installing: Boolean, onRemove: () -> Unit, isW
}
}
data class SessionParamsOptions(
val mode: Int = PackageInstaller.SessionParams.MODE_FULL_INSTALL,
val keepOriginalEnabledSetting: Boolean = false,
val noKill: Boolean = false,
val location: Int = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY,
)
@Composable
private fun ColumnScope.Options(options: SessionParamsOptions, onChange: (SessionParamsOptions) -> Unit) {
Text(
stringResource(R.string.mode), modifier = Modifier.padding(top = 10.dp, start = 8.dp, bottom = 4.dp),
style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary
)
FullWidthRadioButtonItem(R.string.full_install, options.mode == PackageInstaller.SessionParams.MODE_FULL_INSTALL) {
onChange(options.copy(mode = PackageInstaller.SessionParams.MODE_FULL_INSTALL, noKill = false))
}
FullWidthRadioButtonItem(R.string.inherit_existing, options.mode == PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) {
onChange(options.copy(mode = PackageInstaller.SessionParams.MODE_INHERIT_EXISTING))
}
if(Build.VERSION.SDK_INT >= 34) {
AnimatedVisibility(options.mode == PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) {
FullWidthCheckBoxItem(R.string.dont_kill_app, options.noKill) {
onChange(options.copy(noKill = it))
}
}
FullWidthCheckBoxItem(R.string.keep_original_enabled_setting, options.keepOriginalEnabledSetting) {
onChange(options.copy(keepOriginalEnabledSetting = it))
}
}
Text(
stringResource(R.string.install_location), modifier = Modifier.padding(top = 10.dp, start = 8.dp, bottom = 4.dp),
style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary
)
FullWidthRadioButtonItem(R.string.auto, options.location == PackageInfo.INSTALL_LOCATION_AUTO) {
onChange(options.copy(location = PackageInfo.INSTALL_LOCATION_AUTO))
}
FullWidthRadioButtonItem(R.string.internal_only, options.location == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
onChange(options.copy(location = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY))
}
FullWidthRadioButtonItem(R.string.prefer_external, options.location == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
onChange(options.copy(location = PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL))
}
}
@Composable
private fun ResultDialog(result: Intent?, onDialogClose: () -> Unit) {
if(result != null) {
@@ -236,7 +308,7 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
val result = MutableStateFlow<Intent?>(null)
val packages = MutableStateFlow(setOf<Uri>())
val sessionMode = MutableStateFlow(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val options = MutableStateFlow(SessionParamsOptions())
val writtenPackages = MutableStateFlow(setOf<Uri>())
val writingPackage = MutableStateFlow<Uri?>(null)
@@ -254,13 +326,22 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
})
else startInstall()
}
private fun getSessionParams(): PackageInstaller.SessionParams {
return PackageInstaller.SessionParams(options.value.mode).apply {
if(Build.VERSION.SDK_INT >= 34) {
if(options.value.keepOriginalEnabledSetting) setApplicationEnabledSettingPersistent()
setDontKillApp(options.value.noKill)
}
setInstallLocation(options.value.location)
}
}
private fun startInstall() {
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 sessionId = packageInstaller.createSession(getSessionParams())
val session = packageInstaller.openSession(sessionId)
try {
packages.value.forEach { splitPackageUri ->

View File

@@ -40,11 +40,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
@@ -122,14 +125,12 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) {
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }),
trailingIcon = {
Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.clickable(onClick = {
focusMgr.clearFocus()
choosePackage.launch(null)
})
.padding(3.dp))
IconButton({
focusMgr.clearFocus()
choosePackage.launch(null)
}) {
Icon(Icons.AutoMirrored.Default.List, stringResource(R.string.package_chooser))
}
},
textStyle = typography.bodyLarge,
singleLine = true
@@ -173,13 +174,8 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
val deviceOwner = context.isDeviceOwner
val profileOwner = context.isProfileOwner
var suspend by remember { mutableStateOf(false) }
suspend = try{ if(VERSION.SDK_INT >= 24) dpm.isPackageSuspended(receiver, pkgName) else false }
catch(_: NameNotFoundException) { false }
catch(_: IllegalArgumentException) { false }
var hide by remember { mutableStateOf(false) }
hide = dpm.isApplicationHidden(receiver, pkgName)
var blockUninstall by remember { mutableStateOf(false) }
blockUninstall = dpm.isUninstallBlocked(receiver,pkgName)
var appControlAction by remember { mutableIntStateOf(0) }
val focusMgr = LocalFocusManager.current
val appControl: (Boolean) -> Unit = {
@@ -198,6 +194,13 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
3 -> blockUninstall = dpm.isUninstallBlocked(receiver,pkgName)
}
}
LaunchedEffect(pkgName) {
suspend = try{ if(VERSION.SDK_INT >= 24) dpm.isPackageSuspended(receiver, pkgName) else false }
catch(_: NameNotFoundException) { false }
catch(_: IllegalArgumentException) { false }
hide = dpm.isApplicationHidden(receiver, pkgName)
blockUninstall = dpm.isUninstallBlocked(receiver,pkgName)
}
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {

View File

@@ -20,7 +20,6 @@ import android.app.admin.WifiSsidPolicy.WIFI_SSID_POLICY_TYPE_DENYLIST
import android.app.usage.NetworkStats
import android.app.usage.NetworkStatsManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.NameNotFoundException
import android.net.ConnectivityManager
import android.net.IpConfiguration
@@ -82,6 +81,8 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@@ -343,10 +344,12 @@ private fun SavedNetworks(onNavigateToUpdateNetwork: (Bundle) -> Unit) {
val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
val configuredNetworks = remember { mutableStateListOf<WifiConfiguration>() }
var networkDetailsDialog by remember { mutableIntStateOf(-1) } // -1:Hidden, 0+:Index of configuredNetworks
val coroutine = rememberCoroutineScope()
fun refresh() {
configuredNetworks.clear()
wm.configuredNetworks.forEach { network ->
if(configuredNetworks.none { it.networkId == network.networkId }) configuredNetworks += network
coroutine.launch(Dispatchers.IO) {
val list = wm.configuredNetworks.distinctBy { it.networkId }
withContext(Dispatchers.Main) { configuredNetworks.addAll(list) }
}
}
LaunchedEffect(Unit) { refresh() }
@@ -439,6 +442,7 @@ private fun SavedNetworks(onNavigateToUpdateNetwork: (Bundle) -> Unit) {
},
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Edit, null)
Text(stringResource(R.string.edit))
}
TextButton(
@@ -450,6 +454,7 @@ private fun SavedNetworks(onNavigateToUpdateNetwork: (Bundle) -> Unit) {
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error),
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Outlined.Delete, null)
Text(stringResource(R.string.remove))
}
}

View File

@@ -196,7 +196,9 @@ fun UserInfoScreen(onNavigateUp: () -> Unit) {
if(VERSION.SDK_INT >= 23) CardItem(R.string.system_user, userManager.isSystemUser.yesOrNo)
if(VERSION.SDK_INT >= 34) CardItem(R.string.admin_user, userManager.isAdminUser.yesOrNo)
if(VERSION.SDK_INT >= 25) CardItem(R.string.demo_user, userManager.isDemoUser.yesOrNo)
if(VERSION.SDK_INT >= 26) CardItem(R.string.creation_time, parseTimestamp(userManager.getUserCreationTime(user)))
if(VERSION.SDK_INT >= 26) userManager.getUserCreationTime(user).let {
if(it != 0L) CardItem(R.string.creation_time, parseTimestamp(it))
}
if (VERSION.SDK_INT >= 28) {
CardItem(R.string.logout_enabled, dpm.isLogoutEnabled.yesOrNo)
if(context.isDeviceOwner || context.isProfileOwner) {

View File

@@ -99,6 +99,21 @@ fun RadioButtonItem(
}
}
@Composable
fun FullWidthRadioButtonItem(
text: Int,
selected: Boolean,
operation: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable(onClick = operation)
) {
RadioButton(selected = selected, onClick = operation, modifier = Modifier.padding(horizontal = 4.dp))
Text(text = stringResource(text), modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp))
}
}
@Composable
fun CheckBoxItem(
@StringRes text: Int,
@@ -118,6 +133,20 @@ fun CheckBoxItem(
}
}
@Composable
fun FullWidthCheckBoxItem(
@StringRes text: Int,
checked: Boolean,
operation: (Boolean) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().clickable { operation(!checked) }
) {
Checkbox(checked = checked, onCheckedChange = operation, modifier = Modifier.padding(horizontal = 4.dp))
Text(text = stringResource(text), modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp))
}
}
@Composable
fun SwitchItem(

View File

@@ -649,6 +649,12 @@
<string name="mode">Режим</string>
<string name="full_install">Полная установка</string>
<string name="inherit_existing">Наследовать существующие</string>
<!--TODO 5 new strings-->
<string name="keep_original_enabled_setting">Keep original enabled setting</string>
<string name="dont_kill_app">Don\'t kill app</string>
<string name="install_location">Install location</string>
<string name="internal_only">Internal only</string>
<string name="prefer_external">Prefer external</string>
<string name="packages">Пакеты</string>
<string name="add_packages">Добавить пакет(ы)</string>
<string name="status_failure_blocked">Операция была заблокирована: %1$s</string>

View File

@@ -652,6 +652,11 @@
<string name="mode">Mode</string>
<string name="full_install">Full install</string>
<string name="inherit_existing">Inherit existing</string>
<string name="keep_original_enabled_setting">Keep original enabled setting</string>
<string name="dont_kill_app">Don\'t kill app</string>
<string name="install_location">Install location</string>
<string name="internal_only">Internal only</string>
<string name="prefer_external">Prefer external</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>

View File

@@ -631,10 +631,16 @@
<string name="permission_BODY_SENSORS_BACKGROUND">后台使用身体传感器</string>
<string name="permission_ACTIVITY_RECOGNITION">查看使用情况</string>
<string name="package_chooser">包选择器</string>
<string name="app_installer">App安装器</string>
<string name="mode">模式</string>
<string name="full_install">完整安装</string>
<string name="inherit_existing">继承已有</string>
<string name="keep_original_enabled_setting">保持原始启用设置</string>
<string name="dont_kill_app">不要杀死app</string>
<string name="install_location">安装位置</string>
<string name="internal_only">仅内部</string>
<string name="prefer_external">外部优先</string>
<string name="packages">软件包</string>
<string name="add_packages">添加包</string>
<string name="status_failure_blocked">操作被 %1$s 阻止。</string>

View File

@@ -676,6 +676,11 @@
<string name="mode">Mode</string>
<string name="full_install">Full install</string>
<string name="inherit_existing">Inherit existing</string>
<string name="keep_original_enabled_setting">Keep original enabled setting</string>
<string name="dont_kill_app">Don\'t kill app</string>
<string name="install_location">Install location</string>
<string name="internal_only">Internal only</string>
<string name="prefer_external">Prefer external</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>