Loading app/build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -227,6 +227,7 @@ dependencies { // WorkManager implementation(libs.work.runtime.ktx) implementation libs.work.testing // Room ksp(libs.room.compiler) Loading app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt +13 −8 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ package foundation.e.apps.data.gitlab import android.content.Context import android.os.Build import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source Loading Loading @@ -63,7 +64,7 @@ class SystemAppsUpdatesRepository @Inject constructor( return systemAppProjectList.map { it.packageName } } suspend fun fetchUpdatableSystemApps(forceRefresh: Boolean = false) { suspend fun fetchUpdatableSystemApps(forceRefresh: Boolean = false): ResultSupreme<Unit> { val result = handleNetworkResult { if (getUpdatableSystemApps().isNotEmpty() && !forceRefresh) { return@handleNetworkResult Loading @@ -71,18 +72,22 @@ class SystemAppsUpdatesRepository @Inject constructor( val endPoint = getUpdatableSystemAppEndPoint() val response = updatableSystemAppsApi.getUpdatableSystemApps(endPoint) check(response.isSuccessful) { "request failed! code : (${response.code()})" } val body = response.body() if (body.isNullOrEmpty()) { error("response is empty! despite response indicating success") } if (response.isSuccessful && !response.body().isNullOrEmpty()) { systemAppProjectList.clear() response.body()?.let { systemAppProjectList.addAll(it) } } else { Timber.e("Failed to fetch updatable apps: ${response.errorBody()?.string()}") } systemAppProjectList.addAll(body) } if (!result.isSuccess()) { Timber.e("Network error when fetching updatable apps - ${result.message}") Timber.e("error when fetching updatable apps - ${result.message}") } return result } private fun getUpdatableSystemAppEndPoint(): EndPoint { Loading app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +101 −102 Original line number Diff line number Diff line Loading @@ -5,13 +5,13 @@ import android.content.Context import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.NetworkCapabilities import androidx.annotation.VisibleForTesting import androidx.hilt.work.HiltWorker import androidx.preference.PreferenceManager import androidx.work.CoroutineWorker import androidx.work.WorkInfo.State import androidx.work.WorkManager import androidx.work.WorkerParameters import com.aurora.gplayapi.data.models.AuthData import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R Loading @@ -22,7 +22,6 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.login.state.LoginState import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.install.workmanager.AppInstallProcessor Loading @@ -47,7 +46,9 @@ class UpdatesWorker @AssistedInject constructor( companion object { const val IS_AUTO_UPDATE = "IS_AUTO_UPDATE" private const val MAX_RETRY_COUNT = 10 @VisibleForTesting const val MAX_RETRY_COUNT = 10 private const val DELAY_FOR_RETRY = 3000L } Loading @@ -64,9 +65,18 @@ class UpdatesWorker @AssistedInject constructor( return Result.success() } refreshBlockedAppList() systemAppsUpdatesRepository.fetchUpdatableSystemApps(forceRefresh = true) checkForUpdates() if (isAutoUpdate) { check(blockedAppRepository.fetchUpdateOfAppWarningList()) { "failed to update app blocklist" } } val systemAppsUpdateTask = systemAppsUpdatesRepository.fetchUpdatableSystemApps(forceRefresh = true) check(systemAppsUpdateTask.isSuccess()) { "failed to fetch system apps update!" } val enqueueInstall = checkForUpdates() check(enqueueInstall == ResultStatus.OK) { "failed to enqueue all item" } Result.success() } catch (e: Throwable) { Timber.e(e) Loading @@ -78,13 +88,8 @@ class UpdatesWorker @AssistedInject constructor( } } private suspend fun refreshBlockedAppList() { if (isAutoUpdate) { blockedAppRepository.fetchUpdateOfAppWarningList() } } private suspend fun checkManualUpdateRunning(): Boolean { @VisibleForTesting suspend fun checkManualUpdateRunning(): Boolean { val workInfos = withContext(Dispatchers.IO) { WorkManager.getInstance(context).getWorkInfosByTag(UpdatesWorkManager.USER_TAG) Loading @@ -105,110 +110,98 @@ class UpdatesWorker @AssistedInject constructor( return appLoungeDataStore.getUser() } private fun getLoginState(): LoginState { return appLoungeDataStore.getLoginState() private suspend fun checkForUpdates(): ResultStatus { loadSettings() val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val (appsNeededToUpdate, resultStatus) = loadWithRetry(emptyList(), ::getAvailableUpdates) if (resultStatus != ResultStatus.OK) { return resultStatus } private suspend fun checkForUpdates() { Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus") if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) } val jobsEnqueued = triggerUpdateProcessOnSettings(isConnectedToUnMeteredNetwork, appsNeededToUpdate) return if (jobsEnqueued.all { it.second }) ResultStatus.OK else ResultStatus.UNKNOWN } @VisibleForTesting suspend fun triggerUpdateProcessOnSettings( isConnectedToUnmeteredNetwork: Boolean, appsNeededToUpdate: List<Application> ): List<Pair<Application, Boolean>> { fun failedAllResponse() = appsNeededToUpdate.map { app -> Pair(app, false) } val hasStoragePermission = applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED val canContinue = (!isAutoUpdate || automaticInstallEnabled) && hasStoragePermission if (!canContinue) return failedAllResponse() return if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate) } else if (!onlyOnUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate) } else { failedAllResponse() } } @VisibleForTesting suspend fun getAvailableUpdates(): Pair<List<Application>, ResultStatus> { loadSettings() val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf<Application>() val user = getUser() val loginState = getLoginState() val authData = appLoungeDataStore.getAuthData() val authData = authenticatorRepository.getValidatedAuthData().data val resultStatus: ResultStatus if (user in listOf(User.ANONYMOUS, User.GOOGLE)) { if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { /* * Signifies valid Google user and valid auth data to update * apps from Google Play store. * The user check will be more useful in No-Google mode. */ val updateData = fetchUpdatesWithAuthRetry() appsNeededToUpdate.addAll(updateData.first) resultStatus = updateData.second } else if (loginState != LoginState.UNAVAILABLE) { val (apps, status) = updatesManagerRepository.getUpdates() appsNeededToUpdate.addAll(apps) resultStatus = status } else if (user == User.NO_GOOGLE) { /* * If authData is null, update apps from cleanapk only. */ val updateData = updatesManagerRepository.getUpdatesOSS() appsNeededToUpdate.addAll(updateData.first) resultStatus = updateData.second val (apps, status) = updatesManagerRepository.getUpdatesOSS() appsNeededToUpdate.addAll(apps) resultStatus = status } else { /* * If login state is unavailable, don't do anything. * If user in UNAVAILABLE, don't do anything. */ Timber.e("Update is aborted for unavailable user!") return } Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus") if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) resultStatus = ResultStatus.UNKNOWN } if (resultStatus != ResultStatus.OK) { manageRetry(resultStatus.toString()) } else { /* * Show notification only if enabled. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5376 */ retryCount = 0 if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) } triggerUpdateProcessOnSettings( isConnectedToUnMeteredNetwork, appsNeededToUpdate, authData, ) } } private suspend fun fetchUpdatesWithAuthRetry(): Pair<List<Application>, ResultStatus> { val updateData = updatesManagerRepository.getUpdates() if (updateData.second == ResultStatus.OK) { return updateData return Pair(appsNeededToUpdate, resultStatus) } val refreshedAuth = authenticatorRepository.getValidatedAuthData().data val shouldRetry = refreshedAuth != null return if (shouldRetry) updatesManagerRepository.getUpdates() else updateData } private suspend fun manageRetry(extraMessage: String) { @VisibleForTesting suspend fun <T> loadWithRetry( defaultValue: T, loadData: suspend () -> Pair<T, ResultStatus> ): Pair<T, ResultStatus> { while (retryCount <= MAX_RETRY_COUNT) { retryCount++ if (retryCount == 1) { val (data, status) = loadData() if (status == ResultStatus.OK) { return Pair(data, status) } delay(DELAY_FOR_RETRY) EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.RETRY))) } if (retryCount <= MAX_RETRY_COUNT) { delay(DELAY_FOR_RETRY) checkForUpdates() } else { val message = "Update is aborted after trying for $MAX_RETRY_COUNT times! message: $extraMessage" val message = "Update is aborted after trying for $MAX_RETRY_COUNT times!" Timber.e(message) EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.UNKNOWN))) } } private suspend fun triggerUpdateProcessOnSettings( isConnectedToUnmeteredNetwork: Boolean, appsNeededToUpdate: List<Application>, authData: AuthData ) { val hasStoragePermission = applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED if ((!isAutoUpdate || automaticInstallEnabled) && hasStoragePermission) { if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate, authData) } else if (!onlyOnUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate, authData) } } return Pair(defaultValue, ResultStatus.UNKNOWN) } private fun handleNotification( Loading @@ -226,17 +219,22 @@ class UpdatesWorker @AssistedInject constructor( } } private suspend fun startUpdateProcess( appsNeededToUpdate: List<Application>, authData: AuthData ) { appsNeededToUpdate.forEach { fusedApp -> if (!fusedApp.isFree && authData.isAnonymous) { return@forEach } appInstallProcessor.initAppInstall(fusedApp, true) } // returns list of Pair(app, status(success|failed)) @VisibleForTesting suspend fun startUpdateProcess(appsNeededToUpdate: List<Application>): List<Pair<Application, Boolean>> { val response = mutableListOf<Pair<Application, Boolean>>() val authData = authenticatorRepository.getValidatedAuthData() val isNotLoggedIntoPersonalAccount = !authData.isValidData() || authData.data?.isAnonymous == true for (fusedApp in appsNeededToUpdate) { val shouldSkip = (!fusedApp.isFree && isNotLoggedIntoPersonalAccount) if (shouldSkip.or(isStopped)) { // respect the stop signal as well response.add(Pair(fusedApp, false)) continue } val status = appInstallProcessor.initAppInstall(fusedApp, true) response.add(Pair(fusedApp, status)) } return response } private fun loadSettings() { Loading @@ -263,12 +261,13 @@ class UpdatesWorker @AssistedInject constructor( ) } /* /** * Checks if the device is connected to a metered connection or not * @param context current Context * @return returns true if the connections is not metered, false otherwise */ private fun isConnectedToUnMeteredNetwork(context: Context): Boolean { @VisibleForTesting fun isConnectedToUnMeteredNetwork(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val capabilities = Loading app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +42 −29 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package foundation.e.apps.install.workmanager import android.content.Context import androidx.annotation.VisibleForTesting import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R Loading Loading @@ -85,7 +86,7 @@ class AppInstallProcessor @Inject constructor( suspend fun initAppInstall( application: Application, isAnUpdate: Boolean = false ) { ): Boolean { val appInstall = AppInstall( application._id, application.source, Loading Loading @@ -113,7 +114,7 @@ class AppInstallProcessor @Inject constructor( application.status == Status.UPDATABLE || appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(appInstall) enqueueFusedDownload(appInstall, isUpdate, application.isSystemApp) return enqueueFusedDownload(appInstall, isUpdate, application.isSystemApp) } /** Loading @@ -127,8 +128,8 @@ class AppInstallProcessor @Inject constructor( appInstall: AppInstall, isAnUpdate: Boolean = false, isSystemApp: Boolean = false ) { try { ): Boolean { return try { val user = appLoungeDataStore.getUser() if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { val authData = appLoungeDataStore.getAuthData() Loading @@ -137,20 +138,40 @@ class AppInstallProcessor @Inject constructor( } } if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) return if (!canEnqueue(appInstall)) return false val downloadAdded = appInstallComponents.appManagerWrapper.addDownload(appInstall) if (!downloadAdded) { Timber.i("Update adding ABORTED! status: $downloadAdded") return appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) InstallWorkManager.enqueueWork(appInstall, isAnUpdate) true } catch (e: Exception) { Timber.e( e, "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" ) appInstallComponents.appManagerWrapper.installationIssue(appInstall) false } } if (!validateAgeLimit(appInstall)) return @VisibleForTesting suspend fun canEnqueue(appInstall: AppInstall): Boolean { if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) { return false } if (!appInstallComponents.appManagerWrapper.addDownload(appInstall)) { Timber.i("Update adding ABORTED! status") return false } if (!validateAgeLimit(appInstall)) { return false } if (!context.isNetworkAvailable()) { appInstallComponents.appManagerWrapper.installationIssue(appInstall) EventBus.invokeEvent(AppEvent.NoInternetEvent(false)) return return false } if (StorageComputer.spaceMissing(appInstall) > 0) { Loading @@ -158,18 +179,10 @@ class AppInstallProcessor @Inject constructor( storageNotificationManager.showNotEnoughSpaceNotification(appInstall) appInstallComponents.appManagerWrapper.installationIssue(appInstall) EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) return return false } appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) InstallWorkManager.enqueueWork(appInstall, isAnUpdate) } catch (e: Exception) { Timber.e( e, "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" ) appInstallComponents.appManagerWrapper.installationIssue(appInstall) } return true } private suspend fun validateAgeLimit(appInstall: AppInstall): Boolean { Loading Loading @@ -270,7 +283,7 @@ class AppInstallProcessor @Inject constructor( suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, runInForeground: (suspend (String) -> Unit)? = null runInForeground: (suspend (String) -> Unit) ): Result<ResultStatus> { var appInstall: AppInstall? = null try { Loading Loading @@ -306,7 +319,7 @@ class AppInstallProcessor @Inject constructor( ) } runInForeground?.invoke(it.name) runInForeground.invoke(it.name) startAppInstallationProcess(it) } Loading app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt +2 −2 Original line number Diff line number Diff line Loading @@ -60,14 +60,14 @@ class InstallAppWorker @AssistedInject constructor( override suspend fun doWork(): Result { val fusedDownloadId = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: "" val isPackageUpdate = params.inputData.getBoolean(IS_UPDATE_WORK, false) appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title -> val response = appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title -> setForeground( createForegroundInfo( "${context.getString(R.string.installing)} $title" ) ) } return Result.success() return if (response.isSuccess) Result.success() else Result.failure() } private fun createForegroundInfo(progress: String): ForegroundInfo { Loading Loading
app/build.gradle +1 −0 Original line number Diff line number Diff line Loading @@ -227,6 +227,7 @@ dependencies { // WorkManager implementation(libs.work.runtime.ktx) implementation libs.work.testing // Room ksp(libs.room.compiler) Loading
app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt +13 −8 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ package foundation.e.apps.data.gitlab import android.content.Context import android.os.Build import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.enums.Source Loading Loading @@ -63,7 +64,7 @@ class SystemAppsUpdatesRepository @Inject constructor( return systemAppProjectList.map { it.packageName } } suspend fun fetchUpdatableSystemApps(forceRefresh: Boolean = false) { suspend fun fetchUpdatableSystemApps(forceRefresh: Boolean = false): ResultSupreme<Unit> { val result = handleNetworkResult { if (getUpdatableSystemApps().isNotEmpty() && !forceRefresh) { return@handleNetworkResult Loading @@ -71,18 +72,22 @@ class SystemAppsUpdatesRepository @Inject constructor( val endPoint = getUpdatableSystemAppEndPoint() val response = updatableSystemAppsApi.getUpdatableSystemApps(endPoint) check(response.isSuccessful) { "request failed! code : (${response.code()})" } val body = response.body() if (body.isNullOrEmpty()) { error("response is empty! despite response indicating success") } if (response.isSuccessful && !response.body().isNullOrEmpty()) { systemAppProjectList.clear() response.body()?.let { systemAppProjectList.addAll(it) } } else { Timber.e("Failed to fetch updatable apps: ${response.errorBody()?.string()}") } systemAppProjectList.addAll(body) } if (!result.isSuccess()) { Timber.e("Network error when fetching updatable apps - ${result.message}") Timber.e("error when fetching updatable apps - ${result.message}") } return result } private fun getUpdatableSystemAppEndPoint(): EndPoint { Loading
app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +101 −102 Original line number Diff line number Diff line Loading @@ -5,13 +5,13 @@ import android.content.Context import android.content.pm.PackageManager import android.net.ConnectivityManager import android.net.NetworkCapabilities import androidx.annotation.VisibleForTesting import androidx.hilt.work.HiltWorker import androidx.preference.PreferenceManager import androidx.work.CoroutineWorker import androidx.work.WorkInfo.State import androidx.work.WorkManager import androidx.work.WorkerParameters import com.aurora.gplayapi.data.models.AuthData import dagger.assisted.Assisted import dagger.assisted.AssistedInject import foundation.e.apps.R Loading @@ -22,7 +22,6 @@ import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.login.repository.AuthenticatorRepository import foundation.e.apps.data.login.state.LoginState import foundation.e.apps.data.preference.AppLoungeDataStore import foundation.e.apps.data.updates.UpdatesManagerRepository import foundation.e.apps.install.workmanager.AppInstallProcessor Loading @@ -47,7 +46,9 @@ class UpdatesWorker @AssistedInject constructor( companion object { const val IS_AUTO_UPDATE = "IS_AUTO_UPDATE" private const val MAX_RETRY_COUNT = 10 @VisibleForTesting const val MAX_RETRY_COUNT = 10 private const val DELAY_FOR_RETRY = 3000L } Loading @@ -64,9 +65,18 @@ class UpdatesWorker @AssistedInject constructor( return Result.success() } refreshBlockedAppList() systemAppsUpdatesRepository.fetchUpdatableSystemApps(forceRefresh = true) checkForUpdates() if (isAutoUpdate) { check(blockedAppRepository.fetchUpdateOfAppWarningList()) { "failed to update app blocklist" } } val systemAppsUpdateTask = systemAppsUpdatesRepository.fetchUpdatableSystemApps(forceRefresh = true) check(systemAppsUpdateTask.isSuccess()) { "failed to fetch system apps update!" } val enqueueInstall = checkForUpdates() check(enqueueInstall == ResultStatus.OK) { "failed to enqueue all item" } Result.success() } catch (e: Throwable) { Timber.e(e) Loading @@ -78,13 +88,8 @@ class UpdatesWorker @AssistedInject constructor( } } private suspend fun refreshBlockedAppList() { if (isAutoUpdate) { blockedAppRepository.fetchUpdateOfAppWarningList() } } private suspend fun checkManualUpdateRunning(): Boolean { @VisibleForTesting suspend fun checkManualUpdateRunning(): Boolean { val workInfos = withContext(Dispatchers.IO) { WorkManager.getInstance(context).getWorkInfosByTag(UpdatesWorkManager.USER_TAG) Loading @@ -105,110 +110,98 @@ class UpdatesWorker @AssistedInject constructor( return appLoungeDataStore.getUser() } private fun getLoginState(): LoginState { return appLoungeDataStore.getLoginState() private suspend fun checkForUpdates(): ResultStatus { loadSettings() val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val (appsNeededToUpdate, resultStatus) = loadWithRetry(emptyList(), ::getAvailableUpdates) if (resultStatus != ResultStatus.OK) { return resultStatus } private suspend fun checkForUpdates() { Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus") if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) } val jobsEnqueued = triggerUpdateProcessOnSettings(isConnectedToUnMeteredNetwork, appsNeededToUpdate) return if (jobsEnqueued.all { it.second }) ResultStatus.OK else ResultStatus.UNKNOWN } @VisibleForTesting suspend fun triggerUpdateProcessOnSettings( isConnectedToUnmeteredNetwork: Boolean, appsNeededToUpdate: List<Application> ): List<Pair<Application, Boolean>> { fun failedAllResponse() = appsNeededToUpdate.map { app -> Pair(app, false) } val hasStoragePermission = applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED val canContinue = (!isAutoUpdate || automaticInstallEnabled) && hasStoragePermission if (!canContinue) return failedAllResponse() return if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate) } else if (!onlyOnUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate) } else { failedAllResponse() } } @VisibleForTesting suspend fun getAvailableUpdates(): Pair<List<Application>, ResultStatus> { loadSettings() val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf<Application>() val user = getUser() val loginState = getLoginState() val authData = appLoungeDataStore.getAuthData() val authData = authenticatorRepository.getValidatedAuthData().data val resultStatus: ResultStatus if (user in listOf(User.ANONYMOUS, User.GOOGLE)) { if (user in listOf(User.ANONYMOUS, User.GOOGLE) && authData != null) { /* * Signifies valid Google user and valid auth data to update * apps from Google Play store. * The user check will be more useful in No-Google mode. */ val updateData = fetchUpdatesWithAuthRetry() appsNeededToUpdate.addAll(updateData.first) resultStatus = updateData.second } else if (loginState != LoginState.UNAVAILABLE) { val (apps, status) = updatesManagerRepository.getUpdates() appsNeededToUpdate.addAll(apps) resultStatus = status } else if (user == User.NO_GOOGLE) { /* * If authData is null, update apps from cleanapk only. */ val updateData = updatesManagerRepository.getUpdatesOSS() appsNeededToUpdate.addAll(updateData.first) resultStatus = updateData.second val (apps, status) = updatesManagerRepository.getUpdatesOSS() appsNeededToUpdate.addAll(apps) resultStatus = status } else { /* * If login state is unavailable, don't do anything. * If user in UNAVAILABLE, don't do anything. */ Timber.e("Update is aborted for unavailable user!") return } Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus") if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) resultStatus = ResultStatus.UNKNOWN } if (resultStatus != ResultStatus.OK) { manageRetry(resultStatus.toString()) } else { /* * Show notification only if enabled. * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5376 */ retryCount = 0 if (isAutoUpdate && shouldShowNotification) { handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) } triggerUpdateProcessOnSettings( isConnectedToUnMeteredNetwork, appsNeededToUpdate, authData, ) } } private suspend fun fetchUpdatesWithAuthRetry(): Pair<List<Application>, ResultStatus> { val updateData = updatesManagerRepository.getUpdates() if (updateData.second == ResultStatus.OK) { return updateData return Pair(appsNeededToUpdate, resultStatus) } val refreshedAuth = authenticatorRepository.getValidatedAuthData().data val shouldRetry = refreshedAuth != null return if (shouldRetry) updatesManagerRepository.getUpdates() else updateData } private suspend fun manageRetry(extraMessage: String) { @VisibleForTesting suspend fun <T> loadWithRetry( defaultValue: T, loadData: suspend () -> Pair<T, ResultStatus> ): Pair<T, ResultStatus> { while (retryCount <= MAX_RETRY_COUNT) { retryCount++ if (retryCount == 1) { val (data, status) = loadData() if (status == ResultStatus.OK) { return Pair(data, status) } delay(DELAY_FOR_RETRY) EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.RETRY))) } if (retryCount <= MAX_RETRY_COUNT) { delay(DELAY_FOR_RETRY) checkForUpdates() } else { val message = "Update is aborted after trying for $MAX_RETRY_COUNT times! message: $extraMessage" val message = "Update is aborted after trying for $MAX_RETRY_COUNT times!" Timber.e(message) EventBus.invokeEvent(AppEvent.UpdateEvent(ResultSupreme.WorkError(ResultStatus.UNKNOWN))) } } private suspend fun triggerUpdateProcessOnSettings( isConnectedToUnmeteredNetwork: Boolean, appsNeededToUpdate: List<Application>, authData: AuthData ) { val hasStoragePermission = applicationContext.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED if ((!isAutoUpdate || automaticInstallEnabled) && hasStoragePermission) { if (onlyOnUnmeteredNetwork && isConnectedToUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate, authData) } else if (!onlyOnUnmeteredNetwork) { startUpdateProcess(appsNeededToUpdate, authData) } } return Pair(defaultValue, ResultStatus.UNKNOWN) } private fun handleNotification( Loading @@ -226,17 +219,22 @@ class UpdatesWorker @AssistedInject constructor( } } private suspend fun startUpdateProcess( appsNeededToUpdate: List<Application>, authData: AuthData ) { appsNeededToUpdate.forEach { fusedApp -> if (!fusedApp.isFree && authData.isAnonymous) { return@forEach } appInstallProcessor.initAppInstall(fusedApp, true) } // returns list of Pair(app, status(success|failed)) @VisibleForTesting suspend fun startUpdateProcess(appsNeededToUpdate: List<Application>): List<Pair<Application, Boolean>> { val response = mutableListOf<Pair<Application, Boolean>>() val authData = authenticatorRepository.getValidatedAuthData() val isNotLoggedIntoPersonalAccount = !authData.isValidData() || authData.data?.isAnonymous == true for (fusedApp in appsNeededToUpdate) { val shouldSkip = (!fusedApp.isFree && isNotLoggedIntoPersonalAccount) if (shouldSkip.or(isStopped)) { // respect the stop signal as well response.add(Pair(fusedApp, false)) continue } val status = appInstallProcessor.initAppInstall(fusedApp, true) response.add(Pair(fusedApp, status)) } return response } private fun loadSettings() { Loading @@ -263,12 +261,13 @@ class UpdatesWorker @AssistedInject constructor( ) } /* /** * Checks if the device is connected to a metered connection or not * @param context current Context * @return returns true if the connections is not metered, false otherwise */ private fun isConnectedToUnMeteredNetwork(context: Context): Boolean { @VisibleForTesting fun isConnectedToUnMeteredNetwork(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val capabilities = Loading
app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +42 −29 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ package foundation.e.apps.install.workmanager import android.content.Context import androidx.annotation.VisibleForTesting import com.aurora.gplayapi.exceptions.InternalException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R Loading Loading @@ -85,7 +86,7 @@ class AppInstallProcessor @Inject constructor( suspend fun initAppInstall( application: Application, isAnUpdate: Boolean = false ) { ): Boolean { val appInstall = AppInstall( application._id, application.source, Loading Loading @@ -113,7 +114,7 @@ class AppInstallProcessor @Inject constructor( application.status == Status.UPDATABLE || appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(appInstall) enqueueFusedDownload(appInstall, isUpdate, application.isSystemApp) return enqueueFusedDownload(appInstall, isUpdate, application.isSystemApp) } /** Loading @@ -127,8 +128,8 @@ class AppInstallProcessor @Inject constructor( appInstall: AppInstall, isAnUpdate: Boolean = false, isSystemApp: Boolean = false ) { try { ): Boolean { return try { val user = appLoungeDataStore.getUser() if (!isSystemApp && (user == User.GOOGLE || user == User.ANONYMOUS)) { val authData = appLoungeDataStore.getAuthData() Loading @@ -137,20 +138,40 @@ class AppInstallProcessor @Inject constructor( } } if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) return if (!canEnqueue(appInstall)) return false val downloadAdded = appInstallComponents.appManagerWrapper.addDownload(appInstall) if (!downloadAdded) { Timber.i("Update adding ABORTED! status: $downloadAdded") return appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) InstallWorkManager.enqueueWork(appInstall, isAnUpdate) true } catch (e: Exception) { Timber.e( e, "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" ) appInstallComponents.appManagerWrapper.installationIssue(appInstall) false } } if (!validateAgeLimit(appInstall)) return @VisibleForTesting suspend fun canEnqueue(appInstall: AppInstall): Boolean { if (appInstall.type != Type.PWA && !updateDownloadUrls(appInstall)) { return false } if (!appInstallComponents.appManagerWrapper.addDownload(appInstall)) { Timber.i("Update adding ABORTED! status") return false } if (!validateAgeLimit(appInstall)) { return false } if (!context.isNetworkAvailable()) { appInstallComponents.appManagerWrapper.installationIssue(appInstall) EventBus.invokeEvent(AppEvent.NoInternetEvent(false)) return return false } if (StorageComputer.spaceMissing(appInstall) > 0) { Loading @@ -158,18 +179,10 @@ class AppInstallProcessor @Inject constructor( storageNotificationManager.showNotEnoughSpaceNotification(appInstall) appInstallComponents.appManagerWrapper.installationIssue(appInstall) EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) return return false } appInstallComponents.appManagerWrapper.updateAwaiting(appInstall) InstallWorkManager.enqueueWork(appInstall, isAnUpdate) } catch (e: Exception) { Timber.e( e, "Enqueuing App install work is failed for ${appInstall.packageName} exception: ${e.localizedMessage}" ) appInstallComponents.appManagerWrapper.installationIssue(appInstall) } return true } private suspend fun validateAgeLimit(appInstall: AppInstall): Boolean { Loading Loading @@ -270,7 +283,7 @@ class AppInstallProcessor @Inject constructor( suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, runInForeground: (suspend (String) -> Unit)? = null runInForeground: (suspend (String) -> Unit) ): Result<ResultStatus> { var appInstall: AppInstall? = null try { Loading Loading @@ -306,7 +319,7 @@ class AppInstallProcessor @Inject constructor( ) } runInForeground?.invoke(it.name) runInForeground.invoke(it.name) startAppInstallationProcess(it) } Loading
app/src/main/java/foundation/e/apps/install/workmanager/InstallAppWorker.kt +2 −2 Original line number Diff line number Diff line Loading @@ -60,14 +60,14 @@ class InstallAppWorker @AssistedInject constructor( override suspend fun doWork(): Result { val fusedDownloadId = params.inputData.getString(INPUT_DATA_FUSED_DOWNLOAD) ?: "" val isPackageUpdate = params.inputData.getBoolean(IS_UPDATE_WORK, false) appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title -> val response = appInstallProcessor.processInstall(fusedDownloadId, isPackageUpdate) { title -> setForeground( createForegroundInfo( "${context.getString(R.string.installing)} $title" ) ) } return Result.success() return if (response.isSuccess) Result.success() else Result.failure() } private fun createForegroundInfo(progress: String): ForegroundInfo { Loading