diff --git a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt index 3806bfb8eebe66bef744974687a0e2e46f20e5f5..56ba57137d23856dd0a36f81b9d203019824db3c 100644 --- a/app/src/main/java/foundation/e/apps/data/DownloadManager.kt +++ b/app/src/main/java/foundation/e/apps/data/DownloadManager.kt @@ -19,6 +19,7 @@ package foundation.e.apps.data import android.app.DownloadManager import android.content.Context +import android.database.Cursor import android.net.Uri import android.os.Environment import dagger.hilt.android.qualifiers.ApplicationContext @@ -35,6 +36,7 @@ import java.io.File import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton +import kotlin.math.abs import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -124,12 +126,17 @@ class DownloadManager @Inject constructor( downloadManager.query(downloadManagerQuery.setFilterById(downloadId)) .use { cursor -> if (cursor.moveToFirst()) { - val status = + var status = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) val totalSizeBytes = - cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) + getLong(cursor, DownloadManager.COLUMN_TOTAL_SIZE_BYTES) val bytesDownloadedSoFar = - cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + getLong(cursor, DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + val reason = + cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON)) + + status = sanitizeStatus(downloadId, status, reason) + if (status == DownloadManager.STATUS_FAILED) { Timber.d("Download Failed: $filePath=> $bytesDownloadedSoFar/$totalSizeBytes $status") downloadsMaps[downloadId] = false @@ -166,6 +173,34 @@ class DownloadManager @Inject constructor( return getDownloadStatus(downloadId) == DownloadManager.STATUS_FAILED } + fun getSizeRequired(downloadId: Long): Long { + var totalSizeBytes = -1L + var bytesDownloadedSoFar = -1L + + try { + downloadManager.query(downloadManagerQuery.setFilterById(downloadId)) + .use { cursor -> + if (cursor.moveToFirst()) { + totalSizeBytes = getLong(cursor, DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + bytesDownloadedSoFar = + getLong(cursor, DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + } + } + } catch (e: RuntimeException) { + Timber.e(e, "runtime exception on retrieving download file size.") + } + + if (totalSizeBytes <= 0) { + return 0 + } + + if (bytesDownloadedSoFar <= 0) { + return totalSizeBytes + } + + return abs(totalSizeBytes - bytesDownloadedSoFar) + } + private fun getDownloadStatus(downloadId: Long): Int { var status = -1 var reason = -1 @@ -187,7 +222,26 @@ class DownloadManager @Inject constructor( if (status != DownloadManager.STATUS_SUCCESSFUL) { Timber.e("Download Issue: $downloadId status: $status reason: $reason") } - return status + + return sanitizeStatus(downloadId, status, reason) + } + + private fun sanitizeStatus(downloadId: Long, status: Int, reason: Int): Int { + if (reason <= 0) { + return status + } + + if (status in listOf(DownloadManager.STATUS_FAILED, DownloadManager.STATUS_PAUSED)) { + return status + } + + Timber.e("Download Issue: $downloadId : DownloadManager returns status: $status but the failed because: reason: $reason") + + if (reason <= DownloadManager.PAUSED_UNKNOWN) { + return DownloadManager.STATUS_PAUSED + } + + return DownloadManager.STATUS_FAILED } fun getDownloadFailureReason(downloadId: Long): Int { @@ -205,4 +259,7 @@ class DownloadManager @Inject constructor( } return reason } + + private fun getLong(cursor: Cursor, column: String) = + cursor.getLong(cursor.getColumnIndexOrThrow(column)) } diff --git a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt index 07cb797f0c037cb66299913e6a8b2a95e23a5248..5aa83c320f7bfc44dc45372b65b422d65c6751f4 100644 --- a/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/fused/FusedApiImpl.kt @@ -32,7 +32,6 @@ import com.aurora.gplayapi.data.models.SearchBundle import com.aurora.gplayapi.data.models.StreamCluster import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R -import foundation.e.apps.data.Constants.timeoutDurationInMillis import foundation.e.apps.data.ResultSupreme import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.cleanapk.CleanApkRetrofit @@ -68,10 +67,8 @@ import foundation.e.apps.install.pkg.PWAManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import foundation.e.apps.ui.home.model.HomeChildFusedAppDiffUtil import kotlinx.coroutines.Deferred -import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withTimeout import retrofit2.Response import timber.log.Timber import javax.inject.Inject diff --git a/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt b/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt index 0d933302122129947eb4849466225934fbd138bc..905d4182b4624ec9f54e0ba9df707cc89fe14412 100644 --- a/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt +++ b/app/src/main/java/foundation/e/apps/data/fusedDownload/models/FusedDownload.kt @@ -24,7 +24,7 @@ data class FusedDownload( val versionCode: Int = 1, val offerType: Int = -1, val isFree: Boolean = true, - val appSize: Long = 0, + var appSize: Long = 0, var files: List = mutableListOf(), var signature: String = String() ) { diff --git a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt index 42e90174a22d802e1064aa09ebe49de3751cb577..226b43bcbbd2ed67b40b40754c948b26e03f5617 100644 --- a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt @@ -36,12 +36,12 @@ import foundation.e.apps.data.fused.data.FusedApp import foundation.e.apps.data.preference.PreferenceManagerModule import foundation.e.apps.install.pkg.PkgManagerModule import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject class UpdatesManagerImpl @Inject constructor( @ApplicationContext private val context: Context, diff --git a/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt b/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt index f263fc012d0e71189f6e5813bb180ae7526a099d..1438ad18f5a4e1b96ea9ed0cf0e961102c46a357 100644 --- a/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt +++ b/app/src/main/java/foundation/e/apps/install/download/DownloadManagerUtils.kt @@ -20,11 +20,15 @@ package foundation.e.apps.install.download import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R import foundation.e.apps.data.DownloadManager import foundation.e.apps.data.enums.Origin import foundation.e.apps.data.enums.Status import foundation.e.apps.data.fusedDownload.FusedManagerRepository import foundation.e.apps.data.fusedDownload.models.FusedDownload +import foundation.e.apps.install.notification.StorageNotificationManager +import foundation.e.apps.utils.eventBus.AppEvent +import foundation.e.apps.utils.eventBus.EventBus import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -39,7 +43,8 @@ import javax.inject.Singleton class DownloadManagerUtils @Inject constructor( @ApplicationContext private val context: Context, private val fusedManagerRepository: FusedManagerRepository, - private val downloadManager: DownloadManager + private val downloadManager: DownloadManager, + private val storageNotificationManager: StorageNotificationManager, ) { private val mutex = Mutex() @@ -64,10 +69,9 @@ class DownloadManagerUtils @Inject constructor( Timber.d("===> updateDownloadStatus: ${fusedDownload.name}: $downloadId: $numberOfDownloadedItems/${fusedDownload.downloadIdMap.size}") if (downloadManager.hasDownloadFailed(downloadId)) { - handleDownloadFailed(fusedDownload) + handleDownloadFailed(fusedDownload, downloadId) Timber.e( - "Download failed for ${fusedDownload.packageName}, " + - "reason: ${downloadManager.getDownloadFailureReason(downloadId)}" + "Download failed for ${fusedDownload.packageName}, " + "reason: " + "${downloadManager.getDownloadFailureReason(downloadId)}" ) return@launch } @@ -87,27 +91,30 @@ class DownloadManagerUtils @Inject constructor( fusedManagerRepository.updateFusedDownload(fusedDownload) } - private suspend fun handleDownloadFailed(fusedDownload: FusedDownload) { + private suspend fun handleDownloadFailed(fusedDownload: FusedDownload, downloadId: Long) { fusedManagerRepository.installationIssue(fusedDownload) fusedManagerRepository.cancelDownload(fusedDownload) Timber.w("===> Download failed: ${fusedDownload.name} ${fusedDownload.status}") + + if (downloadManager.getDownloadFailureReason(downloadId) == android.app.DownloadManager.ERROR_INSUFFICIENT_SPACE) { + storageNotificationManager.showNotEnoughSpaceNotification(fusedDownload, downloadId) + EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) + } } private suspend fun validateDownload( numberOfDownloadedItems: Int, fusedDownload: FusedDownload, downloadId: Long - ) = downloadManager.isDownloadSuccessful(downloadId) && - areAllFilesDownloaded( - numberOfDownloadedItems, - fusedDownload - ) && checkCleanApkSignatureOK(fusedDownload) + ) = downloadManager.isDownloadSuccessful(downloadId) && areAllFilesDownloaded( + numberOfDownloadedItems, fusedDownload + ) && checkCleanApkSignatureOK(fusedDownload) private fun areAllFilesDownloaded( numberOfDownloadedItems: Int, fusedDownload: FusedDownload - ) = numberOfDownloadedItems == fusedDownload.downloadIdMap.size && - numberOfDownloadedItems == fusedDownload.downloadURLList.size + ) = + numberOfDownloadedItems == fusedDownload.downloadIdMap.size && numberOfDownloadedItems == fusedDownload.downloadURLList.size private suspend fun updateDownloadIdMap( fusedDownload: FusedDownload, @@ -119,8 +126,7 @@ class DownloadManagerUtils @Inject constructor( private suspend fun checkCleanApkSignatureOK(fusedDownload: FusedDownload): Boolean { if (fusedDownload.origin != Origin.CLEANAPK || fusedManagerRepository.isFdroidApplicationSigned( - context, - fusedDownload + context, fusedDownload ) ) { Timber.d("Apk signature is OK") diff --git a/app/src/main/java/foundation/e/apps/install/notification/NotificationManagerUtils.kt b/app/src/main/java/foundation/e/apps/install/notification/NotificationManagerUtils.kt deleted file mode 100644 index 32d24712be3ebb4f74ce2de4e7ff137cd60fe23c..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/apps/install/notification/NotificationManagerUtils.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Apps Quickly and easily install Android apps onto your device! - * Copyright (C) 2021 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.install.notification - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import foundation.e.apps.data.preference.PreferenceManagerModule -import javax.inject.Inject - -class NotificationManagerUtils @Inject constructor( - @ApplicationContext private val context: Context, - private val preferenceManagerModule: PreferenceManagerModule -) diff --git a/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt b/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..be74895ea1fe501207d33b01a5585214bcbaac14 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/install/notification/StorageNotificationManager.kt @@ -0,0 +1,106 @@ +/* + * Apps Quickly and easily install Android apps onto your device! + * Copyright (C) 2021 E FOUNDATION + * Copyright (C) 2023 MURENA SAS + * + * 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.install.notification + +import android.Manifest +import android.app.Notification +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.R +import foundation.e.apps.data.DownloadManager +import foundation.e.apps.data.fusedDownload.models.FusedDownload +import foundation.e.apps.utils.StorageComputer +import javax.inject.Inject + +class StorageNotificationManager @Inject constructor( + @ApplicationContext private val context: Context, + private val downloadManager: DownloadManager, +) { + companion object { + const val NOT_ENOUGH_SPACE_NOTIFICATION_ID = 7874 + } + + fun showNotEnoughSpaceNotification(fusedDownload: FusedDownload, downloadId: Long? = null) { + + with(NotificationManagerCompat.from(context)) { + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED + ) { + return + } + + val content = getNotEnoughSpaceNotificationContent(fusedDownload, downloadId) + + notify( + NOT_ENOUGH_SPACE_NOTIFICATION_ID, + getNotEnoughSpaceNotification(content) + ) + } + } + + private fun getNotEnoughSpaceNotificationContent( + fusedDownload: FusedDownload, + downloadId: Long? = null + ): String { + val requiredInByte = getSpaceMissing(fusedDownload, downloadId) + + if (requiredInByte <= 0L) { + return context.getString(R.string.not_enough_storage) + } + + return context.getString( + R.string.free_space_for_update, + StorageComputer.humanReadableByteCountSI(requiredInByte) + ) + } + + private fun getSpaceMissing(fusedDownload: FusedDownload, downloadId: Long? = null): Long { + if (fusedDownload.appSize > 0L) { + return calculateSpaceMissingFromFusedDownload(fusedDownload) + } + + if (downloadId == null) { + return 0 + } + + return downloadManager.getSizeRequired(downloadId) + } + + private fun calculateSpaceMissingFromFusedDownload(fusedDownload: FusedDownload): Long { + var requiredInByte = StorageComputer.spaceMissing(fusedDownload) + if (requiredInByte <= 0L) { + requiredInByte = fusedDownload.appSize + } + + return requiredInByte + } + + private fun getNotEnoughSpaceNotification(content: String): Notification { + return NotificationCompat.Builder(context, NotificationManagerModule.DOWNLOADS) + .setContentText(content) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setSmallIcon(R.drawable.app_lounge_notification_icon) + .build() + } +} diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt index 988debf4008f3cce99f50bcfcc92ba3013c65cd0..63eafed676847ffe53f5c36d68a79a3eafbce215 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesNotifier.kt @@ -38,12 +38,12 @@ object UpdatesNotifier { private const val UPDATES_NOTIFICATION_CHANNEL_ID = "updates_notification" private const val UPDATES_NOTIFICATION_CHANNEL_TITLE = "App updates" - fun getNotification( + private fun getNotification( context: Context, numberOfApps: Int, installAutomatically: Boolean, - unmeteredNetworkOnly: Boolean, - isConnectedToUnmeteredNetwork: Boolean + unMeteredNetworkOnly: Boolean, + isConnectedToUnMeteredNetwork: Boolean ): Notification { val notificationBuilder = createNotificationBuilder(context) @@ -51,7 +51,7 @@ object UpdatesNotifier { when (numberOfApps) { 0 -> { notificationBuilder.setContentTitle( - "Checking Updates..." + context.resources.getString(R.string.checking_updates) ) } 1 -> { @@ -75,7 +75,7 @@ object UpdatesNotifier { } if (installAutomatically) { notificationBuilder.setContentText(context.getString(R.string.automatically_install_updates_notification_text)) - if (unmeteredNetworkOnly && !isConnectedToUnmeteredNetwork) { + if (unMeteredNetworkOnly && !isConnectedToUnMeteredNetwork) { notificationBuilder.setSubText( context .getString(R.string.updates_notification_unmetered_network_warning) @@ -170,7 +170,7 @@ object UpdatesNotifier { } } - fun getNotification(context: Context, title: String, message: String): Notification { + private fun getNotification(context: Context, title: String, message: String): Notification { val notificationBuilder = createNotificationBuilder(context) notificationBuilder.setContentTitle(title) notificationBuilder.setContentText(message) diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index 7470db10a8332dfb706d418f7d3405a37fb44e03..1f7804cb6bb6a43fc82d04019938cb27bbde3cd2 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -103,7 +103,7 @@ class UpdatesWorker @AssistedInject constructor( private suspend fun checkForUpdates() { loadSettings() - val isConnectedToUnmeteredNetwork = isConnectedToUnmeteredNetwork(applicationContext) + val isConnectedToUnMeteredNetwork = isConnectedToUnMeteredNetwork(applicationContext) val appsNeededToUpdate = mutableListOf() val user = getUser() val authData = loginSourceRepository.getValidatedAuthData().data @@ -134,7 +134,7 @@ class UpdatesWorker @AssistedInject constructor( } Timber.i("Updates found: ${appsNeededToUpdate.size}; $resultStatus") if (isAutoUpdate && shouldShowNotification) { - handleNotification(appsNeededToUpdate.size, isConnectedToUnmeteredNetwork) + handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) } if (resultStatus != ResultStatus.OK) { @@ -146,11 +146,11 @@ class UpdatesWorker @AssistedInject constructor( */ retryCount = 0 if (isAutoUpdate && shouldShowNotification) { - handleNotification(appsNeededToUpdate.size, isConnectedToUnmeteredNetwork) + handleNotification(appsNeededToUpdate.size, isConnectedToUnMeteredNetwork) } triggerUpdateProcessOnSettings( - isConnectedToUnmeteredNetwork, + isConnectedToUnMeteredNetwork, appsNeededToUpdate, /* * If authData is null, only cleanApk data will be present @@ -251,7 +251,7 @@ class UpdatesWorker @AssistedInject constructor( * @param context current Context * @return returns true if the connections is not metered, false otherwise */ - private fun isConnectedToUnmeteredNetwork(context: Context): Boolean { + private fun isConnectedToUnMeteredNetwork(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val capabilities = diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 0c6ac89ae1ea3d5928f4b8e59384972b7ab9c970..e17278ddeaf4172adc65630160b1e375d22043e9 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -19,8 +19,6 @@ package foundation.e.apps.install.workmanager import android.content.Context -import android.os.Environment -import android.os.StatFs import com.aurora.gplayapi.exceptions.ApiException import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.apps.R @@ -35,7 +33,9 @@ import foundation.e.apps.data.fusedDownload.FusedDownloadRepository import foundation.e.apps.data.fusedDownload.FusedManagerRepository import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.updates.UpdatesNotifier +import foundation.e.apps.utils.StorageComputer import foundation.e.apps.utils.eventBus.AppEvent import foundation.e.apps.utils.eventBus.EventBus import foundation.e.apps.utils.getFormattedString @@ -51,7 +51,8 @@ class AppInstallProcessor @Inject constructor( private val fusedDownloadRepository: FusedDownloadRepository, private val fusedManagerRepository: FusedManagerRepository, private val fusedAPIRepository: FusedAPIRepository, - private val dataStoreManager: DataStoreManager + private val dataStoreManager: DataStoreManager, + private val storageNotificationManager: StorageNotificationManager ) { private var isItUpdateWork = false @@ -127,8 +128,9 @@ class AppInstallProcessor @Inject constructor( return } - if (!isStorageAvailable(fusedDownload)) { + if (StorageComputer.spaceMissing(fusedDownload) > 0) { Timber.d("Storage is not available for: ${fusedDownload.name} size: ${fusedDownload.appSize}") + storageNotificationManager.showNotEnoughSpaceNotification(fusedDownload) fusedManagerRepository.installationIssue(fusedDownload) EventBus.invokeEvent(AppEvent.ErrorMessageEvent(R.string.not_enough_storage)) return @@ -137,7 +139,10 @@ class AppInstallProcessor @Inject constructor( fusedManagerRepository.updateAwaiting(fusedDownload) InstallWorkManager.enqueueWork(fusedDownload, isAnUpdate) } catch (e: Exception) { - Timber.e("Enqueuing App install work is failed for ${fusedDownload.packageName} exception: ${e.localizedMessage}", e) + Timber.e( + "Enqueuing App install work is failed for ${fusedDownload.packageName} exception: ${e.localizedMessage}", + e + ) fusedManagerRepository.installationIssue(fusedDownload) } } @@ -151,12 +156,14 @@ class AppInstallProcessor @Inject constructor( EventBus.invokeEvent(AppEvent.AppPurchaseEvent(fusedDownload)) return false } catch (e: Exception) { - Timber.e("Updating download Urls failed for ${fusedDownload.packageName} exception: ${e.localizedMessage}", e) + Timber.e( + "Updating download Urls failed for ${fusedDownload.packageName} exception: ${e.localizedMessage}", + e + ) EventBus.invokeEvent( AppEvent.UpdateEvent( ResultSupreme.WorkError( - ResultStatus.UNKNOWN, - fusedDownload + ResultStatus.UNKNOWN, fusedDownload ) ) ) @@ -169,22 +176,10 @@ class AppInstallProcessor @Inject constructor( fusedDownload: FusedDownload ) { fusedAPIRepository.updateFusedDownloadWithDownloadingInfo( - fusedDownload.origin, - fusedDownload + fusedDownload.origin, fusedDownload ) } - private fun isStorageAvailable(fusedDownload: FusedDownload): Boolean { - val availableSpace = calculateAvailableDiskSpace() - return availableSpace > fusedDownload.appSize + (500 * (1000 * 1000)) - } - - private fun calculateAvailableDiskSpace(): Long { - val path = Environment.getDataDirectory().absolutePath - val statFs = StatFs(path) - return statFs.availableBytes - } - suspend fun processInstall( fusedDownloadId: String, isItUpdateWork: Boolean, @@ -199,8 +194,8 @@ class AppInstallProcessor @Inject constructor( fusedDownload?.let { - this.isItUpdateWork = isItUpdateWork && - fusedManagerRepository.isFusedDownloadInstalled(fusedDownload) + this.isItUpdateWork = + isItUpdateWork && fusedManagerRepository.isFusedDownloadInstalled(fusedDownload) if (!fusedDownload.isAppInstalling()) { Timber.d("!!! returned") @@ -223,7 +218,10 @@ class AppInstallProcessor @Inject constructor( startAppInstallationProcess(it) } } catch (e: Exception) { - Timber.e("Install worker is failed for ${fusedDownload?.packageName} exception: ${e.localizedMessage}", e) + Timber.e( + "Install worker is failed for ${fusedDownload?.packageName} exception: ${e.localizedMessage}", + e + ) fusedDownload?.let { fusedManagerRepository.cancelDownload(fusedDownload) } @@ -234,11 +232,7 @@ class AppInstallProcessor @Inject constructor( } private fun areFilesDownloadedButNotInstalled(fusedDownload: FusedDownload) = - fusedDownload.areFilesDownloaded() && ( - !fusedManagerRepository.isFusedDownloadInstalled( - fusedDownload - ) || fusedDownload.status == Status.INSTALLING - ) + fusedDownload.areFilesDownloaded() && (!fusedManagerRepository.isFusedDownloadInstalled(fusedDownload) || fusedDownload.status == Status.INSTALLING) private suspend fun checkUpdateWork( fusedDownload: FusedDownload? @@ -261,14 +255,11 @@ class AppInstallProcessor @Inject constructor( } private suspend fun isUpdateCompleted(): Boolean { - val downloadListWithoutAnyIssue = - fusedDownloadRepository.getDownloadList() - .filter { - !listOf( - Status.INSTALLATION_ISSUE, - Status.PURCHASE_NEEDED - ).contains(it.status) - } + val downloadListWithoutAnyIssue = fusedDownloadRepository.getDownloadList().filter { + !listOf( + Status.INSTALLATION_ISSUE, Status.PURCHASE_NEEDED + ).contains(it.status) + } return UpdatesDao.successfulUpdatedApps.isNotEmpty() && downloadListWithoutAnyIssue.isEmpty() } @@ -276,9 +267,9 @@ class AppInstallProcessor @Inject constructor( private fun showNotificationOnUpdateEnded() { val locale = dataStoreManager.getAuthData().locale val date = Date().getFormattedString(DATE_FORMAT, locale) - val numberOfUpdatedApps = NumberFormat.getNumberInstance(locale) - .format(UpdatesDao.successfulUpdatedApps.size) - .toString() + val numberOfUpdatedApps = + NumberFormat.getNumberInstance(locale).format(UpdatesDao.successfulUpdatedApps.size) + .toString() UpdatesNotifier.showNotification( context, context.getString(R.string.update), @@ -294,13 +285,12 @@ class AppInstallProcessor @Inject constructor( Timber.i("===> doWork: Download started ${fusedDownload.name} ${fusedDownload.status}") } - fusedDownloadRepository.getDownloadFlowById(fusedDownload.id) - .transformWhile { - emit(it) - isInstallRunning(it) - }.collect { latestFusedDownload -> - handleFusedDownload(latestFusedDownload, fusedDownload) - } + fusedDownloadRepository.getDownloadFlowById(fusedDownload.id).transformWhile { + emit(it) + isInstallRunning(it) + }.collect { latestFusedDownload -> + handleFusedDownload(latestFusedDownload, fusedDownload) + } } /** @@ -360,8 +350,7 @@ class AppInstallProcessor @Inject constructor( else -> { Timber.wtf( - TAG, - "===> ${fusedDownload.name} is in wrong state ${fusedDownload.status}" + TAG, "===> ${fusedDownload.name} is in wrong state ${fusedDownload.status}" ) finishInstallation(fusedDownload) } diff --git a/app/src/main/java/foundation/e/apps/utils/StorageComputer.kt b/app/src/main/java/foundation/e/apps/utils/StorageComputer.kt new file mode 100644 index 0000000000000000000000000000000000000000..f92e855a44aaca7df0e74cfd4b96e5c02a25a69a --- /dev/null +++ b/app/src/main/java/foundation/e/apps/utils/StorageComputer.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 MUREANA SAS + * + * 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.utils + +import android.os.Environment +import android.os.StatFs +import foundation.e.apps.data.fusedDownload.models.FusedDownload +import java.text.CharacterIterator +import java.text.StringCharacterIterator + +object StorageComputer { + fun spaceMissing(fusedDownload: FusedDownload): Long { + return getRequiredSpace(fusedDownload) - calculateAvailableDiskSpace() + } + + private fun getRequiredSpace(fusedDownload: FusedDownload) = + fusedDownload.appSize + (500 * (1000 * 1000)) + + private fun calculateAvailableDiskSpace(): Long { + val path = Environment.getDataDirectory().absolutePath + val statFs = StatFs(path) + return statFs.availableBytes + } + + fun humanReadableByteCountSI(byteValue: Long): String { + var bytes = byteValue + + if (-1000 < bytes && bytes < 1000) { + return "$bytes B" + } + val ci: CharacterIterator = StringCharacterIterator("kMGTPE") + while (bytes <= -999950 || bytes >= 999950) { + bytes /= 1000 + ci.next() + } + return String.format("%.1f %cB", bytes / 1000.0, ci.current()) + } +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1611e44857236bcf563b9cd5d64f0015dc70491d..bc86371592b832135f31405533e1e9470e93371d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -165,4 +165,5 @@ \n \n\t• Micro-Targeting abzuschwächen \n\t• die Auswirkungen zu begrenzen, falls dieses Konto von Google eingeschränkt wird + Bitte %1$s Speicherplatz auf diesem Gerät freimachen, damtit die neueste Aktualisierung installiert werden kann. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1ed92dcb2c44dca667e3fc1d1a5a4225f754a25d..ac0af2df98976529b486815962fef5fca1274939 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -154,4 +154,5 @@ Acerca de PWA y aplicaciones de código abierto O mostrar sólo + Libera %1$s en tu teléfono para recibir las últimas actualizaciones. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 23bbc55a36213a1f3319f8359b343ace95663997..7ec04744cb76218a6088e045d9eb7a38b43afb12 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -165,4 +165,5 @@ \n \n\t• limiter le micro-ciblage \n\t• limiter les impacts au cas où ce compte serait restreint par Google + Libérez %1$s sur votre téléphone afin de bénéficier des dernières mises à jour. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0b480e8923f8b0b54932d075858d3fdd24d63655..537c56f538459b9d299cc21a6cb519913afaf0fe 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -165,4 +165,5 @@ \n \n\t• ridurre il micro-targeting \n\t• limitare l\'impatto nel caso in cui l\'account venga bloccato da Google + Per scaricare l\'aggiornamento, devi liberare %1$s di spazio sullo smartphone. \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66c4effc7ad7a588591d6b658a4fdb21a17ed121..c404c57a8a2786baf15288be3751a73491c97e4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,6 +130,7 @@ Update All + Checking Updates... %1$d app update is available %1$d app updates are available @@ -231,4 +232,6 @@ Request Exodus report Clicking on \"%1$s\" will open a tab in your browser with the app\'s package name prefilled.<br /><br />Click on \"Perform analysis\" to start the analysis by Exodus.<br /><br />When the button \"See the report\" is displayed (it can take a while depending on the app) you can close the tab and go back to the app description in %2$s where you should see the Privacy Score. Sometimes Exodus can fail to analyze the app.<br /><br />NB: it can take up to 10 min for the score to be displayed in the app description. + + Free up %1$s on your phone to get the latest updates. diff --git a/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt b/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt index 8bf8335f77098aefd7d6c9e8436e0fa0aa42a4e9..b7a4c8ed76ddf9d70f11dcf75e93d4223e9cf6fc 100644 --- a/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt +++ b/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt @@ -18,7 +18,6 @@ package foundation.e.apps import android.content.Context -import android.content.pm.ApplicationInfo import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.aurora.gplayapi.data.models.AuthData import foundation.e.apps.data.blockedApps.BlockedAppRepository diff --git a/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt b/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt index f906f6fb983e1c6d262a87fcf15dd520264da4b7..38aa99e046bb2c1519af40c912bfb1c2fb504e70 100644 --- a/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt +++ b/app/src/test/java/foundation/e/apps/gplay/GplyHttpClientTest.kt @@ -34,6 +34,8 @@ import kotlinx.coroutines.test.runTest import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -41,8 +43,6 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations import org.mockito.kotlin.any -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class GplyHttpClientTest { diff --git a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt index c6da9c22ece288c28fafce96a143954867765953..8aca9593ea4edd278337082ee14c474d64c87d26 100644 --- a/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt +++ b/app/src/test/java/foundation/e/apps/installProcessor/AppInstallProcessorTest.kt @@ -28,6 +28,7 @@ import foundation.e.apps.data.fusedDownload.FusedDownloadRepository import foundation.e.apps.data.fusedDownload.IFusedManager import foundation.e.apps.data.fusedDownload.models.FusedDownload import foundation.e.apps.data.preference.DataStoreManager +import foundation.e.apps.install.notification.StorageNotificationManager import foundation.e.apps.install.workmanager.AppInstallProcessor import foundation.e.apps.util.MainCoroutineRule import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -74,6 +75,9 @@ class AppInstallProcessorTest { private lateinit var appInstallProcessor: AppInstallProcessor + @Mock + private lateinit var storageNotificationManager: StorageNotificationManager + @Before fun setup() { MockitoAnnotations.openMocks(this) @@ -87,7 +91,8 @@ class AppInstallProcessorTest { fusedDownloadRepository, fakeFusedManagerRepository, fusedAPIRepository, - dataStoreManager + dataStoreManager, + storageNotificationManager ) } diff --git a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt index 01521a1fff76162299a3a1527e49cdab8f48db24..662ede447617490667aa1d4fb91f69748a4b082c 100644 --- a/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt +++ b/app/src/test/java/foundation/e/apps/login/LoginViewModelTest.kt @@ -67,4 +67,4 @@ class LoginViewModelTest { assert(invalidGplayAuth != null) assert((invalidGplayAuth as AuthObject.GPlayAuth).result.isUnknownError()) } -} \ No newline at end of file +} diff --git a/app/src/test/java/foundation/e/apps/util/FakeCall.kt b/app/src/test/java/foundation/e/apps/util/FakeCall.kt index 1c6aa71d81dc64eb600d897a6eb7078b9f9ebc81..c05e9487a7b1cc2f3953a3c8dcf4629d579c33c9 100644 --- a/app/src/test/java/foundation/e/apps/util/FakeCall.kt +++ b/app/src/test/java/foundation/e/apps/util/FakeCall.kt @@ -23,7 +23,6 @@ import okhttp3.Callback import okhttp3.Protocol import okhttp3.Request import okhttp3.Response -import okhttp3.ResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import okio.Timeout @@ -76,4 +75,4 @@ class FakeCall : Call { override fun timeout(): Timeout { TODO("Not yet implemented") } -} \ No newline at end of file +}