From 17492e275638a3aaa7d2556810bea52301613964 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Fri, 25 Oct 2024 14:40:13 +0600 Subject: [PATCH 01/10] fix: show only gplay result on cleanapk failure --- .../foundation/e/apps/data/NetworkHandler.kt | 1 + .../data/application/search/SearchApiImpl.kt | 33 +++++--- .../login/exceptions/CleanApkException.kt | 4 +- .../data/login/exceptions/GPlayException.kt | 2 + .../login/exceptions/LoginExceptionFactory.kt | 63 +++++++++++++++ .../apps/ui/parentFragment/TimeoutFragment.kt | 29 ++++++- .../e/apps/ui/search/SearchViewModel.kt | 80 ++++++++----------- .../e/apps/utils/NetworkStatusManager.kt | 3 +- app/src/main/res/values/strings.xml | 4 + 9 files changed, 153 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index f5c5cdd50..c31a64f46 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -75,6 +75,7 @@ private fun extractErrorMessage(e: Exception): String { is SocketTimeoutException -> TIMEOUT else -> UNKNOWN } + return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + " $STATUS $status" } diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index 0c16bb68f..909c07138 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import timber.log.Timber +import java.io.IOException import javax.inject.Inject import javax.inject.Singleton @@ -158,7 +159,8 @@ class SearchApiImpl @Inject constructor( query ), appLoungePreference.isGplaySelected() - ) + ), + exception = result.exception ) } @@ -187,7 +189,8 @@ class SearchApiImpl @Inject constructor( query ), appLoungePreference.isGplaySelected() || appLoungePreference.isPWASelected() - ) + ), + exception = result.exception ) } @@ -369,7 +372,8 @@ class SearchApiImpl @Inject constructor( val dummySearchPackageNames = DUMMY_SEARCH_EXPECTED_APPS.map { it.second } val searchedAppsPackageNames = searchedApps.map { it.packageName } - val isSearchContainingResults = searchedAppsPackageNames.containsAll(dummySearchPackageNames) + val isSearchContainingResults = + searchedAppsPackageNames.containsAll(dummySearchPackageNames) if (!isSearchContainingResults) { Timber.d("Search didn't return enough results, refreshing token...") @@ -384,18 +388,23 @@ class SearchApiImpl @Inject constructor( */ private suspend fun replaceWithFDroid(gPlayApps: List): List { - if (gPlayApps.isEmpty()) return emptyList() + try { + if (gPlayApps.isEmpty()) return emptyList() - val packageNames = gPlayApps.map { it.packageName } - val response = appSources.cleanApkAppsRepo.checkAvailablePackages(packageNames) + val packageNames = gPlayApps.map { it.packageName } + val response = appSources.cleanApkAppsRepo.checkAvailablePackages(packageNames) - val availableApps = response.body()?.apps ?: emptyList() + val availableApps = response.body()?.apps ?: emptyList() - return gPlayApps.map { gPlayApp -> - availableApps.find { it.package_name == gPlayApp.packageName }?.apply { - isGplayReplaced = true - updateSource(context) - } ?: gPlayApp.toApplication(context) + return gPlayApps.map { gPlayApp -> + availableApps.find { it.package_name == gPlayApp.packageName }?.apply { + isGplayReplaced = true + updateSource(context) + } ?: gPlayApp.toApplication(context) + } + } catch (e: IOException) { + Timber.e(e, "Failed to replace") + return gPlayApps.map { it.toApplication(context) } } } diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt index 293819914..bc4839227 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/CleanApkException.kt @@ -20,7 +20,9 @@ package foundation.e.apps.data.login.exceptions /** * This exception is for all CleanApk data loading exceptions. */ -class CleanApkException( +open class CleanApkException( val isTimeout: Boolean, message: String? = null, ) : LoginException(message) + +class CleanApkIOException(message: String) : CleanApkException(false, message) diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt index 345208778..2c39dc76e 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/GPlayException.kt @@ -24,3 +24,5 @@ open class GPlayException( val isTimeout: Boolean, message: String? = null, ) : LoginException(message) + +open class GPlayIOException(message: String): GPlayException(false, message) diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt new file mode 100644 index 000000000..53989ea7d --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt @@ -0,0 +1,63 @@ +/* + * Copyright MURENA SAS 2024 + * Apps Quickly and easily install Android apps onto your device! + * + * 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.data.login.exceptions + +import foundation.e.apps.data.ResultSupreme +import java.io.IOException + +object LoginExceptionFactory { + val DATA_LOAD_ERROR = "Data load error" + + fun getLoginException( + isCleanApk: Boolean, + searchResultSupreme: ResultSupreme<*> + ): LoginException = when { + searchResultSupreme.exception != null && + searchResultSupreme.exception is IOException + -> getIOException(isCleanApk, searchResultSupreme.message) + + else -> getBaseException(isCleanApk, searchResultSupreme) + } + + private fun getIOException(isCleanApk: Boolean, message: String): LoginException { + return if (isCleanApk) { + CleanApkIOException(message) + } else { + GPlayIOException(message) + } + } + + private fun getBaseException( + isCleanApk: Boolean, + searchResultSupreme: ResultSupreme<*> + ): LoginException { + return if (isCleanApk) { + CleanApkException( + searchResultSupreme.isTimeout(), + searchResultSupreme.message.ifEmpty { DATA_LOAD_ERROR } + ) + } else { + GPlayException( + searchResultSupreme.isTimeout(), + searchResultSupreme.message.ifEmpty { DATA_LOAD_ERROR } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 6d3d162b8..005905df9 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -34,13 +34,15 @@ import foundation.e.apps.R import foundation.e.apps.data.enums.User import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.login.PlayStoreAuthenticator -import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.data.login.exceptions.CleanApkException +import foundation.e.apps.data.login.exceptions.CleanApkIOException import foundation.e.apps.data.login.exceptions.GPlayException +import foundation.e.apps.data.login.exceptions.GPlayIOException import foundation.e.apps.data.login.exceptions.GPlayLoginException import foundation.e.apps.data.login.exceptions.GPlayValidationException import foundation.e.apps.data.login.exceptions.UnknownSourceException import foundation.e.apps.databinding.DialogErrorLogBinding +import foundation.e.apps.ui.LoginViewModel import foundation.e.apps.ui.MainActivityViewModel import timber.log.Timber @@ -376,8 +378,8 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { } val predefinedDialog = AlertDialog.Builder(requireActivity()).apply { - setTitle(R.string.data_load_error) - setMessage(R.string.data_load_error_desc) + setTitle(getTitle(exception)) + setMessage(getMessage(exception)) setView(dialogView.root) setPositiveButton(R.string.retry) { _, _ -> showLoadingUI() @@ -396,6 +398,22 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { } } + private fun getTitle(exception: Exception): String { + return when(exception) { + is GPlayIOException -> getString(R.string.common_apps_unavailable) + is CleanApkIOException -> getString(R.string.open_source_apps_unavailable) + else -> getString(R.string.data_load_error) + } + } + + private fun getMessage(exception: Exception): String { + return when (exception) { + is GPlayIOException -> getString(R.string.common_apps_data_load_error_description) + is CleanApkIOException -> getString(R.string.open_source_apps_data_load_error_description) + else -> getString(R.string.data_load_error_desc) + } + } + /** * Common code to handle exceptions / errors during data loading. * Can be overridden in child fragments. @@ -407,6 +425,7 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { val gPlayException = exceptions.find { it is GPlayException }?.run { this as GPlayException } + val unknownSourceException = exceptions.find { it is UnknownSourceException } if (gPlayException?.message?.contains(STATUS_TOO_MANY_REQUESTS) == true) { @@ -418,6 +437,10 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * Cases to be defined from most restrictive to least restrictive. */ when { + //Handle IOException + cleanApkException is CleanApkIOException -> showDataLoadError(cleanApkException) + gPlayException is GPlayIOException -> showDataLoadError(gPlayException) + // Handle timeouts cleanApkException?.isTimeout == true -> showTimeout(cleanApkException) gPlayException?.isTimeout == true -> showTimeout(gPlayException) diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index 9d89d8f31..cc94ab47d 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -28,26 +28,24 @@ import com.aurora.gplayapi.data.models.SearchBundle import dagger.hilt.android.lifecycle.HiltViewModel import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository -import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.CleanApkException -import foundation.e.apps.data.login.exceptions.GPlayException -import foundation.e.apps.data.login.exceptions.UnknownSourceException +import foundation.e.apps.data.login.exceptions.LoginExceptionFactory import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject typealias SearchResult = ResultSupreme, Boolean>> @@ -64,7 +62,7 @@ class SearchViewModel @Inject constructor( MutableLiveData() val searchResult: LiveData = _searchResult - val gplaySearchLoaded : MutableLiveData = MutableLiveData(false) + val gplaySearchLoaded: MutableLiveData = MutableLiveData(false) private var lastAuthObjects: List? = null @@ -119,16 +117,15 @@ class SearchViewModel @Inject constructor( this.lastAuthObjects = authObjectList super.onLoadData(authObjectList, { successAuthList, _ -> + successAuthList.find { it is AuthObject.CleanApk }?.run { + fetchCleanApkData(query, null) + } successAuthList.find { it is AuthObject.GPlayAuth }?.run { - getSearchResults(query, result.data!! as AuthData) - return@onLoadData + nextSubBundle = null + fetchGplayData(query) } - successAuthList.find { it is AuthObject.CleanApk }?.run { - getSearchResults(query, null) - return@onLoadData - } }, retryBlock) } @@ -138,7 +135,7 @@ class SearchViewModel @Inject constructor( * without having to wait for all of the apps. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5171 */ - private fun getSearchResults( + private fun fetchCleanApkData( query: String, authData: AuthData? ) { @@ -153,27 +150,9 @@ class SearchViewModel @Inject constructor( if (!searchResultSupreme.isSuccess()) { val exception = - if (authData != null) { - GPlayException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } - ) - } else { - CleanApkException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifBlank { DATA_LOAD_ERROR } - ) - } - + LoginExceptionFactory.getLoginException(isCleanApk = true, searchResultSupreme) handleException(exception) } - - if (authData == null) { - return@launch - } - - nextSubBundle = null - fetchGplayData(query) } } @@ -191,25 +170,30 @@ class SearchViewModel @Inject constructor( } } - private suspend fun fetchGplayData(query: String) { - isLoading = true - val gplaySearchResult = applicationRepository.getGplaySearchResults(query, nextSubBundle) + private fun fetchGplayData(query: String) { + viewModelScope.launch(Dispatchers.IO) { + isLoading = true + val gplaySearchResult = + applicationRepository.getGplaySearchResults(query, nextSubBundle) - if (!gplaySearchResult.isSuccess()) { - handleException(gplaySearchResult.exception ?: UnknownSourceException()) - } + if (!gplaySearchResult.isSuccess()) { + val exception = + LoginExceptionFactory.getLoginException(isCleanApk = false, gplaySearchResult) + handleException(exception) + } - nextSubBundle = gplaySearchResult.data?.second + nextSubBundle = gplaySearchResult.data?.second - val currentAppList = updateCurrentAppList(gplaySearchResult) - val finalResult = ResultSupreme.Success( - Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) - ) + val currentAppList = updateCurrentAppList(gplaySearchResult) + val finalResult = ResultSupreme.Success( + Pair(currentAppList.toList(), nextSubBundle?.isNotEmpty() ?: false) + ) - hasGPlayBeenFetched = true - emitFilteredResults(finalResult) + hasGPlayBeenFetched = true + emitFilteredResults(finalResult) - isLoading = false + isLoading = false + } } private fun updateCurrentAppList(gplaySearchResult: GplaySearchResult): List { diff --git a/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt b/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt index 0473a1bce..0e2b7c5fd 100644 --- a/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt +++ b/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt @@ -68,8 +68,7 @@ object NetworkStatusManager { ) { super.onCapabilitiesChanged(network, networkCapabilities) val hasInternet = - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) Timber.d("Network: onCapabilitiesChanged: ${network.networkHandle}, hasInternet: $hasInternet") sendInternetStatus(hasInternet) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa41bcdf6..003e9090c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -235,5 +235,9 @@ Content Warning The app may contain nudity, profanity, slurs, violence, intense sexuality, political incorrectness, or other potentially disturbing subject matter. This is especially relevant in environments like workplaces, schools, religious and family settings. Gathering content rating for all the apps you\'ve installed. + Common apps unavailable + Open Source apps and PWA unavailable + An error occurred while loading Common apps. Only Open Source apps and PWA are available for now. + An error occurred while loading PWA and Open Source apps. Only Common apps available for now. -- GitLab From 5abe1933fbf04c4b78e5bb6d41f78c0c1fc82586 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 29 Oct 2024 12:38:38 +0600 Subject: [PATCH 02/10] chore: update string --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 003e9090c..1765fb911 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -238,6 +238,6 @@ Common apps unavailable Open Source apps and PWA unavailable An error occurred while loading Common apps. Only Open Source apps and PWA are available for now. - An error occurred while loading PWA and Open Source apps. Only Common apps available for now. + An error occurred while loading PWA and Open Source apps. Only Common apps are available for now. -- GitLab From 49a7a8ec340ec4206974a1b2f34e4fea971d1b6e Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 29 Oct 2024 12:57:33 +0600 Subject: [PATCH 03/10] refactor: improve exception creation in LoginExceptionFactory --- .../login/exceptions/LoginExceptionFactory.kt | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt index 53989ea7d..85c622212 100644 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt +++ b/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt @@ -1,19 +1,18 @@ /* - * Copyright MURENA SAS 2024 - * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2024 e Foundation * - * 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 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. + * 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 . + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . * */ @@ -23,7 +22,7 @@ import foundation.e.apps.data.ResultSupreme import java.io.IOException object LoginExceptionFactory { - val DATA_LOAD_ERROR = "Data load error" + private const val DATA_LOAD_ERROR = "Data load error" fun getLoginException( isCleanApk: Boolean, @@ -48,16 +47,13 @@ object LoginExceptionFactory { isCleanApk: Boolean, searchResultSupreme: ResultSupreme<*> ): LoginException { + val isTimeout = searchResultSupreme.isTimeout() + val message = searchResultSupreme.message.ifEmpty { DATA_LOAD_ERROR } + return if (isCleanApk) { - CleanApkException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifEmpty { DATA_LOAD_ERROR } - ) + CleanApkException(isTimeout, message) } else { - GPlayException( - searchResultSupreme.isTimeout(), - searchResultSupreme.message.ifEmpty { DATA_LOAD_ERROR } - ) + GPlayException(isTimeout, message) } } -} \ No newline at end of file +} -- GitLab From 48227b95c6da141ff02a9d2228a3592796d10ebc Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 29 Oct 2024 16:57:18 +0600 Subject: [PATCH 04/10] refactor: modify Timber's log level from error to warning to reduce Sentry hit --- .../foundation/e/apps/data/application/search/SearchApiImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index 909c07138..11e38c9d1 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -403,7 +403,7 @@ class SearchApiImpl @Inject constructor( } ?: gPlayApp.toApplication(context) } } catch (e: IOException) { - Timber.e(e, "Failed to replace") + Timber.w(e, "Failed to replace Google apps with their F-Droid counterparts.") return gPlayApps.map { it.toApplication(context) } } } -- GitLab From 89ed0f467080d0004d76b64ddba1c40977048dae Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 29 Oct 2024 17:02:26 +0600 Subject: [PATCH 05/10] refactor: move SearchResult typealias to SearchApi for better cohesion --- .../e/apps/data/application/ApplicationRepository.kt | 2 +- .../foundation/e/apps/data/application/search/SearchApi.kt | 3 ++- .../foundation/e/apps/data/application/search/SearchApiImpl.kt | 1 - .../main/java/foundation/e/apps/ui/search/SearchViewModel.kt | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt index 30f068df2..9e32ed817 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationRepository.kt @@ -39,7 +39,7 @@ import foundation.e.apps.data.application.search.GplaySearchResult import foundation.e.apps.data.application.search.SearchApi import foundation.e.apps.data.application.utils.CategoryType import foundation.e.apps.data.install.models.AppInstall -import foundation.e.apps.ui.search.SearchResult +import foundation.e.apps.data.application.search.SearchResult import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt index 34898eefd..4ea26f61b 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApi.kt @@ -23,10 +23,11 @@ import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.SearchBundle import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.data.Application -import foundation.e.apps.ui.search.SearchResult typealias GplaySearchResult = ResultSupreme, Set>> +typealias SearchResult = ResultSupreme, Boolean>> + interface SearchApi { companion object { const val APP_TYPE_ANY = "any" diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index 11e38c9d1..cd32f41a3 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -39,7 +39,6 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.AuthObject import foundation.e.apps.data.preference.AppLoungePreference -import foundation.e.apps.ui.search.SearchResult import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.Deferred diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index cc94ab47d..e14d89592 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -30,6 +30,7 @@ import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.application.search.GplaySearchResult +import foundation.e.apps.data.application.search.SearchResult import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository @@ -47,8 +48,6 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject -typealias SearchResult = ResultSupreme, Boolean>> - @HiltViewModel class SearchViewModel @Inject constructor( private val applicationRepository: ApplicationRepository, -- GitLab From e8636288b0a7d2bce1a9c581bf1ddb3146bed335 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 29 Oct 2024 18:03:23 +0600 Subject: [PATCH 06/10] refactor: pass title and message separately only for CleanAPK and GPlay related IOExceptions --- .../apps/ui/parentFragment/TimeoutFragment.kt | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt index 005905df9..b720fca17 100644 --- a/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/parentFragment/TimeoutFragment.kt @@ -22,6 +22,7 @@ import android.graphics.Paint import android.net.Uri import android.widget.TextView import androidx.annotation.LayoutRes +import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -358,7 +359,11 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * instance if it deems fit. Else it may return null, at which case no error dialog * is shown to the user. */ - fun showDataLoadError(exception: Exception) { + private fun showDataLoadError( + exception: Exception, + @StringRes dialogTitle: Int = R.string.data_load_error, + @StringRes dialogMessage: Int = R.string.data_load_error_desc + ) { val dialogView = DialogErrorLogBinding.inflate(requireActivity().layoutInflater) dialogView.apply { moreInfo.setOnClickListener { @@ -378,8 +383,8 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { } val predefinedDialog = AlertDialog.Builder(requireActivity()).apply { - setTitle(getTitle(exception)) - setMessage(getMessage(exception)) + setTitle(dialogTitle) + setMessage(dialogMessage) setView(dialogView.root) setPositiveButton(R.string.retry) { _, _ -> showLoadingUI() @@ -398,22 +403,6 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { } } - private fun getTitle(exception: Exception): String { - return when(exception) { - is GPlayIOException -> getString(R.string.common_apps_unavailable) - is CleanApkIOException -> getString(R.string.open_source_apps_unavailable) - else -> getString(R.string.data_load_error) - } - } - - private fun getMessage(exception: Exception): String { - return when (exception) { - is GPlayIOException -> getString(R.string.common_apps_data_load_error_description) - is CleanApkIOException -> getString(R.string.open_source_apps_data_load_error_description) - else -> getString(R.string.data_load_error_desc) - } - } - /** * Common code to handle exceptions / errors during data loading. * Can be overridden in child fragments. @@ -437,9 +426,18 @@ abstract class TimeoutFragment(@LayoutRes layoutId: Int) : Fragment(layoutId) { * Cases to be defined from most restrictive to least restrictive. */ when { - //Handle IOException - cleanApkException is CleanApkIOException -> showDataLoadError(cleanApkException) - gPlayException is GPlayIOException -> showDataLoadError(gPlayException) + // Handle IOException + cleanApkException is CleanApkIOException -> showDataLoadError( + exception = cleanApkException, + dialogTitle = R.string.open_source_apps_unavailable, + dialogMessage = R.string.open_source_apps_data_load_error_description, + ) + + gPlayException is GPlayIOException -> showDataLoadError( + exception = gPlayException, + dialogTitle = R.string.common_apps_unavailable, + dialogMessage = R.string.common_apps_data_load_error_description + ) // Handle timeouts cleanApkException?.isTimeout == true -> showTimeout(cleanApkException) -- GitLab From ae084a6de3b8b7ba1734822e4747a6ce420ede81 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 29 Oct 2024 18:11:56 +0600 Subject: [PATCH 07/10] refactor: modify Timber's log level from error to warning to reduce Sentry hit --- .../main/java/foundation/e/apps/di/network/InterceptorModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt b/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt index c42a9f0fb..b2cb62ac1 100644 --- a/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt +++ b/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt @@ -65,7 +65,7 @@ class InterceptorModule { val response = try { chain.proceed(builder.build()) } catch (ioException: IOException) { - Timber.e(ioException) + Timber.w(ioException) return@Interceptor createResponseForIOException(chain.request()) } -- GitLab From 8a4cf8c8450c5f3e549a57b1b7dd6f6678b93ba9 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 31 Oct 2024 16:14:31 +0600 Subject: [PATCH 08/10] refactor: handle CleanAPK and GPlay API exceptions inside data layer Removed custom exception creation from InterceptorModule. It's not needed because app doesn't crash or do anything suspicious if any exception occurs here. Related test is also removed. Removed unnecessary LoginExceptionFactory.kt as the exceptions are created at SearchApiImpl and propagated upwards as ResultSupreme.Error. --- .../data/application/search/SearchApiImpl.kt | 18 ++++-- .../login/exceptions/LoginExceptionFactory.kt | 59 ------------------- .../e/apps/di/network/InterceptorModule.kt | 28 +-------- .../e/apps/ui/search/SearchViewModel.kt | 11 ++-- .../apps/di/network/InterceptorModuleTest.kt | 25 -------- 5 files changed, 20 insertions(+), 121 deletions(-) delete mode 100644 app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt diff --git a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt index cd32f41a3..b040cd662 100644 --- a/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/search/SearchApiImpl.kt @@ -38,6 +38,8 @@ import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.login.AuthObject +import foundation.e.apps.data.login.exceptions.CleanApkIOException +import foundation.e.apps.data.login.exceptions.GPlayIOException import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus @@ -47,7 +49,6 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import timber.log.Timber -import java.io.IOException import javax.inject.Inject import javax.inject.Singleton @@ -94,7 +95,10 @@ class SearchApiImpl @Inject constructor( query: String, authData: AuthData ): SearchResult { - var finalSearchResult: SearchResult = ResultSupreme.Error() + var finalSearchResult: SearchResult = ResultSupreme.Error( + message = "", + exception = CleanApkIOException("Unable to reach CleanAPK API") + ) val packageSpecificResults = fetchPackageSpecificResult(authData, query).data?.first ?: emptyList() @@ -335,7 +339,7 @@ class SearchApiImpl @Inject constructor( query: String, nextPageSubBundle: Set? ): GplaySearchResult { - return handleNetworkResult { + val result = handleNetworkResult { coroutineScope { launch(Dispatchers.IO) { doDummySearch() } } val searchResults = @@ -356,6 +360,11 @@ class SearchApiImpl @Inject constructor( return@handleNetworkResult Pair(fusedAppList.toList(), searchResults.second.toSet()) } + + return if (result.isSuccess()) result else ResultSupreme.Error( + message = "", + exception = GPlayIOException("Unable to reach Google Play API") + ) } // Initiate a dummy search to ensure Google Play returns enough results for the search query @@ -386,7 +395,6 @@ class SearchApiImpl @Inject constructor( * else will show the GPlay app itself. */ private suspend fun replaceWithFDroid(gPlayApps: List): List { - try { if (gPlayApps.isEmpty()) return emptyList() @@ -401,7 +409,7 @@ class SearchApiImpl @Inject constructor( updateSource(context) } ?: gPlayApp.toApplication(context) } - } catch (e: IOException) { + } catch (e: Exception) { Timber.w(e, "Failed to replace Google apps with their F-Droid counterparts.") return gPlayApps.map { it.toApplication(context) } } diff --git a/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt b/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt deleted file mode 100644 index 85c622212..000000000 --- a/app/src/main/java/foundation/e/apps/data/login/exceptions/LoginExceptionFactory.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2024 e Foundation - * - * 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.data.login.exceptions - -import foundation.e.apps.data.ResultSupreme -import java.io.IOException - -object LoginExceptionFactory { - private const val DATA_LOAD_ERROR = "Data load error" - - fun getLoginException( - isCleanApk: Boolean, - searchResultSupreme: ResultSupreme<*> - ): LoginException = when { - searchResultSupreme.exception != null && - searchResultSupreme.exception is IOException - -> getIOException(isCleanApk, searchResultSupreme.message) - - else -> getBaseException(isCleanApk, searchResultSupreme) - } - - private fun getIOException(isCleanApk: Boolean, message: String): LoginException { - return if (isCleanApk) { - CleanApkIOException(message) - } else { - GPlayIOException(message) - } - } - - private fun getBaseException( - isCleanApk: Boolean, - searchResultSupreme: ResultSupreme<*> - ): LoginException { - val isTimeout = searchResultSupreme.isTimeout() - val message = searchResultSupreme.message.ifEmpty { DATA_LOAD_ERROR } - - return if (isCleanApk) { - CleanApkException(isTimeout, message) - } else { - GPlayException(isTimeout, message) - } - } -} diff --git a/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt b/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt index b2cb62ac1..b01b87c55 100644 --- a/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt +++ b/app/src/main/java/foundation/e/apps/di/network/InterceptorModule.kt @@ -25,13 +25,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import foundation.e.apps.BuildConfig import okhttp3.Interceptor -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.logging.HttpLoggingInterceptor -import timber.log.Timber -import java.io.IOException import java.util.Locale import javax.inject.Singleton @@ -42,8 +36,6 @@ class InterceptorModule { companion object { private const val HEADER_USER_AGENT = "User-Agent" private const val HEADER_ACCEPT_LANGUAGE = "Accept-Language" - const val ERROR_RESPONSE_CODE = 999 // Arbitrary value, not to mix with HTTP status code - const val ERROR_RESPONSE_MESSAGE = "IOException occurred." val HEADER_USER_AGENT_VALUE = "Dalvik/2.1.0 (Linux; U; Android ${Build.VERSION.RELEASE};)" } @@ -52,7 +44,7 @@ class InterceptorModule { @Provides fun provideInterceptor(): Interceptor { return Interceptor { chain -> - val builder = + val request = chain .request() .newBuilder() @@ -61,26 +53,12 @@ class InterceptorModule { HEADER_USER_AGENT_VALUE ) .header(HEADER_ACCEPT_LANGUAGE, Locale.getDefault().language) + .build() - val response = try { - chain.proceed(builder.build()) - } catch (ioException: IOException) { - Timber.w(ioException) - return@Interceptor createResponseForIOException(chain.request()) - } - - return@Interceptor response + return@Interceptor chain.proceed(request) } } - private fun createResponseForIOException(request: Request) = Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(ERROR_RESPONSE_CODE) - .message(ERROR_RESPONSE_MESSAGE) - .body(ERROR_RESPONSE_MESSAGE.toResponseBody()) - .build() - @Provides @Singleton fun provideLoggingInterceptor(): HttpLoggingInterceptor { diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt index e14d89592..f4562656a 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchViewModel.kt @@ -35,7 +35,6 @@ import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.exodus.repositories.IAppPrivacyInfoRepository import foundation.e.apps.data.exodus.repositories.PrivacyScoreRepository import foundation.e.apps.data.login.AuthObject -import foundation.e.apps.data.login.exceptions.LoginExceptionFactory import foundation.e.apps.di.CommonUtilsModule.LIST_OF_NULL import foundation.e.apps.ui.parentFragment.LoadingViewModel import kotlinx.coroutines.Dispatchers @@ -148,9 +147,7 @@ class SearchViewModel @Inject constructor( emitFilteredResults(searchResultSupreme) if (!searchResultSupreme.isSuccess()) { - val exception = - LoginExceptionFactory.getLoginException(isCleanApk = true, searchResultSupreme) - handleException(exception) + searchResultSupreme.exception?.let { handleException(it) } } } } @@ -176,9 +173,9 @@ class SearchViewModel @Inject constructor( applicationRepository.getGplaySearchResults(query, nextSubBundle) if (!gplaySearchResult.isSuccess()) { - val exception = - LoginExceptionFactory.getLoginException(isCleanApk = false, gplaySearchResult) - handleException(exception) + gplaySearchResult.exception?.let { + handleException(it) + } } nextSubBundle = gplaySearchResult.data?.second diff --git a/app/src/test/java/foundation/e/apps/di/network/InterceptorModuleTest.kt b/app/src/test/java/foundation/e/apps/di/network/InterceptorModuleTest.kt index cf7afdbe8..929874288 100644 --- a/app/src/test/java/foundation/e/apps/di/network/InterceptorModuleTest.kt +++ b/app/src/test/java/foundation/e/apps/di/network/InterceptorModuleTest.kt @@ -19,8 +19,6 @@ package foundation.e.apps.di.network import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertTrue import okhttp3.Interceptor import okhttp3.Protocol import okhttp3.Request @@ -33,7 +31,6 @@ import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.whenever -import java.io.IOException import java.util.Locale class InterceptorModuleTest { @@ -87,26 +84,4 @@ class InterceptorModuleTest { assertEquals(Locale.getDefault().language, capturedRequest.header("Accept-Language")) assertEquals(response, interceptedResponse) } - - @Test - fun `provideInterceptor should return custom response on IOException`() { - // Mock IOException when proceeding with chain - whenever(chain.proceed(any())).thenThrow(IOException()) - - val interceptor = module.provideInterceptor() - - // Intercept the request, should catch IOException and return custom response - val interceptedResponse = interceptor.intercept(chain) - - assertNotNull(interceptedResponse) - assertEquals(InterceptorModule.ERROR_RESPONSE_CODE, interceptedResponse.code) - assertEquals( - InterceptorModule.ERROR_RESPONSE_MESSAGE, - interceptedResponse.message - ) - assertTrue( - interceptedResponse.body?.string() - ?.contains(InterceptorModule.ERROR_RESPONSE_MESSAGE) == true - ) - } } -- GitLab From 43d7662764bfd10d3ab5d4bec29775384a55f5d0 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 31 Oct 2024 16:20:31 +0600 Subject: [PATCH 09/10] refactor: revert NET_CAPABILITY_VALIDATED removal --- .../java/foundation/e/apps/utils/NetworkStatusManager.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt b/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt index 0e2b7c5fd..3b769604a 100644 --- a/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt +++ b/app/src/main/java/foundation/e/apps/utils/NetworkStatusManager.kt @@ -68,7 +68,11 @@ object NetworkStatusManager { ) { super.onCapabilitiesChanged(network, networkCapabilities) val hasInternet = - networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_INTERNET + ) && networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_VALIDATED + ) Timber.d("Network: onCapabilitiesChanged: ${network.networkHandle}, hasInternet: $hasInternet") sendInternetStatus(hasInternet) -- GitLab From ce71788999c41d4d71f353af6e97c6e9802856d0 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Thu, 31 Oct 2024 16:43:21 +0600 Subject: [PATCH 10/10] chore: add translation for CleanAPK and GPlay API error dialog in German, Spanish, French and Italian --- app/src/main/res/values-de/strings.xml | 8 +++++++- app/src/main/res/values-es/strings.xml | 8 +++++++- app/src/main/res/values-fr/strings.xml | 8 +++++++- app/src/main/res/values-it/strings.xml | 8 +++++++- app/src/main/res/values/strings.xml | 2 ++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 261a9270e..399766b08 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -205,4 +205,10 @@ System-App Achtung – Aktualisierung! App Lounge wird vom System beendet, um eine Aktualisierung durchzuführen. Bitte führen Sie keine anderen Aktionen aus, bis App Lounge aktualisert und beendet ist. - \ No newline at end of file + + + Gemeinsame Apps nicht verfügbar + Open-Source-Apps und PWA nicht verfügbar + Beim Laden der gemeinsamen Apps ist ein Fehler aufgetreten. Nur Open-Source-Apps und PWA sind momentan verfügbar. + Beim Laden von PWA und Open-Source-Apps ist ein Fehler aufgetreten. Nur gemeinsame Apps sind momentan verfügbar. + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ac9f8f586..6d29329f5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -203,4 +203,10 @@ Recopilando la clasificación de contenido de todas las aplicaciones que has instalado. Advertencia del contenido La aplicación puede contener desnudos, blasfemias, insultos, violencia, sexualidad intensa, incorrección política u otros temas potencialmente perturbadores. Esto es especialmente importante en entornos como lugares de trabajo, escuelas, entornos religiosos y familiares. - \ No newline at end of file + + + Aplicaciones comunes no disponibles + Aplicaciones de código abierto y PWA no disponibles + Se produjo un error al cargar las aplicaciones comunes. Solo las aplicaciones de código abierto y PWA están disponibles por ahora. + Se produjo un error al cargar las aplicaciones PWA y de código abierto. Solo las aplicaciones comunes están disponibles por ahora. + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4540074cb..f81846411 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -203,4 +203,10 @@ Cliquer sur \"%1$s\" ouvrira un onglet dans votre navigateur avec le nom du paquet de l\'application pré-rempli.<br /><br /> Cliquez sur \"Perform analysis\" pour lancer l\'analyse par Exodus.<br /><br /> Quand le bouton \"See the report\" apparaît (cela peut prendre un moment selon l\'application) vous pouvez fermer l\'onglet et retourner sur la description de l\'application dans %2$s où vous devriez voir la note de Confidentialité. Parfois Exodus peut échouer à analyser l\'application.<br /><br />NB : cela peut prendre jusqu\'à 10 min pour que le score apparaisse dans la description de l\'application. RENOUVELER LA SESSION Application système - \ No newline at end of file + + + Applications communes indisponibles + Applications Open Source et PWA indisponibles + Une erreur est survenue lors du chargement des applications communes. Seules les applications Open Source et PWA sont disponibles pour l’instant. + Une erreur est survenue lors du chargement des applications PWA et Open Source. Seules les applications communes sont disponibles pour l\'instant. + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d5505806e..34663ceda 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -203,4 +203,10 @@ Avviso sui contenuti App Lounge verrà chiusa dal sistema mentre installa l\'aggiornamento. Si prega di non effettuare alcuna attività su App Lounge finchè non sarà aggiornata e chiusa. Avviso sull\'aggiornamento! - \ No newline at end of file + + + App comuni non disponibili + App Open Source e PWA non disponibili + Si è verificato un errore durante il caricamento delle app comuni. Solo le app Open Source e PWA sono disponibili per ora. + Si è verificato un errore durante il caricamento delle app PWA e Open Source. Solo le app comuni sono disponibili per ora. + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1765fb911..2ab9f4a3a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -235,6 +235,8 @@ Content Warning The app may contain nudity, profanity, slurs, violence, intense sexuality, political incorrectness, or other potentially disturbing subject matter. This is especially relevant in environments like workplaces, schools, religious and family settings. Gathering content rating for all the apps you\'ve installed. + + Common apps unavailable Open Source apps and PWA unavailable An error occurred while loading Common apps. Only Open Source apps and PWA are available for now. -- GitLab