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:
Torsten Grote 2024-02-05 17:29:38 -03:00
parent 81fae1a240
commit 86c603e2d2
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
7 changed files with 136 additions and 33 deletions

View file

@ -39,6 +39,7 @@ import com.stevesoltys.seedvault.restore.install.isInstalled
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageRestoreService import com.stevesoltys.seedvault.storage.StorageRestoreService
import com.stevesoltys.seedvault.transport.TRANSPORT_ID 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.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.AppBackupState import com.stevesoltys.seedvault.ui.AppBackupState
import com.stevesoltys.seedvault.ui.AppBackupState.FAILED import com.stevesoltys.seedvault.ui.AppBackupState.FAILED
@ -70,7 +71,7 @@ import java.util.LinkedList
private val TAG = RestoreViewModel::class.java.simpleName 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( internal class RestoreViewModel(
app: Application, app: Application,

View file

@ -1,19 +1,16 @@
package com.stevesoltys.seedvault.transport package com.stevesoltys.seedvault.transport
import android.app.Service import android.app.Service
import android.app.backup.BackupManager
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.transport.backup.BackupRequester
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager 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.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.get import org.koin.core.context.GlobalContext.get
@ -70,25 +67,10 @@ fun requestBackup(context: Context): Boolean {
val backupManager: IBackupManager = get().get() val backupManager: IBackupManager = get().get()
return if (backupManager.isBackupEnabled) { return if (backupManager.isBackupEnabled) {
val packageService: PackageService = get().get() val packageService: PackageService = get().get()
val packages = packageService.eligiblePackages
val appTotals = packageService.expectedAppTotals
val result = try { Log.d(TAG, "Backup is enabled, request backup...")
Log.d(TAG, "Backup is enabled, request backup...") val backupRequester = BackupRequester(context, backupManager, packageService)
val observer = NotificationBackupObserver(context, packages.size, appTotals) return backupRequester.requestBackup()
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
}
} else { } else {
Log.i(TAG, "Backup is not enabled") Log.i(TAG, "Backup is not enabled")
true // this counts as success true // this counts as success

View file

@ -158,7 +158,8 @@ internal class BackupCoordinator(
*/ */
suspend fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long { suspend fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long {
if (packageName != MAGIC_PACKAGE_MANAGER) { 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)) 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 // 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()) { if (isPmBackup && settingsManager.canDoBackupNow()) {
try { try {
backUpApksOfNotBackedUpPackages() backUpApksOfNotBackedUpPackages()

View file

@ -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
}
}

View file

@ -38,7 +38,7 @@ internal class PackageService(
private val packageManager: PackageManager = context.packageManager private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId() private val myUserId = UserHandle.myUserId()
val eligiblePackages: Array<String> val eligiblePackages: List<String>
@WorkerThread @WorkerThread
@Throws(RemoteException::class) @Throws(RemoteException::class)
get() { get() {
@ -70,11 +70,12 @@ internal class PackageService(
val packageArray = eligibleApps.toMutableList() val packageArray = eligibleApps.toMutableList()
packageArray.add(MAGIC_PACKAGE_MANAGER) 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> val notBackedUpPackages: List<PackageInfo>
@WorkerThread @WorkerThread

View file

@ -53,6 +53,11 @@ internal class BackupNotificationManager(private val context: Context) {
private var expectedOptOutApps: Int? = null private var expectedOptOutApps: Int? = null
private var expectedAppTotals: ExpectedAppTotals? = 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 { private fun getObserverChannel(): NotificationChannel {
val title = context.getString(R.string.notification_channel_title) val title = context.getString(R.string.notification_channel_title)
return NotificationChannel(CHANNEL_ID_OBSERVER, title, IMPORTANCE_LOW).apply { 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]. * This should get called before [onBackupUpdate].
*/ */
fun onOptOutAppBackup(packageName: String, transferred: Int, expected: Int) { fun onOptOutAppBackup(packageName: String, transferred: Int, expected: Int) {
if (optOutAppsDone) return
val text = "Opt-out APK for $packageName" val text = "Opt-out APK for $packageName"
if (expectedApps == null) { if (expectedApps == null) {
updateBackgroundBackupNotification(text) updateBackgroundBackupNotification(text)
@ -112,6 +119,7 @@ internal class BackupNotificationManager(private val context: Context) {
* this type is is expected to get called after [onOptOutAppBackup]. * this type is is expected to get called after [onOptOutAppBackup].
*/ */
fun onBackupUpdate(app: CharSequence, transferred: Int) { fun onBackupUpdate(app: CharSequence, transferred: Int) {
optOutAppsDone = true
val expected = expectedApps ?: error("expectedApps is null") val expected = expectedApps ?: error("expectedApps is null")
val addend = expectedOptOutApps ?: 0 val addend = expectedOptOutApps ?: 0
updateBackupNotification( updateBackupNotification(

View file

@ -10,6 +10,7 @@ import android.util.Log.isLoggable
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.transport.backup.BackupRequester
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -18,6 +19,7 @@ private val TAG = NotificationBackupObserver::class.java.simpleName
internal class NotificationBackupObserver( internal class NotificationBackupObserver(
private val context: Context, private val context: Context,
private val backupRequester: BackupRequester,
private val expectedPackages: Int, private val expectedPackages: Int,
appTotals: ExpectedAppTotals, appTotals: ExpectedAppTotals,
) : IBackupObserver.Stub(), KoinComponent { ) : IBackupObserver.Stub(), KoinComponent {
@ -73,13 +75,15 @@ internal class NotificationBackupObserver(
* as a whole failed. * as a whole failed.
*/ */
override fun backupFinished(status: Int) { override fun backupFinished(status: Int) {
if (isLoggable(TAG, INFO)) { if (backupRequester.requestNext()) {
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status") 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?) { private fun showProgressNotification(packageName: String?) {