Skip to content

Commit

Permalink
Use BackupManagerMonitor to handle K/V with no data changed
Browse files Browse the repository at this point in the history
The fake package manager package is essential for the backup, but when its data doesn't change and we request a normal incremental backup, it doesn't get included, because our transport doesn't even get called for it. Only the BackupMonitor gets a hint that it had no (new?) data via LOG_EVENT_ID_NO_DATA_TO_SEND.
This behavior started with Android 15 that fixed a bug that caused @pm@ to always backup. However, other K/V apps were probably affected before.
  • Loading branch information
grote committed Oct 7, 2024
1 parent f3f710c commit 1c3af13
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 24 deletions.
16 changes: 12 additions & 4 deletions app/src/main/java/com/stevesoltys/seedvault/BackupMonitor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,26 @@ import android.util.Log.DEBUG

private val TAG = BackupMonitor::class.java.name

class BackupMonitor : IBackupManagerMonitor.Stub() {
open class BackupMonitor : IBackupManagerMonitor.Stub() {

override fun onEvent(bundle: Bundle) {
val id = bundle.getInt(EXTRA_LOG_EVENT_ID)
val packageName = bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME, "?")
onEvent(
id = bundle.getInt(EXTRA_LOG_EVENT_ID),
category = bundle.getInt(EXTRA_LOG_EVENT_CATEGORY),
packageName = bundle.getString(EXTRA_LOG_EVENT_PACKAGE_NAME)
?: error("no package name for $bundle"),
bundle = bundle,
)
}

open fun onEvent(id: Int, category: Int, packageName: String, bundle: Bundle) {
if (id == LOG_EVENT_ID_ERROR_PREFLIGHT) {
val preflightResult = bundle.getLong(EXTRA_LOG_PREFLIGHT_ERROR, -1)
Log.w(TAG, "Pre-flight error from $packageName: $preflightResult")
}
if (!Log.isLoggable(TAG, DEBUG)) return
Log.d(TAG, "ID: $id")
Log.d(TAG, "CATEGORY: " + bundle.getInt(EXTRA_LOG_EVENT_CATEGORY, -1))
Log.d(TAG, "CATEGORY: $category")
Log.d(TAG, "PACKAGE: $packageName")
}

Expand Down
12 changes: 7 additions & 5 deletions app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,7 @@ data class PackageMetadata(
companion object {
fun fromSnapshot(app: Snapshot.App) = PackageMetadata(
time = app.time,
backupType = when (app.type) {
Snapshot.BackupType.FULL -> BackupType.FULL
Snapshot.BackupType.KV -> BackupType.KV
else -> null
},
backupType = app.type.toBackupType(),
name = app.name,
chunkIds = app.chunkIdsList.hexFromProto(),
system = app.system,
Expand Down Expand Up @@ -153,6 +149,12 @@ data class PackageMetadata(
it.isNotEmpty()
},
)

fun Snapshot.BackupType.toBackupType() = when (this) {
Snapshot.BackupType.FULL -> BackupType.FULL
Snapshot.BackupType.KV -> BackupType.KV
else -> null
}
}

val isInternalSystem: Boolean = system && !isLaunchableSystemApp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,13 @@ internal class MetadataManager(
/**
* Call this after a package has been backed up successfully.
*
* It updates the packages' metadata
* and writes it encrypted to the given [OutputStream] as well as the internal cache.
*
* Closing the [OutputStream] is the responsibility of the caller.
* It updates the packages' metadata.
*/
@Synchronized
@Throws(IOException::class)
fun onPackageBackedUp(
packageInfo: PackageInfo,
type: BackupType,
type: BackupType?,
size: Long?,
) {
val packageName = packageInfo.packageName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import android.provider.Settings
import android.provider.Settings.Secure.ANDROID_ID
import com.google.protobuf.ByteString
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.BackupType
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageMetadata.Companion.toBackupType
import com.stevesoltys.seedvault.proto.Snapshot
import com.stevesoltys.seedvault.proto.Snapshot.Apk
import com.stevesoltys.seedvault.proto.Snapshot.App
Expand All @@ -28,6 +30,7 @@ import com.stevesoltys.seedvault.transport.backup.isSystemApp
import io.github.oshai.kotlinlogging.KotlinLogging
import org.calyxos.seedvault.core.backends.AppBackupFileType
import org.calyxos.seedvault.core.toHexString
import java.util.concurrent.ConcurrentHashMap

/**
* Assembles snapshot information over the course of a single backup run
Expand All @@ -43,8 +46,8 @@ internal class SnapshotCreator(
private val log = KotlinLogging.logger { }

private val snapshotBuilder = Snapshot.newBuilder()
private val appBuilderMap = mutableMapOf<String, App.Builder>()
private val blobsMap = mutableMapOf<String, Blob>()
private val appBuilderMap = ConcurrentHashMap<String, App.Builder>()
private val blobsMap = ConcurrentHashMap<String, Blob>()

private val launchableSystemApps by lazy {
// as we can't ask [PackageInfo] for this, we keep a set of packages around
Expand Down Expand Up @@ -103,6 +106,50 @@ internal class SnapshotCreator(
metadataManager.onPackageBackedUp(packageInfo, backupType, backupData.size)
}

/**
* Call this when the given [packageName] may not call our transport at all in this run,
* but we need to include data for the package in the current snapshot.
* This may happen for K/V apps like @pm@ that don't call us when their data didn't change.
*
* If we do *not* have data for the given [packageName],
* we try to extract data from the given [snapshot] (ideally we latest we have) and
* add it to the current snapshot under construction.
*/
fun onNoDataInCurrentRun(snapshot: Snapshot, packageName: String) {
log.info { "onKvPackageNotChanged(${snapshot.token}, $packageName)" }

if (appBuilderMap.containsKey(packageName)) {
// the system backs up K/V apps repeatedly, e.g. @pm@
log.info { " Already have data for $packageName in current snapshot, not touching it" }
return
}
val app = snapshot.appsMap[packageName]
if (app == null) {
log.error { " No changed data for $packageName, but we had no data for it" }
return
}

// get chunkIds from last snapshot
val chunkIds = app.chunkIdsList.hexFromProto() +
app.apk.splitsList.flatMap { it.chunkIdsList }.hexFromProto()

// get blobs for chunkIds
val blobMap = mutableMapOf<String, Blob>()
chunkIds.forEach { chunkId ->
val blob = snapshot.blobsMap[chunkId]
if (blob == null) log.error { " No blob for $packageName chunk $chunkId" }
else blobMap[chunkId] = blob
}

// add info to current snapshot
appBuilderMap[packageName] = app.toBuilder()
blobsMap.putAll(blobMap)

// record local metadata
val packageInfo = PackageInfo().apply { this.packageName = packageName }
metadataManager.onPackageBackedUp(packageInfo, app.type.toBackupType(), app.size)
}

/**
* Call this after all blobs for the app icons have been saved to the backend.
*/
Expand Down Expand Up @@ -134,6 +181,8 @@ internal class SnapshotCreator(
putAllApps(appBuilderMap.mapValues { it.value.build() })
putAllBlobs(this@SnapshotCreator.blobsMap)
}.build()
// may as well fail the backup, if @pm@ isn't in it
check(MAGIC_PACKAGE_MANAGER in snapshot.appsMap) { "No metadata for @pm@" }
appBuilderMap.clear()
snapshotBuilder.clear()
blobsMap.clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal class SnapshotManager(
* The latest [Snapshot]. May be stale if [onSnapshotsLoaded] has not returned
* or wasn't called since new snapshots have been created.
*/
@Volatile
var latestSnapshot: Snapshot? = null
private set

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module

val backupModule = module {
factory { BackupTransportMonitor(get(), get()) }
single { BackupInitializer(get()) }
single { InputFactory() }
single {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2024 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/

package com.stevesoltys.seedvault.transport.backup

import android.app.backup.BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY
import android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_NO_DATA_TO_SEND
import android.os.Bundle
import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.repo.AppBackupManager
import com.stevesoltys.seedvault.repo.SnapshotManager
import io.github.oshai.kotlinlogging.KotlinLogging

internal class BackupTransportMonitor(
private val appBackupManager: AppBackupManager,
private val snapshotManager: SnapshotManager,
) : BackupMonitor() {

private val log = KotlinLogging.logger { }

override fun onEvent(id: Int, category: Int, packageName: String, bundle: Bundle) {
super.onEvent(id, category, packageName, bundle)
if (id == LOG_EVENT_ID_NO_DATA_TO_SEND &&
category == LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY
) {
sendNoDataChanged(packageName)
}
}

private fun sendNoDataChanged(packageName: String) {
log.info { "sendNoDataChanged($packageName)" }

val snapshot = snapshotManager.latestSnapshot
if (snapshot == null) {
log.error { "No latest snapshot!" }
} else {
val snapshotCreator = appBackupManager.snapshotCreator ?: error("No SnapshotCreator")
snapshotCreator.onNoDataInCurrentRun(snapshot, packageName)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor
import android.util.Log
import com.stevesoltys.seedvault.NO_DATA_END_SENTINEL
import com.stevesoltys.seedvault.repo.BackupData
import com.stevesoltys.seedvault.repo.BackupReceiver
import java.io.IOException
Expand Down Expand Up @@ -52,6 +53,8 @@ internal class KVBackup(
else -> Log.i(TAG, "Performing K/V backup for $packageName")
}
check(state == null) { "Have unexpected state for ${state?.packageInfo?.packageName}" }
// This fake package name just signals that we've seen all packages without new data
if (packageName == NO_DATA_END_SENTINEL) return TRANSPORT_OK

// initialize state
state = KVBackupState(packageInfo = packageInfo, db = dbManager.getDb(packageName))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,17 @@ internal class PackageService(
logPackages(packages)
}

val eligibleApps = packages.filter(::shouldIncludeAppInBackup).toTypedArray()
val eligibleApps = packages.filter(::shouldIncludeAppInBackup).toMutableList()
// log eligible packages
if (Log.isLoggable(TAG, INFO)) {
Log.i(TAG, "Filtering left ${eligibleApps.size} eligible packages:")
logPackages(eligibleApps.toList())
logPackages(eligibleApps)
}

// add magic @pm@ package (PACKAGE_MANAGER_SENTINEL) which holds package manager data
val packageArray = eligibleApps.toMutableList()
packageArray.add(MAGIC_PACKAGE_MANAGER)
eligibleApps.add(0, MAGIC_PACKAGE_MANAGER)

return packageArray
return eligibleApps
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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.transport.backup.BackupTransportMonitor
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
Expand All @@ -32,6 +32,7 @@ internal const val NUM_PACKAGES_PER_TRANSACTION = 100
internal class BackupRequester(
context: Context,
private val backupManager: IBackupManager,
private val monitor: BackupTransportMonitor,
val packageService: PackageService,
) : KoinComponent {

Expand All @@ -43,7 +44,6 @@ internal class BackupRequester(
backupRequester = this,
requestedPackages = packages.size,
)
private val monitor = BackupMonitor()

/**
* The current package index.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ val workerModule = module {
BackupRequester(
context = androidContext(),
backupManager = get(),
monitor = get(),
packageService = get(),
)
}
Expand Down
Loading

0 comments on commit 1c3af13

Please sign in to comment.