From a091142a3fb6bcd5881d9d98634f44e395c37ebe Mon Sep 17 00:00:00 2001 From: t-m-w <7275539+t-m-w@users.noreply.github.com> Date: Mon, 5 Dec 2022 04:08:19 -0500 Subject: [PATCH] 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 , 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 --- .../seedvault/restore/RestoreViewModel.kt | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) 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 59556ad1..03e6b70b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -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, + 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?) { - 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)