diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index c58ac69901da8def56450a9e580484d1cf82fed6..f6674b8fa520ab43ef04053eace16611ecbac7ac 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -308,7 +308,7 @@ class MainActivity : AppCompatActivity() { setTitle(R.string.sign_in_failed_title) setMessage(R.string.sign_in_failed_desc) setPositiveButton(R.string.retry) { _, _ -> - viewModel.retryFetchingTokenAfterTimeout() + viewModel.checkTokenOnTimeout() } setNegativeButton(R.string.logout) { _, _ -> viewModel.postFalseAuthValidity() diff --git a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt index 00e86427e9979835fa24746592c5b8e9c5c95583..003208560c7373e6bd625a036a6f02fa52c56e4a 100644 --- a/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/MainActivityViewModel.kt @@ -60,6 +60,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ru.beryukhov.reactivenetwork.ReactiveNetwork +import ru.beryukhov.reactivenetwork.internet.observing.InternetObservingSettings +import ru.beryukhov.reactivenetwork.internet.observing.strategy.SocketInternetObservingStrategy import timber.log.Timber import java.io.ByteArrayOutputStream import javax.inject.Inject @@ -132,29 +134,52 @@ class MainActivityViewModel @Inject constructor( return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis } - /* + /** * This method resets the last recorded token fetch time. - * Then it posts authValidity as false. This causes the observer in MainActivity to destroyCredentials - * and fetch new token. + * + * Then if [authData] is not null, it checks the validity of it, + * which automatically updates [authValidity]. + * If [authValidity] is true, the observer in MainActivity calls [generateAuthData], + * which passes the same [authData] to the current displaying fragment once more, + * to trigger data refresh. + * + * If [authData] is null, it posts false in [authValidity], which + * causes the observer in MainActivity to destroyCredentials and fetch new token. + * This again causes the current displaying fragment to re-trigger data refresh. * * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404 + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] */ - fun retryFetchingTokenAfterTimeout() { + fun checkTokenOnTimeout() { firstAuthDataFetchTime = 0 setFirstTokenFetchTime() - if (isUserTypeGoogle()) { - /* - * Change done to show sign in error dialog for Google login. - * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 - */ - if (authDataJson.value.isNullOrEmpty()) { - generateAuthDataBasedOnUserType(User.GOOGLE.name) - } else { - validateAuthData() - } - } else { - postFalseAuthValidity() + /* + * Explanation: + * 1. User type value must be present for all normal functioning. + * If not, post false to log out. + * 2. If authDataJson is empty, we have not yet obtained authData even once. + * Hence generate authData. + * 3. Else condition is where authDataJson is present in shared preferences, meaning + * we had logged in successfully at least once. Thus validate that data. + * + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 + */ + when { + userType.value == null -> postFalseAuthValidity() + authDataJson.value.isNullOrEmpty() -> generateAuthDataBasedOnUserType(userType.value!!) + else -> validateAuthData() } + + /* + * Old code from branch: 5413-timeout-improvement: + * Commented out as this logic is already expected to be fulfilled + * by the above code. + */ + // if (authData.value != null) { + // validateAuthData() + // } else { + // authValidity.postValue(false) + // } } fun uploadFaultyTokenToEcloud(description: String) { @@ -608,7 +633,14 @@ class MainActivityViewModel @Inject constructor( } val internetConnection = liveData { - emitSource(ReactiveNetwork().observeInternetConnectivity().asLiveData(Dispatchers.Default)) + emitSource( + ReactiveNetwork().observeInternetConnectivity( + InternetObservingSettings.builder() + .host("http://204.ecloud.global") + .strategy(SocketInternetObservingStrategy()) + .build() + ).asLiveData(Dispatchers.Default) + ) } fun updateStatusOfFusedApps( diff --git a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt index e0cd5630a56c82c9ee7a6fdfe07e0af5ae2c7185..8454cc7ef6abe417089dc169773edd86cc6f0fdf 100644 --- a/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt +++ b/app/src/main/java/foundation/e/apps/api/cleanapk/RetrofitModule.kt @@ -34,6 +34,7 @@ import foundation.e.apps.api.cleanapk.data.app.Application import foundation.e.apps.api.ecloud.EcloudApiInterface import foundation.e.apps.api.exodus.ExodusTrackerApi import foundation.e.apps.api.fdroid.FdroidApiInterface +import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis import okhttp3.Cache import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -47,6 +48,7 @@ import retrofit2.converter.jackson.JacksonConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory import java.net.ConnectException import java.util.Locale +import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Singleton @@ -194,6 +196,10 @@ object RetrofitModule { @Provides fun provideOkHttpClient(cache: Cache, interceptor: Interceptor): OkHttpClient { return OkHttpClient.Builder() + .connectTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) + .readTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) + .writeTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) + .callTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS) .addInterceptor(interceptor) .cache(cache) .build() 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..5b42a1d95b779d0fbfbd67359f1a65c02d251888 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 @@ -207,6 +207,32 @@ class FusedAPIImpl @Inject constructor( return Triple(categoriesList, applicationCategoryType, apiStatus) } + /** + * Fetch categories only from cleanapk. + * Useful when GPlay times out and we only need to load info from cleanapk. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + suspend fun getCategoriesListOSS(type: Category.Type): Triple, String, ResultStatus> { + val categoriesList = mutableListOf() + val status = runCodeBlockWithTimeout({ + getOpenSourceCategories()?.run { + categoriesList.addAll( + getFusedCategoryBasedOnCategoryType( + this, type, AppTag.OpenSource(context.getString(R.string.open_source)) + ) + ) + } + getPWAsCategories()?.run { + categoriesList.addAll( + getFusedCategoryBasedOnCategoryType( + this, type, AppTag.PWA(context.getString(R.string.pwa)) + ) + ) + } + }) + return Triple(categoriesList, "open", status) + } + /** * Fetches search results from cleanAPK and GPlay servers and returns them * @param query Query @@ -214,6 +240,7 @@ class FusedAPIImpl @Inject constructor( * @return A livedata Pair of list of non-nullable [FusedApp] and * a Boolean signifying if more search results are being loaded. * Observe this livedata to display new apps as they are fetched from the network. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] */ fun getSearchResults( query: String, @@ -265,11 +292,14 @@ class FusedAPIImpl @Inject constructor( /* * If there was a timeout, return it and don't try to fetch anything else. - * Also send true in the pair to signal more results being loaded. + * Also send false in the pair to signal no more results are being loaded. + * If not timeout then send true in the pair, to signal more results are being loaded. */ if (status != ResultStatus.OK) { - emit(ResultSupreme.create(status, Pair(packageSpecificResults, true))) + emit(ResultSupreme.create(status, Pair(packageSpecificResults, false))) return@liveData + } else if (packageSpecificResults.isNotEmpty()) { + emit(ResultSupreme.create(status, Pair(packageSpecificResults, true))) } /* @@ -352,6 +382,94 @@ class FusedAPIImpl @Inject constructor( } } + /** + * Similar to [getSearchResults] but this only gets search results from cleanapk and not GPlay. + * @param query Search query. + * @return A livedata Pair of list of non-nullable [FusedApp] and + * a Boolean signifying if more search results are being loaded. + * Observe this livedata to display new apps as they are fetched from the network. + */ + fun getSearchResultsOSS( + query: String + ): LiveData, Boolean>>> { + return liveData { + /* + * Get package name search. + */ + var packageSpecificResult = FusedApp() + val status = runCodeBlockWithTimeout({ + getCleanapkSearchResult(query).let { + /* Cleanapk always returns something, it is never null. + * If nothing is found, it returns a blank FusedApp() object. + * Blank result to be filtered out. + */ + if (it.isSuccess() && it.data!!.package_name.isNotBlank()) { + packageSpecificResult = it.data!! + } + } + }) + + + /* + * If there was a timeout, return it and don't try to fetch anything else. + * Also send false in the pair to signal no more results are being loaded. + * If not timeout then send true in the pair, to signal more results are being loaded. + */ + if (status != ResultStatus.OK) { + emit(ResultSupreme.create(status, Pair(listOf(), false))) + return@liveData + } else if (packageSpecificResult.package_name.isNotBlank()) { + emit(ResultSupreme.create(status, Pair(listOf(packageSpecificResult), true))) + } + + /* + * Load keyword related searches from cleanapk. + */ + val cleanApkResults = mutableListOf() + when (preferenceManagerModule.preferredApplicationType()) { + APP_TYPE_OPEN, APP_TYPE_ANY -> { + val status = runCodeBlockWithTimeout({ + cleanApkResults.addAll(getCleanAPKSearchResults(query)) + }) + /* + * Send false in pair to signal no more results to load, as only cleanapk + * results are fetched, we don't have to wait for GPlay results. + * + * Also filter out apps with same package name as that of packageSpecificResult, + * to avoid duplicates. + */ + emit( + ResultSupreme.create( + status, + Pair( + cleanApkResults.filter { + it.package_name != packageSpecificResult.package_name + }, + false + ) + ) + ) + } + APP_TYPE_PWA -> { + val status = runCodeBlockWithTimeout({ + cleanApkResults.addAll( + getCleanAPKSearchResults( + query, + CleanAPKInterface.APP_SOURCE_ANY, + CleanAPKInterface.APP_TYPE_PWA + ) + ) + }) + /* + * Send false in pair to signal no more results to load, as only cleanapk + * results are fetched for PWAs. + */ + emit(ResultSupreme.create(status, Pair(cleanApkResults, false))) + } + } + } + } + /* * Method to search cleanapk based on package name. * This is to be only used for showing an entry in search results list. @@ -515,6 +633,32 @@ class FusedAPIImpl @Inject constructor( return Pair(fusedApp, status) } + /* + * Get updates only from cleanapk. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + suspend fun getApplicationDetailsOSS( + packageNameList: List, + ): ResultSupreme> { + val list = mutableListOf() + + val response: Pair, ResultStatus> = + getAppDetailsListFromCleanapk(packageNameList) + response.first.forEach { + if (it.package_name.isNotBlank()) { + it.updateStatus() + it.updateType() + list.add(it) + } + } + + return ResultSupreme.create(response.second, list.toList()).apply { + if (!isSuccess()) { + message = true.toString() + } + } + } + suspend fun getApplicationDetails( packageNameList: List, authData: AuthData, @@ -743,6 +887,23 @@ class FusedAPIImpl @Inject constructor( return Pair(response ?: FusedApp(), status) } + suspend fun getApplicationDetailsOSS( + id: String, + ): Pair { + + var response: FusedApp? = null + + val status = runCodeBlockWithTimeout({ + response = cleanAPKRepository.getAppOrPWADetailsByID(id).body()?.app + response?.let { + it.updateStatus() + it.updateType() + } + }) + + return Pair(response ?: FusedApp(), status) + } + /* * Categories-related internal functions */ @@ -877,9 +1038,10 @@ class FusedAPIImpl @Inject constructor( block: suspend () -> Unit, timeoutBlock: (() -> Unit)? = null, exceptionBlock: (() -> Unit)? = null, + timeoutLimit: Long = timeoutDurationInMillis, ): ResultStatus { return try { - withTimeout(timeoutDurationInMillis) { + withTimeout(timeoutLimit) { block() } ResultStatus.OK 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..915f2cb470bc8a6255e245ee22b5019c468150b7 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 @@ -69,6 +69,12 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getApplicationDetails(packageNameList, authData, origin) } + suspend fun getApplicationDetailsOSS( + packageNameList: List, + ): ResultSupreme> { + return fusedAPIImpl.getApplicationDetailsOSS(packageNameList) + } + suspend fun filterRestrictedGPlayApps( authData: AuthData, appList: List, @@ -89,6 +95,12 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getApplicationDetails(id, packageName, authData, origin) } + suspend fun getApplicationDetailsOSS( + id: String, + ): Pair { + return fusedAPIImpl.getApplicationDetailsOSS(id) + } + suspend fun getCleanapkAppDetails(packageName: String): Pair { return fusedAPIImpl.getCleanapkAppDetails(packageName) } @@ -112,6 +124,10 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getCategoriesList(type, authData) } + suspend fun getCategoriesListOSS(type: Category.Type): Triple, String, ResultStatus> { + return fusedAPIImpl.getCategoriesListOSS(type) + } + suspend fun getSearchSuggestions(query: String, authData: AuthData): List { return fusedAPIImpl.getSearchSuggestions(query, authData) } @@ -128,6 +144,10 @@ class FusedAPIRepository @Inject constructor( return fusedAPIImpl.getSearchResults(query, authData) } + fun getSearchResultsOSS(query: String): LiveData, Boolean>>> { + return fusedAPIImpl.getSearchResultsOSS(query) + } + suspend fun getNextStreamBundle( authData: AuthData, homeUrl: String, @@ -164,6 +184,21 @@ class FusedAPIRepository @Inject constructor( } } + /* + * Get only category apps from cleanapk. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + suspend fun getAppsListOSS( + category: String, + source: String, + ): ResultSupreme> { + return when (source) { + "Open Source" -> fusedAPIImpl.getOpenSourceApps(category) + "PWA" -> fusedAPIImpl.getPWAApps(category) + else -> ResultSupreme.Error() + } + } + fun getFusedAppInstallationStatus(fusedApp: FusedApp): Status { return fusedAPIImpl.getFusedAppInstallationStatus(fusedApp) } diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt index d4c67552c60677538840c456976657f4678b316a..579daefe8f90ef017c3fafe74dde634fb5910d28 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationFragment.kt @@ -325,7 +325,7 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, negativeButtonText = getString(android.R.string.ok), negativeButtonBlock = { @@ -354,6 +354,26 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } } + override fun noAuthRefresh(): Boolean { + if (!isDetailsLoaded) { + /* Show the loading bar. */ + showLoadingUI() + /* Remove trailing slash (if present) that can become part of the packageName */ + val packageName = args.packageName.run { if (endsWith('/')) dropLast(1) else this } + if (isFdroidDeepLink) { + applicationViewModel.getCleanapkAppDetails(packageName) + } else if (args.origin == Origin.CLEANAPK) { + applicationViewModel.getApplicationDetailsOSS( + args.id, + ) + return true + } else { + return false + } + } + return true + } + private fun observeDownloadStatus(view: View) { applicationViewModel.appStatus.observe(viewLifecycleOwner) { status -> val installButton = binding.downloadInclude.installButton diff --git a/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt b/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt index dc094e5ff0f18a9f8e3a1a99d0d9de6e6901c94f..8ee451aa84cb68014b60b3b816f14c0dfd351575 100644 --- a/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt +++ b/app/src/main/java/foundation/e/apps/application/ApplicationViewModel.kt @@ -72,6 +72,20 @@ class ApplicationViewModel @Inject constructor( } } + fun getApplicationDetailsOSS(id: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + fusedApp.postValue( + fusedAPIRepository.getApplicationDetailsOSS(id) + ) + } catch (e: ApiException.AppNotFound) { + _errorMessageLiveData.postValue(R.string.app_not_found) + } catch (e: Exception) { + _errorMessageLiveData.postValue(R.string.unknown_error) + } + } + } + /* * Dedicated method to get app details from cleanapk using package name. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5509 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..6144d4740c90dd856acd0829d7848be7565682d4 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListFragment.kt @@ -195,7 +195,7 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, negativeButtonText = getString(android.R.string.ok), negativeButtonBlock = {}, @@ -260,6 +260,49 @@ class ApplicationListFragment : TimeoutFragment(R.layout.fragment_application_li } } + /* + * Load open source apps in case authentication fails. + * Issue: https://gitlab.e.foundation/e/os/backlog/-/issues/5413 [2] + */ + override fun noAuthRefresh(): Boolean { + + if (args.source != "Open Source" && args.source != "PWA") { + /* + * Prevent running this method for GPlay categories. + * For first time run of App Lounge, authData and authValidity will be both null, + * so this method will be executed. + * If the body of the method is allowed to run for GPlay categories, + * 1. we will fetch cleanapk data, + * 2. that data will be shown and isDetailsLoaded will be set to true, + * 3. Then when refreshData() will be called it will not run as isDetailsLoaded is true. + * + * On the other hand, if GPlay really cannot be reached, then categorie list + * will only show open source or PWAs categories. Then this method body can run fine. + */ + return false + } + + if (!isDetailsLoaded) { + showLoadingUI() + viewModel.getListOSS( + args.category, + args.source + ) + } + + appProgressViewModel.downloadProgress.observe(viewLifecycleOwner) { + updateProgressOfDownloadingItems(binding.recyclerView, it) + } + + /* + * This method is guaranteed to run only for open source apps. If there is timeout + * from cleanapk, it will be handled by this fragment itself in onTimeout(). + * In normal case, we can assume that the apps details will be fetched from cleanapk, + * hence we don't need to try fetching from GPlay (it is open source category any way.) + */ + return true + } + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE 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..9aad626004f045196183c5f33bf1c8c72677b3c0 100644 --- a/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt +++ b/app/src/main/java/foundation/e/apps/applicationlist/ApplicationListViewModel.kt @@ -97,6 +97,21 @@ class ApplicationListViewModel @Inject constructor( } } + /* + * Get apps list from cleanapk only. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + fun getListOSS(category: String, source: String) { + viewModelScope.launch(Dispatchers.IO) { + appListLiveData.postValue( + fusedAPIRepository.getAppsListOSS( + category, + source, + ) + ) + } + } + /** * 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 diff --git a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt index 0335553b1a9fec7e0dc4a209b4a338ad05c6be5b..d180f0eda3c01dddcb840b81efd69aa47cf42ce4 100644 --- a/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/AppsFragment.kt @@ -92,7 +92,7 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { negativeButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, allowCancel = true, ) @@ -107,6 +107,12 @@ class AppsFragment : TimeoutFragment(R.layout.fragment_apps) { ) } + override fun noAuthRefresh(): Boolean { + showLoadingUI() + categoriesViewModel.getCategoriesListOSS(Category.Type.APPLICATION) + return false // returning false so as to try to get GPlay auth data or show timeout dialog + } + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE diff --git a/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt b/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt index ec72f9150b308b3c46fabdf755f2ff95a350d32c..f918be5ab54c7ee4d57ec168e7b175d358a55e01 100644 --- a/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/categories/CategoriesViewModel.kt @@ -44,6 +44,12 @@ class CategoriesViewModel @Inject constructor( } } + fun getCategoriesListOSS(type: Category.Type) { + viewModelScope.launch { + categoriesList.postValue(fusedAPIRepository.getCategoriesListOSS(type)) + } + } + fun isCategoriesEmpty(): Boolean { return categoriesList.value?.first?.isEmpty() ?: true } diff --git a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt index 1094519744559f0633b6132f7bca100c2cfa09d1..7f813a5f83095b3c96a2702ae38c960db3f1a227 100644 --- a/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt +++ b/app/src/main/java/foundation/e/apps/categories/GamesFragment.kt @@ -87,7 +87,7 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { negativeButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, allowCancel = true, ) @@ -102,6 +102,12 @@ class GamesFragment : TimeoutFragment(R.layout.fragment_games) { ) } + override fun noAuthRefresh(): Boolean { + showLoadingUI() + categoriesViewModel.getCategoriesListOSS(Category.Type.GAME) + return false // returning false so as to try to get GPlay auth data or show timeout dialog + } + private fun showLoadingUI() { binding.shimmerLayout.startShimmer() binding.shimmerLayout.visibility = View.VISIBLE 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..7e9886235cbe8ffa1008525e7ac5294ca9b9447a 100644 --- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt @@ -186,7 +186,7 @@ class HomeFragment : TimeoutFragment(R.layout.fragment_home), FusedAPIInterface positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, negativeButtonText = if (homeViewModel.getApplicationCategoryPreference() == FusedAPIImpl.APP_TYPE_ANY) { 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..82236777de0a369ed5f120054816902533b1ba65 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt @@ -193,11 +193,7 @@ class SearchFragment : } } }) - if (searchText.isNotBlank() && !it.isSuccess()) { - /* - * If blank check is not performed then timeout dialog keeps - * popping up whenever search tab is opened. - */ + if (!it.isSuccess()) { onTimeout() } } @@ -215,7 +211,11 @@ class SearchFragment : } override fun onTimeout() { - if (!isTimeoutDialogDisplayed()) { + if (searchText.isNotBlank() && !isTimeoutDialogDisplayed()) { + /* + * If blank check is not performed then timeout dialog keeps + * popping up whenever search tab is opened. + */ binding.loadingProgressBar.isVisible = false stopLoadingUI() displayTimeoutAlertDialog( @@ -226,7 +226,7 @@ class SearchFragment : positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, negativeButtonText = getString(android.R.string.ok), negativeButtonBlock = {}, @@ -237,7 +237,13 @@ class SearchFragment : override fun refreshData(authData: AuthData) { showLoadingUI() - searchViewModel.getSearchResults(searchText, authData, this) + searchViewModel.getSearchResults(searchText, authData, viewLifecycleOwner) + } + + override fun noAuthRefresh(): Boolean { + showLoadingUI() + searchViewModel.getSearchResultsOSS(searchText, viewLifecycleOwner) + return false // return false to allow retrying to fetch auth data, else show timeout. } private fun showLoadingUI() { 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..31966183b2eff97cf9a8fea8f6ff5e4907557362 100644 --- a/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/search/SearchViewModel.kt @@ -59,4 +59,16 @@ class SearchViewModel @Inject constructor( } } } + + /* + * Get cleanapk search results only. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + fun getSearchResultsOSS(query: String, lifecycleOwner: LifecycleOwner) { + viewModelScope.launch(Dispatchers.Main) { + fusedAPIRepository.getSearchResultsOSS(query).observe(lifecycleOwner) { + searchResult.postValue(it) + } + } + } } 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..53b3eba822b9837182bde44ee50f680d7f358a84 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt @@ -74,6 +74,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte private val appProgressViewModel: AppProgressViewModel by viewModels() private var isDownloadObserverAdded = false + private var cleanapkFailed = false override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -86,7 +87,7 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte */ mainActivityViewModel.internetConnection.observe(viewLifecycleOwner) { - if (!updatesViewModel.updatesList.value?.first.isNullOrEmpty()) { + if (!updatesViewModel.updatesList.value?.data.isNullOrEmpty()) { return@observe } refreshDataOrRefreshToken(mainActivityViewModel) @@ -132,33 +133,33 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte } updatesViewModel.updatesList.observe(viewLifecycleOwner) { - listAdapter?.setData(it.first) - if (!isDownloadObserverAdded) { - observeDownloadList() - isDownloadObserverAdded = true - } - stopLoadingUI() - if (!it.first.isNullOrEmpty()) { - binding.button.isEnabled = true - binding.noUpdates.visibility = View.GONE - } else { - binding.noUpdates.visibility = View.VISIBLE - binding.button.isEnabled = false + if (!it.isSuccess()) { + cleanapkFailed = it.message.toBoolean() + onTimeout() } - - WorkManager.getInstance(requireContext()) - .getWorkInfosForUniqueWorkLiveData(INSTALL_WORK_NAME).observe(viewLifecycleOwner) { workInfoList -> - lifecycleScope.launchWhenResumed { - binding.button.isEnabled = !( - it.first.isNullOrEmpty() || - updatesViewModel.checkWorkInfoListHasAnyUpdatableWork(workInfoList) - ) - } + if (it.isValidData()) { + listAdapter?.setData(it.data!!) + if (!isDownloadObserverAdded) { + observeDownloadList() + isDownloadObserverAdded = true + } + if (it.data!!.isNotEmpty()) { + binding.button.isEnabled = true + binding.noUpdates.visibility = View.GONE + } else { + binding.noUpdates.visibility = View.VISIBLE + binding.button.isEnabled = false } - if (it.second != ResultStatus.OK) { - onTimeout() + WorkManager.getInstance(requireContext()) + .getWorkInfosForUniqueWorkLiveData(INSTALL_WORK_NAME).observe(viewLifecycleOwner) { + lifecycleScope.launchWhenResumed { + binding.button.isEnabled = + !updatesViewModel.checkWorkInfoListHasAnyUpdatableWork(it) + } + } } + stopLoadingUI() } } @@ -169,21 +170,21 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte timeoutFragment = this, activity = requireActivity(), message = - if (updatesViewModel.getApplicationCategoryPreference() == FusedAPIImpl.APP_TYPE_ANY) { - getString(R.string.timeout_desc_gplay) - } else { + if (cleanapkFailed) { getString(R.string.timeout_desc_cleanapk) + } else { + getString(R.string.timeout_desc_gplay) }, positiveButtonText = getString(R.string.retry), positiveButtonBlock = { showLoadingUI() resetTimeoutDialogLock() - mainActivityViewModel.retryFetchingTokenAfterTimeout() + mainActivityViewModel.checkTokenOnTimeout() }, negativeButtonText = - if (updatesViewModel.getApplicationCategoryPreference() == FusedAPIImpl.APP_TYPE_ANY) { - getString(R.string.open_settings) - } else null, + if (cleanapkFailed) null + else getString(R.string.open_settings) + , negativeButtonBlock = { openSettings() }, @@ -201,6 +202,16 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte } } + override fun noAuthRefresh(): Boolean { + showLoadingUI() + updatesViewModel.getUpdatesOSS() + binding.button.setOnClickListener { + UpdatesWorkManager.startUpdateAllWork(requireContext().applicationContext) + binding.button.isEnabled = false + } + return false + } + private fun showLoadingUI() { binding.button.isEnabled = false binding.noUpdates.visibility = View.GONE @@ -223,11 +234,11 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), FusedAPIInte private fun observeDownloadList() { mainActivityViewModel.downloadList.observe(viewLifecycleOwner) { list -> - val appList = updatesViewModel.updatesList.value?.first?.toMutableList() ?: emptyList() + val appList = updatesViewModel.updatesList.value?.data?.toMutableList() ?: emptyList() appList.let { mainActivityViewModel.updateStatusOfFusedApps(appList, list) } - updatesViewModel.updatesList.apply { value = Pair(appList, value?.second) } + updatesViewModel.updatesList.apply { value?.setData(appList) } } } diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt index f630fda7ab3de39543cdb3fc26a1a713d3e3f752..f5f348297061991a4cd809a076d651f35ce330be 100644 --- a/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/updates/UpdatesViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope import androidx.work.WorkInfo import com.aurora.gplayapi.data.models.AuthData 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.updates.manager.UpdatesManagerRepository @@ -38,20 +39,28 @@ class UpdatesViewModel @Inject constructor( private val fusedAPIRepository: FusedAPIRepository ) : ViewModel() { - val updatesList: MutableLiveData, ResultStatus?>> = MutableLiveData() + val updatesList: MutableLiveData>> = MutableLiveData() fun getUpdates(authData: AuthData) { viewModelScope.launch { val updatesResult = updatesManagerRepository.getUpdates(authData) + val filteredList = updatesResult.data?.filter { !(!it.isFree && authData.isAnonymous) } ?: listOf() updatesList.postValue( - Pair( - updatesResult.first.filter { !(!it.isFree && authData.isAnonymous) }, - updatesResult.second - ) + ResultSupreme.replicate(updatesResult, filteredList) ) } } + /* + * Get updates only from cleanapk + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + fun getUpdatesOSS() { + viewModelScope.launch { + updatesList.postValue(updatesManagerRepository.getUpdatesOSS()) + } + } + suspend fun checkWorkInfoListHasAnyUpdatableWork(workInfoList: List): Boolean { workInfoList.forEach { workInfo -> if (listOf( @@ -67,7 +76,7 @@ class UpdatesViewModel @Inject constructor( private fun checkWorkIsForUpdateByTag(tags: List): Boolean { updatesList.value?.let { - it.first.find { fusedApp -> tags.contains(fusedApp._id) }?.let { foundApp -> + it.data?.find { fusedApp -> tags.contains(fusedApp._id) }?.let { foundApp -> return listOf( Status.INSTALLED, Status.UPDATABLE diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt index 2c2ce9d2e14cf6d6b88586242e62486381e28a87..ac48a83e863f5074039d043eb86a5d471bb4cd52 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt @@ -20,6 +20,7 @@ package foundation.e.apps.updates.manager import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.api.faultyApps.FaultyAppRepository +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.pkg.PkgManagerModule @@ -37,10 +38,11 @@ class UpdatesManagerImpl @Inject constructor( private val TAG = UpdatesManagerImpl::class.java.simpleName // TODO: MAKE THIS LOGIC MORE SANE - suspend fun getUpdates(authData: AuthData): Pair, ResultStatus> { + suspend fun getUpdates(authData: AuthData): ResultSupreme> { val pkgList = mutableListOf() val updateList = mutableListOf() var status = ResultStatus.OK + var cleanapkFailed = false val userApplications = pkgManagerModule.getAllUserApps() userApplications.forEach { pkgList.add(it.packageName) } @@ -59,6 +61,7 @@ class UpdatesManagerImpl @Inject constructor( cleanAPKResult.second.let { if (it != ResultStatus.OK) { status = it + cleanapkFailed = true } } @@ -77,9 +80,33 @@ class UpdatesManagerImpl @Inject constructor( } } } + + return ResultSupreme.create(status, getNonFaultyApps(updateList), cleanapkFailed.toString()) + } + + /* + * Get updates only from cleanapk. + * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5413 [2] + */ + suspend fun getUpdatesOSS(): ResultSupreme> { + val updateList = mutableListOf() + val pkgList = pkgManagerModule.getAllUserApps().map { it.packageName } + + return if (pkgList.isNotEmpty()) { + fusedAPIRepository.getApplicationDetailsOSS(pkgList).run { + this.data?.forEach { + if (it.status == Status.UPDATABLE) updateList.add(it) + } + ResultSupreme.replicate(this, getNonFaultyApps(updateList)) + } + } else { + ResultSupreme.Success(listOf()) + } + } + + private suspend fun getNonFaultyApps(list: List): List { val faultyAppsPackageNames = faultyAppRepository.getAllFaultyApps().map { it.packageName } - val nonFaultyUpdateList = updateList.filter { !faultyAppsPackageNames.contains(it.package_name) } - return Pair(nonFaultyUpdateList, status) + return list.filter { !faultyAppsPackageNames.contains(it.package_name) } } fun getApplicationCategoryPreference(): String { diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt index 649c82261d8e19dd5af6bf3011ccd870b77e4e40..f1e3d45d592c0b182b0069887835b53aa1301131 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerRepository.kt @@ -19,18 +19,22 @@ package foundation.e.apps.updates.manager import com.aurora.gplayapi.data.models.AuthData +import foundation.e.apps.api.ResultSupreme import foundation.e.apps.api.fused.data.FusedApp -import foundation.e.apps.utils.enums.ResultStatus import javax.inject.Inject class UpdatesManagerRepository @Inject constructor( private val updatesManagerImpl: UpdatesManagerImpl ) { - suspend fun getUpdates(authData: AuthData): Pair, ResultStatus> { + suspend fun getUpdates(authData: AuthData): ResultSupreme> { return updatesManagerImpl.getUpdates(authData) } + suspend fun getUpdatesOSS(): ResultSupreme> { + return updatesManagerImpl.getUpdatesOSS() + } + fun getApplicationCategoryPreference(): String { return updatesManagerImpl.getApplicationCategoryPreference() } diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt index f733705214251057dc8de2395478376d5b6641c5..3266d37372a176dff70bd1b411b9ca08a96bca96 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesWorker.kt @@ -59,7 +59,7 @@ class UpdatesWorker @AssistedInject constructor( loadSettings() val authData = getAuthData() val appsNeededToUpdate = updatesManagerRepository.getUpdates(authData) - .first.filter { !(!it.isFree && authData.isAnonymous) } + .data?.filter { !(!it.isFree && authData.isAnonymous) } ?: listOf() val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) /* * Show notification only if enabled. diff --git a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt index a6abdd6bd76e21523f461f72b7306d1e13e21cdc..410807d2d16038cdd50ebc390ddc4cbbb5c48690 100644 --- a/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt +++ b/app/src/main/java/foundation/e/apps/utils/modules/CommonUtilsModule.kt @@ -47,7 +47,7 @@ import javax.inject.Singleton object CommonUtilsModule { val LIST_OF_NULL = listOf("null") - const val timeoutDurationInMillis: Long = 25000 + const val timeoutDurationInMillis: Long = 10000 // Issue: https://gitlab.e.foundation/e/backlog/-/issues/5709 const val NETWORK_CODE_SUCCESS = 200 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..f3245ef480648ee2f73881a95ed7d2c3023c0dd6 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 @@ -70,20 +70,42 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { */ abstract fun refreshData(authData: AuthData) + /* + * Optional function to execute if above refreshData could not be executed because + * authData could not be obtained from network + * i.e mainActivityViewModel.authData.value is null. + * + * Uses: Used to show cleanapk data if GPlay is unavailable. + * + * If returns true, it means that a fallback logic has been completely implemented, + * then we do not attempt to refresh the token. + * If returns false, then after running the function, we attempt to refresh GPlay token. + */ + open fun noAuthRefresh(): Boolean = false + /* * Checks if network connectivity is present. * -- If yes, then checks if valid authData is present. * ---- If yes, then dismiss timeout dialog (if showing) and call refreshData() - * ---- If no, then request new token data. + * ---- If no: + * ------ Run optional noAuthRefresh() function if not null. + * ------ If it returns false or is null, then refresh GPlay token. + * ------ Don't do anything if noRefreshAuth() returns true. */ fun refreshDataOrRefreshToken(mainActivityViewModel: MainActivityViewModel) { if (mainActivityViewModel.internetConnection.value == true) { - mainActivityViewModel.authData.value?.let { authData -> + val authData: AuthData? = mainActivityViewModel.authData.value + if (authData != null && mainActivityViewModel.authValidity.value == true) { dismissTimeoutDialog() refreshData(authData) - } ?: run { - if (mainActivityViewModel.authValidity.value != null) { // checking at least authvalidity is checked for once - mainActivityViewModel.retryFetchingTokenAfterTimeout() + } else { + val noAuthRefreshResult = noAuthRefresh() + + if (!noAuthRefreshResult && + mainActivityViewModel.authValidity.value != null + // checking at least authValidity is checked for once + ) { + mainActivityViewModel.checkTokenOnTimeout() } } }