diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index 9992a15c..e03e55e8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -39,6 +39,7 @@ import com.stevesoltys.seedvault.restore.install.isInstalled import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.transport.TRANSPORT_ID +import com.stevesoltys.seedvault.transport.backup.NUM_PACKAGES_PER_TRANSACTION import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState.FAILED @@ -70,7 +71,7 @@ import java.util.LinkedList private val TAG = RestoreViewModel::class.java.simpleName -internal const val PACKAGES_PER_CHUNK = 100 +internal const val PACKAGES_PER_CHUNK = NUM_PACKAGES_PER_TRANSACTION internal class RestoreViewModel( app: Application, diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 443badd4..1b9fe3b6 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -1,19 +1,16 @@ package com.stevesoltys.seedvault.transport import android.app.Service -import android.app.backup.BackupManager import android.app.backup.IBackupManager import android.content.Context import android.content.Intent import android.os.IBinder -import android.os.RemoteException import android.util.Log import androidx.annotation.WorkerThread -import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.crypto.KeyManager +import com.stevesoltys.seedvault.transport.backup.BackupRequester import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager -import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.koin.core.context.GlobalContext.get @@ -70,25 +67,10 @@ fun requestBackup(context: Context): Boolean { val backupManager: IBackupManager = get().get() return if (backupManager.isBackupEnabled) { val packageService: PackageService = get().get() - val packages = packageService.eligiblePackages - val appTotals = packageService.expectedAppTotals - val result = try { - Log.d(TAG, "Backup is enabled, request backup...") - val observer = NotificationBackupObserver(context, packages.size, appTotals) - backupManager.requestBackup(packages, observer, BackupMonitor(), 0) - } catch (e: RemoteException) { - Log.e(TAG, "Error during backup: ", e) - val nm: BackupNotificationManager = get().get() - nm.onBackupError() - } - if (result == BackupManager.SUCCESS) { - Log.i(TAG, "Backup request succeeded ") - true - } else { - Log.e(TAG, "Backup request failed: $result") - false - } + Log.d(TAG, "Backup is enabled, request backup...") + val backupRequester = BackupRequester(context, backupManager, packageService) + return backupRequester.requestBackup() } else { Log.i(TAG, "Backup is not enabled") true // this counts as success diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index c6f9e1c6..2f088671 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -158,7 +158,8 @@ internal class BackupCoordinator( */ suspend fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long { if (packageName != MAGIC_PACKAGE_MANAGER) { - // try to back up APK here as later methods are sometimes not called called + // try to back up APK here as later methods are sometimes not called + // TODO move this into BackupWorker backUpApk(context.packageManager.getPackageInfo(packageName, GET_SIGNING_CERTIFICATES)) } @@ -379,6 +380,7 @@ internal class BackupCoordinator( } } // hook in here to back up APKs of apps that are otherwise not allowed for backup + // TODO move this into BackupWorker if (isPmBackup && settingsManager.canDoBackupNow()) { try { backUpApksOfNotBackedUpPackages() diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupRequester.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupRequester.kt new file mode 100644 index 00000000..d1e195fb --- /dev/null +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupRequester.kt @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.transport.backup + +import android.app.backup.BackupManager +import android.app.backup.IBackupManager +import android.content.Context +import android.os.RemoteException +import android.util.Log +import androidx.annotation.WorkerThread +import com.stevesoltys.seedvault.BackupMonitor +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver +import org.koin.core.component.KoinComponent +import org.koin.core.context.GlobalContext + +private val TAG = BackupRequester::class.java.simpleName +internal const val NUM_PACKAGES_PER_TRANSACTION = 100 + +/** + * Used for requesting a backup of all installed packages, + * in chunks if there are more than [NUM_PACKAGES_PER_TRANSACTION]. + * + * Can only be used once for one backup. + * Make a new instance for subsequent backups. + */ +@WorkerThread +internal class BackupRequester( + context: Context, + private val backupManager: IBackupManager, + val packageService: PackageService, +) : KoinComponent { + + private val packages = packageService.eligiblePackages + private val observer = NotificationBackupObserver( + context = context, + backupRequester = this, + expectedPackages = packages.size, + appTotals = packageService.expectedAppTotals, + ) + private val monitor = BackupMonitor() + + /** + * The current package index. + * + * Used for splitting the packages into chunks. + */ + private var packageIndex: Int = 0 + + fun requestBackup(): Boolean { + if (packageIndex != 0) error("requestBackup() called more than once!") + + return request(getNextChunk()) + } + + /** + * Backs up the next chunk of packages. + * + * @return true, if backup for all packages was already requested and false, + * if there are more packages that we just have requested backup for. + */ + fun requestNext(): Boolean { + if (packageIndex <= 0) error("requestBackup() must be called first!") + + // Backup next chunk if there are more packages to back up. + return if (packageIndex < packages.size) { + request(getNextChunk()) + false + } else { + true + } + } + + private fun request(chunk: Array): Boolean { + Log.i(TAG, "${chunk.toList()}") + val result = try { + backupManager.requestBackup(chunk, observer, monitor, 0) + } catch (e: RemoteException) { + Log.e(TAG, "Error during backup: ", e) + val nm: BackupNotificationManager = GlobalContext.get().get() + nm.onBackupError() + } + return if (result == BackupManager.SUCCESS) { + Log.i(TAG, "Backup request succeeded") + true + } else { + Log.e(TAG, "Backup request failed: $result") + false + } + } + + private fun getNextChunk(): Array { + val nextChunkIndex = + (packageIndex + NUM_PACKAGES_PER_TRANSACTION).coerceAtMost(packages.size) + val packageChunk = packages.subList(packageIndex, nextChunkIndex).toTypedArray() + val numBackingUp = packageIndex + packageChunk.size + Log.i(TAG, "Requesting backup for $numBackingUp/${packages.size} packages...") + packageIndex += packageChunk.size + return packageChunk + } + +} diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt index 7d16e173..a8ed0d3a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt @@ -38,7 +38,7 @@ internal class PackageService( private val packageManager: PackageManager = context.packageManager private val myUserId = UserHandle.myUserId() - val eligiblePackages: Array + val eligiblePackages: List @WorkerThread @Throws(RemoteException::class) get() { @@ -70,11 +70,12 @@ internal class PackageService( val packageArray = eligibleApps.toMutableList() packageArray.add(MAGIC_PACKAGE_MANAGER) - return packageArray.toTypedArray() + return packageArray } /** - * A list of packages that will not be backed up. + * A list of packages that will not be backed up, + * because they are currently force-stopped for example. */ val notBackedUpPackages: List @WorkerThread diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index 8308fc2a..2b9a0e5b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -53,6 +53,11 @@ internal class BackupNotificationManager(private val context: Context) { private var expectedOptOutApps: Int? = null private var expectedAppTotals: ExpectedAppTotals? = null + /** + * Used as a (temporary) hack to fix progress reporting when fake d2d is enabled. + */ + private var optOutAppsDone = false + private fun getObserverChannel(): NotificationChannel { val title = context.getString(R.string.notification_channel_title) return NotificationChannel(CHANNEL_ID_OBSERVER, title, IMPORTANCE_LOW).apply { @@ -98,6 +103,8 @@ internal class BackupNotificationManager(private val context: Context) { * This should get called before [onBackupUpdate]. */ fun onOptOutAppBackup(packageName: String, transferred: Int, expected: Int) { + if (optOutAppsDone) return + val text = "Opt-out APK for $packageName" if (expectedApps == null) { updateBackgroundBackupNotification(text) @@ -112,6 +119,7 @@ internal class BackupNotificationManager(private val context: Context) { * this type is is expected to get called after [onOptOutAppBackup]. */ fun onBackupUpdate(app: CharSequence, transferred: Int) { + optOutAppsDone = true val expected = expectedApps ?: error("expectedApps is null") val addend = expectedOptOutApps ?: 0 updateBackupNotification( diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index d959dd05..ea7bc4a2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -10,6 +10,7 @@ import android.util.Log.isLoggable import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.transport.backup.BackupRequester import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -18,6 +19,7 @@ private val TAG = NotificationBackupObserver::class.java.simpleName internal class NotificationBackupObserver( private val context: Context, + private val backupRequester: BackupRequester, private val expectedPackages: Int, appTotals: ExpectedAppTotals, ) : IBackupObserver.Stub(), KoinComponent { @@ -73,13 +75,15 @@ internal class NotificationBackupObserver( * as a whole failed. */ override fun backupFinished(status: Int) { - if (isLoggable(TAG, INFO)) { - Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status") + if (backupRequester.requestNext()) { + if (isLoggable(TAG, INFO)) { + Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status") + } + val success = status == 0 + val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null + val size = if (success) metadataManager.getPackagesBackupSize() else 0L + nm.onBackupFinished(success, numBackedUp, size) } - val success = status == 0 - val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null - val size = if (success) metadataManager.getPackagesBackupSize() else 0L - nm.onBackupFinished(success, numBackedUp, size) } private fun showProgressNotification(packageName: String?) {