Use BackupRequester to request backup in chunks
Otherwise users with lots of installed apps with request a lot of packages causing binder transactions to reach their size limit and crash.
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 9992a15..e03e55e 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.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 @@
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 443badd..1b9fe3b 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 @@
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 c6f9e1c..2f08867 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 @@
*/
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 @@
}
}
// 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 0000000..d1e195f
--- /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<String>): 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<String> {
+ 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 7d16e17..a8ed0d3 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 @@
private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId()
- val eligiblePackages: Array<String>
+ val eligiblePackages: List<String>
@WorkerThread
@Throws(RemoteException::class)
get() {
@@ -70,11 +70,12 @@
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<PackageInfo>
@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 8308fc2..2b9a0e5 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 @@
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 @@
* 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 @@
* 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 d959dd0..ea7bc4a 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 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 @@
internal class NotificationBackupObserver(
private val context: Context,
+ private val backupRequester: BackupRequester,
private val expectedPackages: Int,
appTotals: ExpectedAppTotals,
) : IBackupObserver.Stub(), KoinComponent {
@@ -73,13 +75,15 @@
* 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?) {