Skip to content

Commit

Permalink
Merge pull request #849 from grote/metered-network
Browse files Browse the repository at this point in the history
Try everything to abort backups on metered network (when not allowed)
  • Loading branch information
grote authored Feb 14, 2025
2 parents 6945a10 + e41405d commit 7b48b53
Show file tree
Hide file tree
Showing 28 changed files with 373 additions and 73 deletions.
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@
android:resource="@xml/device_filter" />
</receiver>

<receiver
android:name=".settings.TryAgainBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.stevesoltys.seedvault.action.TRY_AGAIN" />
</intent-filter>
</receiver>

<receiver
android:name=".restore.RestoreErrorBroadcastReceiver"
android:exported="false">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class BackendManager(
* @return true if a backup is possible, false if not.
*/
@WorkerThread
fun canDoBackupNow(): Boolean {
override fun canDoBackupNow(): Boolean {
val storage = backendProperties ?: return false
return !isOnUnavailableUsb() &&
!storage.isUnavailableNetwork(context, settingsManager.useMeteredNetwork)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,17 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun onMenuItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.action_backup -> {
viewModel.backupNow()
if (!backendManager.canDoBackupNow()) {
// if USB isn't plugged in, this action shouldn't be enabled,
// so this leaves only that we are on a metered network
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.settings_backup_metered_title))
.setMessage(getString(R.string.settings_backup_metered_text))
.setNeutralButton(getString(R.string.restore_storage_got_it)) { dialog, _ ->
dialog.dismiss()
}
.show()
}
true
}
R.id.action_restore -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/

package com.stevesoltys.seedvault.settings

import android.app.backup.IBackupManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.worker.BackupRequester.Companion.requestFilesAndAppBackup
import org.koin.core.context.GlobalContext.get

internal const val ACTION_TRY_AGAIN = "com.stevesoltys.seedvault.action.TRY_AGAIN"

class TryAgainBroadcastReceiver : BroadcastReceiver() {

// using KoinComponent would crash robolectric tests :(
private val notificationManager: BackupNotificationManager by lazy { get().get() }
private val backendManager: BackendManager by lazy { get().get() }
private val settingsManager: SettingsManager by lazy { get().get() }
private val backupManager: IBackupManager by lazy { get().get() }

override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION_TRY_AGAIN) return

notificationManager.onBackupErrorSeen()

val reschedule = !backendManager.isOnRemovableDrive
requestFilesAndAppBackup(context, settingsManager, backupManager, reschedule)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ internal class BackupCoordinator(
flags: Int,
): Int {
state.cancelReason = UNKNOWN_ERROR
if (!backendManager.canDoBackupNow()) {
Log.w(TAG, "performIncrementalBackup(): Can't do backup now, rejecting...")
return TRANSPORT_PACKAGE_REJECTED
}
return kv.performBackup(packageInfo, data, flags)
}

Expand All @@ -229,6 +233,10 @@ internal class BackupCoordinator(
}

fun checkFullBackupSize(size: Long): Int {
if (!backendManager.canDoBackupNow()) {
Log.w(TAG, "checkFullBackupSize(): Can't do backup now, rejecting...")
return TRANSPORT_PACKAGE_REJECTED
}
val result = full.checkFullBackupSize(size)
if (result == TRANSPORT_PACKAGE_REJECTED) state.cancelReason = NO_DATA
else if (result == TRANSPORT_QUOTA_EXCEEDED) state.cancelReason = QUOTA_EXCEEDED
Expand All @@ -241,6 +249,10 @@ internal class BackupCoordinator(
flags: Int,
): Int {
state.cancelReason = UNKNOWN_ERROR
if (!backendManager.canDoBackupNow()) {
Log.w(TAG, "performFullBackup(): Can't do backup now, rejecting...")
return TRANSPORT_PACKAGE_REJECTED
}
return full.performFullBackup(targetPackage, fileDescriptor, flags)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import android.app.NotificationManager
import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager.IMPORTANCE_LOW
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity
import android.app.PendingIntent.getBroadcast
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
Expand All @@ -38,6 +38,7 @@ import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
import com.stevesoltys.seedvault.restore.RestoreActivity
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
import com.stevesoltys.seedvault.settings.ACTION_TRY_AGAIN
import com.stevesoltys.seedvault.settings.SettingsActivity
import com.stevesoltys.seedvault.ui.check.ACTION_FINISHED
import com.stevesoltys.seedvault.ui.check.ACTION_SHOW
Expand Down Expand Up @@ -229,19 +230,30 @@ internal class BackupNotificationManager(private val context: Context) {
nm.notify(NOTIFICATION_ID_SUCCESS, notification)
}

fun onBackupError() {
fun onBackupError(meteredNetwork: Boolean = false) {
val intent = Intent(context, SettingsActivity::class.java)
val pendingIntent = getActivity(context, 0, intent, FLAG_IMMUTABLE)
val actionIntent = Intent(ACTION_TRY_AGAIN).apply { setPackage(context.packageName) }
val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
val actionPendingIntent = getBroadcast(context, REQUEST_CODE_UNINSTALL, actionIntent, flags)
val actionText = context.getString(R.string.recovery_code_verification_try_again)
val action = Action(null, actionText, actionPendingIntent)
val text = if (meteredNetwork) {
context.getString(R.string.notification_failed_metered_text)
} else {
context.getString(R.string.notification_failed_text)
}
val notification = Builder(context, CHANNEL_ID_ERROR).apply {
setSmallIcon(R.drawable.ic_cloud_error)
setContentTitle(context.getString(R.string.notification_failed_title))
setContentText(context.getString(R.string.notification_failed_text))
setContentText(text)
setOngoing(false)
setShowWhen(true)
setAutoCancel(true)
setContentIntent(pendingIntent)
setWhen(System.currentTimeMillis())
setProgress(0, 0, false)
addAction(action)
priority = PRIORITY_LOW
}.build()
Log.i(TAG, "Canceling NOTIFICATION_ID_OBSERVER")
Expand Down Expand Up @@ -314,7 +326,7 @@ internal class BackupNotificationManager(private val context: Context) {
}
val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
val pendingIntent =
PendingIntent.getBroadcast(context, REQUEST_CODE_UNINSTALL, intent, flags)
getBroadcast(context, REQUEST_CODE_UNINSTALL, intent, flags)
val actionText = context.getString(R.string.notification_restore_error_action)
val action = Action(R.drawable.ic_warning, actionText, pendingIntent)
val notification = Builder(context, CHANNEL_ID_RESTORE_ERROR).apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.stevesoltys.seedvault.ERROR_BACKUP_CANCELLED
import com.stevesoltys.seedvault.ERROR_BACKUP_NOT_ALLOWED
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.repo.AppBackupManager
Expand All @@ -44,6 +45,7 @@ internal class NotificationBackupObserver(
private val settingsManager: SettingsManager by inject()
private val metadataManager: MetadataManager by inject()
private val appBackupManager: AppBackupManager by inject()
private val backendManager: BackendManager by inject()
private var currentPackage: String? = null
private var numPackages: Int = 0
private var numPackagesToReport: Int = 0
Expand Down Expand Up @@ -148,16 +150,17 @@ internal class NotificationBackupObserver(
settingsManager.disableBackup(packageName)
}
}
// FIXME we should consider not requesting backup of more chunks of packages,
// if the backup has already failed for this chunk,
// because it will result in incomplete snapshots
// since the rest of packages from the failed chunk won't get backed up.
// So we either re-include those packages somehow (may fail again in a loop!)
// or we simply fail the entire backup which may cause more failures for users :(
if (backupRequester.requestNext()) {
if (isLoggable(TAG, INFO)) {
Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")
}
Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")
if (backupRequester.hasNext && !backendManager.canDoBackupNow()) {
Log.w(TAG, "Not requesting another backup, likely on metered network. ")
nm.onBackupError(true)
} else if (backupRequester.requestNext()) {
// FIXME we should consider not requesting backup of more chunks of packages,
// if the backup has already failed for this chunk,
// because it will result in incomplete snapshots
// since the rest of packages from the failed chunk won't get backed up.
// So we either re-include those packages somehow (may fail again in a loop!)
// or we simply fail the entire backup which may cause more failures for users :(
var success = status == 0
val total = try {
packageService.allUserPackages.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package com.stevesoltys.seedvault.worker
import android.content.Context
import android.content.pm.PackageInfo
import android.util.Log
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
Expand All @@ -24,6 +25,7 @@ import java.io.IOException
internal class ApkBackupManager(
private val context: Context,
private val appBackupManager: AppBackupManager,
private val backendManager: BackendManager,
private val settingsManager: SettingsManager,
private val snapshotManager: SnapshotManager,
private val metadataManager: MetadataManager,
Expand All @@ -43,6 +45,7 @@ internal class ApkBackupManager(
// Since an APK backup does not change the [packageState], we first record it for all
// packages that don't get backed up.
recordNotBackedUpPackages()
if (!backendManager.canDoBackupNow()) throw IllegalStateException("can't do backup now")
// Upload current icons, so we can show them to user before restore
uploadIcons()
// Now, if APK backups are enabled by the user, we back those up.
Expand All @@ -60,6 +63,9 @@ internal class ApkBackupManager(
private suspend fun backUpApks() {
val apps = packageService.allUserPackages
apps.forEachIndexed { i, packageInfo ->
// the situation may change, so stop backup when it does by throwing exception
if (!backendManager.canDoBackupNow()) throw IllegalStateException("can't do backup now")

val packageName = packageInfo.packageName
val name = getAppName(context, packageName)
nm.onApkBackup(packageName, name, i, apps.size)
Expand All @@ -68,6 +74,7 @@ internal class ApkBackupManager(
}

// TODO we could use BackupMonitor for this. It emits LOG_EVENT_ID_PACKAGE_STOPPED
// need to check if it has something for NOT_ALLOWED as well
private fun recordNotBackedUpPackages() {
nm.onAppsNotBackedUp()
packageService.notBackedUpPackages.forEach { packageInfo ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,21 @@ class AppBackupWorker(
} catch (e: Exception) {
Log.e(TAG, "Error while running setForeground: ", e)
}
val freeSpace = backendManager.getFreeSpace()
Log.i(TAG, "freeSpace: $freeSpace")
if (freeSpace != null && freeSpace < MIN_FREE_SPACE) {
nm.onInsufficientSpaceError()
return Result.failure()
}
return try {
if (isStopped) {
if (isStopped || !backendManager.canDoBackupNow()) {
Result.retry()
} else {
val freeSpace = backendManager.getFreeSpace()
Log.i(TAG, "freeSpace: $freeSpace")
if (freeSpace != null && freeSpace < MIN_FREE_SPACE) {
nm.onInsufficientSpaceError()
return Result.failure()
}
val result = doBackup()
// show error notification if backup wasn't successful (maybe only when no retry?)
if (result != Result.success()) nm.onBackupError()
// show error notification if backup wasn't successful
if (result != Result.success()) {
nm.onBackupError(meteredNetwork = !backendManager.canDoBackupNow())
}
// only allow retrying if rescheduling is allowed
if (tags.contains(TAG_RESCHEDULE)) return result
else Result.success()
Expand All @@ -149,32 +151,36 @@ class AppBackupWorker(
}

private suspend fun doBackup(): Result {
var result: Result = Result.success()
if (!isStopped) {
if (!isStopped && backendManager.canDoBackupNow()) {
Log.i(TAG, "Initializing backup info...")
try {
appBackupManager.beforeBackup()
} catch (e: Exception) {
Log.e(TAG, "Error during 'beforeBackup': ", e)
return Result.retry()
}
} else {
Log.i(TAG, "Stopping, because s:$isStopped c:${backendManager.canDoBackupNow()}")
}
try {
Log.i(TAG, "Starting APK backup... (stopped: $isStopped)")
if (!isStopped) apkBackupManager.backup()
if (!isStopped && backendManager.canDoBackupNow()) {
Log.i(TAG, "Starting APK backup...")
apkBackupManager.backup()
} else {
Log.i(TAG, "Stopping, because s:$isStopped c:${backendManager.canDoBackupNow()}")
}
} catch (e: Exception) {
Log.e(TAG, "Error backing up APKs: ", e)
result = Result.retry()
} finally {
Log.i(TAG, "Requesting app data backup... (stopped: $isStopped)")
val requestSuccess = if (!isStopped && backupRequester.isBackupEnabled) {
Log.d(TAG, "Backup is enabled, request backup...")
backupRequester.requestBackup()
} else true
Log.d(TAG, "Have requested backup.")
if (!requestSuccess) result = Result.retry()
return Result.retry()
}
Log.i(TAG, "Requesting app data backup... (stopped: $isStopped)")
if (!isStopped && backupRequester.isBackupEnabled && backendManager.canDoBackupNow()) {
Log.i(TAG, "Backup is enabled, request backup...")
if (!backupRequester.requestBackup()) return Result.retry()
} else {
Log.i(TAG, "Stopping, because s:$isStopped c:${backendManager.canDoBackupNow()}")
}
return result
return Result.success()
}

private fun createForegroundInfo() = ForegroundInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,10 @@ internal class BackupRequester(

val isBackupEnabled: Boolean get() = backupManager.isBackupEnabled

private val packages = packageService.eligiblePackages
private val observer = NotificationBackupObserver(
context = context,
backupRequester = this,
requestedPackages = packages.size,
)
private val packages by lazy { packageService.eligiblePackages }
private val observer by lazy {
NotificationBackupObserver(context, this, packages.size)
}

/**
* The current package index.
Expand All @@ -93,6 +91,11 @@ internal class BackupRequester(
return request(getNextChunk())
}

/**
* Returns true, if there are more packages waiting to get backed up by calling [requestNext].
*/
val hasNext: Boolean get() = packageIndex < packages.size

/**
* Backs up the next chunk of packages.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ val workerModule = module {
ApkBackupManager(
context = androidContext(),
appBackupManager = get(),
backendManager = get(),
settingsManager = get(),
snapshotManager = get(),
metadataManager = get(),
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
<string name="settings_backup_new_code_dialog_title">New recovery code required</string>
<string name="settings_backup_new_code_dialog_message">To continue using app backups, you need to generate a new recovery code.\n\nWe are sorry for the inconvenience.</string>
<string name="settings_backup_new_code_code_dialog_ok">New code</string>
<string name="settings_backup_metered_title">Backup stopped</string>
<string name="settings_backup_metered_text">The backup won\'t proceed because your device is using mobile data.\n\nYou can enable backups on mobile data under \"Backup scheduling\".</string>


<string name="settings_scheduling_frequency_title">Backup frequency</string>
<string name="settings_scheduling_frequency_12_hours">Every 12 hours</string>
Expand Down Expand Up @@ -169,6 +172,7 @@
<string name="notification_success_text">%1$d of %2$d apps backed up (%3$s). Tap to learn more.</string>
<string name="notification_failed_title">Backup failed</string>
<string name="notification_failed_text">An error occurred while running the backup.</string>
<string name="notification_failed_metered_text">The backup has been aborted. Are you using mobile data? Connect to Wi-Fi to continue.</string>

<string name="notification_error_channel_title">Error notification</string>
<string name="notification_error_title">Backup error</string>
Expand Down
Loading

0 comments on commit 7b48b53

Please sign in to comment.