Tolerate backup attempts when flash drive is not plugged in
Also remove hardcoding of PACKAGE_MANAGER_SENTINEL constant
This commit is contained in:
parent
08018fcc9b
commit
470b5a2ccf
8 changed files with 59 additions and 10 deletions
|
@ -1,6 +1,7 @@
|
||||||
package com.stevesoltys.backup
|
package com.stevesoltys.backup
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context.BACKUP_SERVICE
|
import android.content.Context.BACKUP_SERVICE
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -33,4 +34,6 @@ class Backup : Application() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
|
||||||
|
|
||||||
fun isDebugBuild() = Build.TYPE == "userdebug"
|
fun isDebugBuild() = Build.TYPE == "userdebug"
|
||||||
|
|
|
@ -68,7 +68,7 @@ class NotificationBackupObserver(context: Context, private val userInitiated: Bo
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAppName(pm: PackageManager, packageId: String): CharSequence {
|
fun getAppName(pm: PackageManager, packageId: String): CharSequence {
|
||||||
if (packageId == "@pm@") return packageId
|
if (packageId == MAGIC_PACKAGE_MANAGER) return packageId
|
||||||
val appInfo = pm.getApplicationInfo(packageId, 0)
|
val appInfo = pm.getApplicationInfo(packageId, 0)
|
||||||
return pm.getApplicationLabel(appInfo)
|
return pm.getApplicationLabel(appInfo)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.android.collect.Sets.newArraySet
|
import com.google.android.collect.Sets.newArraySet
|
||||||
import com.stevesoltys.backup.Backup
|
import com.stevesoltys.backup.Backup
|
||||||
|
import com.stevesoltys.backup.MAGIC_PACKAGE_MANAGER
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
private val TAG = PackageService::class.java.simpleName
|
private val TAG = PackageService::class.java.simpleName
|
||||||
|
@ -49,7 +50,7 @@ class PackageService {
|
||||||
|
|
||||||
// add magic @pm@ package (PACKAGE_MANAGER_SENTINEL) which holds package manager data
|
// add magic @pm@ package (PACKAGE_MANAGER_SENTINEL) which holds package manager data
|
||||||
val packageArray = eligibleApps.toMutableList()
|
val packageArray = eligibleApps.toMutableList()
|
||||||
packageArray.add("@pm@")
|
packageArray.add(MAGIC_PACKAGE_MANAGER)
|
||||||
|
|
||||||
return packageArray.toTypedArray()
|
return packageArray.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.stevesoltys.backup.settings
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeUnit.DAYS
|
||||||
|
|
||||||
private val SETTING = Settings.Secure.BACKUP_MANAGER_CONSTANTS
|
private val SETTING = Settings.Secure.BACKUP_MANAGER_CONSTANTS
|
||||||
private const val DELIMITER = ','
|
private const val DELIMITER = ','
|
||||||
|
@ -11,13 +13,19 @@ private const val FULL_BACKUP_INTERVAL_MILLISECONDS = "full_backup_interval_mill
|
||||||
|
|
||||||
object BackupManagerSettings {
|
object BackupManagerSettings {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This clears the backup settings, so that default values will be used.
|
||||||
|
*/
|
||||||
fun enableAutomaticBackups(resolver: ContentResolver) {
|
fun enableAutomaticBackups(resolver: ContentResolver) {
|
||||||
// setting this to null will cause the BackupManagerConstants to use default values
|
// setting this to null will cause the BackupManagerConstants to use default values
|
||||||
setSettingValue(resolver, null)
|
setSettingValue(resolver, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This sets the backup intervals to a longer than default value. Currently 30 days
|
||||||
|
*/
|
||||||
fun disableAutomaticBackups(resolver: ContentResolver) {
|
fun disableAutomaticBackups(resolver: ContentResolver) {
|
||||||
val value = Long.MAX_VALUE
|
val value = DAYS.toMillis(30)
|
||||||
val kv = "$KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS=$value"
|
val kv = "$KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS=$value"
|
||||||
val full = "$FULL_BACKUP_INTERVAL_MILLISECONDS=$value"
|
val full = "$FULL_BACKUP_INTERVAL_MILLISECONDS=$value"
|
||||||
setSettingValue(resolver, "$kv$DELIMITER$full")
|
setSettingValue(resolver, "$kv$DELIMITER$full")
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
package com.stevesoltys.backup.transport.backup
|
package com.stevesoltys.backup.transport.backup
|
||||||
|
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.*
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.stevesoltys.backup.BackupNotificationManager
|
import com.stevesoltys.backup.BackupNotificationManager
|
||||||
|
import com.stevesoltys.backup.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.backup.metadata.MetadataWriter
|
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||||
import com.stevesoltys.backup.settings.SettingsManager
|
import com.stevesoltys.backup.settings.SettingsManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.DAYS
|
||||||
|
|
||||||
private val TAG = BackupCoordinator::class.java.simpleName
|
private val TAG = BackupCoordinator::class.java.simpleName
|
||||||
|
|
||||||
|
@ -63,7 +63,8 @@ class BackupCoordinator(
|
||||||
TRANSPORT_OK
|
TRANSPORT_OK
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error initializing device", e)
|
Log.e(TAG, "Error initializing device", e)
|
||||||
nm.onBackupError()
|
// Show error notification if we were ready for backups
|
||||||
|
if (getBackupBackoff() == 0L) nm.onBackupError()
|
||||||
TRANSPORT_ERROR
|
TRANSPORT_ERROR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,6 +102,12 @@ class BackupCoordinator(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
||||||
|
// 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 (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
|
||||||
|
return TRANSPORT_PACKAGE_REJECTED
|
||||||
|
}
|
||||||
|
|
||||||
val result = kv.performBackup(packageInfo, data, flags)
|
val result = kv.performBackup(packageInfo, data, flags)
|
||||||
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
||||||
return result
|
return result
|
||||||
|
@ -194,7 +201,7 @@ class BackupCoordinator(
|
||||||
|
|
||||||
private fun getBackupBackoff(): Long {
|
private fun getBackupBackoff(): Long {
|
||||||
val noBackoff = 0L
|
val noBackoff = 0L
|
||||||
val defaultBackoff = MINUTES.toMillis(10)
|
val defaultBackoff = DAYS.toMillis(30)
|
||||||
|
|
||||||
// back off if there's no storage set
|
// back off if there's no storage set
|
||||||
val storage = settingsManager.getStorage() ?: return defaultBackoff
|
val storage = settingsManager.getStorage() ?: return defaultBackoff
|
||||||
|
|
|
@ -42,7 +42,7 @@ class DocumentsProviderBackupPlugin(
|
||||||
}
|
}
|
||||||
|
|
||||||
override val providerPackageName: String? by lazy {
|
override val providerPackageName: String? by lazy {
|
||||||
val authority = storage.rootBackupDir?.uri?.authority ?: return@lazy null
|
val authority = storage.getAuthority() ?: return@lazy null
|
||||||
val providerInfo = packageManager.resolveContentProvider(authority, 0) ?: return@lazy null
|
val providerInfo = packageManager.resolveContentProvider(authority, 0) ?: return@lazy null
|
||||||
providerInfo.packageName
|
providerInfo.packageName
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,8 @@ class DocumentsStorage(private val context: Context, private val settingsManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAuthority(): String? = storage?.uri?.authority
|
||||||
|
|
||||||
fun getSetDir(token: Long = currentToken): DocumentFile? {
|
fun getSetDir(token: Long = currentToken): DocumentFile? {
|
||||||
if (token == currentToken) return currentSetDir
|
if (token == currentToken) return currentSetDir
|
||||||
return rootBackupDir?.findFile(token.toString())
|
return rootBackupDir?.findFile(token.toString())
|
||||||
|
|
|
@ -2,8 +2,12 @@ package com.stevesoltys.backup.transport.backup
|
||||||
|
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.backup.BackupNotificationManager
|
import com.stevesoltys.backup.BackupNotificationManager
|
||||||
|
import com.stevesoltys.backup.getRandomString
|
||||||
import com.stevesoltys.backup.metadata.MetadataWriter
|
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||||
|
import com.stevesoltys.backup.settings.Storage
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -40,8 +44,11 @@ internal class BackupCoordinatorTest: BackupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `device initialization fails`() {
|
fun `error notification when device initialization fails`() {
|
||||||
|
val storage = Storage(Uri.EMPTY, getRandomString(), false)
|
||||||
|
|
||||||
every { plugin.initializeDevice() } throws IOException()
|
every { plugin.initializeDevice() } throws IOException()
|
||||||
|
every { settingsManager.getStorage() } returns storage
|
||||||
every { notificationManager.onBackupError() } just Runs
|
every { notificationManager.onBackupError() } just Runs
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
||||||
|
@ -54,6 +61,27 @@ internal class BackupCoordinatorTest: BackupTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no error notification when device initialization fails on unplugged USB storage`() {
|
||||||
|
val storage = mockk<Storage>()
|
||||||
|
val documentFile = mockk<DocumentFile>()
|
||||||
|
|
||||||
|
every { plugin.initializeDevice() } throws IOException()
|
||||||
|
every { settingsManager.getStorage() } returns storage
|
||||||
|
every { storage.isUsb } returns true
|
||||||
|
every { storage.getDocumentFile(context) } returns documentFile
|
||||||
|
every { documentFile.isDirectory } returns false
|
||||||
|
|
||||||
|
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
|
||||||
|
|
||||||
|
// finish will only be called when TRANSPORT_OK is returned, so it should throw
|
||||||
|
every { kv.hasState() } returns false
|
||||||
|
every { full.hasState() } returns false
|
||||||
|
assertThrows(IllegalStateException::class.java) {
|
||||||
|
backup.finishBackup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getBackupQuota() delegates to right plugin`() {
|
fun `getBackupQuota() delegates to right plugin`() {
|
||||||
val isFullBackup = Random.nextBoolean()
|
val isFullBackup = Random.nextBoolean()
|
||||||
|
|
Loading…
Add table
Reference in a new issue