Blacklist: Add support for import & export

This commit is contained in:
Rahul Patel
2024-12-05 20:25:06 +05:30
parent b989030e7b
commit d3d30100a0
7 changed files with 138 additions and 8 deletions

View File

@@ -20,14 +20,23 @@
package com.aurora.store.view.ui.commons package com.aurora.store.view.ui.commons
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.ContextThemeWrapper
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.PopupMenu
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.aurora.extensions.toast
import com.aurora.store.AuroraApp import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.BusEvent import com.aurora.store.data.event.BusEvent
import com.aurora.store.databinding.FragmentGenericWithSearchBinding import com.aurora.store.databinding.FragmentGenericWithSearchBinding
import com.aurora.store.view.epoxy.views.BlackListViewModel_ import com.aurora.store.view.epoxy.views.BlackListViewModel_
@@ -35,12 +44,23 @@ import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_
import com.aurora.store.viewmodel.all.BlacklistViewModel import com.aurora.store.viewmodel.all.BlacklistViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint @AndroidEntryPoint
class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() { class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: BlacklistViewModel by viewModels() private val viewModel: BlacklistViewModel by viewModels()
private val mimeType = "application/json"
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(mimeType)) {
if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -53,7 +73,15 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
// Toolbar // Toolbar
binding.layoutToolbarNative.apply { binding.layoutToolbarNative.apply {
imgActionPrimary.visibility = View.VISIBLE imgActionPrimary.visibility = View.VISIBLE
imgActionSecondary.visibility = View.GONE imgActionSecondary.apply {
visibility = View.VISIBLE
setImageDrawable(
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_menu)
)
setOnClickListener {
showMenu(it)
}
}
imgActionPrimary.setOnClickListener { imgActionPrimary.setOnClickListener {
viewModel.blacklistProvider.blacklist = viewModel.selected viewModel.blacklistProvider.blacklist = viewModel.selected
@@ -79,7 +107,8 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
start: Int, start: Int,
count: Int, count: Int,
after: Int after: Int
) {} ) {
}
}) })
} }
} }
@@ -89,6 +118,37 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
viewModel.blacklistProvider.blacklist = viewModel.selected viewModel.blacklistProvider.blacklist = viewModel.selected
} }
private fun showMenu(anchor: View) {
val popupMenu = PopupMenu(
ContextThemeWrapper(
requireContext(),
R.style.AppTheme_PopupMenu
), anchor
)
val inflater: MenuInflater = popupMenu.menuInflater
inflater.inflate(R.menu.menu_import_export, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
R.id.action_import -> {
startForDocumentImport.launch(arrayOf(mimeType))
true
}
R.id.action_export -> {
startForDocumentExport.launch(
"aurora_store_blacklist_${Calendar.getInstance().time.time}.json"
)
true
}
else -> false
}
}
popupMenu.show()
}
private fun updateController(packages: List<PackageInfo>?) { private fun updateController(packages: List<PackageInfo>?) {
binding.recycler.withModels { binding.recycler.withModels {
setFilterDuplicates(true) setFilterDuplicates(true)
@@ -125,4 +185,15 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
} }
} }
} }
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

@@ -45,11 +45,11 @@ class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
private val mimeType = "application/json" private val mimeType = "application/json"
private val startForDocumentImport = private val startForDocumentImport =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { registerForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) importDeviceConfig(it) else toast(R.string.toast_fav_import_failed) if (it != null) importFavourites(it) else toast(R.string.toast_fav_import_failed)
} }
private val startForDocumentExport = private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) { registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) {
if (it != null) exportDeviceConfig(it) else toast(R.string.toast_fav_export_failed) if (it != null) exportFavourites(it) else toast(R.string.toast_fav_export_failed)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -111,13 +111,13 @@ class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
} }
} }
private fun importDeviceConfig(uri: Uri) { private fun importFavourites(uri: Uri) {
viewModel.importFavourites(requireContext(), uri) viewModel.importFavourites(requireContext(), uri)
binding.recycler.requestModelBuild() binding.recycler.requestModelBuild()
toast(R.string.toast_fav_import_success) toast(R.string.toast_fav_import_success)
} }
private fun exportDeviceConfig(uri: Uri) { private fun exportFavourites(uri: Uri) {
viewModel.exportFavourites(requireContext(), uri) viewModel.exportFavourites(requireContext(), uri)
toast(R.string.toast_fav_export_success) toast(R.string.toast_fav_export_success)
} }

View File

@@ -21,11 +21,14 @@ package com.aurora.store.viewmodel.all
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -37,6 +40,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BlacklistViewModel @Inject constructor( class BlacklistViewModel @Inject constructor(
val blacklistProvider: BlacklistProvider, val blacklistProvider: BlacklistProvider,
val gson: Gson,
@ApplicationContext private val context: Context @ApplicationContext private val context: Context
) : ViewModel() { ) : ViewModel() {
@@ -60,4 +64,36 @@ class BlacklistViewModel @Inject constructor(
} }
} }
} }
fun importBlacklist(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openInputStream(uri)?.use {
val importedSet: MutableSet<String> = gson.fromJson(
it.bufferedReader().readText(),
object : TypeToken<MutableSet<String?>?>() {}.type
)
val knownSet = blacklistProvider.blacklist
knownSet.addAll(importedSet)
selected = knownSet
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to import blacklist", exception)
}
}
}
fun exportBlacklist(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
try {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(blacklistProvider.blacklist).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to export blacklist", exception)
}
}
}
} }

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,800q-33,0 -56.5,-23.5T400,720q0,-33 23.5,-56.5T480,640q33,0 56.5,23.5T560,720q0,33 -23.5,56.5T480,800ZM480,560q-33,0 -56.5,-23.5T400,480q0,-33 23.5,-56.5T480,400q33,0 56.5,23.5T560,480q0,33 -23.5,56.5T480,560ZM480,320q-33,0 -56.5,-23.5T400,240q0,-33 23.5,-56.5T480,160q33,0 56.5,23.5T560,240q0,33 -23.5,56.5T480,320Z"
android:fillColor="#e8eaed"/>
</vector>

View File

@@ -5,5 +5,6 @@
<item name="chipStyle">@style/Chip.Filter</item> <item name="chipStyle">@style/Chip.Filter</item>
<item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item>
<item name="android:statusBarColor">@color/colorTransparent</item> <item name="android:statusBarColor">@color/colorTransparent</item>
<item name="popupMenuStyle">@style/AppTheme.PopupMenu</item>
</style> </style>
</resources> </resources>

View File

@@ -437,6 +437,12 @@
<string name="toast_fav_import_success">Favourites imported!</string> <string name="toast_fav_import_success">Favourites imported!</string>
<string name="toast_fav_export_success">Favourites exported!</string> <string name="toast_fav_export_success">Favourites exported!</string>
<!-- BlacklistFragment-->
<string name="toast_black_import_failed">Failed to import blacklist!</string>
<string name="toast_black_export_failed">Failed to export blacklist!</string>
<string name="toast_black_import_success">Blacklist imported!</string>
<string name="toast_black_export_success">Blacklist exported!</string>
<!-- ExportWorker --> <!-- ExportWorker -->
<string name="export_app_title">File Exporter</string> <string name="export_app_title">File Exporter</string>
<string name="export_app_summary">Hold on, exporting your file</string> <string name="export_app_summary">Hold on, exporting your file</string>

View File

@@ -24,6 +24,7 @@
<item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item> <item name="preferenceTheme">@style/AppTheme.PreferenceThemeOverlay</item>
<item name="bottomSheetStyle">@style/AppTheme.BottomSheetStyle</item> <item name="bottomSheetStyle">@style/AppTheme.BottomSheetStyle</item>
<item name="android:statusBarColor">@color/colorTransparent</item> <item name="android:statusBarColor">@color/colorTransparent</item>
<item name="popupMenuStyle">@style/AppTheme.PopupMenu</item>
</style> </style>
<style name="AppTheme.Translucent" parent="Theme.Material3.DynamicColors.DayNight.NoActionBar"> <style name="AppTheme.Translucent" parent="Theme.Material3.DynamicColors.DayNight.NoActionBar">
@@ -47,17 +48,23 @@
<item name="widgetLayout">@layout/preference_material_switch</item> <item name="widgetLayout">@layout/preference_material_switch</item>
</style> </style>
<style name="AppTheme.PopupMenu" parent="Widget.Material3.PopupMenu">
<item name="android:popupBackground">?attr/colorSurfaceContainer</item>
<item name="android:textColor">?attr/colorOnSurface</item>
<item name="android:elevation">4dp</item>
</style>
<style name="Chip.Filter" parent="@style/Widget.Material3.Chip.Filter" /> <style name="Chip.Filter" parent="@style/Widget.Material3.Chip.Filter" />
<style name="Chip.TopChart" parent="@style/Widget.Material3.Chip.Filter"> <style name="Chip.TopChart" parent="@style/Widget.Material3.Chip.Filter">
<item name="android:textAppearance">@style/TextAppearance.Aurora.Line1</item> <item name="android:textAppearance">@style/TextAppearance.Aurora.Line1</item>
<item name="chipStrokeColor">?colorControlHighlight</item> <item name="chipStrokeColor">?attr/colorControlHighlight</item>
</style> </style>
<style name="Chip.Tag" parent="@style/Widget.Material3.Chip.Assist.Elevated"> <style name="Chip.Tag" parent="@style/Widget.Material3.Chip.Assist.Elevated">
<item name="android:clickable">false</item> <item name="android:clickable">false</item>
<item name="android:textAppearance">@style/TextAppearance.Aurora.Line2</item> <item name="android:textAppearance">@style/TextAppearance.Aurora.Line2</item>
<item name="chipBackgroundColor">?colorControlHighlight</item> <item name="chipBackgroundColor">?attr/colorControlHighlight</item>
<item name="chipStrokeWidth">0dp</item> <item name="chipStrokeWidth">0dp</item>
</style> </style>
</resources> </resources>