diff --git a/app/build.gradle b/app/build.gradle
index 694c3c9d6d0d749671ee6d70f51f4c060c0750bc..aba6722a948956bd4396904056aad1e45951b8f4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,6 +5,7 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint' version '10.2.0'
id 'androidx.navigation.safeargs.kotlin'
id 'dagger.hilt.android.plugin'
+ id 'kotlin-allopen'
}
def versionMajor = 2
@@ -92,6 +93,11 @@ kapt {
correctErrorTypes true
}
+allOpen {
+ // allows mocking for classes w/o directly opening them for release builds
+ annotation 'foundation.e.apps.OpenClass'
+}
+
dependencies {
api "com.gitlab.AuroraOSS:gplayapi:0e224071f3"
@@ -109,6 +115,15 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ // Optional -- Robolectric environment
+ testImplementation "androidx.test:core:1.4.0"
+ // Optional -- Mockito framework
+ testImplementation "org.mockito:mockito-core:4.6.1"
+ // Optional -- mockito-kotlin
+ testImplementation "org.mockito.kotlin:mockito-kotlin:3.2.0"
+ testImplementation 'org.mockito:mockito-inline:2.13.0'
+
+
// Coil and PhotoView
implementation "io.coil-kt:coil:1.4.0"
@@ -161,6 +176,7 @@ dependencies {
def coroutines_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
+ testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
// Room
def roomVersion = "2.4.1"
diff --git a/app/src/debug/java/foundation/e/apps/OpenForTesting.kt b/app/src/debug/java/foundation/e/apps/OpenForTesting.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d7facea95e6df64636ff04a462afddc234cb0494
--- /dev/null
+++ b/app/src/debug/java/foundation/e/apps/OpenForTesting.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 ECORP
+ *
+ * This program 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package foundation.e.apps
+
+/**
+ * This annotation allows us to open some classes for mocking purposes while they are final in
+ * release builds.
+ */
+@Target(AnnotationTarget.ANNOTATION_CLASS)
+annotation class OpenClass
+
+/**
+ * Annotate a class with [OpenForTesting] if you want it to be extendable in debug builds.
+ */
+@OpenClass
+@Target(AnnotationTarget.CLASS)
+annotation class OpenForTesting
diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt
index 00e86427e9979835fa24746592c5b8e9c5c95583..875997773f127f10c1e9516f0bac87947ffe37e2 100644
--- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt
+++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt
@@ -19,6 +19,7 @@
package foundation.e.apps
import android.content.Context
+import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.SystemClock
@@ -56,6 +57,7 @@ import foundation.e.apps.utils.enums.isUnFiltered
import foundation.e.apps.utils.modules.CommonUtilsModule.NETWORK_CODE_SUCCESS
import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis
import foundation.e.apps.utils.modules.DataStoreModule
+import foundation.e.apps.utils.modules.PWAManagerModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -71,6 +73,7 @@ class MainActivityViewModel @Inject constructor(
private val fusedAPIRepository: FusedAPIRepository,
private val fusedManagerRepository: FusedManagerRepository,
private val pkgManagerModule: PkgManagerModule,
+ private val pwaManagerModule: PWAManagerModule,
private val ecloudRepository: EcloudRepository,
private val blockedAppRepository: BlockedAppRepository,
private val aC2DMTask: AC2DMTask,
@@ -122,6 +125,10 @@ class MainActivityViewModel @Inject constructor(
private var isGoogleLoginRunning = false
}
+ fun getUser(): User {
+ return User.valueOf(userType.value ?: User.UNAVAILABLE.name)
+ }
+
private fun setFirstTokenFetchTime() {
if (firstAuthDataFetchTime == 0L) {
firstAuthDataFetchTime = SystemClock.uptimeMillis()
@@ -631,4 +638,12 @@ class MainActivityViewModel @Inject constructor(
fun getAppNameByPackageName(packageName: String): String {
return pkgManagerModule.getAppNameFromPackageName(packageName)
}
+
+ fun getLaunchIntentForPackageName(packageName: String): Intent? {
+ return pkgManagerModule.getLaunchIntent(packageName)
+ }
+
+ fun launchPwa(fusedApp: FusedApp) {
+ pwaManagerModule.launchPwa(fusedApp)
+ }
}
diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt
index 9bc56e6fe410b1c407b8842027438073adf28ae0..45c47a58dca7c91ba0385fa0215ea1b55e563b1d 100644
--- a/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt
+++ b/app/src/main/java/foundation/e/apps/api/cleanapk/CleanAPKRepository.kt
@@ -18,6 +18,7 @@
package foundation.e.apps.api.cleanapk
+import foundation.e.apps.OpenForTesting
import foundation.e.apps.api.cleanapk.data.app.Application
import foundation.e.apps.api.cleanapk.data.categories.Categories
import foundation.e.apps.api.cleanapk.data.download.Download
@@ -26,6 +27,7 @@ import foundation.e.apps.api.cleanapk.data.search.Search
import retrofit2.Response
import javax.inject.Inject
+@OpenForTesting
class CleanAPKRepository @Inject constructor(
private val cleanAPKInterface: CleanAPKInterface,
private val cleanApkAppDetailApi: CleanApkAppDetailApi
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 233ae8f1f98601993eb980ba3f9eb4939bf00928..226c26de80d7952d2cd7a72775306a9421a13951 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
@@ -47,6 +47,7 @@ import foundation.e.apps.api.fused.data.FusedHome
import foundation.e.apps.api.fused.data.Ratings
import foundation.e.apps.api.fused.utils.CategoryUtils
import foundation.e.apps.api.gplay.GPlayAPIRepository
+import foundation.e.apps.home.model.HomeChildFusedAppDiffUtil
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.AppTag
@@ -1260,4 +1261,74 @@ class FusedAPIImpl @Inject constructor(
}
return list
}
+
+ /**
+ * @return true, if any change is found, otherwise false
+ */
+ fun isHomeDataUpdated(
+ newHomeData: List,
+ oldHomeData: List
+ ): Boolean {
+ if (newHomeData.size != oldHomeData.size) {
+ return true
+ }
+
+ oldHomeData.forEach {
+ val fusedHome = newHomeData[oldHomeData.indexOf(it)]
+ if (!it.title.contentEquals(fusedHome.title) || !areFusedAppsUpdated(it, fusedHome)) {
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun areFusedAppsUpdated(
+ oldFusedHome: FusedHome,
+ newFusedHome: FusedHome,
+ ): Boolean {
+ val fusedAppDiffUtil = HomeChildFusedAppDiffUtil()
+
+ oldFusedHome.list.forEach { oldFusedApp ->
+ val indexOfOldFusedApp = oldFusedHome.list.indexOf(oldFusedApp)
+ val fusedApp = newFusedHome.list[indexOfOldFusedApp]
+ if (!fusedAppDiffUtil.areContentsTheSame(oldFusedApp, fusedApp)) {
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * @return returns true if there is changes in data, otherwise false
+ */
+ fun isAnyFusedAppUpdated(
+ newFusedApps: List,
+ oldFusedApps: List
+ ): Boolean {
+ val fusedAppDiffUtil = HomeChildFusedAppDiffUtil()
+ if (newFusedApps.size != oldFusedApps.size) {
+ return true
+ }
+
+ newFusedApps.forEach {
+ val indexOfNewFusedApp = newFusedApps.indexOf(it)
+ if (!fusedAppDiffUtil.areContentsTheSame(it, oldFusedApps[indexOfNewFusedApp])) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fun isAnyAppInstallStatusChanged(currentList: List): Boolean {
+ currentList.forEach {
+ if (it.status == Status.INSTALLATION_ISSUE) {
+ return@forEach
+ }
+ val currentAppStatus = pkgManagerModule.getPackageStatus(it.package_name, it.latest_version_code)
+ if (it.status != currentAppStatus) {
+ return true
+ }
+ }
+ return false
+ }
}
diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt
index e3bdedcd21b87b9da99fc8a6d18f86277b47a784..6502866f94e414ec3b295fc4fb8e6632f8e20dc7 100644
--- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt
+++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt
@@ -124,7 +124,10 @@ class FusedAPIRepository @Inject constructor(
return fusedAPIImpl.fetchAuthData(email, aasToken)
}
- fun getSearchResults(query: String, authData: AuthData): LiveData, Boolean>>> {
+ fun getSearchResults(
+ query: String,
+ authData: AuthData
+ ): LiveData, Boolean>>> {
return fusedAPIImpl.getSearchResults(query, authData)
}
@@ -167,4 +170,17 @@ class FusedAPIRepository @Inject constructor(
fun getFusedAppInstallationStatus(fusedApp: FusedApp): Status {
return fusedAPIImpl.getFusedAppInstallationStatus(fusedApp)
}
+
+ fun isHomeDataUpdated(
+ newHomeData: List,
+ oldHomeData: List
+ ) = fusedAPIImpl.isHomeDataUpdated(newHomeData, oldHomeData)
+
+ fun isAnyFusedAppUpdated(
+ newFusedApps: List,
+ oldFusedApps: List
+ ) = fusedAPIImpl.isAnyFusedAppUpdated(newFusedApps, oldFusedApps)
+
+ fun isAnyAppInstallStatusChanged(currentList: List) =
+ fusedAPIImpl.isAnyAppInstallStatusChanged(currentList)
}
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 cb02d69392562f8befce4e5a21c42eb7ce1ade77..eed527c4cb633d7374eb5c018b4bd42f36b1611f 100644
--- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt
+++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt
@@ -36,6 +36,7 @@ import foundation.e.apps.AppProgressViewModel
import foundation.e.apps.MainActivityViewModel
import foundation.e.apps.PrivacyInfoViewModel
import foundation.e.apps.R
+import foundation.e.apps.api.ResultSupreme
import foundation.e.apps.api.fused.FusedAPIInterface
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.application.subFrags.ApplicationDialogFragment
@@ -44,14 +45,15 @@ import foundation.e.apps.databinding.FragmentApplicationListBinding
import foundation.e.apps.manager.download.data.DownloadProgress
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Status
-import foundation.e.apps.utils.enums.User
import foundation.e.apps.utils.modules.PWAManagerModule
import foundation.e.apps.utils.parentFragment.TimeoutFragment
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
-class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_list), FusedAPIInterface {
+class ApplicationListFragment :
+ TimeoutFragment(R.layout.fragment_application_list),
+ FusedAPIInterface {
private val args: ApplicationListFragmentArgs by navArgs()
@@ -70,15 +72,7 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li
private var _binding: FragmentApplicationListBinding? = null
private val binding get() = _binding!!
- /*
- * Prevent reloading apps.
- * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/478
- */
- private var isDetailsLoaded = false
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- }
+ private lateinit var listAdapter: ApplicationListRVAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -90,6 +84,32 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li
view.findNavController().navigate(R.id.categoriesFragment)
}
}
+
+ setupRecyclerView(view)
+ observeAppListLiveData()
+
+ /*
+ * Explanation of double observers in HomeFragment.kt
+ */
+
+ mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) {
+ refreshDataOrRefreshToken(mainActivityViewModel)
+ }
+ mainActivityViewModel.authData.observe(viewLifecycleOwner) {
+ refreshDataOrRefreshToken(mainActivityViewModel)
+ }
+ }
+
+ private fun setupRecyclerView(view: View) {
+ val recyclerView = initRecyclerView()
+ findNavController().currentDestination?.id?.let {
+ listAdapter = initAppListAdapter(it)
+ }
+
+ recyclerView.apply {
+ adapter = listAdapter
+ layoutManager = LinearLayoutManager(view?.context)
+ }
}
private fun observeDownloadList(adapter: ApplicationListRVAdapter) {
@@ -115,75 +135,99 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li
override fun onResume() {
super.onResume()
- binding.shimmerLayout.startShimmer()
- val recyclerView = binding.recyclerView
- recyclerView.recycledViewPool.setMaxRecycledViews(0, 0)
- val listAdapter =
- findNavController().currentDestination?.id?.let {
- ApplicationListRVAdapter(
- this,
- privacyInfoViewModel,
- appInfoFetchViewModel,
- mainActivityViewModel,
- it,
- pkgManagerModule,
- pwaManagerModule,
- User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
- viewLifecycleOwner
- ) { fusedApp ->
- if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(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 = {
- getApplication(fusedApp)
- },
- cancelButtonText = getString(R.string.dialog_cancel),
- ).show(childFragmentManager, "HomeFragment")
- }
- }
+ if (listAdapter.currentList.isNotEmpty() && viewModel.hasAnyAppInstallStatusChanged(listAdapter.currentList)) {
+ mainActivityViewModel.authData.value?.let {
+ refreshData(it)
}
-
- recyclerView.apply {
- adapter = listAdapter
- layoutManager = LinearLayoutManager(view?.context)
}
+ }
+ private fun observeAppListLiveData() {
viewModel.appListLiveData.observe(viewLifecycleOwner) {
+ stopLoadingUI()
if (!it.isSuccess()) {
onTimeout()
} else {
- isDetailsLoaded = true
- listAdapter?.setData(it.data!!)
- listAdapter?.let { adapter ->
- observeDownloadList(adapter)
+ if (!isFusedAppsUpdated(it)) {
+ return@observe
}
-
+ updateAppListRecyclerView(listAdapter, it)
appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) {
updateProgressOfDownloadingItems(binding.recyclerView, it)
}
}
- stopLoadingUI()
}
+ }
- /*
- * Explanation of double observers in HomeFragment.kt
- */
+ private fun isFusedAppsUpdated(it: ResultSupreme>) =
+ listAdapter.currentList.isEmpty() || it.data != null && viewModel.isAnyAppUpdated(
+ it.data!!,
+ listAdapter.currentList
+ )
+
+ private fun initAppListAdapter(
+ currentDestinationId: Int
+ ): ApplicationListRVAdapter {
+ return ApplicationListRVAdapter(
+ this,
+ privacyInfoViewModel,
+ appInfoFetchViewModel,
+ mainActivityViewModel,
+ currentDestinationId,
+ viewLifecycleOwner
+ ) { fusedApp ->
+ if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(fusedApp)) {
+ showPaidAppMessage(fusedApp)
+ }
+ }
+ }
- mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) {
- refreshDataOrRefreshToken(mainActivityViewModel)
+ private fun showPaidAppMessage(fusedApp: 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 = {
+ getApplication(fusedApp)
+ },
+ cancelButtonText = getString(R.string.dialog_cancel),
+ ).show(childFragmentManager, "HomeFragment")
+ }
+
+ private fun initRecyclerView(): RecyclerView {
+ val recyclerView = binding.recyclerView
+ recyclerView.recycledViewPool.setMaxRecycledViews(0, 0)
+ return recyclerView
+ }
+
+ private fun updateAppListRecyclerView(
+ listAdapter: ApplicationListRVAdapter?,
+ fusedAppResult: ResultSupreme>
+ ) {
+ val currentList = listAdapter?.currentList
+ if (!isFusedAppsUpdated(fusedAppResult, currentList)
+ ) {
+ return
}
- mainActivityViewModel.authData.observe(viewLifecycleOwner) {
- refreshDataOrRefreshToken(mainActivityViewModel)
+ listAdapter?.setData(fusedAppResult.data!!)
+ listAdapter?.let { adapter ->
+ observeDownloadList(adapter)
}
}
+ private fun isFusedAppsUpdated(
+ fusedAppResult: ResultSupreme>,
+ currentList: MutableList?
+ ) = currentList.isNullOrEmpty() || fusedAppResult.data != null && viewModel.isFusedAppUpdated(
+ fusedAppResult.data!!,
+ currentList
+ )
+
override fun onTimeout() {
if (!isTimeoutDialogDisplayed()) {
stopLoadingUI()
@@ -220,15 +264,13 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li
*
* Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/478
*/
- if (!isDetailsLoaded) {
- showLoadingUI()
- viewModel.getList(
- args.category,
- args.browseUrl,
- authData,
- args.source
- )
- }
+ showLoadingUI()
+ viewModel.getList(
+ args.category,
+ args.browseUrl,
+ authData,
+ args.source
+ )
if (args.source != "Open Source" && args.source != "PWA") {
/*
@@ -280,7 +322,8 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li
lifecycleScope.launch {
adapter.currentList.forEach { fusedApp ->
if (fusedApp.status == Status.DOWNLOADING) {
- val progress = appProgressViewModel.calculateProgress(fusedApp, downloadProgress)
+ val progress =
+ appProgressViewModel.calculateProgress(fusedApp, downloadProgress)
if (progress == -1) {
return@forEach
}
diff --git a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt
index 0d689ef75e9b25f621b60da1a308689f85d6cd5e..ad4884760d610bc7957c5a24a0d47f6f7fdddae9 100644
--- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt
+++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt
@@ -76,7 +76,7 @@ class ApplicationListViewModel @Inject constructor(
private var hasNextStreamCluster = false
fun getList(category: String, browseUrl: String, authData: AuthData, source: String) {
- if (appListLiveData.value?.data?.isNotEmpty() == true || isLoading) {
+ if (isLoading) {
return
}
viewModelScope.launch(Dispatchers.IO) {
@@ -97,6 +97,16 @@ class ApplicationListViewModel @Inject constructor(
}
}
+ /**
+ * @return returns true if there is changes in data, otherwise false
+ */
+ fun isFusedAppUpdated(
+ newFusedApps: List,
+ oldFusedApps: List
+ ): Boolean {
+ return fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps)
+ }
+
/**
* Add a placeholder app at the end if more data can be loaded.
* "Placeholder" app shows a simple progress bar in the RecyclerView, indicating that
@@ -187,9 +197,10 @@ class ApplicationListViewModel @Inject constructor(
private suspend fun getAdjustedFirstCluster(
authData: AuthData,
): ResultSupreme {
- return fusedAPIRepository.getAdjustedFirstCluster(authData, streamBundle, clusterPointer).apply {
- if (isValidData()) addNewClusterData(this.data!!)
- }
+ return fusedAPIRepository.getAdjustedFirstCluster(authData, streamBundle, clusterPointer)
+ .apply {
+ if (isValidData()) addNewClusterData(this.data!!)
+ }
}
/**
@@ -352,4 +363,15 @@ class ApplicationListViewModel @Inject constructor(
private fun getOrigin(source: String) =
if (source.contentEquals("Open Source")) Origin.CLEANAPK else Origin.GPLAY
+
+ /**
+ * @return returns true if there is changes in data, otherwise false
+ */
+ fun isAnyAppUpdated(
+ newFusedApps: List,
+ oldFusedApps: List
+ ) = fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps)
+
+ fun hasAnyAppInstallStatusChanged(currentList: List) =
+ fusedAPIRepository.isAnyAppInstallStatusChanged(currentList)
}
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 1f4fc9fcb9efed0fe35565d04d66fe4e26545783..da23c7326d8fbc78a4441da2ae26c40f0a143b6f 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
@@ -47,13 +47,11 @@ import foundation.e.apps.api.fused.FusedAPIInterface
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.applicationlist.ApplicationListFragmentDirections
import foundation.e.apps.databinding.ApplicationListItemBinding
-import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.search.SearchFragmentDirections
import foundation.e.apps.updates.UpdatesFragmentDirections
import foundation.e.apps.utils.enums.Origin
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
-import foundation.e.apps.utils.modules.PWAManagerModule
import javax.inject.Singleton
@Singleton
@@ -63,9 +61,6 @@ class ApplicationListRVAdapter(
private val appInfoFetchViewModel: AppInfoFetchViewModel,
private val mainActivityViewModel: MainActivityViewModel,
private val currentDestinationId: Int,
- private val pkgManagerModule: PkgManagerModule,
- private val pwaManagerModule: PWAManagerModule,
- private val user: User,
private var lifecycleOwner: LifecycleOwner?,
private var paidAppHandler: ((FusedApp) -> Unit)? = null
) : ListAdapter(ApplicationDiffUtil()) {
@@ -267,7 +262,7 @@ class ApplicationListRVAdapter(
installButton.apply {
isEnabled = true
setOnClickListener {
- val errorMsg = when (user) {
+ val errorMsg = when (mainActivityViewModel.getUser()) {
User.ANONYMOUS,
User.UNAVAILABLE -> view.context.getString(R.string.install_blocked_anonymous)
User.GOOGLE -> view.context.getString(R.string.install_blocked_google)
@@ -458,9 +453,9 @@ class ApplicationListRVAdapter(
strokeColor = ContextCompat.getColorStateList(view.context, R.color.colorAccent)
setOnClickListener {
if (searchApp.is_pwa) {
- pwaManagerModule.launchPwa(searchApp)
+ mainActivityViewModel.launchPwa(searchApp)
} else {
- context.startActivity(pkgManagerModule.getLaunchIntent(searchApp.package_name))
+ context.startActivity(mainActivityViewModel.getLaunchIntentForPackageName(searchApp.package_name))
}
}
}
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 83f7c1cee6abeaf5522d58a858925ea002775a53..7866ead064c31a7069626c511fadaf55802e238f 100644
--- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt
+++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt
@@ -36,6 +36,7 @@ import foundation.e.apps.R
import foundation.e.apps.api.fused.FusedAPIImpl
import foundation.e.apps.api.fused.FusedAPIInterface
import foundation.e.apps.api.fused.data.FusedApp
+import foundation.e.apps.api.fused.data.FusedHome
import foundation.e.apps.application.subFrags.ApplicationDialogFragment
import foundation.e.apps.databinding.FragmentHomeBinding
import foundation.e.apps.home.model.HomeChildRVAdapter
@@ -85,6 +86,48 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface
}
}
+ loadHomePageData()
+
+ homeParentRVAdapter = initHomeParentRVAdapter()
+
+ binding.parentRV.apply {
+ adapter = homeParentRVAdapter
+ layoutManager = LinearLayoutManager(view.context)
+ }
+
+ observeHomeScreenData()
+ }
+
+ private fun observeHomeScreenData() {
+ homeViewModel.homeScreenData.observe(viewLifecycleOwner) {
+ stopLoadingUI()
+ if (it.second != ResultStatus.OK) {
+ onTimeout()
+ return@observe
+ }
+
+ if (!isHomeDataUpdated(it)) {
+ return@observe
+ }
+
+ dismissTimeoutDialog()
+ homeParentRVAdapter?.setData(it.first)
+ }
+ }
+
+ private fun initHomeParentRVAdapter() = HomeParentRVAdapter(
+ this,
+ pkgManagerModule,
+ pwaManagerModule,
+ User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
+ mainActivityViewModel, appInfoFetchViewModel, viewLifecycleOwner
+ ) { fusedApp ->
+ if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(fusedApp)) {
+ showPaidAppMessage(fusedApp)
+ }
+ }
+
+ private fun loadHomePageData() {
/*
* Previous code:
* internetConnection.observe {
@@ -129,46 +172,30 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface
mainActivityViewModel.authData.observe(viewLifecycleOwner) {
refreshDataOrRefreshToken(mainActivityViewModel)
}
+ }
- homeParentRVAdapter = HomeParentRVAdapter(
- this,
- pkgManagerModule,
- pwaManagerModule,
- User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
- mainActivityViewModel, appInfoFetchViewModel, viewLifecycleOwner
- ) { fusedApp ->
- if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(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 = {
- getApplication(fusedApp)
- },
- cancelButtonText = getString(R.string.dialog_cancel),
- ).show(childFragmentManager, "HomeFragment")
- }
- }
-
- binding.parentRV.apply {
- adapter = homeParentRVAdapter
- layoutManager = LinearLayoutManager(view.context)
- }
-
- homeViewModel.homeScreenData.observe(viewLifecycleOwner) {
- stopLoadingUI()
- if (it.second == ResultStatus.OK) {
- if (!homeParentRVAdapter?.currentList.isNullOrEmpty()) {
- return@observe
- }
- dismissTimeoutDialog()
- homeParentRVAdapter?.setData(it.first)
- } else {
- onTimeout()
- }
- }
+ private fun showPaidAppMessage(fusedApp: 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 = {
+ getApplication(fusedApp)
+ },
+ cancelButtonText = getString(R.string.dialog_cancel),
+ ).show(childFragmentManager, "HomeFragment")
}
+ private fun isHomeDataUpdated(homeScreenResult: Pair, ResultStatus>) =
+ homeParentRVAdapter?.currentList?.isEmpty() == true || homeViewModel.isHomeDataUpdated(
+ homeScreenResult.first,
+ homeParentRVAdapter?.currentList as List
+ )
+
override fun onTimeout() {
if (homeViewModel.isFusedHomesEmpty() && !isTimeoutDialogDisplayed()) {
mainActivityViewModel.uploadFaultyTokenToEcloud("From " + this::class.java.name)
@@ -240,21 +267,30 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface
childRV: RecyclerView
) {
lifecycleScope.launch {
- adapter.currentList.forEach { fusedApp ->
- if (fusedApp.status == Status.DOWNLOADING) {
- val progress =
- appProgressViewModel.calculateProgress(fusedApp, downloadProgress)
- if (progress == -1) {
- return@forEach
- }
- val childViewHolder = childRV.findViewHolderForAdapterPosition(
- adapter.currentList.indexOf(fusedApp)
- )
- childViewHolder?.let {
- (childViewHolder as HomeChildRVAdapter.ViewHolder).binding.installButton.text =
- "$progress%"
- }
- }
+ updateDownloadProgressOfAppList(adapter, downloadProgress, childRV)
+ }
+ }
+
+ private suspend fun updateDownloadProgressOfAppList(
+ adapter: HomeChildRVAdapter,
+ downloadProgress: DownloadProgress,
+ childRV: RecyclerView
+ ) {
+ adapter.currentList.forEach { fusedApp ->
+ if (fusedApp.status != Status.DOWNLOADING) {
+ return@forEach
+ }
+ val progress =
+ appProgressViewModel.calculateProgress(fusedApp, downloadProgress)
+ if (progress == -1) {
+ return@forEach
+ }
+ val childViewHolder = childRV.findViewHolderForAdapterPosition(
+ adapter.currentList.indexOf(fusedApp)
+ )
+ childViewHolder?.let {
+ (childViewHolder as HomeChildRVAdapter.ViewHolder).binding.installButton.text =
+ "$progress%"
}
}
}
@@ -266,6 +302,12 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface
appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) {
updateProgressOfDownloadingAppItemViews(homeParentRVAdapter, it)
}
+
+ if (homeViewModel.isAnyAppInstallStatusChanged(homeParentRVAdapter?.currentList)) {
+ mainActivityViewModel.authData.value?.let {
+ refreshData(it)
+ }
+ }
}
override fun onPause() {
diff --git a/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt b/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt
index 5e20a1033bf67cf8b36feb08cba95b55796712a9..2e19e78b55aa590f108ffe43044e82910e89dcfa 100644
--- a/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt
+++ b/app/src/main/java/foundation/e/apps/home/HomeViewModel.kt
@@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.AuthData
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.api.fused.data.FusedHome
import foundation.e.apps.utils.enums.ResultStatus
import kotlinx.coroutines.launch
@@ -31,7 +32,7 @@ import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
- private val fusedAPIRepository: FusedAPIRepository
+ private val fusedAPIRepository: FusedAPIRepository,
) : ViewModel() {
/*
@@ -44,7 +45,8 @@ class HomeViewModel @Inject constructor(
fun getHomeScreenData(authData: AuthData) {
viewModelScope.launch {
- homeScreenData.postValue(fusedAPIRepository.getHomeScreenData(authData))
+ val screenData = fusedAPIRepository.getHomeScreenData(authData)
+ homeScreenData.postValue(screenData)
}
}
@@ -57,4 +59,19 @@ class HomeViewModel @Inject constructor(
fusedAPIRepository.isFusedHomesEmpty(it)
} ?: true
}
+
+ fun isHomeDataUpdated(
+ newHomeData: List,
+ oldHomeData: List
+ ) = fusedAPIRepository.isHomeDataUpdated(newHomeData, oldHomeData)
+
+ fun isAnyAppInstallStatusChanged(currentList: List?): Boolean {
+ if (currentList == null) {
+ return false
+ }
+
+ val appList = mutableListOf()
+ currentList.forEach { appList.addAll(it.list) }
+ return fusedAPIRepository.isAnyAppInstallStatusChanged(appList)
+ }
}
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 ae17f2793e3084fa94aa995b2fda2b02d26f480d..d9b7aa5d77cd44798c8302d401982d1fd6419891 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
@@ -41,19 +41,14 @@ import foundation.e.apps.api.fused.FusedAPIInterface
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.databinding.HomeChildListItemBinding
import foundation.e.apps.home.HomeFragmentDirections
-import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Origin
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
-import foundation.e.apps.utils.modules.PWAManagerModule
class HomeChildRVAdapter(
private var fusedAPIInterface: FusedAPIInterface?,
- private val pkgManagerModule: PkgManagerModule,
- private val pwaManagerModule: PWAManagerModule,
private val appInfoFetchViewModel: AppInfoFetchViewModel,
private val mainActivityViewModel: MainActivityViewModel,
- private val user: User,
private var lifecycleOwner: LifecycleOwner?,
private var paidAppHandler: ((FusedApp) -> Unit)? = null
) : ListAdapter(HomeChildFusedAppDiffUtil()) {
@@ -153,7 +148,7 @@ class HomeChildRVAdapter(
private fun HomeChildListItemBinding.handleBlocked(view: View) {
installButton.setOnClickListener {
- val errorMsg = when (user) {
+ val errorMsg = when (mainActivityViewModel.getUser()) {
User.ANONYMOUS,
User.UNAVAILABLE -> view.context.getString(R.string.install_blocked_anonymous)
User.GOOGLE -> view.context.getString(R.string.install_blocked_google)
@@ -265,9 +260,9 @@ class HomeChildRVAdapter(
ContextCompat.getColorStateList(view.context, R.color.colorAccent)
setOnClickListener {
if (homeApp.is_pwa) {
- pwaManagerModule.launchPwa(homeApp)
+ mainActivityViewModel.launchPwa(homeApp)
} else {
- context.startActivity(pkgManagerModule.getLaunchIntent(homeApp.package_name))
+ context.startActivity(mainActivityViewModel.getLaunchIntentForPackageName(homeApp.package_name))
}
}
}
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 4d65c1112ff75140e1dbc5a6370a594cd42f52be..ab44628ed4f7ddbfeecdf4fdcf0dfe5ed101f41f 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
@@ -62,11 +62,8 @@ class HomeParentRVAdapter(
val homeChildRVAdapter =
HomeChildRVAdapter(
fusedAPIInterface,
- pkgManagerModule,
- pwaManagerModule,
appInfoFetchViewModel,
mainActivityViewModel,
- user,
lifecycleOwner,
paidAppHandler
)
diff --git a/app/src/main/java/foundation/e/apps/manager/database/DatabaseRepository.kt b/app/src/main/java/foundation/e/apps/manager/database/DatabaseRepository.kt
index 277f0dee7ded50a5e697bdeaaf8b80f73bf27b16..6344a6c7550fe585eac4ccf9093c3d27d1c066aa 100644
--- a/app/src/main/java/foundation/e/apps/manager/database/DatabaseRepository.kt
+++ b/app/src/main/java/foundation/e/apps/manager/database/DatabaseRepository.kt
@@ -2,6 +2,7 @@ package foundation.e.apps.manager.database
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
+import foundation.e.apps.OpenForTesting
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.manager.database.fusedDownload.FusedDownloadDAO
import kotlinx.coroutines.flow.Flow
@@ -9,6 +10,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
+@OpenForTesting
class DatabaseRepository @Inject constructor(
private val fusedDownloadDAO: FusedDownloadDAO
) {
diff --git a/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt b/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt
index 20ca99948eae7ff07402eed882f5edafa685b276..c501109e0819bb8924973c7d80ad6341f884e5c3 100644
--- a/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt
+++ b/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt
@@ -29,6 +29,7 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import dagger.hilt.android.qualifiers.ApplicationContext
+import foundation.e.apps.OpenForTesting
import foundation.e.apps.api.fused.FusedAPIImpl
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.utils.enums.Origin
@@ -41,6 +42,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
+@OpenForTesting
class PkgManagerModule @Inject constructor(
@ApplicationContext private val context: Context
) {
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 4bf1e3bd209fc9859ec7aaf9430323250566575b..2544fd0b940cca4aa6c7eb7722f32fe47e739b67 100644
--- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt
+++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt
@@ -53,7 +53,6 @@ import foundation.e.apps.databinding.FragmentSearchBinding
import foundation.e.apps.manager.download.data.DownloadProgress
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Status
-import foundation.e.apps.utils.enums.User
import foundation.e.apps.utils.modules.PWAManagerModule
import foundation.e.apps.utils.parentFragment.TimeoutFragment
import kotlinx.coroutines.launch
@@ -133,25 +132,10 @@ class SearchFragment :
appInfoFetchViewModel,
mainActivityViewModel,
it,
- pkgManagerModule,
- pwaManagerModule,
- User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
viewLifecycleOwner
) { fusedApp ->
if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(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 = {
- getApplication(fusedApp)
- },
- cancelButtonText = getString(R.string.dialog_cancel),
- ).show(childFragmentManager, "SearchFragment")
+ showPaidAppMessage(fusedApp)
}
}
}
@@ -165,6 +149,14 @@ class SearchFragment :
if (it.data?.first.isNullOrEmpty()) {
noAppsFoundLayout?.visibility = View.VISIBLE
} else {
+ val currentList = listAdapter?.currentList
+ if (it.data?.first != null && !currentList.isNullOrEmpty() && !searchViewModel.isAnyAppUpdated(
+ it.data?.first!!,
+ currentList
+ )
+ ) {
+ return@observe
+ }
listAdapter?.setData(it.data!!.first)
binding.loadingProgressBar.isVisible = it.data!!.second
stopLoadingUI()
@@ -203,6 +195,22 @@ class SearchFragment :
}
}
+ private fun showPaidAppMessage(fusedApp: 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 = {
+ getApplication(fusedApp)
+ },
+ cancelButtonText = getString(R.string.dialog_cancel),
+ ).show(childFragmentManager, "SearchFragment")
+ }
+
private fun observeDownloadList(applicationListRVAdapter: ApplicationListRVAdapter) {
mainActivityViewModel.downloadList.observe(viewLifecycleOwner) { list ->
val searchList =
@@ -237,7 +245,7 @@ class SearchFragment :
override fun refreshData(authData: AuthData) {
showLoadingUI()
- searchViewModel.getSearchResults(searchText, authData, this)
+ searchViewModel.getSearchResults(searchText, authData, viewLifecycleOwner)
}
private fun showLoadingUI() {
@@ -281,8 +289,19 @@ class SearchFragment :
appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) {
updateProgressOfInstallingApps(it)
}
+
+ if (shouldRefreshData()) {
+ mainActivityViewModel.authData.value?.let {
+ refreshData(it)
+ }
+ }
}
+ private fun shouldRefreshData() =
+ searchText.isNotEmpty() && recyclerView?.adapter != null && searchViewModel.hasAnyAppInstallStatusChanged(
+ (recyclerView?.adapter as ApplicationListRVAdapter).currentList
+ )
+
override fun onPause() {
binding.shimmerLayout.stopShimmer()
super.onPause()
@@ -330,6 +349,7 @@ class SearchFragment :
_binding = null
searchView = null
shimmerLayout = null
+ recyclerView?.adapter = null
recyclerView = null
searchHintLayout = null
noAppsFoundLayout = null
diff --git a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt
index 3ae636d47a5d84a5270367cfa5a500cde8471d04..2c80d5b0c833925a08b70671f9c94ae5f13edae4 100644
--- a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt
+++ b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt
@@ -28,17 +28,20 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import foundation.e.apps.api.ResultSupreme
import foundation.e.apps.api.fused.FusedAPIRepository
import foundation.e.apps.api.fused.data.FusedApp
+import foundation.e.apps.manager.fused.FusedManagerRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SearchViewModel @Inject constructor(
- private val fusedAPIRepository: FusedAPIRepository
+ private val fusedAPIRepository: FusedAPIRepository,
+ private val fusedManagerRepository: FusedManagerRepository
) : ViewModel() {
val searchSuggest: MutableLiveData?> = MutableLiveData()
- val searchResult: MutableLiveData, Boolean>>> = MutableLiveData()
+ val searchResult: MutableLiveData, Boolean>>> =
+ MutableLiveData()
fun getSearchSuggestions(query: String, authData: AuthData) {
viewModelScope.launch(Dispatchers.IO) {
@@ -59,4 +62,15 @@ class SearchViewModel @Inject constructor(
}
}
}
+
+ /**
+ * @return returns true if there is changes in data, otherwise false
+ */
+ fun isAnyAppUpdated(
+ newFusedApps: List,
+ oldFusedApps: List
+ ) = fusedAPIRepository.isAnyFusedAppUpdated(newFusedApps, oldFusedApps)
+
+ fun hasAnyAppInstallStatusChanged(currentList: List) =
+ fusedAPIRepository.isAnyAppInstallStatusChanged(currentList)
}
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 c8654b0a603efa1b43a3d3ba0eec717c3bc03d6b..99e04d020e93d409adb5d18cd504b1639034e4ac 100644
--- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt
+++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt
@@ -48,7 +48,6 @@ import foundation.e.apps.manager.workmanager.InstallWorkManager.INSTALL_WORK_NAM
import foundation.e.apps.updates.manager.UpdatesWorkManager
import foundation.e.apps.utils.enums.ResultStatus
import foundation.e.apps.utils.enums.Status
-import foundation.e.apps.utils.enums.User
import foundation.e.apps.utils.modules.CommonUtilsModule.safeNavigate
import foundation.e.apps.utils.modules.PWAManagerModule
import foundation.e.apps.utils.parentFragment.TimeoutFragment
@@ -103,25 +102,10 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte
appInfoFetchViewModel,
mainActivityViewModel,
it,
- pkgManagerModule,
- pwaManagerModule,
- User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
viewLifecycleOwner,
) { fusedApp ->
if (!mainActivityViewModel.shouldShowPaidAppsSnackBar(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 = {
- getApplication(fusedApp)
- },
- cancelButtonText = getString(R.string.dialog_cancel),
- ).show(childFragmentManager, "UpdatesFragment")
+ showPurchasedAppMessage(fusedApp)
}
}
}
@@ -162,6 +146,22 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte
}
}
+ private fun showPurchasedAppMessage(fusedApp: 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 = {
+ getApplication(fusedApp)
+ },
+ cancelButtonText = getString(R.string.dialog_cancel),
+ ).show(childFragmentManager, "UpdatesFragment")
+ }
+
override fun onTimeout() {
if (!isTimeoutDialogDisplayed()) {
stopLoadingUI()
diff --git a/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt
index 1ea385d85ac2909da014e8ea11532c3bacf760f2..6d3faa62ada9e0ff36fb516770815a5459f3b5c0 100644
--- a/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt
+++ b/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt
@@ -12,6 +12,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import dagger.hilt.android.qualifiers.ApplicationContext
+import foundation.e.apps.OpenForTesting
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.manager.database.DatabaseRepository
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
@@ -21,6 +22,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
+@OpenForTesting
class PWAManagerModule @Inject constructor(
@ApplicationContext private val context: Context,
private val databaseRepository: DatabaseRepository
diff --git a/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt
index 959dc2efbb7a8fb6983b3fff00f32c0fd5500325..e5e4c6453d5244e475da70b6d227374ee9752fa4 100644
--- a/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt
+++ b/app/src/main/java/foundation/e/apps/utils/parentFragment/TimeoutFragment.kt
@@ -182,7 +182,8 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) {
*/
try {
timeoutAlertDialog?.dismiss()
- } catch (_: Exception) {}
+ } catch (_: Exception) {
+ }
timeoutAlertDialog = timeoutAlertDialogBuilder.create()
timeoutAlertDialog?.show()
@@ -210,7 +211,8 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) {
if (isTimeoutDialogDisplayed()) {
try {
timeoutAlertDialog?.dismiss()
- } catch (_: Exception) {}
+ } catch (_: Exception) {
+ }
}
}
}
diff --git a/app/src/release/java/foundation/e/apps/OpenForTesting.kt b/app/src/release/java/foundation/e/apps/OpenForTesting.kt
new file mode 100644
index 0000000000000000000000000000000000000000..372dc732db5e38129be837e6974ca84cc26f1877
--- /dev/null
+++ b/app/src/release/java/foundation/e/apps/OpenForTesting.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 ECORP
+ *
+ * This program 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package foundation.e.apps
+
+@Target(AnnotationTarget.CLASS)
+annotation class OpenForTesting
diff --git a/app/src/test/java/foundation/e/apps/FaultyAppRepositoryTest.kt b/app/src/test/java/foundation/e/apps/FaultyAppRepositoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9172759a289f1f786baa980d37dded9976c26849
--- /dev/null
+++ b/app/src/test/java/foundation/e/apps/FaultyAppRepositoryTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2022 ECORP
+ *
+ * This program 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package foundation.e.apps
+
+import foundation.e.apps.api.faultyApps.FaultyApp
+import foundation.e.apps.api.faultyApps.FaultyAppDao
+import foundation.e.apps.api.faultyApps.FaultyAppRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class FaultyAppRepositoryTest {
+ private lateinit var faultyAppRepository: FaultyAppRepository
+ private val fakeFaultyAppDao = FakeFaultyAppDao()
+
+ @Before
+ fun setup() {
+ faultyAppRepository = FaultyAppRepository(fakeFaultyAppDao)
+ }
+
+ @Test
+ fun addFaultyApp_CheckSize() = runTest {
+ faultyAppRepository.addFaultyApp("foundation.e.apps", "")
+ assertEquals("testAddFaultyApp", 1, fakeFaultyAppDao.faultyAppList.size)
+ }
+
+ @Test
+ fun getAllFaultyApps_ReturnsAppList() = runTest {
+ fakeFaultyAppDao.faultyAppList.add(FaultyApp("foundation.e.apps", ""))
+ fakeFaultyAppDao.faultyAppList.add(FaultyApp("foundation.e.edrive", ""))
+ fakeFaultyAppDao.faultyAppList.add(FaultyApp("foundation.e.privacycentral", ""))
+ val faultyAppList = faultyAppRepository.getAllFaultyApps()
+ assertTrue("testGetAllFaultyApps", faultyAppList[0].packageName.contentEquals("foundation.e.apps"))
+ }
+
+ @Test
+ fun deleteFaultyApps_CheckSize() = runTest {
+ fakeFaultyAppDao.faultyAppList.add(FaultyApp("foundation.e.apps", ""))
+ fakeFaultyAppDao.faultyAppList.add(FaultyApp("foundation.e.edrive", ""))
+ fakeFaultyAppDao.faultyAppList.add(FaultyApp("foundation.e.privacycentral", ""))
+ faultyAppRepository.deleteFaultyAppByPackageName("foundation.e.apps")
+ assertTrue("testDeleteFaultyApps", fakeFaultyAppDao.faultyAppList.size == 2)
+ }
+
+ class FakeFaultyAppDao : FaultyAppDao {
+ val faultyAppList: MutableList = mutableListOf()
+
+ override suspend fun addFaultyApp(faultyApp: FaultyApp): Long {
+ faultyAppList.add(faultyApp)
+ return -1
+ }
+
+ override suspend fun getFaultyApps(): List {
+ return faultyAppList
+ }
+
+ override suspend fun deleteFaultyAppByPackageName(packageName: String): Int {
+ val isSuccess = faultyAppList.removeIf {
+ it.packageName.contentEquals(packageName)
+ }
+ return if (isSuccess) 1 else -1
+ }
+ }
+}
diff --git a/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..89f05057476478daa906a81ae03c8b2085209435
--- /dev/null
+++ b/app/src/test/java/foundation/e/apps/FusedApiImplTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2022 ECORP
+ *
+ * This program 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package foundation.e.apps
+
+import android.content.Context
+import foundation.e.apps.api.cleanapk.CleanAPKRepository
+import foundation.e.apps.api.fused.FusedAPIImpl
+import foundation.e.apps.api.fused.data.FusedApp
+import foundation.e.apps.api.gplay.GPlayAPIRepository
+import foundation.e.apps.manager.pkg.PkgManagerModule
+import foundation.e.apps.utils.enums.Status
+import foundation.e.apps.utils.modules.PWAManagerModule
+import foundation.e.apps.utils.modules.PreferenceManagerModule
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
+
+class FusedApiImplTest {
+ private lateinit var fusedAPIImpl: FusedAPIImpl
+
+ @Mock
+ private lateinit var pwaManagerModule: PWAManagerModule
+
+ @Mock
+ private lateinit var pkgManagerModule: PkgManagerModule
+
+ @Mock
+ private lateinit var context: Context
+
+ @Mock
+ private lateinit var cleanApkRepository: CleanAPKRepository
+
+ @Mock
+ private lateinit var gPlayAPIRepository: GPlayAPIRepository
+
+ @Mock
+ private lateinit var preferenceManagerModule: PreferenceManagerModule
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ fusedAPIImpl = FusedAPIImpl(
+ cleanApkRepository,
+ gPlayAPIRepository,
+ pkgManagerModule,
+ pwaManagerModule,
+ preferenceManagerModule,
+ context
+ )
+ }
+
+ @Test
+ fun `is any app updated when new list is empty`() {
+ val oldAppList = mutableListOf(
+ FusedApp(
+ _id = "111",
+ status = Status.UNAVAILABLE,
+ name = "Demo One",
+ package_name = "foundation.e.demoone"
+ ),
+ FusedApp(
+ _id = "112",
+ status = Status.INSTALLED,
+ name = "Demo Two",
+ package_name = "foundation.e.demotwo"
+ ),
+ FusedApp(
+ _id = "113",
+ status = Status.UNAVAILABLE,
+ name = "Demo Three",
+ package_name = "foundation.e.demothree"
+ )
+ )
+ val newAppList = mutableListOf()
+
+ val isFusedAppUpdated = fusedAPIImpl.isAnyFusedAppUpdated(newAppList, oldAppList)
+ assertTrue("isAnyAppUpdated", isFusedAppUpdated)
+ }
+
+ @Test
+ fun `is any app updated when both list are empty`() {
+ val isFusedAppUpdated = fusedAPIImpl.isAnyFusedAppUpdated(listOf(), listOf())
+ assertFalse("isAnyAppUpdated", isFusedAppUpdated)
+ }
+
+ @Test
+ fun `is any app updated when any app is uninstalled`() {
+ val oldAppList = mutableListOf(
+ FusedApp(
+ _id = "111",
+ status = Status.UNAVAILABLE,
+ name = "Demo One",
+ package_name = "foundation.e.demoone"
+ ),
+ FusedApp(
+ _id = "112",
+ status = Status.INSTALLED,
+ name = "Demo Two",
+ package_name = "foundation.e.demotwo"
+ ),
+ FusedApp(
+ _id = "113",
+ status = Status.UNAVAILABLE,
+ name = "Demo Three",
+ package_name = "foundation.e.demothree"
+ )
+ )
+ val newAppList = mutableListOf(
+ FusedApp(
+ _id = "111",
+ status = Status.UNAVAILABLE,
+ name = "Demo One",
+ package_name = "foundation.e.demoone"
+ ),
+ FusedApp(
+ _id = "112",
+ status = Status.UNAVAILABLE,
+ name = "Demo Two",
+ package_name = "foundation.e.demotwo"
+ ),
+ FusedApp(
+ _id = "113",
+ status = Status.UNAVAILABLE,
+ name = "Demo Three",
+ package_name = "foundation.e.demothree"
+ )
+ )
+
+ val isFusedAppUpdated = fusedAPIImpl.isAnyFusedAppUpdated(newAppList, oldAppList)
+ assertTrue("isAnyFusedAppUpdated", isFusedAppUpdated)
+ }
+
+ @Test
+ fun `has any app install status changed when changed`() {
+ val oldAppList = mutableListOf(
+ FusedApp(
+ _id = "111",
+ status = Status.UNAVAILABLE,
+ name = "Demo One",
+ package_name = "foundation.e.demoone",
+ latest_version_code = 123
+ ),
+ FusedApp(
+ _id = "112",
+ status = Status.INSTALLED,
+ name = "Demo Two",
+ package_name = "foundation.e.demotwo",
+ latest_version_code = 123
+ ),
+ FusedApp(
+ _id = "113",
+ status = Status.UNAVAILABLE,
+ name = "Demo Three",
+ package_name = "foundation.e.demothree",
+ latest_version_code = 123
+ )
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demoone"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demotwo"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demothree"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ val isAppStatusUpdated = fusedAPIImpl.isAnyAppInstallStatusChanged(oldAppList)
+ assertTrue("hasInstallStatusUpdated", isAppStatusUpdated)
+ }
+
+ @Test
+ fun `has any app install status changed when not changed`() {
+ val oldAppList = mutableListOf(
+ FusedApp(
+ _id = "111",
+ status = Status.UNAVAILABLE,
+ name = "Demo One",
+ package_name = "foundation.e.demoone",
+ latest_version_code = 123
+ ),
+ FusedApp(
+ _id = "112",
+ status = Status.INSTALLED,
+ name = "Demo Two",
+ package_name = "foundation.e.demotwo",
+ latest_version_code = 123
+ ),
+ FusedApp(
+ _id = "113",
+ status = Status.UNAVAILABLE,
+ name = "Demo Three",
+ package_name = "foundation.e.demothree",
+ latest_version_code = 123
+ )
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demoone"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demotwo"), eq(123)))
+ .thenReturn(
+ Status.INSTALLED
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demothree"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ val isAppStatusUpdated = fusedAPIImpl.isAnyAppInstallStatusChanged(oldAppList)
+ assertFalse("hasInstallStatusUpdated", isAppStatusUpdated)
+ }
+
+ @Test
+ fun `has any app install status changed when installation_issue`() {
+ val oldAppList = mutableListOf(
+ FusedApp(
+ _id = "111",
+ status = Status.INSTALLATION_ISSUE,
+ name = "Demo One",
+ package_name = "foundation.e.demoone",
+ latest_version_code = 123
+ ),
+ FusedApp(
+ _id = "112",
+ status = Status.INSTALLED,
+ name = "Demo Two",
+ package_name = "foundation.e.demotwo",
+ latest_version_code = 123
+ ),
+ FusedApp(
+ _id = "113",
+ status = Status.UNAVAILABLE,
+ name = "Demo Three",
+ package_name = "foundation.e.demothree",
+ latest_version_code = 123
+ )
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demoone"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demotwo"), eq(123)))
+ .thenReturn(
+ Status.INSTALLED
+ )
+ Mockito.`when`(pkgManagerModule.getPackageStatus(eq("foundation.e.demothree"), eq(123)))
+ .thenReturn(
+ Status.UNAVAILABLE
+ )
+ val isAppStatusUpdated = fusedAPIImpl.isAnyAppInstallStatusChanged(oldAppList)
+ assertFalse("hasInstallStatusUpdated", isAppStatusUpdated)
+ }
+}
diff --git a/app/src/test/java/foundation/e/apps/FusedApiRepositoryTest.kt b/app/src/test/java/foundation/e/apps/FusedApiRepositoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e3842065ca97dcdcf474d4c055e95b3d60a53063
--- /dev/null
+++ b/app/src/test/java/foundation/e/apps/FusedApiRepositoryTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 ECORP
+ *
+ * This program 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package foundation.e.apps
+
+import foundation.e.apps.api.fused.FusedAPIImpl
+import foundation.e.apps.api.fused.FusedAPIRepository
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+
+class FusedApiRepositoryTest {
+ private lateinit var fusedApiRepository: FusedAPIRepository
+ @Mock
+ private lateinit var fusedApiImple: FusedAPIImpl
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+ fusedApiRepository = FusedAPIRepository(fusedApiImple)
+ }
+
+ @Test
+ fun isAnyAppUpdated_ReturnsTrue() {
+ Mockito.`when`(fusedApiImple.isAnyFusedAppUpdated(any(), any())).thenReturn(true)
+ val isAnyAppUpdated = fusedApiRepository.isAnyFusedAppUpdated(listOf(), listOf())
+ assertTrue("isAnyAppUpdated", isAnyAppUpdated)
+ }
+
+ @Test
+ fun isAnyInstallStatusChanged_ReturnsTrue() {
+ Mockito.`when`(fusedApiImple.isAnyAppInstallStatusChanged(any())).thenReturn(true)
+ val isAnyAppUpdated = fusedApiRepository.isAnyAppInstallStatusChanged(listOf())
+ assertTrue("isAnyAppUpdated", isAnyAppUpdated)
+ }
+}
diff --git a/build.gradle b/build.gradle
index 74adc17df7085fd155f48cac3bbb70d231f8cb45..47455671491fd4a4c52c442db3feeb11a3ac6ace 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,6 +9,7 @@ buildscript {
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
classpath "com.google.dagger:hilt-android-gradle-plugin:2.40.5"
+ classpath "org.jetbrains.kotlin:kotlin-allopen:1.6.10"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files