Fix binder exception when restoring a large number of applications

This commit is contained in:
Steve Soltys 2023-09-24 23:20:11 +00:00
parent a091142a3f
commit 6c7afd5f55

View file

@ -1,6 +1,7 @@
package com.stevesoltys.seedvault.restore package com.stevesoltys.seedvault.restore
import android.app.Application import android.app.Application
import android.app.backup.BackupTransport
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.app.backup.IRestoreObserver import android.app.backup.IRestoreObserver
import android.app.backup.IRestoreSession import android.app.backup.IRestoreSession
@ -64,11 +65,10 @@ 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.restore.RestoreService.Companion.EXTRA_USER_ID
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
import java.util.LinkedList import java.util.LinkedList
import kotlin.math.min
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 = 10
internal class RestoreViewModel( internal class RestoreViewModel(
app: Application, app: Application,
@ -140,6 +140,7 @@ internal class RestoreViewModel(
Log.d(TAG, "Ignoring RestoreSet with no last backup time: $token.") Log.d(TAG, "Ignoring RestoreSet with no last backup time: $token.")
null null
} }
else -> RestorableBackup(metadata) else -> RestorableBackup(metadata)
} }
} }
@ -203,12 +204,13 @@ internal class RestoreViewModel(
return return
} }
// we need to retrieve the restore sets before starting the restore // We need to retrieve the restore sets before starting the restore.
// otherwise restorePackages() won't work as they need the restore sets cached internally // Otherwise, restorePackages() won't work as they need the restore sets cached internally.
val packages = mChosenRestorableBackup.value?.packageMetadataMap?.keys?.toMutableList() val packages = mChosenRestorableBackup.value?.packageMetadataMap?.keys?.toList()
?: mutableListOf() ?: emptyList()
// The chunked restoration is performed within the RestoreObserver.
val observer = RestoreObserver(session, token, packages, monitor) val observer = RestoreObserver(session, token, packages, monitor)
if (session.getAvailableRestoreSets(observer, monitor) != 0) { if (session.getAvailableRestoreSets(observer, monitor) != 0) {
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value") Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
mRestoreBackupResult.postValue( mRestoreBackupResult.postValue(
@ -225,6 +227,7 @@ internal class RestoreViewModel(
// check previous package first and change status // check previous package first and change status
updateLatestPackage(list) updateLatestPackage(list)
// add current package // add current package
list.addFirst(AppRestoreResult(packageName, getAppName(app, packageName), IN_PROGRESS)) list.addFirst(AppRestoreResult(packageName, getAppName(app, packageName), IN_PROGRESS))
mRestoreProgress.postValue(list) mRestoreProgress.postValue(list)
@ -293,10 +296,17 @@ internal class RestoreViewModel(
private inner class RestoreObserver( private inner class RestoreObserver(
private val session: IRestoreSession, private val session: IRestoreSession,
private val token: Long, private val token: Long,
private val packages: MutableList<String>, private val packages: List<String>,
private val monitor: BackupMonitor private val monitor: BackupMonitor,
) : IRestoreObserver.Stub() { ) : IRestoreObserver.Stub() {
/**
* The current package index.
*
* Used for splitting the packages into chunks.
*/
private var packageIndex: Int = 0
/** /**
* Supply a list of the restore datasets available from the current transport. * Supply a list of the restore datasets available from the current transport.
* This method is invoked as a callback following the application's use of the * This method is invoked as a callback following the application's use of the
@ -312,14 +322,20 @@ internal class RestoreViewModel(
restoreNextPackages() restoreNextPackages()
} }
/**
* Restore the next chunk of packages.
*
* We need to restore in chunks, otherwise [BackupTransport.startRestore] in the
* framework's [PerformUnifiedRestoreTask] may fail due to an oversize Binder
* transaction, causing the entire restoration to fail.
*/
private fun restoreNextPackages() { private fun restoreNextPackages() {
// Packages are restored in chunks, or else transport.startRestore() in the framework's val chunkSize = (packageIndex + PACKAGES_PER_CHUNK).coerceAtMost(packages.size)
// PerformUnifiedRestoreTask.java may fail due to an oversized Binder transaction, val packageChunk = packages.subList(packageIndex, chunkSize).toTypedArray()
// causing the entire restoration to fail. packageIndex += packageChunk.size
val packagesChunk = packages.subList(0, min(packages.size, PACKAGES_PER_CHUNK))
val result = val result = session.restorePackages(token, this, packageChunk, monitor)
session.restorePackages(token, this, packagesChunk.toTypedArray(), monitor)
packagesChunk.clear()
if (result != 0) { if (result != 0) {
Log.e(TAG, "restorePackages() returned non-zero value") Log.e(TAG, "restorePackages() returned non-zero value")
} }
@ -356,10 +372,13 @@ internal class RestoreViewModel(
* as a whole failed. * as a whole failed.
*/ */
override fun restoreFinished(result: Int) { override fun restoreFinished(result: Int) {
if (result == 0 && packages.size > 0) {
// Restore next chunk if successful and there are more packages to restore.
if (result == 0 && packageIndex < packages.size) {
restoreNextPackages() restoreNextPackages()
return return
} }
val restoreResult = RestoreBackupResult( val restoreResult = RestoreBackupResult(
if (result == 0) null if (result == 0) null
else app.getString(R.string.restore_finished_error) else app.getString(R.string.restore_finished_error)