diff --git a/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt b/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt index 032805196..42b73cfed 100644 --- a/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt +++ b/app/src/main/java/com/aurora/store/data/room/AuroraDatabase.kt @@ -6,9 +6,12 @@ import androidx.room.TypeConverters import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.download.DownloadConverter +import com.aurora.store.data.room.favourites.Favourite +import com.aurora.store.data.room.favourites.FavouriteDao -@Database(entities = [Download::class], version = 1, exportSchema = false) +@Database(entities = [Download::class, Favourite::class], version = 2, exportSchema = false) @TypeConverters(DownloadConverter::class) abstract class AuroraDatabase : RoomDatabase() { abstract fun downloadDao(): DownloadDao + abstract fun favouriteDao(): FavouriteDao } diff --git a/app/src/main/java/com/aurora/store/data/room/RoomModule.kt b/app/src/main/java/com/aurora/store/data/room/RoomModule.kt index cea751959..6a96fb15e 100644 --- a/app/src/main/java/com/aurora/store/data/room/RoomModule.kt +++ b/app/src/main/java/com/aurora/store/data/room/RoomModule.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Room import com.aurora.store.data.room.download.DownloadConverter import com.aurora.store.data.room.download.DownloadDao +import com.aurora.store.data.room.favourites.FavouriteDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -33,4 +34,9 @@ object RoomModule { fun providesDownloadDao(auroraDatabase: AuroraDatabase): DownloadDao { return auroraDatabase.downloadDao() } + + @Provides + fun providesFavouriteDao(auroraDatabase: AuroraDatabase): FavouriteDao { + return auroraDatabase.favouriteDao() + } } diff --git a/app/src/main/java/com/aurora/store/data/room/favourites/Favourite.kt b/app/src/main/java/com/aurora/store/data/room/favourites/Favourite.kt new file mode 100644 index 000000000..e47750463 --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/room/favourites/Favourite.kt @@ -0,0 +1,23 @@ +package com.aurora.store.data.room.favourites + +import android.os.Parcelable +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.parcelize.Parcelize + +@Parcelize +@Entity(tableName = "favourite") +data class Favourite( + @PrimaryKey + val packageName: String, + val displayName: String, + val iconURL: String, + val added: Long, + val mode: Mode +) : Parcelable { + + enum class Mode { + MANUAL, + IMPORT + } +} diff --git a/app/src/main/java/com/aurora/store/data/room/favourites/FavouriteDao.kt b/app/src/main/java/com/aurora/store/data/room/favourites/FavouriteDao.kt new file mode 100644 index 000000000..d9aef8c9b --- /dev/null +++ b/app/src/main/java/com/aurora/store/data/room/favourites/FavouriteDao.kt @@ -0,0 +1,32 @@ +package com.aurora.store.data.room.favourites; + +import androidx.room.Dao; +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface FavouriteDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(favourite: Favourite) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(favourites: List) + + @Query("SELECT * FROM favourite") + fun favourites(): Flow> + + @Query("SELECT EXISTS(SELECT 1 FROM favourite WHERE packageName = :packageName)") + suspend fun isFavourite(packageName: String): Boolean + + @Query("DELETE FROM favourite WHERE packageName = :packageName") + suspend fun delete(packageName: String) + + @Query("DELETE FROM favourite") + suspend fun deleteAll() + + @Query("SELECT COUNT(*) FROM favourite") + suspend fun count(): Int +} diff --git a/app/src/main/java/com/aurora/store/view/epoxy/views/FavouriteView.kt b/app/src/main/java/com/aurora/store/view/epoxy/views/FavouriteView.kt new file mode 100644 index 000000000..4c3f3cdbb --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/epoxy/views/FavouriteView.kt @@ -0,0 +1,84 @@ +/* + * 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.epoxy.views + +import android.content.Context +import android.util.AttributeSet +import android.widget.RelativeLayout +import coil.load +import coil.transform.RoundedCornersTransformation +import com.airbnb.epoxy.CallbackProp +import com.airbnb.epoxy.ModelProp +import com.airbnb.epoxy.ModelView +import com.aurora.gplayapi.data.models.App +import com.aurora.store.R +import com.aurora.store.data.room.favourites.Favourite +import com.aurora.store.databinding.ViewFavBinding + +@ModelView( + autoLayout = ModelView.Size.MATCH_WIDTH_WRAP_HEIGHT, + baseModelClass = BaseView::class +) +class FavouriteView : RelativeLayout { + + private lateinit var binding: ViewFavBinding + + constructor(context: Context?) : super(context) { + init(context) + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init(context) + } + + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init(context) + } + + private fun init(context: Context?) { + val view = inflate(context, R.layout.view_fav, this) + binding = ViewFavBinding.bind(view) + } + + @ModelProp + fun favourite(favourite: Favourite) { + binding.imgIcon.load(favourite.iconURL) { + placeholder(R.drawable.bg_placeholder) + transformations(RoundedCornersTransformation(25F)) + } + + binding.txtLine1.text = favourite.displayName + binding.txtLine2.text = favourite.packageName + } + + @CallbackProp + fun onClick(onClickListener: OnClickListener?) { + binding.root.setOnClickListener(onClickListener) + } + + @CallbackProp + fun onFavourite(onClickListener: OnClickListener?) { + binding.btnFavourite.setOnClickListener(onClickListener) + } +} 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 new file mode 100644 index 000000000..d1e98263a --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/commons/FavouriteFragment.kt @@ -0,0 +1,98 @@ +/* + * 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.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.aurora.store.R +import com.aurora.store.data.room.favourites.Favourite +import com.aurora.store.databinding.FragmentFavouriteBinding +import com.aurora.store.view.epoxy.views.FavouriteViewModel_ +import com.aurora.store.view.epoxy.views.app.NoAppViewModel_ +import com.aurora.store.view.epoxy.views.shimmer.AppListViewShimmerModel_ +import com.aurora.store.viewmodel.all.FavouriteViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class FavouriteFragment : BaseFragment(R.layout.fragment_favourite) { + + private var _binding: FragmentFavouriteBinding? = null + private val binding: FragmentFavouriteBinding + get() = _binding!! + + private val viewModel: FavouriteViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + _binding = FragmentFavouriteBinding.bind(view) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.favouritesList.collect { + updateController(it) + } + } + + // Toolbar + binding.toolbar.setNavigationOnClickListener { + findNavController().navigateUp() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun updateController(favourites: List?) { + binding.recycler.withModels { + setFilterDuplicates(true) + if (favourites == null) { + for (i in 1..10) { + add( + AppListViewShimmerModel_() + .id(i) + ) + } + } else if(favourites.isEmpty()) { + add( + NoAppViewModel_() + .id("no_app") + .icon(R.drawable.ic_favorite_unchecked) + .message(getString(R.string.details_no_favourites)) + ) + } else { + favourites.forEach { + add( + FavouriteViewModel_() + .id(it.packageName.hashCode()) + .favourite(it) + .onClick { _ -> openDetailsFragment(it.packageName) } + .onFavourite { _ -> viewModel.removeFavourite(it.packageName) } + ) + } + } + } + } +} 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 6c296dce7..17a62e127 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 @@ -280,6 +280,11 @@ class MoreDialogFragment : DialogFragment() { icon = R.drawable.ic_blacklist, destinationID = R.id.blacklistFragment ), + Option( + title = R.string.title_favourites_manager, + icon = R.drawable.ic_favorite_unchecked, + destinationID = R.id.favouriteFragment + ), Option( title = R.string.title_spoof_manager, icon = R.drawable.ic_spoof, diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt index 68f0c3782..eff00b6cd 100644 --- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt @@ -368,6 +368,7 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { findNavController().navigateUp() } } + inflateMenu(R.menu.menu_details) setOnMenuItemClickListener { @@ -383,6 +384,10 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { view.context.share(app) } + R.id.action_favourite -> { + viewModel.toggleFavourite(app) + } + R.id.action_uninstall -> { AppInstaller.uninstall(requireContext(), app.packageName) } @@ -428,6 +433,20 @@ class AppDetailsFragment : BaseFragment(R.layout.fragment_details) { uninstallActionEnabled = app.isInstalled } } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.favourite.collect { + if (it) { + binding.layoutDetailsToolbar.toolbar.menu + ?.findItem(R.id.action_favourite) + ?.setIcon(R.drawable.ic_favorite_checked) + } else { + binding.layoutDetailsToolbar.toolbar.menu + ?.findItem(R.id.action_favourite) + ?.setIcon(R.drawable.ic_favorite_unchecked) + } + } + } } override fun onResume() { 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 new file mode 100644 index 000000000..24f89eaf5 --- /dev/null +++ b/app/src/main/java/com/aurora/store/viewmodel/all/FavouriteViewModel.kt @@ -0,0 +1,45 @@ +/* + * 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.viewmodel.all + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aurora.store.data.room.favourites.FavouriteDao +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FavouriteViewModel @Inject constructor( + private val favouriteDao: FavouriteDao, +) : ViewModel() { + + val favouritesList = favouriteDao.favourites() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + fun removeFavourite(packageName: String) { + viewModelScope.launch(Dispatchers.IO) { + favouriteDao.delete(packageName) + } + } +} 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 8d74b2721..aba8e6453 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 @@ -13,12 +13,16 @@ import com.aurora.store.data.model.ExodusReport import com.aurora.store.data.model.Report import com.aurora.store.data.network.HttpClient import com.aurora.store.data.providers.AuthProvider +import com.aurora.store.data.room.favourites.Favourite +import com.aurora.store.data.room.favourites.FavouriteDao import com.aurora.store.util.DownloadWorkerUtil import com.google.gson.GsonBuilder import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.json.JSONObject import java.lang.reflect.Modifier @@ -27,7 +31,8 @@ import javax.inject.Inject @HiltViewModel class AppDetailsViewModel @Inject constructor( private val downloadWorkerUtil: DownloadWorkerUtil, - private val authProvider: AuthProvider + private val authProvider: AuthProvider, + private val favouriteDao: FavouriteDao ) : ViewModel() { private val TAG = AppDetailsViewModel::class.java.simpleName @@ -50,11 +55,16 @@ class AppDetailsViewModel @Inject constructor( private val _testingProgramStatus = MutableSharedFlow() val testingProgramStatus = _testingProgramStatus.asSharedFlow() + private val _favourite = MutableStateFlow(false) + val favourite = _favourite.asStateFlow() + val downloadsList get() = downloadWorkerUtil.downloadsList fun fetchAppDetails(context: Context, packageName: String) { viewModelScope.launch(Dispatchers.IO) { try { + isFavourite(packageName) + _app.emit( AppDetailsHelper(authProvider.authData) .using(HttpClient.getPreferredClient(context)) @@ -70,9 +80,11 @@ class AppDetailsViewModel @Inject constructor( fun fetchAppReviews(context: Context, packageName: String) { viewModelScope.launch(Dispatchers.IO) { try { - _reviews.emit(ReviewsHelper(authProvider.authData) - .using(HttpClient.getPreferredClient(context)) - .getReviewSummary(packageName)) + _reviews.emit( + ReviewsHelper(authProvider.authData) + .using(HttpClient.getPreferredClient(context)) + .getReviewSummary(packageName) + ) } catch (exception: Exception) { Log.e(TAG, "Failed to fetch app reviews", exception) _reviews.emit(emptyList()) @@ -83,15 +95,17 @@ class AppDetailsViewModel @Inject constructor( fun postAppReview(context: Context, packageName: String, review: Review, isBeta: Boolean) { viewModelScope.launch(Dispatchers.IO) { try { - _userReview.emit(ReviewsHelper(authProvider.authData) - .using(HttpClient.getPreferredClient(context)) - .addOrEditReview( - packageName, - review.title, - review.comment, - review.rating, - isBeta - )!!) + _userReview.emit( + ReviewsHelper(authProvider.authData) + .using(HttpClient.getPreferredClient(context)) + .addOrEditReview( + packageName, + review.title, + review.comment, + review.rating, + isBeta + )!! + ) } catch (exception: Exception) { Log.e(TAG, "Failed to post review", exception) _userReview.emit(Review()) @@ -100,7 +114,7 @@ class AppDetailsViewModel @Inject constructor( } - fun fetchAppReport(context: Context,packageName: String) { + fun fetchAppReport(context: Context, packageName: String) { viewModelScope.launch(Dispatchers.IO) { try { val headers: MutableMap = mutableMapOf() @@ -143,10 +157,36 @@ class AppDetailsViewModel @Inject constructor( viewModelScope.launch { downloadWorkerUtil.cancelDownload(app.packageName) } } + private fun isFavourite(packageName: String) { + viewModelScope.launch(Dispatchers.IO) { + _favourite.value = favouriteDao.isFavourite(packageName) + } + } + + fun toggleFavourite(app: App) { + viewModelScope.launch(Dispatchers.IO) { + if (favourite.value) { + favouriteDao.delete(app.packageName) + } else { + favouriteDao.insert( + Favourite( + packageName = app.packageName, + displayName = app.displayName, + iconURL = app.iconArtwork.url, + mode = Favourite.Mode.MANUAL, + added = System.currentTimeMillis(), + ) + ) + } + + _favourite.value = !favourite.value + } + } + private fun parseResponse(response: String, packageName: String): List { try { val gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT, Modifier.STATIC) - .create() + .create() val jsonObject = JSONObject(response) val exodusObject = jsonObject.getJSONObject(packageName) diff --git a/app/src/main/res/drawable/ic_favorite_checked.xml b/app/src/main/res/drawable/ic_favorite_checked.xml new file mode 100644 index 000000000..15bc3b481 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_checked.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_favorite_unchecked.xml b/app/src/main/res/drawable/ic_favorite_unchecked.xml new file mode 100644 index 000000000..6ac55d5e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_favorite_unchecked.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_favourite.xml b/app/src/main/res/layout/fragment_favourite.xml new file mode 100644 index 000000000..349f82b9c --- /dev/null +++ b/app/src/main/res/layout/fragment_favourite.xml @@ -0,0 +1,45 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_fav.xml b/app/src/main/res/layout/view_fav.xml new file mode 100644 index 000000000..34fee2b1b --- /dev/null +++ b/app/src/main/res/layout/view_fav.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_details.xml b/app/src/main/res/menu/menu_details.xml index 16170e64c..4319b4837 100644 --- a/app/src/main/res/menu/menu_details.xml +++ b/app/src/main/res/menu/menu_details.xml @@ -19,6 +19,11 @@ + + "Disable" "Export" "Import" + "Favourite" "Filters" "All" "Apply" @@ -119,6 +120,7 @@ "More about app" "No ads" "No app match found" + "No favourite apps found" "No Dependencies" "No permissions" "No updates available" @@ -280,6 +282,7 @@ "Apps on sale" "Provider - bestappsales.com" "Blacklist manager" + "Favourite Apps" "Device" "Downloads" "Play Store"