diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index f3403de79..84469065f 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -2,6 +2,7 @@ package com.stevesoltys.seedvault import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL import android.app.Application +import android.app.backup.BackupManager import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL import android.app.backup.IBackupManager import android.content.Context @@ -117,9 +118,10 @@ open class App : Application() { } -const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL +const val MAGIC_PACKAGE_MANAGER: String = PACKAGE_MANAGER_SENTINEL const val ANCESTRAL_RECORD_KEY = "@ancestral_record@" const val GLOBAL_METADATA_KEY = "@meta@" +const val ERROR_BACKUP_CANCELLED: Int = BackupManager.ERROR_BACKUP_CANCELLED // TODO this doesn't work for LineageOS as they do public debug builds fun isDebugBuild() = Build.TYPE == "userdebug" diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt index 47176a091..b441ae227 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt @@ -143,6 +143,15 @@ class SettingsManager(private val context: Context) { fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName) + /** + * Disables backup for an app. Similar to [onAppBackupStatusChanged]. + */ + fun disableBackup(packageName: String) { + if (blacklistedApps.add(packageName)) { + prefs.edit().putStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, blacklistedApps).apply() + } + } + fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false) @UiThread diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 47235213e..553587fd2 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -1,15 +1,18 @@ package com.stevesoltys.seedvault.ui.notification import android.app.backup.BackupProgress +import android.app.backup.BackupTransport.AGENT_ERROR import android.app.backup.IBackupObserver import android.content.Context import android.content.pm.PackageManager.NameNotFoundException import android.util.Log import android.util.Log.INFO import android.util.Log.isLoggable +import com.stevesoltys.seedvault.ERROR_BACKUP_CANCELLED import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.MetadataManager +import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.BackupRequester import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals import org.koin.core.component.KoinComponent @@ -26,9 +29,12 @@ internal class NotificationBackupObserver( private val nm: BackupNotificationManager by inject() private val metadataManager: MetadataManager by inject() + private val settingsManager: SettingsManager by inject() private var currentPackage: String? = null private var numPackages: Int = 0 + private var errorPackageName: String? = null + init { // Inform the notification manager that a backup has started // and inform about the expected numbers, so it can compute a total. @@ -63,6 +69,16 @@ internal class NotificationBackupObserver( if (isLoggable(TAG, INFO)) { Log.i(TAG, "Completed. Target: $target, status: $status") } + // Apps that get killed while interacting with their [BackupAgent] cancel the entire backup. + // In order to prevent them from DoSing us, we remember them here to auto-disable them. + // We noticed that the same app behavior can cause a status of + // either AGENT_ERROR or ERROR_BACKUP_CANCELLED, so we need to handle both. + errorPackageName = if (status == AGENT_ERROR || status == ERROR_BACKUP_CANCELLED) { + target + } else { + null // To not disable apps by mistake, we reset it when getting a new non-error result. + } + // often [onResult] gets called right away without any [onUpdate] call showProgressNotification(target) } @@ -75,6 +91,15 @@ internal class NotificationBackupObserver( * as a whole failed. */ override fun backupFinished(status: Int) { + if (status == ERROR_BACKUP_CANCELLED) { + val packageName = errorPackageName + if (packageName == null) { + Log.e(TAG, "Backup got cancelled, but there we have no culprit :(") + } else { + Log.w(TAG, "App $packageName misbehaved, will disable backup for it...") + settingsManager.disableBackup(packageName) + } + } if (backupRequester.requestNext()) { if (isLoggable(TAG, INFO)) { Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")