blacklist: fix search & group selected apps under sticky headers
This commit is contained in:
@@ -5,10 +5,12 @@
|
||||
|
||||
package com.aurora.store.compose.ui.blacklist
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -21,10 +23,13 @@ import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.material3.AppBarWithSearch
|
||||
import androidx.compose.material3.ExpandedFullScreenSearchBar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberSearchBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -46,20 +51,22 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.aurora.Constants
|
||||
import com.aurora.extensions.toast
|
||||
import com.aurora.store.R
|
||||
import com.aurora.store.compose.composable.BlackListItem
|
||||
import com.aurora.store.compose.composable.ContainedLoadingIndicator
|
||||
import com.aurora.store.compose.composable.ScrollHint
|
||||
import com.aurora.store.compose.composable.TextDividerComposable
|
||||
import com.aurora.store.compose.preview.ThemePreviewProvider
|
||||
import com.aurora.store.compose.ui.blacklist.menu.BlacklistMenu
|
||||
import com.aurora.store.compose.ui.blacklist.menu.MenuItem
|
||||
import com.aurora.store.util.PackageUtil
|
||||
import com.aurora.store.data.model.BlacklistAppItem
|
||||
import com.aurora.store.viewmodel.blacklist.BlacklistViewModel
|
||||
import java.util.Calendar
|
||||
import kotlinx.coroutines.android.awaitFrame
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -72,7 +79,6 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
|
||||
packages = packages,
|
||||
onNavigateUp = onNavigateUp,
|
||||
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
|
||||
isPackageFiltered = { pkgInfo -> viewModel.isFiltered(pkgInfo) },
|
||||
onBlacklistImport = { uri ->
|
||||
viewModel.importBlacklist(uri)
|
||||
context.toast(R.string.toast_black_import_success)
|
||||
@@ -91,10 +97,9 @@ fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hi
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(
|
||||
packages: List<PackageInfo>? = null,
|
||||
packages: List<BlacklistAppItem>? = null,
|
||||
onNavigateUp: () -> Unit = {},
|
||||
isPackageBlacklisted: (packageName: String) -> Boolean = { false },
|
||||
isPackageFiltered: (packageInfo: PackageInfo) -> Boolean = { false },
|
||||
onBlacklistImport: (uri: Uri) -> Unit = {},
|
||||
onBlacklistExport: (uri: Uri) -> Unit = {},
|
||||
onBlacklist: (packageName: String) -> Unit = {},
|
||||
@@ -119,6 +124,7 @@ private fun ScreenContent(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val docExportLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE),
|
||||
onResult = {
|
||||
@@ -164,11 +170,23 @@ private fun ScreenContent(
|
||||
|
||||
@Composable
|
||||
fun SearchBar() {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collectLatest { interaction ->
|
||||
if (interaction is PressInteraction.Press) {
|
||||
awaitFrame()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val inputField = @Composable {
|
||||
SearchBarDefaults.InputField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
searchBarState = searchBarState,
|
||||
textFieldState = textFieldState,
|
||||
interactionSource = interactionSource,
|
||||
onSearch = { query -> onRequestSearch(query) },
|
||||
placeholder = {
|
||||
Text(
|
||||
@@ -212,11 +230,30 @@ private fun ScreenContent(
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = { if (!packages.isNullOrEmpty()) SetupMenu() },
|
||||
actions = { SetupMenu() },
|
||||
colors = SearchBarDefaults.appBarWithSearchColors(
|
||||
appBarContainerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
ExpandedFullScreenSearchBar(state = searchBarState, inputField = inputField) {
|
||||
if (textFieldState.text.length >= 3) {
|
||||
packages?.take(10)?.forEach { pkg ->
|
||||
Text(
|
||||
text = pkg.displayName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onRequestSearch(pkg.displayName) }
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -228,28 +265,24 @@ private fun ScreenContent(
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xxsmall)
|
||||
)
|
||||
) {
|
||||
items(items = packages ?: emptyList(), key = { p ->
|
||||
p.packageName.hashCode()
|
||||
}) { pkg ->
|
||||
if (packages == null) {
|
||||
ContainedLoadingIndicator()
|
||||
} else {
|
||||
val (selectedPackages, otherPackages) = packages.partition { pkg ->
|
||||
isPackageBlacklisted(pkg.packageName) || pkg.isFiltered
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BlacklistRow(pkg: BlacklistAppItem) {
|
||||
val isBlacklisted = isPackageBlacklisted(pkg.packageName)
|
||||
val isFiltered = isPackageFiltered(pkg)
|
||||
BlackListItem(
|
||||
icon = PackageUtil.getIconForPackage(context, pkg.packageName)!!,
|
||||
displayName = pkg.applicationInfo!!.loadLabel(
|
||||
context.packageManager
|
||||
).toString(),
|
||||
icon = pkg.icon,
|
||||
displayName = pkg.displayName,
|
||||
packageName = pkg.packageName,
|
||||
versionName = pkg.versionName!!,
|
||||
versionCode = PackageInfoCompat.getLongVersionCode(pkg),
|
||||
isChecked = isBlacklisted || isFiltered,
|
||||
isEnabled = !isFiltered,
|
||||
versionName = pkg.versionName,
|
||||
versionCode = pkg.versionCode,
|
||||
isChecked = isBlacklisted || pkg.isFiltered,
|
||||
isEnabled = !pkg.isFiltered,
|
||||
onClick = {
|
||||
if (isBlacklisted) {
|
||||
onWhitelist(pkg.packageName)
|
||||
@@ -259,12 +292,50 @@ private fun ScreenContent(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
dimensionResource(R.dimen.margin_xxsmall)
|
||||
)
|
||||
) {
|
||||
if (selectedPackages.isNotEmpty()) {
|
||||
stickyHeader(key = "header_selected") {
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
TextDividerComposable(
|
||||
title = stringResource(
|
||||
R.string.header_blacklist_selected
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
items(items = selectedPackages, key = { p ->
|
||||
p.packageName.hashCode()
|
||||
}) { pkg -> BlacklistRow(pkg) }
|
||||
}
|
||||
|
||||
if (otherPackages.isNotEmpty()) {
|
||||
stickyHeader(key = "header_others") {
|
||||
Surface(modifier = Modifier.fillMaxWidth()) {
|
||||
TextDividerComposable(
|
||||
title = stringResource(
|
||||
R.string.header_blacklist_others
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
items(items = otherPackages, key = { p ->
|
||||
p.packageName.hashCode()
|
||||
}) { pkg -> BlacklistRow(pkg) }
|
||||
}
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
ScrollHint(
|
||||
listState = listState,
|
||||
bottomPadding = 5.dp,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.aurora.store.data.model
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
data class BlacklistAppItem(
|
||||
val packageName: String,
|
||||
val displayName: String,
|
||||
val versionName: String,
|
||||
val versionCode: Long,
|
||||
val icon: Bitmap,
|
||||
val isFiltered: Boolean
|
||||
)
|
||||
@@ -10,12 +10,14 @@ import android.content.pm.PackageInfo
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aurora.extensions.TAG
|
||||
import com.aurora.store.AuroraApp
|
||||
import com.aurora.store.data.event.BusEvent
|
||||
import com.aurora.store.data.helper.UpdateHelper
|
||||
import com.aurora.store.data.model.BlacklistAppItem
|
||||
import com.aurora.store.data.providers.BlacklistProvider
|
||||
import com.aurora.store.util.CertUtil
|
||||
import com.aurora.store.util.PackageUtil
|
||||
@@ -44,8 +46,8 @@ class BlacklistViewModel @Inject constructor(
|
||||
private val isExtendedUpdateEnabled =
|
||||
Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
|
||||
|
||||
private val packages = MutableStateFlow<List<PackageInfo>?>(null)
|
||||
private val _filteredPackages = MutableStateFlow<List<PackageInfo>?>(null)
|
||||
private val packages = MutableStateFlow<List<BlacklistAppItem>?>(null)
|
||||
private val _filteredPackages = MutableStateFlow<List<BlacklistAppItem>?>(null)
|
||||
val filteredPackages = _filteredPackages.asStateFlow()
|
||||
|
||||
val blacklist = mutableStateListOf<String>()
|
||||
@@ -58,28 +60,42 @@ class BlacklistViewModel @Inject constructor(
|
||||
private fun fetchApps() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
packages.value = PackageUtil.getAllValidPackages(context).also { pkgList ->
|
||||
_filteredPackages.value = pkgList
|
||||
}
|
||||
packages.value = PackageUtil.getAllValidPackages(context)
|
||||
.mapNotNull { it.toBlacklistAppItem() }
|
||||
.also { _filteredPackages.value = it }
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, "Failed to fetch apps", exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PackageInfo.toBlacklistAppItem(): BlacklistAppItem? {
|
||||
val icon = PackageUtil.getIconForPackage(context, packageName) ?: return null
|
||||
return BlacklistAppItem(
|
||||
packageName = packageName,
|
||||
displayName = applicationInfo!!.loadLabel(context.packageManager).toString(),
|
||||
versionName = versionName ?: "",
|
||||
versionCode = PackageInfoCompat.getLongVersionCode(this),
|
||||
icon = icon,
|
||||
isFiltered = isFiltered(this)
|
||||
)
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
if (query.isNotBlank()) {
|
||||
_filteredPackages.value = packages.value!!
|
||||
.filter {
|
||||
it.applicationInfo!!.loadLabel(context.packageManager).contains(query, true) ||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val allPackages = packages.value ?: return@launch
|
||||
if (query.isNotBlank()) {
|
||||
_filteredPackages.value = allPackages.filter {
|
||||
it.displayName.contains(query, true) ||
|
||||
it.packageName.contains(query, true)
|
||||
}
|
||||
} else {
|
||||
_filteredPackages.value = packages.value
|
||||
} else {
|
||||
_filteredPackages.value = allPackages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isFiltered(packageInfo: PackageInfo): Boolean = when {
|
||||
private fun isFiltered(packageInfo: PackageInfo): Boolean = when {
|
||||
!isExtendedUpdateEnabled && !packageInfo.applicationInfo!!.enabled -> true
|
||||
|
||||
isAuroraOnlyFilterEnabled -> !CertUtil.isAuroraStoreApp(
|
||||
@@ -99,7 +115,8 @@ class BlacklistViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun blacklistAll() {
|
||||
blacklistProvider.blacklist = packages.value!!.map { it.packageName }.toMutableSet()
|
||||
val allPackages = packages.value ?: return
|
||||
blacklistProvider.blacklist = allPackages.map { it.packageName }.toMutableSet()
|
||||
blacklist.apply {
|
||||
clear()
|
||||
addAll(blacklistProvider.blacklist)
|
||||
@@ -126,7 +143,10 @@ class BlacklistViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
val validImportedSet = importedSet
|
||||
.filter { pkgName -> packages.value!!.any { it.packageName == pkgName } }
|
||||
.filter { pkgName ->
|
||||
packages.value?.any { it.packageName == pkgName } ==
|
||||
true
|
||||
}
|
||||
blacklistProvider.blacklist.addAll(validImportedSet)
|
||||
blacklist.apply {
|
||||
clear()
|
||||
|
||||
Reference in New Issue
Block a user