compose: Replicate BlacklistFragment in compose

Introduce a new activity to host composables as View system behaves
quite bad when composable are used inside it.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2025-02-09 14:00:07 +07:00
parent f4434c02fb
commit a9b2f0acf2
27 changed files with 772 additions and 254 deletions

View File

@@ -42,7 +42,9 @@ import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.getSystemService
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.ComposeActivity
import com.aurora.store.R
import com.aurora.store.compose.navigation.Screen
private const val TAG = "Context"
@@ -153,3 +155,10 @@ fun Context.isDomainVerified(domain: String): Boolean {
true
}
}
fun Context.navigate(screen: Screen) {
val intent = Intent(this, ComposeActivity::class.java).apply {
putExtra(Screen.PARCEL_KEY, screen)
}
startActivity(intent)
}

View File

@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.content.IntentCompat
import androidx.navigation.compose.rememberNavController
import com.aurora.store.compose.navigation.NavGraph
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.compose.theme.AuroraTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// TODO: Change startDestination logic to mirror MainActivity
val startDestination = IntentCompat.getParcelableExtra(
intent,
Screen.PARCEL_KEY,
Screen::class.java
) ?: Screen.Blacklist
setContent {
AuroraTheme {
val navController = rememberNavController()
NavGraph(navHostController = navController, startDestination = startDestination)
}
}
}
}

View File

@@ -41,6 +41,7 @@ import com.aurora.store.R
* @param versionName versionName of the package
* @param versionCode versionCode of the package
* @param isChecked Whether the app is blacklisted
* @param isEnabled Whether this app is allowed to be blacklisted
* @param onClick Callback when the composable is clicked
*/
@Composable
@@ -51,12 +52,13 @@ fun BlackListComposable(
versionName: String,
versionCode: Long,
isChecked: Boolean = false,
isEnabled: Boolean = true,
onClick: () -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.clickable(enabled = isEnabled) { onClick() }
.padding(
horizontal = dimensionResource(R.dimen.padding_medium),
vertical = dimensionResource(R.dimen.padding_xsmall)
@@ -87,11 +89,13 @@ fun BlackListComposable(
)
Text(
text = stringResource(R.string.version, versionName, versionCode),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Checkbox(checked = isChecked, onCheckedChange = { onClick() })
Checkbox(checked = isChecked, enabled = isEnabled, onCheckedChange = { onClick() })
}
}
@@ -104,6 +108,7 @@ private fun BlackListComposablePreview() {
packageName = BuildConfig.APPLICATION_ID,
versionName = BuildConfig.VERSION_NAME,
versionCode = BuildConfig.VERSION_CODE.toLong(),
isChecked = true
isChecked = true,
isEnabled = false
)
}

View File

@@ -84,7 +84,9 @@ fun InstalledAppComposable(
)
Text(
text = stringResource(R.string.version, versionName, versionCode),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp)
style = MaterialTheme.typography.bodySmall.copy(fontSize = 10.sp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}

View File

@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
/**
* A top app bar composable to be used with Scaffold in different Screen
* @param searchHint Hint to show to the user in search bar
* @param onNavigateUp Action when user clicks the navigation icon
* @param onSearch Callback for a search
* @param actions Actions to display on the top app bar (for e.g. menu)
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun SearchAppBarComposable(
@StringRes searchHint: Int? = null,
onNavigateUp: () -> Unit,
onSearch: (query: String) -> Unit,
actions: @Composable (RowScope.() -> Unit) = {}
) {
val focusManager = LocalFocusManager.current
var query by rememberSaveable { mutableStateOf("") }
TopAppBar(
title = {
TextField(
modifier = Modifier.fillMaxWidth(),
value = query,
onValueChange = { newQuery ->
query = newQuery.trim()
onSearch(query)
},
maxLines = 1,
placeholder = { if (searchHint != null) Text(text = stringResource(searchHint)) },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { focusManager.clearFocus() })
)
},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
}
},
actions = {
if (query.isNotBlank()) {
IconButton(
onClick = {
query = ""
onSearch(query)
focusManager.clearFocus(force = true)
}
) {
Icon(imageVector = Icons.Default.Clear, contentDescription = null)
}
}
actions()
}
)
}

View File

@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
/**
* A top app bar composable to be used with Scaffold in different Screen
* @param title Title of the screen
* @param onNavigateUp Action when user clicks the navigation icon
* @param actions Actions to display on the top app bar (for e.g. menu)
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun TopAppBarComposable(
@StringRes title: Int? = null,
onNavigateUp: () -> Unit,
actions: @Composable (RowScope.() -> Unit) = {}
) {
TopAppBar(
title = {
if (title != null) Text(text = stringResource(title))
},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null
)
}
},
actions = actions
)
}

View File

@@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.composables
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
/**
* A transparent icon for occupying space
*
* This is useful for occupying spaces in composable where alignment is not respected such as
* DropDownMenu.
*/
@Composable
fun TransparentIconComposable() {
Icon(
painter = painterResource(R.drawable.ic_transparent),
contentDescription = null
)
}
@Preview(showBackground = true)
@Composable
private fun TransparentIconComposablePreview() {
TransparentIconComposable()
}

View File

@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.menu
import androidx.compose.foundation.layout.Box
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.aurora.store.R
import com.aurora.store.compose.composables.TransparentIconComposable
import com.aurora.store.compose.menu.items.BlacklistMenuItem
/**
* Menu for the blacklist screen
* @param onMenuItemClicked Callback when a menu item has been clicked
* @see BlacklistMenuItem
*/
@Composable
fun BlacklistMenu(onMenuItemClicked: (blacklistMenuItem: BlacklistMenuItem) -> Unit = {}) {
var expanded by remember { mutableStateOf(false) }
fun onClick(blacklistMenuItem: BlacklistMenuItem) {
onMenuItemClicked(blacklistMenuItem)
expanded = false
}
Box {
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.menu))
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_select_all)) },
onClick = { onClick(BlacklistMenuItem.SELECT_ALL) },
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_select_all),
contentDescription = null
)
}
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_remove_all)) },
onClick = { onClick(BlacklistMenuItem.REMOVE_ALL) },
leadingIcon = { TransparentIconComposable() }
)
HorizontalDivider()
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_import)) },
onClick = { onClick(BlacklistMenuItem.IMPORT) },
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_file_import),
contentDescription = null
)
}
)
DropdownMenuItem(
text = { Text(text = stringResource(R.string.action_export)) },
onClick = { onClick(BlacklistMenuItem.EXPORT) },
leadingIcon = {
Icon(
painter = painterResource(R.drawable.ic_file_export),
contentDescription = null
)
}
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun BlacklistMenuPreview() {
BlacklistMenu()
}

View File

@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.menu.items
/**
* Valid menu items for the purpose of handling clicks
*/
enum class BlacklistMenuItem {
SELECT_ALL,
REMOVE_ALL,
IMPORT,
EXPORT
}

View File

@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.navigation
import androidx.activity.compose.LocalActivity
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.aurora.store.compose.ui.commons.BlacklistScreen
/**
* Navigation graph for compose screens
* @param navHostController [NavHostController] to navigate with compose
* @param startDestination Starting destination for the activity/app
*/
@Composable
fun NavGraph(navHostController: NavHostController, startDestination: Screen) {
// TODO: Drop this logic once everything is in compose
val activity = LocalActivity.current
fun onNavigateUp() {
if (navHostController.previousBackStackEntry != null) {
navHostController.navigateUp()
} else {
activity?.finish()
}
}
NavHost(navController = navHostController, startDestination = startDestination) {
composable<Screen.Blacklist> {
BlacklistScreen(onNavigateUp = { onNavigateUp() })
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.navigation
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.aurora.store.R
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
/**
* Destination (Screen) for navigation in compose
* @param label Label of the screen
* @param icon Optional icon for the screen; Must not be null if screen is a top-level destination
*/
@Parcelize
@Serializable
sealed class Screen(@StringRes val label: Int, @DrawableRes val icon: Int? = null): Parcelable {
companion object {
const val PARCEL_KEY = "SCREEN"
}
@Serializable
data object Blacklist : Screen(label = R.string.title_blacklist_manager)
}

View File

@@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2025 The Calyx Institute
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.compose.ui.commons
import android.content.pm.PackageInfo
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.content.pm.PackageInfoCompat
import androidx.hilt.navigation.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.composables.BlackListComposable
import com.aurora.store.compose.composables.SearchAppBarComposable
import com.aurora.store.compose.menu.BlacklistMenu
import com.aurora.store.compose.menu.items.BlacklistMenuItem
import com.aurora.store.util.PackageUtil
import com.aurora.store.viewmodel.all.BlacklistViewModel
import java.util.Calendar
@Composable
fun BlacklistScreen(onNavigateUp: () -> Unit, viewModel: BlacklistViewModel = hiltViewModel()) {
val ctx = LocalContext.current
val packages by viewModel.filteredPackages.collectAsStateWithLifecycle()
ScreenContent(
packages = packages,
onNavigateUp = onNavigateUp,
isPackageBlacklisted = { pkgName -> pkgName in viewModel.blacklist },
isPackageFiltered = { pkgInfo -> viewModel.isFiltered(pkgInfo) },
onBlacklistImport = { uri ->
viewModel.importBlacklist(ctx, uri)
ctx.toast(R.string.toast_black_import_success)
},
onBlacklistExport = { uri ->
viewModel.exportBlacklist(ctx, uri)
ctx.toast(R.string.toast_black_export_success)
},
onBlacklist = { packageName -> viewModel.blacklist(packageName) },
onBlacklistAll = { viewModel.blacklistAll() },
onWhitelist = { packageName -> viewModel.whitelist(packageName) },
onWhitelistAll = { viewModel.whitelistAll() },
onSearch = { query -> viewModel.search(query) }
)
}
@Composable
private fun ScreenContent(
packages: List<PackageInfo>? = 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 = {},
onBlacklistAll: () -> Unit = {},
onWhitelist: (packageName: String) -> Unit = {},
onWhitelistAll: () -> Unit = {},
onSearch: (query: String) -> Unit = {}
) {
val ctx = LocalContext.current
val docImportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument(),
onResult = {
if (it != null) onBlacklistImport(it) else ctx.toast(R.string.toast_black_import_failed)
}
)
val docExportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE),
onResult = {
if (it != null) onBlacklistExport(it) else ctx.toast(R.string.toast_black_export_failed)
}
)
@Composable
fun SetupMenu() {
BlacklistMenu { blacklistMenuItem ->
when (blacklistMenuItem) {
BlacklistMenuItem.SELECT_ALL -> onBlacklistAll()
BlacklistMenuItem.REMOVE_ALL -> onWhitelistAll()
BlacklistMenuItem.IMPORT -> {
docImportLauncher.launch(arrayOf(Constants.JSON_MIME_TYPE))
}
BlacklistMenuItem.EXPORT -> {
docExportLauncher.launch(
"aurora_store_apps_${Calendar.getInstance().time.time}.json"
)
}
}
}
}
Scaffold(
topBar = {
SearchAppBarComposable(
onNavigateUp = onNavigateUp,
actions = { if (!packages.isNullOrEmpty()) SetupMenu() },
searchHint = R.string.search_hint,
onSearch = { query -> onSearch(query) }
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_xxsmall))
) {
items(items = packages ?: emptyList(), key = { p -> p.packageName.hashCode() }) { pkg ->
val isBlacklisted = isPackageBlacklisted(pkg.packageName)
val isFiltered = isPackageFiltered(pkg)
BlackListComposable(
icon = PackageUtil.getIconForPackage(ctx, pkg.packageName)!!,
displayName = pkg.applicationInfo!!.loadLabel(ctx.packageManager).toString(),
packageName = pkg.packageName,
versionName = pkg.versionName!!,
versionCode = PackageInfoCompat.getLongVersionCode(pkg),
isChecked = isBlacklisted || isFiltered,
isEnabled = !isFiltered,
onClick = {
if (isBlacklisted) {
onWhitelist(pkg.packageName)
} else {
onBlacklist(pkg.packageName)
}
}
)
}
}
}
}
@Preview
@Composable
private fun BlacklistScreenPreview() {
ScreenContent()
}

View File

@@ -113,6 +113,13 @@ class UpdateHelper @Inject constructor(
updateDao.delete(packageName)
}
/**
* Delete all updates from the database
*/
suspend fun deleteAllUpdates() {
updateDao.deleteAll()
}
/**
* Cancels the automated updates check
* @see [UpdateWorker]

View File

@@ -1,179 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* Aurora Store is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.view.ui.commons
import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent
import com.aurora.store.databinding.FragmentGenericWithSearchBinding
import com.aurora.store.view.epoxy.views.BlackListViewModel_
import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.all.BlacklistViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint
class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: BlacklistViewModel by viewModels()
private val startForDocumentImport =
registerForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) importBlacklist(it) else toast(R.string.toast_black_import_failed)
}
private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.packages.collect {
updateController(it)
}
}
// Toolbar
binding.toolbar.apply {
inflateMenu(R.menu.menu_blacklist)
setNavigationOnClickListener {
viewModel.blacklistProvider.blacklist = viewModel.selected
findNavController().navigateUp()
}
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_import -> {
startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE))
}
R.id.action_export -> {
startForDocumentExport.launch(
"aurora_store_apps_${Calendar.getInstance().time.time}.json"
)
}
R.id.action_select_all -> {
viewModel.selectAll()
binding.recycler.requestModelBuild()
true
}
R.id.action_remove_all -> {
viewModel.removeAll()
binding.recycler.requestModelBuild()
true
}
}
true
}
}
binding.searchBar.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s.isNullOrEmpty()) {
updateController(viewModel.packages.value)
} else {
val filteredPackages = viewModel.packages.value?.filter {
it.applicationInfo!!.loadLabel(requireContext().packageManager)
.contains(s, true) || it.packageName.contains(s, true)
}
updateController(filteredPackages)
}
}
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int
) {
}
})
}
override fun onPause() {
super.onPause()
viewModel.blacklistProvider.blacklist = viewModel.selected
}
private fun updateController(packages: List<PackageInfo>?) {
binding.recycler.withModels {
setFilterDuplicates(true)
if (packages == null) {
for (i in 1..10) {
add(
AppListViewShimmerModel_()
.id(i)
)
}
} else {
packages
.sortedByDescending { app ->
viewModel.blacklistProvider.isBlacklisted(app.packageName)
}
.forEach {
add(
BlackListViewModel_()
.id(it.packageName.hashCode())
.packageInfo(it)
.markChecked(viewModel.selected.contains(it.packageName))
.checked { _, isChecked ->
if (isChecked) {
viewModel.selected.add(it.packageName)
AuroraApp.events.send(BusEvent.Blacklisted(it.packageName))
} else {
viewModel.selected.remove(it.packageName)
}
requestModelBuild()
}
)
}
}
}
}
private fun importBlacklist(uri: Uri) {
viewModel.importBlacklist(requireContext(), uri)
binding.recycler.requestModelBuild()
toast(R.string.toast_black_import_success)
}
private fun exportBlacklist(uri: Uri) {
viewModel.exportBlacklist(requireContext(), uri)
toast(R.string.toast_black_export_success)
}
}

View File

@@ -57,9 +57,11 @@ import com.aurora.Constants
import com.aurora.Constants.URL_TOS
import com.aurora.extensions.browse
import com.aurora.extensions.getStyledAttributeColor
import com.aurora.extensions.navigate
import com.aurora.extensions.setAppTheme
import com.aurora.store.MR
import com.aurora.store.R
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.util.Preferences
import com.aurora.store.compose.theme.AuroraTheme
import com.aurora.store.viewmodel.commons.MoreViewModel
@@ -76,12 +78,23 @@ class MoreDialogFragment : DialogFragment() {
private var secondaryColor: Color = Color.White
private var onSecondaryColor: Color = Color.Black
private data class Option(
@StringRes val title: Int,
@DrawableRes val icon: Int,
@IdRes val destinationID: Int
private abstract class Option(
@StringRes open val title: Int,
@DrawableRes open val icon: Int,
)
private data class ViewOption(
override val title: Int,
override val icon: Int,
@IdRes val destinationID: Int
) : Option(title, icon)
private data class ComposeOption(
override val title: Int,
override val icon: Int,
val screen: Screen
) : Option(title, icon)
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext())
.setView(customDialogView(requireContext()))
@@ -134,7 +147,16 @@ class MoreDialogFragment : DialogFragment() {
OptionItem(
option = option,
tintColor = onPrimaryColor,
textColor = onSecondaryColor
textColor = onSecondaryColor,
onClick = {
when (option) {
is ViewOption -> {
findNavController().navigate(option.destinationID)
}
is ComposeOption -> context.navigate(option.screen)
}
}
)
}
}
@@ -142,7 +164,16 @@ class MoreDialogFragment : DialogFragment() {
OptionItem(
option = option,
tintColor = onPrimaryColor,
textColor = onPrimaryColor
textColor = onPrimaryColor,
onClick = {
when (option) {
is ViewOption -> {
findNavController().navigate(option.destinationID)
}
is ComposeOption -> context.navigate(option.screen)
}
}
)
}
Footer(onPrimaryColor)
@@ -310,12 +341,13 @@ class MoreDialogFragment : DialogFragment() {
private fun OptionItem(
option: Option,
tintColor: Color = Color.Blue,
textColor: Color = Color.Black
textColor: Color = Color.Black,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { findNavController().navigate(option.destinationID) }
.clickable { onClick() }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start)
@@ -391,22 +423,22 @@ class MoreDialogFragment : DialogFragment() {
private fun getOptions(): List<Option> {
return listOf(
Option(
ViewOption(
title = R.string.title_apps_games,
icon = R.drawable.ic_apps,
destinationID = R.id.appsGamesFragment
),
Option(
ComposeOption(
title = R.string.title_blacklist_manager,
icon = R.drawable.ic_blacklist,
destinationID = R.id.blacklistFragment
screen = Screen.Blacklist
),
Option(
ViewOption(
title = R.string.title_favourites_manager,
icon = R.drawable.ic_favorite_unchecked,
destinationID = R.id.favouriteFragment
),
Option(
ViewOption(
title = R.string.title_spoof_manager,
icon = R.drawable.ic_spoof,
destinationID = R.id.spoofFragment
@@ -416,12 +448,12 @@ class MoreDialogFragment : DialogFragment() {
private fun getExtraOptions(): List<Option> {
return listOf(
Option(
ViewOption(
title = R.string.title_settings,
icon = R.drawable.ic_menu_settings,
destinationID = R.id.settingsFragment
),
Option(
ViewOption(
title = R.string.title_about,
icon = R.drawable.ic_menu_about,
destinationID = R.id.aboutFragment

View File

@@ -37,9 +37,11 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.extensions.hide
import com.aurora.extensions.isMAndAbove
import com.aurora.extensions.navigate
import com.aurora.extensions.show
import com.aurora.gplayapi.helpers.AuthHelper
import com.aurora.store.R
import com.aurora.store.compose.navigation.Screen
import com.aurora.store.data.model.AuthState
import com.aurora.store.databinding.FragmentSplashBinding
import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE
@@ -93,7 +95,7 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_blacklist_manager -> {
findNavController().navigate(R.id.blacklistFragment)
requireContext().navigate(Screen.Blacklist)
}
R.id.menu_spoof_manager -> {

View File

@@ -23,10 +23,16 @@ import android.content.Context
import android.content.pm.PackageInfo
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.AuroraApp
import com.aurora.store.data.event.BusEvent
import com.aurora.store.data.helper.UpdateHelper
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.CertUtil
import com.aurora.store.util.PackageUtil
import com.aurora.store.util.Preferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -39,38 +45,87 @@ import javax.inject.Inject
@HiltViewModel
class BlacklistViewModel @Inject constructor(
val blacklistProvider: BlacklistProvider,
val gson: Gson,
private val gson: Gson,
private val updateHelper: UpdateHelper,
private val blacklistProvider: BlacklistProvider,
@ApplicationContext private val context: Context
) : ViewModel() {
private val TAG = BlacklistViewModel::class.java.simpleName
private val _packages = MutableStateFlow<List<PackageInfo>?>(null)
val packages = _packages.asStateFlow()
private val isAuroraOnlyFilterEnabled =
Preferences.getBoolean(context, Preferences.PREFERENCE_FILTER_AURORA_ONLY, false)
private val isFDroidFilterEnabled =
Preferences.getBoolean(context, Preferences.PREFERENCE_FILTER_FDROID, true)
private val isExtendedUpdateEnabled =
Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
var selected: MutableSet<String> = blacklistProvider.blacklist
private val _packages = MutableStateFlow<List<PackageInfo>?>(null)
private val _filteredPackages = MutableStateFlow<List<PackageInfo>?>(null)
val filteredPackages = _filteredPackages.asStateFlow()
val blacklist = mutableStateListOf<String>()
init {
blacklist.addAll(blacklistProvider.blacklist)
fetchApps()
}
private fun fetchApps() {
viewModelScope.launch(Dispatchers.IO) {
try {
_packages.value = PackageUtil.getAllValidPackages(context)
_packages.value = PackageUtil.getAllValidPackages(context).also { pkgList ->
_filteredPackages.value = pkgList
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch apps", exception)
}
}
}
fun selectAll() {
selected.addAll(packages.value?.map { it.packageName } ?: emptyList())
fun search(query: String) {
if (query.isNotBlank()) {
_filteredPackages.value = _packages.value!!
.filter { it.applicationInfo!!.loadLabel(context.packageManager)
.contains(query, true) || it.packageName.contains(query, true)
}
} else {
_filteredPackages.value = _packages.value
}
}
fun removeAll() {
selected.clear()
fun isFiltered(packageInfo: PackageInfo): Boolean {
return when {
!isExtendedUpdateEnabled && !packageInfo.applicationInfo!!.enabled -> true
isAuroraOnlyFilterEnabled -> !CertUtil.isAuroraStoreApp(context, packageInfo.packageName)
isFDroidFilterEnabled -> CertUtil.isFDroidApp(context, packageInfo.packageName)
else -> false
}
}
fun blacklist(packageName: String) {
blacklist.add(packageName)
blacklistProvider.blacklist(packageName)
AuroraApp.events.send(BusEvent.Blacklisted(packageName))
}
fun blacklistAll() {
blacklistProvider.blacklist = _packages.value!!.map { it.packageName }.toMutableSet()
blacklist.apply {
clear()
addAll(blacklistProvider.blacklist)
}
viewModelScope.launch { updateHelper.deleteAllUpdates() }
}
fun whitelist(packageName: String) {
blacklist.remove(packageName)
blacklistProvider.whitelist(packageName)
}
fun whitelistAll() {
blacklist.clear()
blacklistProvider.blacklist = mutableSetOf()
}
fun importBlacklist(context: Context, uri: Uri) {
@@ -82,10 +137,13 @@ class BlacklistViewModel @Inject constructor(
object : TypeToken<MutableSet<String?>?>() {}.type
)
val knownSet = blacklistProvider.blacklist
knownSet.addAll(importedSet)
selected = knownSet
val validImportedSet = importedSet
.filter { pkgName -> _packages.value!!.any { it.packageName == pkgName } }
blacklistProvider.blacklist.addAll(validImportedSet)
blacklist.apply {
clear()
addAll(blacklistProvider.blacklist)
}
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to import blacklist", exception)