From b8d682093b654ef2fa894f5f5494e99be60af7ea Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Tue, 29 Mar 2022 23:02:40 +0600 Subject: [PATCH 1/2] App Lounge: implemented UX for paidapps --- .../e/apps/api/fused/FusedAPIImpl.kt | 19 +++++++----- .../e/apps/api/fused/data/FusedApp.kt | 1 + .../e/apps/application/ApplicationFragment.kt | 30 ++++++++++++++++--- .../subFrags/ApplicationDialogFragment.kt | 27 ++++++++++++----- .../ApplicationListFragment.kt | 12 +++++++- .../model/ApplicationListRVAdapter.kt | 9 ++++-- .../foundation/e/apps/home/HomeFragment.kt | 12 +++++++- .../e/apps/home/model/HomeChildRVAdapter.kt | 12 ++++++-- .../e/apps/home/model/HomeParentRVAdapter.kt | 5 ++-- .../e/apps/search/SearchFragment.kt | 12 +++++++- .../e/apps/updates/UpdatesFragment.kt | 12 +++++++- app/src/main/res/values/strings.xml | 6 ++++ 12 files changed, 128 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt index 9adb16161..013f9933a 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt @@ -20,6 +20,7 @@ package foundation.e.apps.api.fused import android.content.Context import android.text.format.Formatter +import android.util.Log import com.aurora.gplayapi.SearchSuggestEntry import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.Artwork @@ -201,7 +202,8 @@ class FusedAPIImpl @Inject constructor( } return response?.apps } else { - return gPlayAPIRepository.listApps(browseUrl, authData).map { app -> + val listApps = gPlayAPIRepository.listApps(browseUrl, authData) + return listApps.map { app -> app.transformToFusedApp() } } @@ -495,7 +497,11 @@ class FusedAPIImpl @Inject constructor( } private suspend fun getGplaySearchResults(query: String, authData: AuthData): List { - return gPlayAPIRepository.getSearchResults(query, authData).map { app -> + val searchResults = gPlayAPIRepository.getSearchResults(query, authData) + searchResults.forEach { + Log.d(TAG, "getGplaySearchResults: ${it.displayName}: ${it.isFree} ${it.price}") + } + return searchResults.map { app -> app.transformToFusedApp() } } @@ -638,19 +644,18 @@ class FusedAPIImpl @Inject constructor( origin = Origin.GPLAY, shareUrl = this.shareUrl, appSize = Formatter.formatFileSize(context, this.size), - isFree = this.isFree + isFree = this.isFree, + price = this.price ) app.updateStatus() return app } private fun FusedApp.updateStatus() { - this.status = if (this.isFree) { - pkgManagerModule.getPackageStatus(this.package_name, this.latest_version_code) - } else if (this.status == Status.INSTALLATION_ISSUE) { + this.status = if (this.status == Status.INSTALLATION_ISSUE) { this.status } else { - Status.BLOCKED + pkgManagerModule.getPackageStatus(this.package_name, this.latest_version_code) } } diff --git a/app/src/main/java/foundation/e/apps/api/fused/data/FusedApp.kt b/app/src/main/java/foundation/e/apps/api/fused/data/FusedApp.kt index 1429dec05..c676eeed2 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/data/FusedApp.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/data/FusedApp.kt @@ -44,6 +44,7 @@ data class FusedApp( val shareUrl: String = String(), val appSize: String = String(), var source: String = String(), + val price: String = String(), val isFree: Boolean = true, val is_pwa: Boolean = false, val url: String = String(), diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index 61175a9a8..9cfe4eeec 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -261,7 +261,13 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { val fusedApp = applicationViewModel.fusedApp.value ?: FusedApp() when (status) { - Status.INSTALLED -> handleInstalled(installButton, view, fusedApp, downloadPB, appSize) + Status.INSTALLED -> handleInstalled( + installButton, + view, + fusedApp, + downloadPB, + appSize + ) Status.UPDATABLE -> handleUpdatable( installButton, view, @@ -270,7 +276,12 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { appSize ) Status.UNAVAILABLE -> handleUnavaiable(installButton, fusedApp, downloadPB, appSize) - Status.QUEUED, Status.AWAITING -> handleQueued(installButton, fusedApp, downloadPB, appSize) + Status.QUEUED, Status.AWAITING -> handleQueued( + installButton, + fusedApp, + downloadPB, + appSize + ) Status.DOWNLOADING -> handleDownloading( installButton, fusedApp, @@ -388,10 +399,21 @@ class ApplicationFragment : Fragment(R.layout.fragment_application) { appSize: MaterialTextView ) { installButton.apply { - text = getString(R.string.install) + text = if (fusedApp.isFree) getString(R.string.install) else fusedApp.price setOnClickListener { applicationIcon?.let { - mainActivityViewModel.getApplication(fusedApp, it) + if (fusedApp.isFree) { + mainActivityViewModel.getApplication(fusedApp, it) + } else { + ApplicationDialogFragment( + title = getString(R.string.dialog_title_paid_app, fusedApp.name), + message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + positiveButtonText = getString(R.string.dialog_confirm), + positiveButtonAction = { + }, + cancelButtonText = getString(R.string.dialog_cancel), + ).show(childFragmentManager, "ApplicationFragment") + } } } } diff --git a/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt b/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt index 202d3b8e6..c3e5a455a 100644 --- a/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt @@ -33,20 +33,33 @@ import foundation.e.apps.R @AndroidEntryPoint class ApplicationDialogFragment( - private val drawable: Int, + private val drawable: Int = -1, private val title: String, - private val message: String + private val message: String, + private val positiveButtonText: String = "", + private val positiveButtonAction: (() -> Unit)? = null, + private val cancelButtonText: String = "", + private val cancelButtonAction: (() -> Unit)? = null, ) : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(requireContext()) - .setIcon(drawable) - .setTitle(title) + val positiveButtonText = + positiveButtonText.ifEmpty { getString(R.string.ok) } + val materialAlertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) + .setTitle(Html.fromHtml(title, Html.FROM_HTML_MODE_COMPACT)) .setMessage(Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT)) - .setPositiveButton(getString(R.string.ok)) { _, _ -> + .setPositiveButton(positiveButtonText) { _, _ -> + positiveButtonAction?.invoke() this.dismiss() } - .create() + .setNegativeButton(cancelButtonText) { _, _ -> + cancelButtonAction?.invoke() + this.dismiss() + } + if (drawable != -1) { + materialAlertDialogBuilder.setIcon(drawable) + } + return materialAlertDialogBuilder.create() } override fun onResume() { diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt index aeb6fb05c..c7b02d519 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -38,6 +38,7 @@ import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.model.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentApplicationListBinding import foundation.e.apps.manager.download.data.DownloadProgress @@ -114,7 +115,16 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), viewLifecycleOwner - ) + ) { fusedApp -> + ApplicationDialogFragment( + title = getString(R.string.dialog_title_paid_app, fusedApp.name), + message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + positiveButtonText = getString(R.string.dialog_confirm), + positiveButtonAction = { + }, + cancelButtonText = getString(R.string.dialog_cancel), + ).show(childFragmentManager, "HomeFragment") + } } recyclerView.apply { diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt index 3179ce134..048d23f7f 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt @@ -57,6 +57,7 @@ class ApplicationListRVAdapter( private val pkgManagerModule: PkgManagerModule, private val user: User, private val lifecycleOwner: LifecycleOwner, + private val paidAppHandler: ((FusedApp) -> Unit)? = null ) : ListAdapter(ApplicationDiffUtil()) { private val TAG = ApplicationListRVAdapter::class.java.simpleName @@ -295,13 +296,17 @@ class ApplicationListRVAdapter( ) { installButton.apply { isEnabled = true - text = context.getString(R.string.install) + text = if (searchApp.isFree) context.getString(R.string.install) else searchApp.price setTextColor(context.getColor(R.color.colorAccent)) backgroundTintList = ContextCompat.getColorStateList(view.context, android.R.color.transparent) strokeColor = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { - installApplication(searchApp, appIcon) + if (searchApp.isFree) { + installApplication(searchApp, appIcon) + } else { + paidAppHandler?.invoke(searchApp) + } } } } diff --git a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt index 7773a4537..5c30d86c2 100644 --- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt @@ -34,6 +34,7 @@ import foundation.e.apps.MainActivityViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.FragmentHomeBinding import foundation.e.apps.home.model.HomeChildRVAdapter import foundation.e.apps.home.model.HomeParentRVAdapter @@ -84,7 +85,16 @@ class HomeFragment : Fragment(R.layout.fragment_home), FusedAPIInterface { pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), mainActivityViewModel, viewLifecycleOwner - ) + ) { fusedApp -> + ApplicationDialogFragment( + title = getString(R.string.dialog_title_paid_app, fusedApp.name), + message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + positiveButtonText = getString(R.string.dialog_confirm), + positiveButtonAction = { + }, + cancelButtonText = getString(R.string.dialog_cancel), + ).show(childFragmentManager, "HomeFragment") + } binding.parentRV.apply { adapter = homeParentRVAdapter diff --git a/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt index a877dcd14..0d4b74f32 100644 --- a/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt @@ -44,7 +44,8 @@ import foundation.e.apps.utils.enums.User class HomeChildRVAdapter( private val fusedAPIInterface: FusedAPIInterface, private val pkgManagerModule: PkgManagerModule, - private val user: User + private val user: User, + private val paidAppHandler: ((FusedApp) -> Unit)? = null ) : ListAdapter(HomeChildFusedAppDiffUtil()) { private val shimmer = Shimmer.ColorHighlightBuilder() @@ -124,7 +125,8 @@ class HomeChildRVAdapter( } Status.UNAVAILABLE -> { installButton.apply { - text = context.getString(R.string.install) + text = + if (homeApp.isFree) context.getString(R.string.install) else homeApp.price setTextColor(context.getColor(R.color.colorAccent)) backgroundTintList = ContextCompat.getColorStateList( view.context, @@ -133,7 +135,11 @@ class HomeChildRVAdapter( strokeColor = ContextCompat.getColorStateList(view.context, R.color.colorAccent) setOnClickListener { - installApplication(homeApp, appIcon) + if (homeApp.isFree) { + installApplication(homeApp, appIcon) + } else { + paidAppHandler?.invoke(homeApp) + } } } } diff --git a/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt b/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt index 30ca74d22..1595866c3 100644 --- a/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt @@ -38,7 +38,8 @@ class HomeParentRVAdapter( private val pkgManagerModule: PkgManagerModule, private val user: User, private val mainActivityViewModel: MainActivityViewModel, - private val lifecycleOwner: LifecycleOwner + private val lifecycleOwner: LifecycleOwner, + private val paidAppHandler: ((FusedApp) -> Unit)? = null ) : ListAdapter(FusedHomeDiffUtil()) { private val viewPool = RecyclerView.RecycledViewPool() @@ -55,7 +56,7 @@ class HomeParentRVAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val fusedHome = getItem(position) val homeChildRVAdapter = - HomeChildRVAdapter(fusedAPIInterface, pkgManagerModule, user) + HomeChildRVAdapter(fusedAPIInterface, pkgManagerModule, user, paidAppHandler) homeChildRVAdapter.setData(fusedHome.list) holder.binding.titleTV.text = fusedHome.title diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index 3bc4bb727..c5ea9d887 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -46,6 +46,7 @@ import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.model.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentSearchBinding import foundation.e.apps.manager.pkg.PkgManagerModule @@ -118,7 +119,16 @@ class SearchFragment : pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), viewLifecycleOwner - ) + ) { fusedApp -> + ApplicationDialogFragment( + title = getString(R.string.dialog_title_paid_app, fusedApp.name), + message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + positiveButtonText = getString(R.string.dialog_confirm), + positiveButtonAction = { + }, + cancelButtonText = getString(R.string.dialog_cancel), + ).show(childFragmentManager, "SearchFragment") + } } recyclerView?.apply { diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt index 4fb3b683f..f9b2962e9 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt @@ -35,6 +35,7 @@ import foundation.e.apps.PrivacyInfoViewModel import foundation.e.apps.R import foundation.e.apps.api.fused.FusedAPIInterface import foundation.e.apps.api.fused.data.FusedApp +import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.applicationlist.model.ApplicationListRVAdapter import foundation.e.apps.databinding.FragmentUpdatesBinding import foundation.e.apps.manager.download.data.DownloadProgress @@ -86,7 +87,16 @@ class UpdatesFragment : Fragment(R.layout.fragment_updates), FusedAPIInterface { pkgManagerModule, User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name), viewLifecycleOwner, - ) + ) { fusedApp -> + ApplicationDialogFragment( + title = getString(R.string.dialog_title_paid_app, fusedApp.name), + message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + positiveButtonText = getString(R.string.dialog_confirm), + positiveButtonAction = { + }, + cancelButtonText = getString(R.string.dialog_cancel), + ).show(childFragmentManager, "UpdatesFragment") + } } recyclerView.apply { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2a657ddb..751f3ac38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -149,4 +149,10 @@ Can\'t connect! Please check your internet connection and try again + + %1$s]]> + When clicked confirm, it will take you to Google Play page for the app to complete the purchase using your browser. Click confirm to purchase %1$s for %2$s. + CONFIRM + CANCEL + -- GitLab From 7290bac7e1e1b68883442997859176a3df8020e3 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 1 Apr 2022 13:07:40 +0600 Subject: [PATCH 2/2] App Lounge: Puchasing and Installing for paid apps done showing message,purchasing app anonymously paid apps update ignored for anonymous user --- .../java/foundation/e/apps/MainActivity.kt | 61 +++++++++--- .../e/apps/MainActivityViewModel.kt | 83 ++++++++++++++++- .../subFrags/ApplicationDialogFragment.kt | 4 +- .../ApplicationListFragment.kt | 6 +- .../foundation/e/apps/home/HomeFragment.kt | 1 + .../database/fusedDownload/FusedDownload.kt | 7 +- .../e/apps/manager/fused/FusedManagerImpl.kt | 10 ++ .../manager/fused/FusedManagerRepository.kt | 8 ++ .../manager/workmanager/InstallAppWorker.kt | 25 +++-- .../e/apps/purchase/AppPurchaseFragment.kt | 93 +++++++++++++++++++ .../e/apps/search/SearchFragment.kt | 16 +++- .../e/apps/updates/UpdatesViewModel.kt | 4 +- .../e/apps/updates/manager/UpdatesWorker.kt | 20 +++- .../foundation/e/apps/utils/enums/Status.kt | 3 +- .../main/res/layout/fragment_app_purchase.xml | 13 +++ .../res/navigation/navigation_resource.xml | 15 +++ app/src/main/res/values/strings.xml | 3 +- 17 files changed, 331 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/purchase/AppPurchaseFragment.kt create mode 100644 app/src/main/res/layout/fragment_app_purchase.xml diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index 35ac779e3..cc8e20ef7 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -26,13 +26,16 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.NavOptions +import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.aurora.gplayapi.exceptions.ApiException import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import foundation.e.apps.application.subFrags.ApplicationDialogFragment import foundation.e.apps.databinding.ActivityMainBinding import foundation.e.apps.manager.workmanager.InstallWorkManager +import foundation.e.apps.purchase.AppPurchaseFragmentDirections import foundation.e.apps.setup.signin.SignInViewModel import foundation.e.apps.updates.UpdatesNotifier import foundation.e.apps.utils.enums.Status @@ -156,17 +159,6 @@ class MainActivity : AppCompatActivity() { // Observe and handle downloads viewModel.downloadList.observe(this) { list -> -// val shouldDownload = list.any { -// it.status == Status.INSTALLING || it.status == Status.DOWNLOADING || it.status == Status.INSTALLED -// } -// if (!shouldDownload && list.isNotEmpty()) { -// for (item in list) { -// if (item.status == Status.QUEUED) { -// viewModel.downloadApp(item) -// break -// } -// } -// } list.forEach { if (it.status == Status.QUEUED) { lifecycleScope.launch { @@ -178,10 +170,41 @@ class MainActivity : AppCompatActivity() { } } + viewModel.purchaseAppLiveData.observe(this) { + val action = + AppPurchaseFragmentDirections.actionGlobalAppPurchaseFragment(it.package_name) + findNavController(R.id.fragment).navigate(action) + } + viewModel.errorMessage.observe(this) { when (it) { is ApiException.AppNotPurchased -> showSnackbarMessage(getString(R.string.message_app_available_later)) - else -> showSnackbarMessage(it.localizedMessage ?: getString(R.string.unknown_error)) + else -> showSnackbarMessage( + it.localizedMessage ?: getString(R.string.unknown_error) + ) + } + } + + viewModel.errorMessageStringResource.observe(this) { + showSnackbarMessage(getString(it)) + } + + viewModel.isAppPurchased.observe(this) { + if (it.isNotEmpty()) { + startInstallationOfPurchasedApp(viewModel, it) + ApplicationDialogFragment( + title = "Purchase complete!", + message = "Your app will automatically be downloaded in this device", + positiveButtonText = "OK" + ).show(supportFragmentManager, TAG) + } + } + + viewModel.purchaseDeclined.observe(this) { + if (it.isNotEmpty()) { + lifecycleScope.launch { + viewModel.updateUnavailableForPurchaseDeclined(it) + } } } @@ -190,6 +213,20 @@ class MainActivity : AppCompatActivity() { } } + private fun startInstallationOfPurchasedApp( + viewModel: MainActivityViewModel, + it: String + ) { + lifecycleScope.launch { + val fusedDownload = viewModel.updateAwaitingForPurchasedApp(it) + if (fusedDownload != null) { + InstallWorkManager.enqueueWork(applicationContext, fusedDownload) + } else { + showSnackbarMessage(getString(R.string.paid_app_anonymous_message)) + } + } + } + private fun showSnackbarMessage(message: String) { Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() } diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt index 1b0fe29ed..cb4561cc6 100644 --- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt @@ -21,6 +21,7 @@ package foundation.e.apps import android.graphics.Bitmap import android.os.Build import android.util.Base64 +import android.util.Log import android.widget.ImageView import androidx.annotation.RequiresApi import androidx.core.graphics.drawable.toBitmap @@ -31,12 +32,14 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.liveData import androidx.lifecycle.viewModelScope import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.exceptions.ApiException import com.google.gson.Gson import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.database.fusedDownload.FusedDownload import foundation.e.apps.manager.fused.FusedManagerRepository +import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.Type import foundation.e.apps.utils.modules.DataStoreModule @@ -61,6 +64,10 @@ class MainActivityViewModel @Inject constructor( private var _authData: MutableLiveData = MutableLiveData() val authData: LiveData = _authData val authValidity: MutableLiveData = MutableLiveData() + private val _purchaseAppLiveData: MutableLiveData = MutableLiveData() + val purchaseAppLiveData: LiveData = _purchaseAppLiveData + val isAppPurchased: MutableLiveData = MutableLiveData() + val purchaseDeclined: MutableLiveData = MutableLiveData() var authRequestRunning = false // Downloads @@ -68,10 +75,17 @@ class MainActivityViewModel @Inject constructor( var installInProgress = false private val _errorMessage = MutableLiveData() val errorMessage: LiveData = _errorMessage + + private val _errorMessageStringResource = MutableLiveData() + val errorMessageStringResource: LiveData = _errorMessageStringResource /* * Authentication related functions */ + companion object { + private const val TAG = "MainActivityViewModel" + } + fun getAuthData() { if (!authRequestRunning) { authRequestRunning = true @@ -124,6 +138,10 @@ class MainActivityViewModel @Inject constructor( } fun getApplication(app: FusedApp, imageView: ImageView?) { + if (!app.isFree && authData.value?.isAnonymous == true) { + _errorMessageStringResource.value = R.string.paid_app_anonymous_message + return + } viewModelScope.launch { val fusedDownload: FusedDownload try { @@ -141,9 +159,16 @@ class MainActivityViewModel @Inject constructor( mutableMapOf(), app.status, app.type, - appIcon + appIcon, + app.latest_version_code, + app.offer_type, + app.isFree ) } catch (e: Exception) { + if (e is ApiException.AppNotPurchased) { + handleAppNotPurchased(imageView, app) + return@launch + } _errorMessage.value = e return@launch } @@ -155,10 +180,66 @@ class MainActivityViewModel @Inject constructor( } } + private fun handleAppNotPurchased( + imageView: ImageView?, + app: FusedApp + ) { + val appIcon = imageView?.let { getImageBase64(it) } ?: "" + val fusedDownload = FusedDownload( + app._id, + app.origin, + Status.PURCHASE_NEEDED, + app.name, + app.package_name, + mutableListOf(), + mutableMapOf(), + app.status, + app.type, + appIcon, + app.latest_version_code, + app.offer_type, + app.isFree + ) + viewModelScope.launch { + fusedManagerRepository.addFusedDownloadPurchaseNeeded(fusedDownload) + _purchaseAppLiveData.postValue(fusedDownload) + } + } + suspend fun updateAwaiting(fusedDownload: FusedDownload) { fusedManagerRepository.updateAwaiting(fusedDownload) } + suspend fun updateAwaitingForPurchasedApp(packageName: String): FusedDownload? { + val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) + authData.value?.let { + if (!it.isAnonymous) { + try { + fusedDownload.downloadURLList = fusedAPIRepository.getDownloadLink( + fusedDownload.id, + fusedDownload.package_name, + fusedDownload.versionCode, + fusedDownload.offerType, + it, + Origin.GPLAY + ).toMutableList() + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + _errorMessage.value = e + return null + } + updateAwaiting(fusedDownload) + return fusedDownload + } + } + return null + } + + suspend fun updateUnavailableForPurchaseDeclined(packageName: String) { + val fusedDownload = fusedManagerRepository.getFusedDownload(packageName = packageName) + fusedManagerRepository.updateUnavailable(fusedDownload) + } + fun cancelDownload(app: FusedApp) { viewModelScope.launch { val fusedDownload = diff --git a/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt b/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt index c3e5a455a..545f3983e 100644 --- a/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/subFrags/ApplicationDialogFragment.kt @@ -52,10 +52,12 @@ class ApplicationDialogFragment( positiveButtonAction?.invoke() this.dismiss() } - .setNegativeButton(cancelButtonText) { _, _ -> + if (cancelButtonText.isNotEmpty()) { + materialAlertDialogBuilder.setNegativeButton(cancelButtonText) { _, _ -> cancelButtonAction?.invoke() this.dismiss() } + } if (drawable != -1) { materialAlertDialogBuilder.setIcon(drawable) } diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt index c7b02d519..f3d8ed137 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -19,7 +19,6 @@ package foundation.e.apps.applicationlist import android.os.Bundle -import android.util.Log import android.view.View import android.widget.ImageView import androidx.fragment.app.Fragment @@ -121,6 +120,7 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), positiveButtonText = getString(R.string.dialog_confirm), positiveButtonAction = { + getApplication(fusedApp) }, cancelButtonText = getString(R.string.dialog_cancel), ).show(childFragmentManager, "HomeFragment") @@ -171,10 +171,6 @@ class ApplicationListFragment : Fragment(R.layout.fragment_application_list), Fu val progress = appProgressViewModel.calculateProgress(fusedApp, it) val downloadProgress = ((progress.second / progress.first.toDouble()) * 100).toInt() - Log.d( - "HomeParentAdapter", - "download progress of ===> ${fusedApp.name} : $downloadProgress" - ) val viewHolder = recyclerView.findViewHolderForAdapterPosition( adapter.currentList.indexOf(fusedApp) ) diff --git a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt index 5c30d86c2..f54faf6d0 100644 --- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt @@ -91,6 +91,7 @@ class HomeFragment : Fragment(R.layout.fragment_home), FusedAPIInterface { message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), positiveButtonText = getString(R.string.dialog_confirm), positiveButtonAction = { + getApplication(fusedApp) }, cancelButtonText = getString(R.string.dialog_cancel), ).show(childFragmentManager, "HomeFragment") diff --git a/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt b/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt index dcf5d065b..7b9c2be82 100644 --- a/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt +++ b/app/src/main/java/foundation/e/apps/manager/database/fusedDownload/FusedDownload.kt @@ -13,9 +13,12 @@ data class FusedDownload( var status: Status = Status.UNAVAILABLE, val name: String = String(), val package_name: String = String(), - val downloadURLList: MutableList = mutableListOf(), + var downloadURLList: MutableList = mutableListOf(), var downloadIdMap: MutableMap = mutableMapOf(), val orgStatus: Status = Status.UNAVAILABLE, val type: Type = Type.NATIVE, - val iconByteArray: String = String() + val iconByteArray: String = String(), + val versionCode: Int = 1, + val offerType: Int = -1, + val isFree: Boolean = true ) diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt index 3e9b76d56..42081827b 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerImpl.kt @@ -191,7 +191,17 @@ class FusedManagerImpl @Inject constructor( databaseRepository.updateDownload(fusedDownload) } + suspend fun updateUnavailable(fusedDownload: FusedDownload) { + fusedDownload.status = Status.UNAVAILABLE + databaseRepository.updateDownload(fusedDownload) + } + suspend fun updateFusedDownload(fusedDownload: FusedDownload) { databaseRepository.updateDownload(fusedDownload) } + + suspend fun insertFusedDownloadPurchaseNeeded(fusedDownload: FusedDownload) { + fusedDownload.status = Status.PURCHASE_NEEDED + databaseRepository.addDownload(fusedDownload) + } } diff --git a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt index bfcd44e67..ab138ef8a 100644 --- a/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/manager/fused/FusedManagerRepository.kt @@ -28,6 +28,10 @@ class FusedManagerRepository @Inject constructor( return fusedManagerImpl.addDownload(fusedDownload) } + suspend fun addFusedDownloadPurchaseNeeded(fusedDownload: FusedDownload) { + fusedManagerImpl.insertFusedDownloadPurchaseNeeded(fusedDownload) + } + suspend fun clearInstallationIssue(fusedDownload: FusedDownload) { return fusedManagerImpl.clearInstallationIssue(fusedDownload) } @@ -68,6 +72,10 @@ class FusedManagerRepository @Inject constructor( fusedManagerImpl.updateAwaiting(fusedDownload) } + suspend fun updateUnavailable(fusedDownload: FusedDownload) { + fusedManagerImpl.updateUnavailable(fusedDownload) + } + suspend fun updateFusedDownload(fusedDownload: FusedDownload) { fusedManagerImpl.updateFusedDownload(fusedDownload) } diff --git a/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt b/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt index 4cd6544a0..f09f19038 100644 --- a/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt +++ b/app/src/main/java/foundation/e/apps/manager/workmanager/InstallAppWorker.kt @@ -40,18 +40,23 @@ class InstallAppWorker @AssistedInject constructor( } override suspend fun doWork(): Result { - val fusedDownloadString = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: "" - Log.d(TAG, "Fused download name $fusedDownloadString") - - val fusedDownload = databaseRepository.getDownloadById(fusedDownloadString) - fusedDownload?.let { - if (fusedDownload.status != Status.AWAITING) { - return@let + try { + val fusedDownloadString = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: "" + Log.d(TAG, "Fused download name $fusedDownloadString") + + val fusedDownload = databaseRepository.getDownloadById(fusedDownloadString) + fusedDownload?.let { + if (fusedDownload.status != Status.AWAITING) { + return@let + } + startAppInstallationProcess(it) } - startAppInstallationProcess(it) - } - return Result.success() + return Result.success() + } catch (e: Exception) { + Log.e(TAG, "doWork: Failed: ${e.stackTraceToString()}") + return Result.failure() + } } private suspend fun startAppInstallationProcess( diff --git a/app/src/main/java/foundation/e/apps/purchase/AppPurchaseFragment.kt b/app/src/main/java/foundation/e/apps/purchase/AppPurchaseFragment.kt new file mode 100644 index 000000000..b24939084 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/purchase/AppPurchaseFragment.kt @@ -0,0 +1,93 @@ +package foundation.e.apps.purchase + +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.webkit.CookieManager +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import foundation.e.apps.MainActivityViewModel +import foundation.e.apps.databinding.FragmentAppPurchaseBinding + +/** + * A simple [Fragment] subclass. + * Use the [AppPurchaseFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class AppPurchaseFragment : Fragment() { + private lateinit var binding: FragmentAppPurchaseBinding + + companion object { + private const val TAG = "AppPurchaseFragment" + } + + private val mainActivityViewModel: MainActivityViewModel by activityViewModels() + + private var isAppPurchased = false + private var packageName = "" + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // Inflate the layout for this fragment + binding = FragmentAppPurchaseBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + packageName = arguments?.getString("package_name") ?: "" + val url = "https://play.google.com/store/apps/details?id=$packageName" + setupWebView(url) + } + + private fun setupWebView(url: String) { + val cookieManager = CookieManager.getInstance() + cookieManager.removeAllCookies(null) + cookieManager.acceptThirdPartyCookies(binding.playStoreWebView) + cookieManager.setAcceptThirdPartyCookies(binding.playStoreWebView, true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + binding.playStoreWebView.settings.safeBrowsingEnabled = false + } + + binding.playStoreWebView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + Log.d(TAG, "onPageFinished: $url") + if (url.contains("https://play.google.com/store/apps/details") && url.contains("raii") && + url.contains("raboi") && url.contains("rasi") && url.contains("rapt") + ) { + isAppPurchased = true + } + } + } + + binding.playStoreWebView.apply { + settings.apply { + allowContentAccess = true + databaseEnabled = true + domStorageEnabled = true + javaScriptEnabled = true + cacheMode = WebSettings.LOAD_DEFAULT + } + loadUrl(url) + } + } + + override fun onDestroyView() { + if (isAppPurchased) { + mainActivityViewModel.isAppPurchased.value = packageName + } else { + mainActivityViewModel.purchaseDeclined.value = packageName + } + super.onDestroyView() + } +} diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt index c5ea9d887..589cd6c37 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -22,7 +22,6 @@ import android.app.Activity import android.database.MatrixCursor import android.os.Bundle import android.provider.BaseColumns -import android.util.Log import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.ImageView @@ -122,9 +121,14 @@ class SearchFragment : ) { fusedApp -> ApplicationDialogFragment( title = getString(R.string.dialog_title_paid_app, fusedApp.name), - message = getString(R.string.dialog_paidapp_message, fusedApp.name, fusedApp.price), + message = getString( + R.string.dialog_paidapp_message, + fusedApp.name, + fusedApp.price + ), positiveButtonText = getString(R.string.dialog_confirm), positiveButtonAction = { + getApplication(fusedApp) }, cancelButtonText = getString(R.string.dialog_cancel), ).show(childFragmentManager, "SearchFragment") @@ -144,10 +148,12 @@ class SearchFragment : val progress = appProgressViewModel.calculateProgress(fusedApp, it) val downloadProgress = ((progress.second / progress.first.toDouble()) * 100).toInt() - Log.d("HomeParentAdapter", "download progress of ===> ${fusedApp.name} : $downloadProgress") - val viewHolder = recyclerView?.findViewHolderForAdapterPosition(adapter.currentList.indexOf(fusedApp)) + val viewHolder = recyclerView?.findViewHolderForAdapterPosition( + adapter.currentList.indexOf(fusedApp) + ) viewHolder?.let { - (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = "$downloadProgress%" + (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = + "$downloadProgress%" } } } diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt index a00198c23..50500fa55 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt @@ -37,7 +37,9 @@ class UpdatesViewModel @Inject constructor( fun getUpdates(authData: AuthData) { viewModelScope.launch { - updatesList.postValue(updatesManagerRepository.getUpdates(authData)) + updatesList.postValue( + updatesManagerRepository.getUpdates(authData) + .filter { !(!it.isFree && authData.isAnonymous) }) } } } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt index f1a6d510d..42cece105 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt @@ -14,6 +14,7 @@ import androidx.preference.PreferenceManager import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.aurora.gplayapi.data.models.AuthData +import com.aurora.gplayapi.helpers.PurchaseHelper import com.google.gson.Gson import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -59,6 +60,7 @@ class UpdatesWorker @AssistedInject constructor( loadSettings() val authData = getAuthData() val appsNeededToUpdate = updatesManagerRepository.getUpdates(authData) + .filter { !(!it.isFree && authData.isAnonymous) } val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) handleNotification(appsNeededToUpdate, isConnectedToUnmeteredNetwork) triggerUpdateProcessOnSettings( @@ -109,6 +111,14 @@ class UpdatesWorker @AssistedInject constructor( authData: AuthData ) { appsNeededToUpdate.forEach { fusedApp -> + if (!fusedApp.isFree) { + val purchaseHelper = PurchaseHelper(authData) + purchaseHelper.purchase( + fusedApp.package_name, + fusedApp.latest_version_code, + fusedApp.offer_type + ) + } val downloadList = getAppDownloadLink(fusedApp, authData).toMutableList() val iconBase64 = getIconImageToBase64(fusedApp) @@ -122,11 +132,17 @@ class UpdatesWorker @AssistedInject constructor( mutableMapOf(), fusedApp.status, fusedApp.type, - iconBase64 + iconBase64, + fusedApp.latest_version_code, + fusedApp.offer_type, + fusedApp.isFree ) fusedManagerRepository.addDownload(fusedDownload) fusedManagerRepository.updateAwaiting(fusedDownload) - Log.d(TAG, "startUpdateProcess: Enqueued for update: ${fusedDownload.name} ${fusedDownload.id} ${fusedDownload.status}") + Log.d( + TAG, + "startUpdateProcess: Enqueued for update: ${fusedDownload.name} ${fusedDownload.id} ${fusedDownload.status}" + ) InstallWorkManager.enqueueWork(context, fusedDownload) } } diff --git a/app/src/main/java/foundation/e/apps/utils/enums/Status.kt b/app/src/main/java/foundation/e/apps/utils/enums/Status.kt index 4bee474d2..2b2eaad97 100644 --- a/app/src/main/java/foundation/e/apps/utils/enums/Status.kt +++ b/app/src/main/java/foundation/e/apps/utils/enums/Status.kt @@ -28,5 +28,6 @@ enum class Status { QUEUED, BLOCKED, INSTALLATION_ISSUE, - AWAITING + AWAITING, + PURCHASE_NEEDED } diff --git a/app/src/main/res/layout/fragment_app_purchase.xml b/app/src/main/res/layout/fragment_app_purchase.xml new file mode 100644 index 000000000..239aa1841 --- /dev/null +++ b/app/src/main/res/layout/fragment_app_purchase.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/navigation_resource.xml b/app/src/main/res/navigation/navigation_resource.xml index a7273e709..72834907f 100644 --- a/app/src/main/res/navigation/navigation_resource.xml +++ b/app/src/main/res/navigation/navigation_resource.xml @@ -207,4 +207,19 @@ app:popUpTo="@id/googleSignInFragment" app:popUpToInclusive="true" /> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 751f3ac38..1377f50a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -149,10 +149,11 @@ Can\'t connect! Please check your internet connection and try again - + %1$s]]> When clicked confirm, it will take you to Google Play page for the app to complete the purchase using your browser. Click confirm to purchase %1$s for %2$s. CONFIRM CANCEL + Paid apps cannot be installed in anonymous mode. Please log into your Google account to install paid apps. -- GitLab