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.
This commit is contained in:
parent
81fae1a240
commit
86c603e2d2
7 changed files with 136 additions and 33 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -38,7 +38,7 @@ internal class PackageService(
|
|||
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 @@ 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<PackageInfo>
|
||||
@WorkerThread
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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?) {
|
||||
|
|
Loading…
Reference in a new issue