From 9540ac0c3b0cc821965b2804aa171337b37b57b2 Mon Sep 17 00:00:00 2001 From: Rahul Patel Date: Mon, 16 Dec 2024 22:07:47 +0530 Subject: [PATCH] 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 --- app/src/main/java/com/aurora/Constants.kt | 5 +- .../store/data/room/favourite/Favourite.kt | 13 ++++ .../store/view/ui/all/AppsGamesFragment.kt | 60 ++++++++++++++++++- .../view/ui/commons/BlacklistFragment.kt | 6 +- .../view/ui/commons/FavouriteFragment.kt | 6 +- .../store/viewmodel/all/FavouriteViewModel.kt | 29 ++++++--- .../store/viewmodel/all/InstalledViewModel.kt | 20 +++++++ .../viewmodel/details/AppDetailsViewModel.kt | 4 +- 8 files changed, 125 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/aurora/Constants.kt b/app/src/main/java/com/aurora/Constants.kt index f691fe06a..16732cfe3 100644 --- a/app/src/main/java/com/aurora/Constants.kt +++ b/app/src/main/java/com/aurora/Constants.kt @@ -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" } diff --git a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt index 4ab8da49b..34765092b 100644 --- a/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt +++ b/app/src/main/java/com/aurora/store/data/room/favourite/Favourite.kt @@ -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 diff --git a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt index e391a10b0..db9d20e2e 100644 --- a/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/all/AppsGamesFragment.kt @@ -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() { 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() { // 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() { } } + 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?) { binding.recycler.withModels { setFilterDuplicates(true) @@ -137,4 +190,9 @@ class AppsGamesFragment : BaseFragment() { } } } + + private fun exportInstalledApps(uri: Uri) { + viewModel.exportApps(requireContext(), uri) + toast(R.string.toast_fav_export_success) + } } 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 index f72a71461..df0758433 100644 --- 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 @@ -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() { 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() { popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> when (menuItem.itemId) { R.id.action_import -> { - startForDocumentImport.launch(arrayOf(mimeType)) + startForDocumentImport.launch(arrayOf(Constants.JSON_MIME_TYPE)) true } diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt index 202368320..29651dc9a 100644 --- a/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt @@ -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() { 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() { 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" diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt index 87ae60bea..09a42246e 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt @@ -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) } } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt index 442953a96..73353fee6 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/InstalledViewModel.kt @@ -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 = 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) + } + } + } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt index a9aa2f057..367e61074 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/details/AppDetailsViewModel.kt @@ -236,8 +236,8 @@ class AppDetailsViewModel @Inject constructor( private fun getLatestExodusReport(packageName: String): Report? { val headers: MutableMap = 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