Allow restoring data for a greater number of apps

Restore app data in smaller batches when performing a full restoration
from a backup set, to prevent a Binder exception that causes the
process to fail entirely.

Android may encounter this exception when trying to call the
transport.startRestore() method if too many packages are involved; in
testing, 300 is an example of too many. Instead of using
IRestoreSession.restoreAll(), use restorePackages() and provide the
package names in batches of 100.

This issue reveals itself when using SeedVault with the D2D patch and
with an OS, such as stock Pixel OS, that includes an abundance of
packages. (Prior to this patch, the call to restoreAll() meant that the
framework would request data restoration for all packages installed,
even if they were not in the metadata.)

In logs, this issue appears as follows:
```
I BackupManagerService: Full restore; asking about 300 apps
W BpBinder: Large outgoing transaction of 528540 bytes, interface descriptor <uncached descriptor>, code 14
E JavaBinder: !!! FAILED BINDER TRANSACTION !!!  (parcel size = 528540)
E BackupManagerService: Unable to contact transport for restore: data parcel size 528540 bytes
```

Change-Id: Ibb5bb4572d9e873beccd6056da5fe3ae4dce71c2
This commit is contained in:
t-m-w 2022-12-05 04:08:19 -05:00 committed by Steve Soltys
parent 6ce2e27a99
commit a091142a3f

View file

@ -64,9 +64,12 @@ import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTA
import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
import java.util.LinkedList
import kotlin.math.min
private val TAG = RestoreViewModel::class.java.simpleName
internal const val PACKAGES_PER_CHUNK = 100
internal class RestoreViewModel(
app: Application,
settingsManager: SettingsManager,
@ -201,18 +204,11 @@ internal class RestoreViewModel(
}
// we need to retrieve the restore sets before starting the restore
// otherwise restoreAll() won't work as they need the restore sets cached internally
val observer = RestoreObserver { observer ->
// this lambda gets executed after we got the restore sets
// now we can start the restore of all available packages
val restoreAllResult = session.restoreAll(token, observer, monitor)
if (restoreAllResult != 0) {
Log.e(TAG, "restoreAll() returned non-zero value")
mRestoreBackupResult.postValue(
RestoreBackupResult(app.getString(R.string.restore_finished_error))
)
}
}
// otherwise restorePackages() won't work as they need the restore sets cached internally
val packages = mChosenRestorableBackup.value?.packageMetadataMap?.keys?.toMutableList()
?: mutableListOf()
// The chunked restoration is performed within the RestoreObserver.
val observer = RestoreObserver(session, token, packages, monitor)
if (session.getAvailableRestoreSets(observer, monitor) != 0) {
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
mRestoreBackupResult.postValue(
@ -294,8 +290,12 @@ internal class RestoreViewModel(
}
@WorkerThread
private inner class RestoreObserver(private val startRestore: (RestoreObserver) -> Unit) :
IRestoreObserver.Stub() {
private inner class RestoreObserver(
private val session: IRestoreSession,
private val token: Long,
private val packages: MutableList<String>,
private val monitor: BackupMonitor
) : IRestoreObserver.Stub() {
/**
* Supply a list of the restore datasets available from the current transport.
@ -307,7 +307,22 @@ internal class RestoreViewModel(
* the current device. If no applicable datasets exist, restoreSets will be null.
*/
override fun restoreSetsAvailable(restoreSets: Array<out RestoreSet>?) {
startRestore(this)
// this gets executed after we got the restore sets
// now we can start the restore of all available packages
restoreNextPackages()
}
private fun restoreNextPackages() {
// Packages are restored in chunks, or else transport.startRestore() in the framework's
// PerformUnifiedRestoreTask.java may fail due to an oversized Binder transaction,
// causing the entire restoration to fail.
val packagesChunk = packages.subList(0, min(packages.size, PACKAGES_PER_CHUNK))
val result =
session.restorePackages(token, this, packagesChunk.toTypedArray(), monitor)
packagesChunk.clear()
if (result != 0) {
Log.e(TAG, "restorePackages() returned non-zero value")
}
}
/**
@ -341,6 +356,10 @@ internal class RestoreViewModel(
* as a whole failed.
*/
override fun restoreFinished(result: Int) {
if (result == 0 && packages.size > 0) {
restoreNextPackages()
return
}
val restoreResult = RestoreBackupResult(
if (result == 0) null
else app.getString(R.string.restore_finished_error)