AppsGamesFragment: Allow exporting list as favourites

- This will allow isers to export there current installed app & then do bulk import as favourites
- Probably allowing a bulk install in favourites later would be a great addition
This commit is contained in:
Rahul Patel
2024-12-16 22:07:47 +05:30
parent fbeee2300c
commit 9540ac0c3b
8 changed files with 125 additions and 18 deletions

View File

@@ -35,7 +35,8 @@ object Constants {
const val SHARE_URL = "https://play.google.com/store/apps/details?id="
const val UPDATE_URL_STABLE = "https://gitlab.com/AuroraOSS/AuroraStore/raw/master/updates.json"
const val UPDATE_URL_NIGHTLY = "https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json"
const val UPDATE_URL_NIGHTLY =
"https://auroraoss.com/downloads/AuroraStore/Feeds/nightly_feed.json"
const val NOTIFICATION_CHANNEL_EXPORT = "NOTIFICATION_CHANNEL_EXPORT"
const val NOTIFICATION_CHANNEL_INSTALL = "NOTIFICATION_CHANNEL_INSTALL"
@@ -56,4 +57,6 @@ object Constants {
const val PAGE_TYPE = "PAGE_TYPE"
const val TOP_CHART_TYPE = "TOP_CHART_TYPE"
const val TOP_CHART_CATEGORY = "TOP_CHART_CATEGORY"
const val JSON_MIME_TYPE = "application/json"
}

View File

@@ -3,6 +3,7 @@ package com.aurora.store.data.room.favourite
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.aurora.gplayapi.data.models.App
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -16,6 +17,18 @@ data class Favourite(
val mode: Mode
) : Parcelable {
companion object {
fun fromApp(app: App, mode: Mode): Favourite {
return Favourite(
packageName = app.packageName,
displayName = app.displayName,
iconURL = app.iconArtwork.url,
added = System.currentTimeMillis(),
mode = mode
)
}
}
enum class Mode {
MANUAL,
IMPORT

View File

@@ -19,15 +19,25 @@
package com.aurora.store.view.ui.all
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.ContextThemeWrapper
import android.view.MenuInflater
import android.view.MenuItem
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.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.toast
import com.aurora.gplayapi.data.models.App
import com.aurora.store.AuroraApp
import com.aurora.store.R
import com.aurora.store.data.event.InstallerEvent
import com.aurora.store.data.model.MinimalApp
import com.aurora.store.databinding.FragmentGenericWithSearchBinding
@@ -38,12 +48,18 @@ import com.aurora.store.view.ui.commons.BaseFragment
import com.aurora.store.viewmodel.all.InstalledViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.util.Calendar
@AndroidEntryPoint
class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
private val viewModel: InstalledViewModel by viewModels()
private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportInstalledApps(it) else toast(R.string.toast_fav_export_failed)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -69,9 +85,17 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
// Toolbar
binding.layoutToolbarNative.apply {
imgActionPrimary.visibility = View.VISIBLE
imgActionSecondary.visibility = View.GONE
imgActionSecondary.visibility = View.VISIBLE
imgActionPrimary.setOnClickListener { findNavController().navigateUp() }
imgActionSecondary.apply {
setImageDrawable(
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_menu)
)
setOnClickListener {
showMenu(it)
}
}
inputSearch.addTextChangedListener(object : TextWatcher {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
@@ -97,6 +121,35 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
}
}
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.menu.removeItem(R.id.action_import)
popupMenu.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
R.id.action_export -> {
startForDocumentExport.launch(
"aurora_store_apps_${Calendar.getInstance().time.time}.json"
)
true
}
else -> false
}
}
popupMenu.show()
}
private fun updateController(packages: List<App>?) {
binding.recycler.withModels {
setFilterDuplicates(true)
@@ -137,4 +190,9 @@ class AppsGamesFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
}
}
}
private fun exportInstalledApps(uri: Uri) {
viewModel.exportApps(requireContext(), uri)
toast(R.string.toast_fav_export_success)
}
}

View File

@@ -34,6 +34,7 @@ import androidx.appcompat.content.res.AppCompatResources
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
@@ -51,13 +52,12 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
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)) {
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportBlacklist(it) else toast(R.string.toast_black_export_failed)
}
@@ -132,7 +132,7 @@ class BlacklistFragment : BaseFragment<FragmentGenericWithSearchBinding>() {
popupMenu.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
R.id.action_import -> {
startForDocumentImport.launch(arrayOf(mimeType))
startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE))
true
}

View File

@@ -26,6 +26,7 @@ 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.R
import com.aurora.store.data.room.favourite.Favourite
@@ -42,13 +43,12 @@ import java.util.Calendar
class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
private val viewModel: FavouriteViewModel by viewModels()
private val mimeType = "application/json"
private val startForDocumentImport =
registerForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) importFavourites(it) else toast(R.string.toast_fav_import_failed)
}
private val startForDocumentExport =
registerForActivityResult(ActivityResultContracts.CreateDocument(mimeType)) {
registerForActivityResult(ActivityResultContracts.CreateDocument(Constants.JSON_MIME_TYPE)) {
if (it != null) exportFavourites(it) else toast(R.string.toast_fav_export_failed)
}
@@ -65,7 +65,7 @@ class FavouriteFragment : BaseFragment<FragmentFavouriteBinding>() {
binding.toolbar.apply {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_import -> startForDocumentImport.launch(arrayOf(mimeType))
R.id.action_import -> startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE))
R.id.action_export -> {
startForDocumentExport.launch(
"aurora_store_favourites_${Calendar.getInstance().time.time}.json"

View File

@@ -21,6 +21,7 @@ package com.aurora.store.viewmodel.all
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.room.favourite.Favourite
@@ -39,26 +40,38 @@ class FavouriteViewModel @Inject constructor(
private val favouriteDao: FavouriteDao,
private val gson: Gson
) : ViewModel() {
private val TAG = FavouriteViewModel::class.java.simpleName
val favouritesList = favouriteDao.favourites()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun importFavourites(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)?.use {
val importExport =
gson.fromJson(it.bufferedReader().readText(), ImportExport::class.java)
favouriteDao.insertAll(
importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) }
)
try {
context.contentResolver.openInputStream(uri)?.use {
val importExport = gson.fromJson(
it.bufferedReader().readText(),
ImportExport::class.java
)
favouriteDao.insertAll(
importExport.favourites.map { fav -> fav.copy(mode = Favourite.Mode.IMPORT) }
)
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to import favourites", exception)
}
}
}
fun exportFavourites(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray())
try {
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favouritesList.value!!)).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to export favourites", exception)
}
}
}

View File

@@ -20,13 +20,17 @@
package com.aurora.store.viewmodel.all
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.helpers.web.WebAppDetailsHelper
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.ImportExport
import com.aurora.store.util.PackageUtil
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -39,6 +43,7 @@ import javax.inject.Inject
class InstalledViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val blacklistProvider: BlacklistProvider,
private val gson: Gson
) : ViewModel() {
private val TAG = InstalledViewModel::class.java.simpleName
@@ -69,4 +74,19 @@ class InstalledViewModel @Inject constructor(
}
}
}
fun exportApps(context: Context, uri: Uri) {
viewModelScope.launch(Dispatchers.IO) {
try {
val favourites: List<Favourite> = apps.value!!.map { app ->
Favourite.fromApp(app, Favourite.Mode.IMPORT)
}
context.contentResolver.openOutputStream(uri)?.use {
it.write(gson.toJson(ImportExport(favourites)).encodeToByteArray())
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to installed apps", exception)
}
}
}
}

View File

@@ -236,8 +236,8 @@ class AppDetailsViewModel @Inject constructor(
private fun getLatestExodusReport(packageName: String): Report? {
val headers: MutableMap<String, String> = mutableMapOf()
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["Content-Type"] = Constants.JSON_MIME_TYPE
headers["Accept"] = Constants.JSON_MIME_TYPE
headers["Authorization"] = "Token ${BuildConfig.EXODUS_API_KEY}"
val url = Constants.EXODUS_SEARCH_URL + packageName