From 682afa1c695816d9766d85713c32bf615936f0bf Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 3 Oct 2024 12:09:06 -0300 Subject: [PATCH] Try to recover data for force stopped apps from latest snapshot The system doesn't allow us to backup forced stopped apps, but if we had data for them once, we can at least carry it along. --- .../seedvault/repo/SnapshotCreator.kt | 16 +++++++++++----- .../seedvault/settings/AppStatusAdapter.kt | 12 ++++++++++++ .../seedvault/worker/ApkBackupManager.kt | 11 +++++++++++ .../stevesoltys/seedvault/worker/WorkerModule.kt | 1 + .../seedvault/worker/ApkBackupManagerTest.kt | 16 ++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/repo/SnapshotCreator.kt b/app/src/main/java/com/stevesoltys/seedvault/repo/SnapshotCreator.kt index b5eb185c..d24ee5ab 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/repo/SnapshotCreator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/repo/SnapshotCreator.kt @@ -114,8 +114,10 @@ internal class SnapshotCreator( * 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. + * + * @param warnNoData log a warning, if [snapshot] had no data for the given [packageName]. */ - fun onNoDataInCurrentRun(snapshot: Snapshot, packageName: String) { + fun onNoDataInCurrentRun(snapshot: Snapshot, packageName: String, isStopped: Boolean = false) { log.info { "onKvPackageNotChanged(${snapshot.token}, $packageName)" } if (appBuilderMap.containsKey(packageName)) { @@ -125,7 +127,9 @@ internal class SnapshotCreator( } val app = snapshot.appsMap[packageName] if (app == null) { - log.error { " No changed data for $packageName, but we had no data for it" } + if (!isStopped) log.error { + " No changed data for $packageName, but we had no data for it" + } return } @@ -145,9 +149,11 @@ internal class SnapshotCreator( 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) + // record local metadata if this is not a stopped app + if (!isStopped) { + val packageInfo = PackageInfo().apply { this.packageName = packageName } + metadataManager.onPackageBackedUp(packageInfo, app.type.toBackupType(), app.size) + } } /** diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt index 6cd85797..3f8cfba2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.settings +import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS @@ -24,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.NO_POSITION import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_WAS_STOPPED import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED import com.stevesoltys.seedvault.ui.AppViewHolder import com.stevesoltys.seedvault.ui.toRelativeTime @@ -122,7 +124,17 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe " (${formatShortFileSize(v.context, item.size)})" } appInfo.visibility = VISIBLE + } else if (item.status == FAILED_WAS_STOPPED && item.time > 0) { + @SuppressLint("SetTextI18n") + appInfo.text = if (item.size == null) { + item.time.toRelativeTime(context).toString() + } else { + item.time.toRelativeTime(context).toString() + + " (${formatShortFileSize(v.context, item.size)})" + } + "\n${item.status.getBackupText(context)}" + appInfo.visibility = VISIBLE } + // setState() above sets appInfo state for other cases already checkBox.visibility = INVISIBLE } // show disabled items differently diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt index 65d9b400..693a991e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackupManager.kt @@ -11,6 +11,7 @@ import android.util.Log import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED +import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.SnapshotManager import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService @@ -22,6 +23,7 @@ import java.io.IOException internal class ApkBackupManager( private val context: Context, + private val appBackupManager: AppBackupManager, private val settingsManager: SettingsManager, private val snapshotManager: SnapshotManager, private val metadataManager: MetadataManager, @@ -84,6 +86,15 @@ internal class ApkBackupManager( } catch (e: IOException) { Log.e(TAG, "Error storing new metadata for $packageName: ", e) } + // see if there's data in latest snapshot for this app and re-use it + // this can be helpful for backing up recently STOPPED apps + snapshotManager.latestSnapshot?.let { snapshot -> + appBackupManager.snapshotCreator?.onNoDataInCurrentRun( + snapshot = snapshot, + packageName = packageName, + isStopped = true, + ) + } } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt index a45131fd..40dfd4c7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt @@ -38,6 +38,7 @@ val workerModule = module { single { ApkBackupManager( context = androidContext(), + appBackupManager = get(), settingsManager = get(), snapshotManager = get(), metadataManager = get(), diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt index 28d5d638..ae8940e7 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupManagerTest.kt @@ -15,6 +15,8 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED +import com.stevesoltys.seedvault.repo.AppBackupManager +import com.stevesoltys.seedvault.repo.SnapshotCreator import com.stevesoltys.seedvault.repo.SnapshotManager import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.backup.PackageService @@ -33,6 +35,7 @@ import org.junit.jupiter.api.Test internal class ApkBackupManagerTest : TransportTest() { + private val appBackupManager: AppBackupManager = mockk() private val snapshotManager: SnapshotManager = mockk() private val packageService: PackageService = mockk() private val apkBackup: ApkBackup = mockk() @@ -43,6 +46,7 @@ internal class ApkBackupManagerTest : TransportTest() { private val apkBackupManager = ApkBackupManager( context = context, + appBackupManager = appBackupManager, settingsManager = settingsManager, snapshotManager = snapshotManager, metadataManager = metadataManager, @@ -53,9 +57,11 @@ internal class ApkBackupManagerTest : TransportTest() { ) private val packageMetadata: PackageMetadata = mockk() + private val snapshotCreator: SnapshotCreator = mockk() init { every { backendManager.backend } returns backend + every { appBackupManager.snapshotCreator } returns snapshotCreator } @Test @@ -63,6 +69,8 @@ internal class ApkBackupManagerTest : TransportTest() { every { nm.onAppsNotBackedUp() } just Runs every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { settingsManager.isBackupEnabled(packageInfo.packageName) } returns true + every { snapshotManager.latestSnapshot } returns snapshot + every { snapshotCreator.onNoDataInCurrentRun(snapshot, packageName, true) } just Runs expectUploadIcons() @@ -87,6 +95,8 @@ internal class ApkBackupManagerTest : TransportTest() { every { nm.onAppsNotBackedUp() } just Runs every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { settingsManager.isBackupEnabled(packageInfo.packageName) } returns true + every { snapshotManager.latestSnapshot } returns snapshot + every { snapshotCreator.onNoDataInCurrentRun(snapshot, packageName, true) } just Runs expectUploadIcons() @@ -117,6 +127,8 @@ internal class ApkBackupManagerTest : TransportTest() { every { nm.onAppsNotBackedUp() } just Runs every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { settingsManager.isBackupEnabled(packageInfo.packageName) } returns true + every { snapshotManager.latestSnapshot } returns snapshot + every { snapshotCreator.onNoDataInCurrentRun(snapshot, packageName, true) } just Runs expectUploadIcons() @@ -141,6 +153,8 @@ internal class ApkBackupManagerTest : TransportTest() { every { nm.onAppsNotBackedUp() } just Runs every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { settingsManager.isBackupEnabled(packageInfo.packageName) } returns true + every { snapshotManager.latestSnapshot } returns snapshot + every { snapshotCreator.onNoDataInCurrentRun(snapshot, packageName, true) } just Runs expectUploadIcons() @@ -223,6 +237,8 @@ internal class ApkBackupManagerTest : TransportTest() { every { nm.onAppsNotBackedUp() } just Runs every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { settingsManager.isBackupEnabled(packageInfo.packageName) } returns true + every { snapshotManager.latestSnapshot } returns snapshot + every { snapshotCreator.onNoDataInCurrentRun(snapshot, packageName, true) } just Runs expectUploadIcons()