Skip to content

Commit e94aba2

Browse files
committed
Support disabling intent filter (#329)
Load app list only when needed (#338)
1 parent 34d6796 commit e94aba2

7 files changed

Lines changed: 129 additions & 58 deletions

File tree

app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import android.content.Context
44
import android.database.sqlite.SQLiteDatabase
55
import android.database.sqlite.SQLiteOpenHelper
66

7-
class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 9) {
7+
class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 10) {
88
override fun onCreate(db: SQLiteDatabase) {
99
db.execSQL(DHIZUKU_CLIENTS_TABLE)
1010
db.execSQL(SECURITY_LOGS_TABLE)
@@ -29,6 +29,9 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 9) {
2929
db.execSQL(DELETE_CPIF)
3030
db.execSQL(CPIF2_TABLE)
3131
}
32+
if (oldVersion < 10) {
33+
db.execSQL(CPIF2_ADD_STATUS)
34+
}
3235
}
3336
companion object {
3437
const val DHIZUKU_CLIENTS_TABLE = "CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," +
@@ -47,5 +50,6 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 9) {
4750
const val CPIF2_TABLE = "CREATE TABLE cpif2 (id INTEGER PRIMARY KEY," +
4851
"action_str TEXT, category TEXT, mime_type TEXT, direction INTEGER," +
4952
"created_at INTEGER)"
53+
const val CPIF2_ADD_STATUS = "ALTER TABLE cpif2 ADD COLUMN enabled INTEGER DEFAULT TRUE"
5054
}
5155
}

app/src/main/java/com/bintianqi/owndroid/feature/applications/AppFeaturesScreen.kt

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.padding
1616
import androidx.compose.foundation.layout.size
1717
import androidx.compose.foundation.lazy.LazyColumn
1818
import androidx.compose.foundation.lazy.items
19-
import androidx.compose.foundation.lazy.itemsIndexed
2019
import androidx.compose.foundation.text.KeyboardOptions
2120
import androidx.compose.material.icons.Icons
2221
import androidx.compose.material.icons.filled.Clear
@@ -677,18 +676,19 @@ fun PackageFunctionScreen(
677676
}
678677
var filtersSheet by remember { mutableStateOf(false) }
679678
val allPackages by allPackagesState.collectAsState()
680-
val filteredPackages = allPackages.filter {
681-
filterApp(it, filters, query)
682-
}.map { it.info }
683679
var a2zSort by remember { mutableStateOf(true) }
684-
val sortedPackages = if (a2zSort) {
685-
filteredPackages.sortedBy { it.label }
680+
val displayedPackages = if (listView) {
681+
packages.mapNotNull { name ->
682+
allPackages.find { it.info.name == name }?.info
683+
}
686684
} else {
687-
filteredPackages.sortedByDescending { it.label }
685+
allPackages.filter {
686+
filterApp(it, filters, query)
687+
}.let { list ->
688+
if (a2zSort) list.sortedBy { it.info.label }
689+
else list.sortedByDescending { it.info.label }
690+
}.map { it.info }
688691
}
689-
val activePackagesInfo = allPackages.filter {
690-
it.info.name in packages
691-
}.map { it.info }
692692
LaunchedEffect(Unit) {
693693
onGet()
694694
getAllPackages()
@@ -807,15 +807,15 @@ fun PackageFunctionScreen(
807807
}
808808
) { paddingValues ->
809809
LazyColumn(Modifier.padding(paddingValues)) {
810-
if (!listView) item {
811-
if (allPackages.isEmpty()) {
810+
item {
811+
if (listView && displayedPackages.size != packages.size) {
812812
Text(
813813
stringResource(R.string.loading), Modifier.fillMaxWidth(),
814814
textAlign = TextAlign.Center
815815
)
816816
}
817817
}
818-
if (!listView) itemsIndexed(sortedPackages, { _, it -> it.name }) { _, app ->
818+
items(displayedPackages, { it.name }) { app ->
819819
Row(
820820
Modifier
821821
.fillMaxWidth()
@@ -840,23 +840,26 @@ fun PackageFunctionScreen(
840840
Text(app.name, Modifier.alpha(0.8F), style = typography.bodyMedium)
841841
}
842842
}
843-
Switch(packages.any { it == app.name }, {
844-
onSet(listOf(app.name), it)
845-
})
846-
}
847-
}
848-
if (listView) items(activePackagesInfo, { it.name }) {
849-
ApplicationItem(it) {
850-
onSet(listOf(it.name), false)
851-
coroutine.launch {
852-
val result = snackbar.showSnackbar(
853-
res.getString(R.string.package_removed, it.name),
854-
res.getString(R.string.undo),
855-
true, SnackbarDuration.Short
856-
)
857-
if (result == SnackbarResult.ActionPerformed) {
858-
onSet(listOf(it.name), true)
843+
if (listView) {
844+
IconButton({
845+
onSet(listOf(app.name), false)
846+
coroutine.launch {
847+
val result = snackbar.showSnackbar(
848+
res.getString(R.string.package_removed, app.name),
849+
res.getString(R.string.undo),
850+
true, SnackbarDuration.Short
851+
)
852+
if (result == SnackbarResult.ActionPerformed) {
853+
onSet(listOf(app.name), true)
854+
}
855+
}
856+
}) {
857+
Icon(Icons.Default.Clear, null)
859858
}
859+
} else {
860+
Switch(packages.any { it == app.name }, {
861+
onSet(listOf(app.name), it)
862+
})
860863
}
861864
}
862865
}

app/src/main/java/com/bintianqi/owndroid/feature/applications/AppFeaturesViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,9 @@ class AppFeaturesViewModel(
379379

380380
fun getAllPackages() {
381381
viewModelScope.launch(Dispatchers.IO) {
382-
allPackagesState.value = emptyList()
383382
val apps = application.packageManager.getInstalledApplications(getInstalledAppsFlags)
383+
if (apps.size == allPackagesState.value.size) return@launch
384+
allPackagesState.value = emptyList()
384385
apps.sortBy { it.flags and ApplicationInfo.FLAG_SYSTEM }
385386
apps.forEach { app ->
386387
launch(Dispatchers.IO) {

app/src/main/java/com/bintianqi/owndroid/feature/work_profile/CrossProfileIntentFilterModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ data class IntentFilterOptions(
1212

1313
/** Represents a row in database table `cpif` */
1414
class IntentFilterEntry(
15-
val id: Int, val options: IntentFilterOptions, val time: Long
15+
val id: Int, val options: IntentFilterOptions, val time: Long, val enabled: Boolean = true
1616
)
1717

1818
val directionTextMap = mapOf(

app/src/main/java/com/bintianqi/owndroid/feature/work_profile/CrossProfileIntentFilterRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ class CrossProfileIntentFilterRepository(val dbHelper: MyDbHelper) {
2424
dbHelper.writableDatabase.update("cpif2", cv, "id = ?", arrayOf(id.toString()))
2525
}
2626

27+
fun setEnabled(id: Int, enabled: Boolean) {
28+
val cv = ContentValues()
29+
cv.put("enabled", if (enabled) 1 else 0)
30+
dbHelper.writableDatabase.update("cpif2", cv, "id = ?", arrayOf(id.toString()))
31+
}
32+
2733
fun getAllFilters(): List<IntentFilterEntry> {
2834
val list = mutableListOf<IntentFilterEntry>()
2935
dbHelper.readableDatabase.rawQuery(
@@ -33,7 +39,7 @@ class CrossProfileIntentFilterRepository(val dbHelper: MyDbHelper) {
3339
val options = IntentFilterOptions(
3440
it.getString(1), it.getString(2), it.getString(3), it.getInt(4)
3541
)
36-
list += IntentFilterEntry(it.getInt(0), options, it.getLong(5))
42+
list += IntentFilterEntry(it.getInt(0), options, it.getLong(5), it.getInt(6) == 1)
3743
}
3844
}
3945
return list

app/src/main/java/com/bintianqi/owndroid/feature/work_profile/CrossProfileIntentFilterScreen.kt

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ package com.bintianqi.owndroid.feature.work_profile
22

33
import androidx.activity.compose.rememberLauncherForActivityResult
44
import androidx.activity.result.contract.ActivityResultContracts
5-
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.combinedClickable
67
import androidx.compose.foundation.layout.Arrangement
78
import androidx.compose.foundation.layout.Box
89
import androidx.compose.foundation.layout.Column
@@ -29,25 +30,31 @@ import androidx.compose.material3.ExposedDropdownMenuAnchorType
2930
import androidx.compose.material3.ExposedDropdownMenuBox
3031
import androidx.compose.material3.ExposedDropdownMenuDefaults
3132
import androidx.compose.material3.FilledIconButton
33+
import androidx.compose.material3.FilledTonalButton
3234
import androidx.compose.material3.HorizontalDivider
3335
import androidx.compose.material3.Icon
3436
import androidx.compose.material3.IconButton
3537
import androidx.compose.material3.MaterialTheme
3638
import androidx.compose.material3.OutlinedTextField
39+
import androidx.compose.material3.Switch
3740
import androidx.compose.material3.Text
3841
import androidx.compose.material3.TextButton
3942
import androidx.compose.runtime.Composable
4043
import androidx.compose.runtime.LaunchedEffect
4144
import androidx.compose.runtime.collectAsState
4245
import androidx.compose.runtime.getValue
4346
import androidx.compose.runtime.mutableIntStateOf
47+
import androidx.compose.runtime.mutableStateListOf
4448
import androidx.compose.runtime.mutableStateOf
4549
import androidx.compose.runtime.remember
4650
import androidx.compose.runtime.saveable.rememberSaveable
4751
import androidx.compose.runtime.setValue
4852
import androidx.compose.ui.Alignment
4953
import androidx.compose.ui.Modifier
5054
import androidx.compose.ui.draw.alpha
55+
import androidx.compose.ui.graphics.Color
56+
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
57+
import androidx.compose.ui.platform.LocalHapticFeedback
5158
import androidx.compose.ui.res.painterResource
5259
import androidx.compose.ui.res.stringResource
5360
import androidx.compose.ui.text.input.ImeAction
@@ -77,24 +84,37 @@ fun CrossProfileIntentFilterScreen(
7784
) {
7885
if (it != null) vm.exportFilters(it)
7986
}
87+
val selectedItems = remember { mutableStateListOf<Int>() }
8088
var confirmDeleteDialog by remember { mutableStateOf(false) }
89+
val hf = LocalHapticFeedback.current
8190
MyLazyScaffold(R.string.intent_filter, onNavigateUp, {
82-
IconButton({
83-
navigate(Destination.AddCrossProfileIntentFilter(-1))
84-
}) {
85-
Icon(Icons.Default.Add, null)
86-
}
87-
IconButton({
88-
navigate(Destination.CrossProfileIntentFilterPresets)
89-
}) {
90-
Icon(Icons.AutoMirrored.Default.List, null)
91-
}
92-
if (filtersChanged) FilledIconButton(vm::applyFilters) {
93-
Icon(Icons.Default.Check, null)
91+
if (selectedItems.isEmpty()) {
92+
IconButton({
93+
navigate(Destination.AddCrossProfileIntentFilter(-1))
94+
}) {
95+
Icon(Icons.Default.Add, null)
96+
}
97+
IconButton({
98+
navigate(Destination.CrossProfileIntentFilterPresets)
99+
}) {
100+
Icon(Icons.AutoMirrored.Default.List, null)
101+
}
102+
if (filtersChanged) FilledIconButton(vm::applyFilters) {
103+
Icon(Icons.Default.Check, null)
104+
}
105+
} else {
106+
IconButton({
107+
selectedItems.forEach {
108+
vm.deleteEntry(it)
109+
}
110+
selectedItems.clear()
111+
}) {
112+
Icon(Icons.Outlined.Delete, null)
113+
}
94114
}
95115
var menu by remember { mutableStateOf(false) }
96116
Box {
97-
IconButton({ menu = !menu }) {
117+
if (selectedItems.isEmpty()) IconButton({ menu = !menu }) {
98118
Icon(Icons.Default.MoreVert, null)
99119
}
100120
DropdownMenu(menu, { menu = false }) {
@@ -136,13 +156,29 @@ fun CrossProfileIntentFilterScreen(
136156
}
137157
}
138158
items(filterList, { it.id }) {
159+
val mod = Modifier
160+
.background(
161+
if (it.id in selectedItems) MaterialTheme.colorScheme.primaryContainer
162+
else Color.Transparent
163+
)
164+
.combinedClickable(onLongClick = {
165+
if (it.id !in selectedItems) selectedItems += it.id
166+
hf.performHapticFeedback(HapticFeedbackType.LongPress)
167+
}) {
168+
if (selectedItems.isEmpty()) {
169+
navigate(Destination.AddCrossProfileIntentFilter(it.id))
170+
} else {
171+
if (it.id in selectedItems) {
172+
selectedItems -= it.id
173+
} else {
174+
selectedItems += it.id
175+
}
176+
}
177+
}
178+
.padding(16.dp, 4.dp, 8.dp, 4.dp)
139179
Column(Modifier.animateItem()) {
140180
Row(
141-
Modifier
142-
.clickable {
143-
navigate(Destination.AddCrossProfileIntentFilter(it.id))
144-
}
145-
.padding(16.dp, 4.dp, 8.dp, 4.dp),
181+
mod,
146182
Arrangement.SpaceBetween, Alignment.CenterVertically
147183
) {
148184
Column(Modifier.weight(1F)) {
@@ -168,11 +204,9 @@ fun CrossProfileIntentFilterScreen(
168204
Modifier.alpha(0.6F), style = MaterialTheme.typography.bodyMedium
169205
)
170206
}
171-
IconButton({
172-
vm.deleteEntry(it.id)
173-
}) {
174-
Icon(Icons.Outlined.Delete, null)
175-
}
207+
Switch(it.enabled, { enabled ->
208+
vm.setEnabled(it.id, enabled)
209+
})
176210
}
177211
HorizontalDivider()
178212
}
@@ -289,6 +323,13 @@ fun AddCrossProfileIntentFilterScreen(
289323
) {
290324
Text(stringResource(if (params.editingId == -1) R.string.add else R.string.update))
291325
}
326+
Spacer(Modifier.height(4.dp))
327+
if (params.editingId != -1) FilledTonalButton({
328+
vm.deleteEntry(params.editingId)
329+
navigateUp()
330+
}, Modifier.fillMaxWidth()) {
331+
Text(stringResource(R.string.delete))
332+
}
292333
}
293334
}
294335

app/src/main/java/com/bintianqi/owndroid/feature/work_profile/CrossProfileIntentFilterViewModel.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,24 @@ class CrossProfileIntentFilterViewModel(
4444
filterListState.update { list ->
4545
val index = list.indexOfFirst { it.id == id }
4646
val newList = list.toMutableList()
47-
newList[index] = IntentFilterEntry(id, options, System.currentTimeMillis())
47+
newList[index] = IntentFilterEntry(
48+
id, options, System.currentTimeMillis(), list[index].enabled
49+
)
4850
newList.sortedByDescending { it.time }
4951
}
52+
setFilterChanged(true)
53+
}
54+
55+
fun setEnabled(id: Int, enabled: Boolean) {
56+
repo.setEnabled(id, enabled)
57+
filterListState.update { list ->
58+
val index = list.indexOfFirst { it.id == id }
59+
val old = list[index]
60+
val newList = list.toMutableList()
61+
newList[index] = IntentFilterEntry(id, old.options, old.time, enabled)
62+
newList
63+
}
64+
setFilterChanged(true)
5065
}
5166

5267
fun addPreset(preset: IntentFilterPreset, direction: Int) {
@@ -73,6 +88,7 @@ class CrossProfileIntentFilterViewModel(
7388
ph.safeDpmCall {
7489
dpm.clearCrossProfileIntentFilters(dar)
7590
filterListState.value.forEach {
91+
if (!it.enabled) return@forEach
7692
val filter = IntentFilter(it.options.action)
7793
if (it.options.category.isNotEmpty()) filter.addCategory(it.options.category)
7894
if (it.options.mimeType.isNotEmpty()) filter.addDataType(it.options.mimeType)

0 commit comments

Comments
 (0)