feat: improve intent filter presets

Fix app installer and uninstall app bug
This commit is contained in:
BinTianqi
2026-03-02 23:44:58 +08:00
parent f79474224c
commit 735177ac46
9 changed files with 135 additions and 30 deletions

View File

@@ -10,14 +10,20 @@ import androidx.fragment.app.FragmentActivity
import com.bintianqi.owndroid.feature.applications.AppInstaller import com.bintianqi.owndroid.feature.applications.AppInstaller
import com.bintianqi.owndroid.feature.applications.AppInstallerViewModel import com.bintianqi.owndroid.feature.applications.AppInstallerViewModel
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import com.bintianqi.owndroid.utils.viewModelFactory
class AppInstallerActivity:FragmentActivity() { class AppInstallerActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val vm by viewModels<AppInstallerViewModel>() val myApp = application as MyApplication
val vm by viewModels<AppInstallerViewModel> {
viewModelFactory {
AppInstallerViewModel(myApp, myApp.container.settingsRepo)
}
}
vm.initialize(intent) vm.initialize(intent)
val themeState = (application as MyApplication).container.themeState val themeState = myApp.container.themeState
setContent { setContent {
val theme by themeState.collectAsState() val theme by themeState.collectAsState()
OwnDroidTheme(theme) { OwnDroidTheme(theme) {

View File

@@ -10,7 +10,24 @@ data class IntentFilterOptions(
val direction: Int // 1: private to work, 2: work to private, 3: both val direction: Int // 1: private to work, 2: work to private, 3: both
) )
val crossProfileIntentFilterPresets = mapOf( val directionTextMap = mapOf(
R.string.allow_file_sharing to 1 to R.string.personal_to_work,
IntentFilterOptions(Intent.ACTION_SEND, Intent.CATEGORY_DEFAULT, "*/*", 3) 2 to R.string.work_to_personal,
3 to R.string.both_direction
)
class IntentFilterPreset(
val name: Int, val action: String, val category: String = "", val mimeType: String = ""
)
val crossProfileIntentFilterPresets = listOf(
IntentFilterPreset(R.string.open_file, Intent.ACTION_VIEW, Intent.CATEGORY_DEFAULT, "*/*"),
IntentFilterPreset(R.string.share, Intent.ACTION_SEND, Intent.CATEGORY_DEFAULT, "*/*"),
IntentFilterPreset(R.string.share_multiple, Intent.ACTION_SEND_MULTIPLE),
IntentFilterPreset(R.string.edit, Intent.ACTION_EDIT, Intent.CATEGORY_DEFAULT, "*/*"),
IntentFilterPreset(R.string.get_content, Intent.ACTION_GET_CONTENT, Intent.CATEGORY_DEFAULT, "*/*"),
IntentFilterPreset(R.string.install_app, Intent.ACTION_INSTALL_PACKAGE),
IntentFilterPreset(R.string.uninstall_app, Intent.ACTION_UNINSTALL_PACKAGE),
IntentFilterPreset(R.string.choose_file, Intent.ACTION_OPEN_DOCUMENT, Intent.CATEGORY_DEFAULT, "*/*"),
IntentFilterPreset(R.string.choose_folder, Intent.ACTION_OPEN_DOCUMENT_TREE)
) )

View File

@@ -2,6 +2,7 @@ package com.bintianqi.owndroid.feature.work_profile
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -9,11 +10,13 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -26,6 +29,7 @@ import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -39,6 +43,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -46,17 +51,17 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bintianqi.owndroid.R import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.ui.MyLazyScaffold
import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.Notes
import com.bintianqi.owndroid.utils.BottomPadding import com.bintianqi.owndroid.utils.BottomPadding
import com.bintianqi.owndroid.utils.HorizontalPadding import com.bintianqi.owndroid.utils.HorizontalPadding
import com.bintianqi.owndroid.utils.adaptiveInsets import com.bintianqi.owndroid.utils.adaptiveInsets
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CrossProfileIntentFilterScreen( fun CrossProfileIntentFilterScreen(
vm: CrossProfileIntentFilterViewModel, onNavigateUp: () -> Unit vm: CrossProfileIntentFilterViewModel, onNavigateUp: () -> Unit, navigateToPresets: () -> Unit
) { ) {
val focusMgr = LocalFocusManager.current val focusMgr = LocalFocusManager.current
var action by remember { mutableStateOf("") } var action by remember { mutableStateOf("") }
@@ -66,7 +71,6 @@ fun CrossProfileIntentFilterScreen(
var mimeType by remember { mutableStateOf("") } var mimeType by remember { mutableStateOf("") }
var dropdown by remember { mutableStateOf(false) } var dropdown by remember { mutableStateOf(false) }
var direction by remember { mutableIntStateOf(3) } var direction by remember { mutableIntStateOf(3) }
var dialog by remember { mutableStateOf(false) }
val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) vm.importFilters(it) if (it != null) vm.importFilters(it)
} }
@@ -90,7 +94,7 @@ fun CrossProfileIntentFilterScreen(
DropdownMenuItem( DropdownMenuItem(
{ Text(stringResource(R.string.presets)) }, { Text(stringResource(R.string.presets)) },
{ {
dialog = true navigateToPresets()
menu = false menu = false
}, },
leadingIcon = { leadingIcon = {
@@ -124,11 +128,6 @@ fun CrossProfileIntentFilterScreen(
}, },
contentWindowInsets = adaptiveInsets() contentWindowInsets = adaptiveInsets()
) { paddingValues -> ) { paddingValues ->
val directionTextMap = mapOf(
1 to R.string.personal_to_work,
2 to R.string.work_to_personal,
3 to R.string.both_direction
)
Column( Column(
Modifier Modifier
.padding(paddingValues) .padding(paddingValues)
@@ -205,24 +204,82 @@ fun CrossProfileIntentFilterScreen(
Notes(R.string.info_cross_profile_intent_filter) Notes(R.string.info_cross_profile_intent_filter)
Spacer(Modifier.height(BottomPadding)) Spacer(Modifier.height(BottomPadding))
} }
if (dialog) AlertDialog( }
title = { Text(stringResource(R.string.presets)) }, }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrossProfileIntentFilterPresetsScreen(
vm: CrossProfileIntentFilterViewModel, navigateUp: () -> Unit
) {
var dialog by remember { mutableStateOf<IntentFilterPreset?>(null) }
MyLazyScaffold(R.string.presets, navigateUp) {
items(crossProfileIntentFilterPresets) {
Row(
Modifier.padding(start = HorizontalPadding, end = 8.dp, bottom = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(Modifier.weight(1F)) {
Text(stringResource(it.name))
Text(
it.action,
Modifier.alpha(0.7F),
style = MaterialTheme.typography.bodyMedium
)
}
IconButton({ dialog = it }) {
Icon(Icons.Default.Add, null)
}
}
}
}
if (dialog != null) {
var direction by remember { mutableIntStateOf(3) }
AlertDialog(
title = {
Text(stringResource(dialog!!.name))
},
text = { text = {
crossProfileIntentFilterPresets.forEach { Column {
Button({ var dropdown by remember { mutableStateOf(false) }
vm.addFilter(it.value) Text(dialog!!.action)
dialog = false ExposedDropdownMenuBox(
}) { dropdown, { dropdown = it }, Modifier.padding(top = 5.dp)
Text(stringResource(it.key)) ) {
OutlinedTextField(
stringResource(directionTextMap[direction]!!), {},
Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
textStyle = MaterialTheme.typography.bodyLarge,
label = { Text(stringResource(R.string.direction)) }, readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(dropdown) }
)
ExposedDropdownMenu(dropdown, { dropdown = false }) {
directionTextMap.forEach {
DropdownMenuItem({ Text(stringResource(it.value)) }, {
direction = it.key
dropdown = false
})
}
}
} }
} }
}, },
confirmButton = { confirmButton = {
TextButton({ dialog = false }) { TextButton({
vm.addPreset(dialog!!, direction)
dialog = null
}) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton({ dialog = null }) {
Text(stringResource(R.string.cancel)) Text(stringResource(R.string.cancel))
} }
}, },
onDismissRequest = { dialog = false } onDismissRequest = { dialog = null }
) )
} }
} }

View File

@@ -25,6 +25,10 @@ class CrossProfileIntentFilterViewModel(
toastChannel.sendStatus(true) toastChannel.sendStatus(true)
} }
fun addPreset(preset: IntentFilterPreset, direction: Int) {
addFilter(IntentFilterOptions(preset.action, preset.category, preset.mimeType, direction))
}
fun clearFilters() = ph.safeDpmCall { fun clearFilters() = ph.safeDpmCall {
dpm.clearCrossProfileIntentFilters(dar) dpm.clearCrossProfileIntentFilters(dar)
repo.deleteAllCrossProfileIntentFilters() repo.deleteAllCrossProfileIntentFilters()

View File

@@ -60,6 +60,7 @@ sealed class Destination : NavKey {
@Serializable object CreateWorkProfile : Destination() @Serializable object CreateWorkProfile : Destination()
@Serializable object SuspendPersonalApp : Destination() @Serializable object SuspendPersonalApp : Destination()
@Serializable object CrossProfileIntentFilter : Destination() @Serializable object CrossProfileIntentFilter : Destination()
@Serializable object CrossProfileIntentFilterPresets: Destination()
@Serializable object DeleteWorkProfile : Destination() @Serializable object DeleteWorkProfile : Destination()
@Serializable object ApplicationFeatures : Destination() @Serializable object ApplicationFeatures : Destination()

View File

@@ -95,6 +95,7 @@ import com.bintianqi.owndroid.feature.users.UserSessionMessageScreen
import com.bintianqi.owndroid.feature.users.UsersOptionsScreen import com.bintianqi.owndroid.feature.users.UsersOptionsScreen
import com.bintianqi.owndroid.feature.users.UsersScreen import com.bintianqi.owndroid.feature.users.UsersScreen
import com.bintianqi.owndroid.feature.work_profile.CreateWorkProfileScreen import com.bintianqi.owndroid.feature.work_profile.CreateWorkProfileScreen
import com.bintianqi.owndroid.feature.work_profile.CrossProfileIntentFilterPresetsScreen
import com.bintianqi.owndroid.feature.work_profile.CrossProfileIntentFilterScreen import com.bintianqi.owndroid.feature.work_profile.CrossProfileIntentFilterScreen
import com.bintianqi.owndroid.feature.work_profile.DeleteWorkProfileScreen import com.bintianqi.owndroid.feature.work_profile.DeleteWorkProfileScreen
import com.bintianqi.owndroid.feature.work_profile.SuspendPersonalAppScreen import com.bintianqi.owndroid.feature.work_profile.SuspendPersonalAppScreen
@@ -360,7 +361,14 @@ fun myEntryProvider(
entry<Destination.CrossProfileIntentFilter> { entry<Destination.CrossProfileIntentFilter> {
CrossProfileIntentFilterScreen( CrossProfileIntentFilterScreen(
viewModel(factory = container.viewModelFactory), ::navigateUp viewModel(factory = container.viewModelFactory), ::navigateUp
) ) {
navigate(Destination.CrossProfileIntentFilterPresets)
}
}
entry<Destination.CrossProfileIntentFilterPresets>(
metadata = navParentKey(Destination.CrossProfileIntentFilter)
) {
CrossProfileIntentFilterPresetsScreen(viewModel(), ::navigateUp)
} }
entry<Destination.DeleteWorkProfile>( entry<Destination.DeleteWorkProfile>(
metadata = navParentKey(Destination.WorkProfile) metadata = navParentKey(Destination.WorkProfile)

View File

@@ -233,9 +233,11 @@ fun uninstallPackage(
val receiver = object : BroadcastReceiver() { val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999) val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
if(statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) { if (statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
@SuppressWarnings("UnsafeIntentLaunch") @SuppressWarnings("UnsafeIntentLaunch")
context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?) val confirmIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
confirmIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(confirmIntent)
} else { } else {
context.unregisterReceiver(this) context.unregisterReceiver(this)
if (statusExtra == PackageInstaller.STATUS_SUCCESS) { if (statusExtra == PackageInstaller.STATUS_SUCCESS) {

View File

@@ -338,7 +338,12 @@
<string name="personal_app_suspended_because_timeout">个人应用已经因此挂起:%1$s</string> <string name="personal_app_suspended_because_timeout">个人应用已经因此挂起:%1$s</string>
<string name="intent_filter">Intent过滤器</string> <string name="intent_filter">Intent过滤器</string>
<string name="presets">预设</string> <string name="presets">预设</string>
<string name="allow_file_sharing">允许分享文件</string> <string name="open_file">打开文件</string>
<string name="share">分享</string>
<string name="share_multiple">分享(多个)</string>
<string name="get_content">获取内容</string>
<string name="choose_file">选择文件</string>
<string name="choose_folder">选择文件夹</string>
<string name="direction">方向</string> <string name="direction">方向</string>
<string name="both_direction">双向</string> <string name="both_direction">双向</string>
<string name="work_to_personal">工作到个人</string> <string name="work_to_personal">工作到个人</string>

View File

@@ -372,7 +372,12 @@
<string name="personal_app_suspended_because_timeout">Personal app suspended because of this: %1$s</string> <string name="personal_app_suspended_because_timeout">Personal app suspended because of this: %1$s</string>
<string name="intent_filter">Intent filter</string> <string name="intent_filter">Intent filter</string>
<string name="presets">Presets</string> <string name="presets">Presets</string>
<string name="allow_file_sharing">Allow file sharing</string> <string name="open_file">Open file</string>
<string name="share">Share</string>
<string name="share_multiple">Share (multiple)</string>
<string name="get_content">Get content</string>
<string name="choose_file">Choose file</string>
<string name="choose_folder">Choose folder</string>
<string name="direction">Direction</string> <string name="direction">Direction</string>
<string name="both_direction">Both direction</string> <string name="both_direction">Both direction</string>
<string name="work_to_personal">Work to personal</string> <string name="work_to_personal">Work to personal</string>