Request backoff when asked to backup to network storage while no internet available

K/V backups are normally only attempted when charging and having an (un-metered) internet connection. However, if the system could not do a backup for more than a day, it ignores these requirements and still attempts a backup run. If a backup storage is used that is only accessible on the internet, but there is no internet connection, the backup attempt will fail. Therefore, we check if our storage requires the internet and if so, we treat it similar to a removable storage, by rejecting backup attempts and suppressing error notifications.
This commit is contained in:
Torsten Grote 2020-10-19 15:44:36 -03:00
parent f356f56746
commit 7401ead553
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
7 changed files with 44 additions and 19 deletions

View file

@ -25,7 +25,8 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need
## Permissions
* `android.permission.BACKUP` to back up application data.
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots.
* `android.permission.ACCESS_NETWORK_STATE` to check if there is internet access when network storage is used.
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots.
* `android.permission.MANAGE_USB` to access the serial number of USB mass storage devices.
* `android.permission.WRITE_SECURE_SETTINGS` to change system backup settings and enable call log backup.
* `android.permission.QUERY_ALL_PACKAGES` to get information about all installed apps for backup.

View file

@ -9,6 +9,9 @@
android:name="android.permission.BACKUP"
tools:ignore="ProtectedPermissions" />
<!-- This is needed to check for internet access when backup is stored on network storage -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- This is needed to retrieve the available storage roots -->
<uses-permission
android:name="android.permission.MANAGE_DOCUMENTS"

View file

@ -16,6 +16,7 @@ internal const val PREF_KEY_BACKUP_APK = "backup_apk"
private const val PREF_KEY_STORAGE_URI = "storageUri"
private const val PREF_KEY_STORAGE_NAME = "storageName"
private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb"
private const val PREF_KEY_STORAGE_REQUIRES_NETWORK = "storageRequiresNetwork"
private const val PREF_KEY_FLASH_DRIVE_NAME = "flashDriveName"
private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber"
@ -63,6 +64,7 @@ class SettingsManager(context: Context) {
.putString(PREF_KEY_STORAGE_URI, storage.uri.toString())
.putString(PREF_KEY_STORAGE_NAME, storage.name)
.putBoolean(PREF_KEY_STORAGE_IS_USB, storage.isUsb)
.putBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, storage.requiresNetwork)
.apply()
}
@ -72,7 +74,8 @@ class SettingsManager(context: Context) {
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null)
?: throw IllegalStateException("no storage name")
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
return Storage(uri, name, isUsb)
val requiresNetwork = prefs.getBoolean(PREF_KEY_STORAGE_REQUIRES_NETWORK, false)
return Storage(uri, name, isUsb, requiresNetwork)
}
fun setFlashDrive(usb: FlashDrive?) {
@ -119,7 +122,8 @@ class SettingsManager(context: Context) {
data class Storage(
val uri: Uri,
val name: String,
val isUsb: Boolean
val isUsb: Boolean,
val requiresNetwork: Boolean
) {
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
?: throw AssertionError("Should only happen on API < 21.")

View file

@ -14,6 +14,8 @@ import android.app.backup.RestoreSet
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.annotation.VisibleForTesting
@ -32,6 +34,7 @@ import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import java.io.IOException
import java.util.concurrent.TimeUnit.DAYS
import java.util.concurrent.TimeUnit.HOURS
private val TAG = BackupCoordinator::class.java.simpleName
@ -212,12 +215,11 @@ internal class BackupCoordinator(
): Int {
cancelReason = UNKNOWN_ERROR
val packageName = packageInfo.packageName
if (packageName == MAGIC_PACKAGE_MANAGER) {
// backups of package manager metadata do not respect backoff
// we need to reject them manually when now is not a good time for a backup
if (getBackupBackoff() != 0L) {
return TRANSPORT_PACKAGE_REJECTED
}
// K/V backups (typically starting with package manager metadata)
// are scheduled with JobInfo.Builder#setOverrideDeadline() and thus do not respect backoff.
// We need to reject them manually when now is not a good time for a backup.
if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
return TRANSPORT_PACKAGE_REJECTED
}
val result = kv.performBackup(packageInfo, data, flags)
if (result == TRANSPORT_OK && packageName == MAGIC_PACKAGE_MANAGER) {
@ -430,14 +432,23 @@ internal class BackupCoordinator(
private fun getBackupBackoff(): Long {
val noBackoff = 0L
val defaultBackoff = DAYS.toMillis(30)
val longBackoff = DAYS.toMillis(30)
// back off if there's no storage set
val storage = settingsManager.getStorage() ?: return defaultBackoff
// don't back off if storage is not ejectable or available right now
return if (!storage.isUsb || storage.getDocumentFile(context).isDirectory) noBackoff
// otherwise back off
else defaultBackoff
val storage = settingsManager.getStorage() ?: return longBackoff
// back off if storage is removable and not available right now
return if (storage.isUsb && !storage.getDocumentFile(context).isDirectory) longBackoff
// back off if storage is on network, but we have no access
else if (storage.requiresNetwork && !hasInternet()) HOURS.toMillis(1)
// otherwise no back off
else noBackoff
}
private fun hasInternet(): Boolean {
val cm = context.getSystemService(ConnectivityManager::class.java)
val capabilities = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
return capabilities.hasCapability(NET_CAPABILITY_INTERNET)
}
}

View file

@ -22,6 +22,7 @@ import android.provider.DocumentsContract.Root.COLUMN_ICON
import android.provider.DocumentsContract.Root.COLUMN_ROOT_ID
import android.provider.DocumentsContract.Root.COLUMN_SUMMARY
import android.provider.DocumentsContract.Root.COLUMN_TITLE
import android.provider.DocumentsContract.Root.FLAG_LOCAL_ONLY
import android.provider.DocumentsContract.Root.FLAG_REMOVABLE_USB
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_CREATE
import android.provider.DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
@ -50,6 +51,7 @@ data class StorageRoot(
internal val summary: String?,
internal val availableBytes: Long?,
internal val isUsb: Boolean,
internal val requiresNetwork: Boolean,
internal val enabled: Boolean = true,
internal val overrideClickListener: (() -> Unit)? = null
) {
@ -144,10 +146,11 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
if (!supportsCreate || !supportsIsChild) return null
val rootId = cursor.getString(COLUMN_ROOT_ID)!!
if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null
val documentId = cursor.getString(COLUMN_DOCUMENT_ID) ?: return null
return StorageRoot(
authority = authority,
rootId = rootId,
documentId = cursor.getString(COLUMN_DOCUMENT_ID)!!,
documentId = documentId,
icon = getIcon(context, authority, rootId, cursor.getInt(COLUMN_ICON)),
title = cursor.getString(COLUMN_TITLE)!!,
summary = cursor.getString(COLUMN_SUMMARY),
@ -155,7 +158,8 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
// AOSP 11 reports -1 instead of null
if (bytes == -1L) null else bytes
},
isUsb = flags and FLAG_REMOVABLE_USB != 0
isUsb = flags and FLAG_REMOVABLE_USB != 0,
requiresNetwork = flags and FLAG_LOCAL_ONLY == 0 // not local only == requires network
)
}
@ -175,6 +179,7 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
summary = context.getString(R.string.storage_fake_drive_summary),
availableBytes = null,
isUsb = true,
requiresNetwork = false,
enabled = false
)
roots.add(root)
@ -216,6 +221,7 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
summary = context.getString(summaryRes),
availableBytes = null,
isUsb = false,
requiresNetwork = true,
enabled = !isInstalled || isRestore,
overrideClickListener = {
if (isInstalled) context.startActivity(intent)

View file

@ -101,7 +101,7 @@ internal abstract class StorageViewModel(
} else {
root.title
}
val storage = Storage(uri, name, root.isUsb)
val storage = Storage(uri, name, root.isUsb, root.requiresNetwork)
settingsManager.setStorage(storage)
if (storage.isUsb) {

View file

@ -63,7 +63,7 @@ internal class BackupCoordinatorTest : BackupTest() {
private val metadataOutputStream = mockk<OutputStream>()
private val fileDescriptor: ParcelFileDescriptor = mockk()
private val packageMetadata: PackageMetadata = mockk()
private val storage = Storage(Uri.EMPTY, getRandomString(), false)
private val storage = Storage(Uri.EMPTY, getRandomString(), false, false)
@Test
fun `starting a new restore set works as expected`() = runBlocking {