diff --git a/Readme-en.md b/Readme-en.md index 6e9c16d..78983c6 100644 --- a/Readme-en.md +++ b/Readme-en.md @@ -4,6 +4,11 @@ Use Android Device owner privilege to manage your device. +## Download + +[IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/com.bintianqi.owndroid) +[Releases on GitHub](https://github.com/BinTianqi/OwnDroid/releases) + ## Features - System diff --git a/Readme.md b/Readme.md index 3f5eb0f..a4643b1 100644 --- a/Readme.md +++ b/Readme.md @@ -4,6 +4,11 @@ 使用安卓Device owner特权管理你的设备。 +## 下载 + +[IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/com.bintianqi.owndroid) +[Releases on GitHub](https://github.com/BinTianqi/OwnDroid/releases) + ## 功能 - 系统 diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt index 0340f66..d206121 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt @@ -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 = setOf(Uri.parse("https://example.com")), onPackageRemove: (Uri) -> Unit = {}, onPackageChoose: (List) -> 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, onRemove: (Uri) -> Unit, onChoose: (List) -> Unit, writtenPackages: Set, 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(null) val packages = MutableStateFlow(setOf()) - val sessionMode = MutableStateFlow(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + val options = MutableStateFlow(SessionParamsOptions()) val writtenPackages = MutableStateFlow(setOf()) val writingPackage = MutableStateFlow(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() 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 -> diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 3976e42..6e75add 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -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()) ) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index b17dfa4..9847db3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -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() } 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)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 67f5314..8f90578 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -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) { diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 399c4cc..15d423b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -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( diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e949d81..3e59209 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -649,6 +649,12 @@ Режим Полная установка Наследовать существующие + + Keep original enabled setting + Don\'t kill app + Install location + Internal only + Prefer external Пакеты Добавить пакет(ы) Операция была заблокирована: %1$s diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 431c5a1..eae0615 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -652,6 +652,11 @@ Mode Full install Inherit existing + Keep original enabled setting + Don\'t kill app + Install location + Internal only + Prefer external Packages Add package(s) The operation was blocked by: %1$s diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fe761ca..454898d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -631,10 +631,16 @@ 后台使用身体传感器 查看使用情况 + 包选择器 App安装器 模式 完整安装 继承已有 + 保持原始启用设置 + 不要杀死app + 安装位置 + 仅内部 + 外部优先 软件包 添加包 操作被 %1$s 阻止。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc1e3ca..5ec1a1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -676,6 +676,11 @@ Mode Full install Inherit existing + Keep original enabled setting + Don\'t kill app + Install location + Internal only + Prefer external Packages Add package(s) The operation was blocked by: %1$s