From d2daf25355e33e6af75f26c68b22f349dd8ab282 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 b5eb185c0..d24ee5ab0 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 6cd857971..3f8cfba2d 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 65d9b400a..693a991ed 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 a45131fdf..40dfd4c7c 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 28d5d6388..ae8940e75 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()