diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 32f146c1d..211f40cd9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -26,6 +26,7 @@ plugins {
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.jetbrains.kotlin.compose)
alias(libs.plugins.jetbrains.kotlin.parcelize)
+ alias(libs.plugins.jetbrains.kotlin.serialization)
alias(libs.plugins.google.ksp)
alias(libs.plugins.androidx.navigation)
alias(libs.plugins.ktlint)
@@ -147,15 +148,19 @@ dependencies {
implementation(libs.androidx.browser)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.process)
- implementation(libs.androidx.navigation.fragment.ktx)
- implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.viewpager2)
implementation(libs.androidx.work.runtime.ktx)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
+
implementation(libs.androidx.material3)
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
@@ -205,6 +210,7 @@ dependencies {
//Hilt
ksp(libs.hilt.android.compiler)
ksp(libs.hilt.androidx.compiler)
+ implementation(libs.androidx.hilt.navigation)
implementation(libs.hilt.android.core)
implementation(libs.hilt.androidx.work)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5873e2185..48d4e3c35 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -104,6 +104,11 @@
+
+
+
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
)
}
diff --git a/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt
index 7b0f1ad31..ab6c3975b 100644
--- a/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt
+++ b/app/src/main/java/com/aurora/store/compose/composables/InstalledAppComposable.kt
@@ -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
)
}
}
diff --git a/app/src/main/java/com/aurora/store/compose/composables/SearchAppBarComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/SearchAppBarComposable.kt
new file mode 100644
index 000000000..74fea776c
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/composables/SearchAppBarComposable.kt
@@ -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()
+ }
+ )
+}
diff --git a/app/src/main/java/com/aurora/store/compose/composables/TopAppBarComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/TopAppBarComposable.kt
new file mode 100644
index 000000000..98249a516
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/composables/TopAppBarComposable.kt
@@ -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
+ )
+}
diff --git a/app/src/main/java/com/aurora/store/compose/composables/TransparentIconComposable.kt b/app/src/main/java/com/aurora/store/compose/composables/TransparentIconComposable.kt
new file mode 100644
index 000000000..d9fbad770
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/composables/TransparentIconComposable.kt
@@ -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()
+}
diff --git a/app/src/main/java/com/aurora/store/compose/menu/BlacklistMenu.kt b/app/src/main/java/com/aurora/store/compose/menu/BlacklistMenu.kt
new file mode 100644
index 000000000..2c8b23c87
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/menu/BlacklistMenu.kt
@@ -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()
+}
diff --git a/app/src/main/java/com/aurora/store/compose/menu/items/BlacklistMenuItem.kt b/app/src/main/java/com/aurora/store/compose/menu/items/BlacklistMenuItem.kt
new file mode 100644
index 000000000..59f8b79bd
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/menu/items/BlacklistMenuItem.kt
@@ -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
+}
diff --git a/app/src/main/java/com/aurora/store/compose/navigation/NavGraph.kt b/app/src/main/java/com/aurora/store/compose/navigation/NavGraph.kt
new file mode 100644
index 000000000..5ba69343e
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/navigation/NavGraph.kt
@@ -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 {
+ BlacklistScreen(onNavigateUp = { onNavigateUp() })
+ }
+ }
+}
diff --git a/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt
new file mode 100644
index 000000000..927502aed
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/navigation/Screen.kt
@@ -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)
+}
diff --git a/app/src/main/java/com/aurora/store/compose/ui/commons/BlacklistScreen.kt b/app/src/main/java/com/aurora/store/compose/ui/commons/BlacklistScreen.kt
new file mode 100644
index 000000000..c8e6e4e6b
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/compose/ui/commons/BlacklistScreen.kt
@@ -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? = 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()
+}
diff --git a/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt b/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt
index c63a6e288..802ad64d8 100644
--- a/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt
+++ b/app/src/main/java/com/aurora/store/data/helper/UpdateHelper.kt
@@ -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]
diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt
deleted file mode 100644
index 35a251b74..000000000
--- a/app/src/main/java/com/aurora/store/view/ui/commons/BlacklistFragment.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Aurora Store
- * Copyright (C) 2021, Rahul Kumar Patel
- *
- * 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 .
- *
- */
-
-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() {
-
- 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?) {
- 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)
- }
-}
diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt
index f466eb969..79db6a636 100644
--- a/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt
+++ b/app/src/main/java/com/aurora/store/view/ui/commons/MoreDialogFragment.kt
@@ -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