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