Merge pull request #42 from grote/usb

Various optimizations for using a USB flash drive as backup storage medium
This commit is contained in:
Steve Soltys 2019-09-25 01:06:17 -04:00 committed by GitHub
commit d944f985a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 604 additions and 179 deletions

View file

@ -3,8 +3,6 @@ package com.stevesoltys.backup
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4 import androidx.test.runner.AndroidJUnit4
import com.stevesoltys.backup.settings.getBackupToken
import com.stevesoltys.backup.settings.getStorage
import com.stevesoltys.backup.transport.backup.plugins.DocumentsStorage import com.stevesoltys.backup.transport.backup.plugins.DocumentsStorage
import com.stevesoltys.backup.transport.backup.plugins.createOrGetFile import com.stevesoltys.backup.transport.backup.plugins.createOrGetFile
import org.junit.After import org.junit.After
@ -21,9 +19,8 @@ private const val filename = "test-file"
class DocumentsStorageTest { class DocumentsStorageTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val token = getBackupToken(context) private val settingsManager = (context.applicationContext as Backup).settingsManager
private val folderUri = getStorage(context) private val storage = DocumentsStorage(context, settingsManager)
private val storage = DocumentsStorage(context, folderUri, token)
private lateinit var file: DocumentFile private lateinit var file: DocumentFile

View file

@ -14,6 +14,16 @@
android:name="android.permission.MANAGE_DOCUMENTS" android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<!-- This is needed to access the serial number of USB mass storage devices -->
<uses-permission
android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions" />
<!-- This is needed to change system backup settings -->
<uses-permission
android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<application <application
android:name=".Backup" android:name=".Backup"
android:allowBackup="false" android:allowBackup="false"
@ -72,5 +82,16 @@
</intent-filter> </intent-filter>
</service> </service>
<receiver
android:name=".UsbIntentReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</receiver>
</application> </application>
</manifest> </manifest>

View file

@ -1,14 +1,14 @@
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.net.Uri
import android.os.Build import android.os.Build
import android.os.ServiceManager.getService import android.os.ServiceManager.getService
import com.stevesoltys.backup.crypto.KeyManager import com.stevesoltys.backup.crypto.KeyManager
import com.stevesoltys.backup.crypto.KeyManagerImpl import com.stevesoltys.backup.crypto.KeyManagerImpl
import com.stevesoltys.backup.ui.storage.AUTHORITY_STORAGE import com.stevesoltys.backup.settings.SettingsManager
/** /**
* @author Steve Soltys * @author Steve Soltys
@ -25,10 +25,15 @@ class Backup : Application() {
} }
} }
val settingsManager by lazy {
SettingsManager(this)
}
val notificationManager by lazy { val notificationManager by lazy {
BackupNotificationManager(this) BackupNotificationManager(this)
} }
} }
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
fun isDebugBuild() = Build.TYPE == "userdebug" fun isDebugBuild() = Build.TYPE == "userdebug"

View file

@ -79,9 +79,9 @@ class BackupNotificationManager(private val context: Context) {
val notification = errorBuilder.apply { val notification = errorBuilder.apply {
setContentTitle(context.getString(R.string.notification_error_title)) setContentTitle(context.getString(R.string.notification_error_title))
setContentText(context.getString(R.string.notification_error_text)) setContentText(context.getString(R.string.notification_error_text))
addAction(action)
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setAutoCancel(true) setAutoCancel(true)
mActions = arrayListOf(action)
}.build() }.build()
nm.notify(NOTIFICATION_ID_ERROR, notification) nm.notify(NOTIFICATION_ID_ERROR, notification)
} }

View file

@ -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)
} }

View file

@ -0,0 +1,104 @@
package com.stevesoltys.backup
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.database.ContentObserver
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager.*
import android.net.Uri
import android.os.Handler
import android.provider.DocumentsContract
import android.util.Log
import com.stevesoltys.backup.settings.FlashDrive
import com.stevesoltys.backup.transport.requestBackup
import com.stevesoltys.backup.ui.storage.AUTHORITY_STORAGE
import java.util.*
import java.util.concurrent.TimeUnit.HOURS
private val TAG = UsbIntentReceiver::class.java.simpleName
class UsbIntentReceiver : UsbMonitor() {
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
if (action != ACTION_USB_DEVICE_ATTACHED) return false
Log.d(TAG, "Checking if this is the current backup drive.")
val settingsManager = (context.applicationContext as Backup).settingsManager
val savedFlashDrive = settingsManager.getFlashDrive() ?: return false
val attachedFlashDrive = FlashDrive.from(device)
return if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, checking backup time...")
if (Date().time - settingsManager.getBackupTime() >= HOURS.toMillis(24)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
true
} else {
Log.d(TAG, "We have a recent backup, not requesting a new one.")
false
}
} else {
Log.d(TAG, "Different device attached, ignoring...")
false
}
}
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
Thread {
requestBackup(context)
}.start()
}
}
/**
* When we get the [ACTION_USB_DEVICE_ATTACHED] broadcast, the storage is not yet available.
* So we need to use a ContentObserver to request a backup only once available.
*/
abstract class UsbMonitor : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action ?: return
if (intent.action == ACTION_USB_DEVICE_ATTACHED || intent.action == ACTION_USB_DEVICE_DETACHED) {
val device = intent.extras?.getParcelable<UsbDevice>(EXTRA_DEVICE) ?: return
Log.d(TAG, "New USB mass-storage device attached.")
device.log()
if (!shouldMonitorStatus(context, action, device)) return
val rootsUri = DocumentsContract.buildRootsUri(AUTHORITY_STORAGE)
val contentResolver = context.contentResolver
val observer = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
onStatusChanged(context, action, device)
contentResolver.unregisterContentObserver(this)
}
}
contentResolver.registerContentObserver(rootsUri, true, observer)
}
}
abstract fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean
abstract fun onStatusChanged(context: Context, action: String, device: UsbDevice)
}
internal fun UsbDevice.isMassStorage(): Boolean {
for (i in 0 until interfaceCount) {
if (getInterface(i).isMassStorage()) return true
}
return false
}
private fun UsbInterface.isMassStorage(): Boolean {
return interfaceClass == 8 && interfaceProtocol == 80 && interfaceSubclass == 6
}
private fun UsbDevice.log() {
Log.d(TAG, " name: $manufacturerName $productName")
Log.d(TAG, " serialNumber: $serialNumber")
Log.d(TAG, " productId: $productId")
Log.d(TAG, " vendorId: $vendorId")
Log.d(TAG, " isMassStorage: ${isMassStorage()}")
}

View file

@ -18,8 +18,6 @@ import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION; import static android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_BACKUP_REQUEST_CODE; import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_BACKUP_REQUEST_CODE;
import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE; import static com.stevesoltys.backup.activity.MainActivity.OPEN_DOCUMENT_TREE_REQUEST_CODE;
import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword;
import static com.stevesoltys.backup.settings.SettingsManagerKt.getStorage;
/** /**
* @author Steve Soltys * @author Steve Soltys
@ -41,7 +39,7 @@ public class MainActivityController {
} }
boolean isChangeBackupLocationButtonVisible(Activity parent) { boolean isChangeBackupLocationButtonVisible(Activity parent) {
return getStorage(parent) != null; return false;
} }
private void showChooseFolderActivity(Activity parent, boolean continueToBackup) { private void showChooseFolderActivity(Activity parent, boolean continueToBackup) {
@ -74,15 +72,8 @@ public class MainActivityController {
} }
boolean onAutomaticBackupsButtonClicked(Activity parent) { boolean onAutomaticBackupsButtonClicked(Activity parent) {
if (getStorage(parent) == null || getBackupPassword(parent) == null) { Toast.makeText(parent, "Please make at least one manual backup first.", Toast.LENGTH_SHORT).show();
Toast.makeText(parent, "Please make at least one manual backup first.", Toast.LENGTH_SHORT).show(); return false;
return false;
}
// show Toast informing the user
Toast.makeText(parent, "REMOVED", Toast.LENGTH_SHORT).show();
return true;
} }
void onChangeBackupLocationButtonClicked(Activity parent) { void onChangeBackupLocationButtonClicked(Activity parent) {

View file

@ -21,8 +21,6 @@ import com.stevesoltys.backup.service.backup.BackupService;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword;
/** /**
* @author Steve Soltys * @author Steve Soltys
*/ */
@ -70,12 +68,7 @@ class CreateBackupActivityController {
} }
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) { void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) {
String password = getBackupPassword(parent); backupService.backupPackageData(selectedPackages, parent);
if (password == null) {
showEnterPasswordAlert(selectedPackages, parent);
} else {
backupService.backupPackageData(selectedPackages, parent);
}
} }
private void showEnterPasswordAlert(Set<String> selectedPackages, Activity parent) { private void showEnterPasswordAlert(Set<String> selectedPackages, Activity parent) {

View file

@ -10,10 +10,10 @@ import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.getAppName import com.stevesoltys.backup.getAppName
import com.stevesoltys.backup.isDebugBuild import com.stevesoltys.backup.isDebugBuild
import com.stevesoltys.backup.settings.getStorage
import kotlinx.android.synthetic.main.fragment_restore_progress.* import kotlinx.android.synthetic.main.fragment_restore_progress.*
class RestoreProgressFragment : Fragment() { class RestoreProgressFragment : Fragment() {
@ -49,7 +49,8 @@ class RestoreProgressFragment : Fragment() {
if (finished == 0) { if (finished == 0) {
// success // success
currentPackageView.text = getString(R.string.restore_finished_success) currentPackageView.text = getString(R.string.restore_finished_success)
warningView.text = if (getStorage(requireContext())?.ejectable == true) { val settingsManager = (requireContext().applicationContext as Backup).settingsManager
warningView.text = if (settingsManager.getStorage()?.isUsb == true) {
getString(R.string.restore_finished_warning_only_installed, getString(R.string.restore_finished_warning_ejectable)) getString(R.string.restore_finished_warning_only_installed, getString(R.string.restore_finished_warning_ejectable))
} else { } else {
getString(R.string.restore_finished_warning_only_installed, null) getString(R.string.restore_finished_warning_only_installed, null)

View file

@ -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()
} }

View file

@ -0,0 +1,38 @@
package com.stevesoltys.backup.settings
import android.content.ContentResolver
import android.provider.Settings
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.DAYS
private val SETTING = Settings.Secure.BACKUP_MANAGER_CONSTANTS
private const val DELIMITER = ','
private const val KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS = "key_value_backup_interval_milliseconds"
private const val FULL_BACKUP_INTERVAL_MILLISECONDS = "full_backup_interval_milliseconds"
object BackupManagerSettings {
/**
* This clears the backup settings, so that default values will be used.
*/
fun enableAutomaticBackups(resolver: ContentResolver) {
// setting this to null will cause the BackupManagerConstants to use default values
setSettingValue(resolver, null)
}
/**
* This sets the backup intervals to a longer than default value. Currently 30 days
*/
fun disableAutomaticBackups(resolver: ContentResolver) {
val value = DAYS.toMillis(30)
val kv = "$KEY_VALUE_BACKUP_INTERVAL_MILLISECONDS=$value"
val full = "$FULL_BACKUP_INTERVAL_MILLISECONDS=$value"
setSettingValue(resolver, "$kv$DELIMITER$full")
}
private fun setSettingValue(resolver: ContentResolver, value: String?) {
Settings.Secure.putString(resolver, SETTING, value)
}
}

View file

@ -1,11 +1,18 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.content.Context
import android.content.Context.BACKUP_SERVICE import android.content.Context.BACKUP_SERVICE
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_ATTACHED
import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_DETACHED
import android.os.Bundle import android.os.Bundle
import android.os.RemoteException import android.os.RemoteException
import android.provider.Settings import android.provider.Settings
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.text.format.DateUtils.MINUTE_IN_MILLIS
import android.text.format.DateUtils.getRelativeTimeSpanString
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -17,7 +24,10 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.TwoStatePreference import androidx.preference.TwoStatePreference
import com.stevesoltys.backup.Backup import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.UsbMonitor
import com.stevesoltys.backup.isMassStorage
import com.stevesoltys.backup.restore.RestoreActivity import com.stevesoltys.backup.restore.RestoreActivity
import java.util.*
private val TAG = SettingsFragment::class.java.name private val TAG = SettingsFragment::class.java.name
@ -26,16 +36,35 @@ class SettingsFragment : PreferenceFragmentCompat() {
private val backupManager = Backup.backupManager private val backupManager = Backup.backupManager
private lateinit var viewModel: SettingsViewModel private lateinit var viewModel: SettingsViewModel
private lateinit var settingsManager: SettingsManager
private lateinit var backup: TwoStatePreference private lateinit var backup: TwoStatePreference
private lateinit var autoRestore: TwoStatePreference private lateinit var autoRestore: TwoStatePreference
private lateinit var backupLocation: Preference private lateinit var backupLocation: Preference
private var menuBackupNow: MenuItem? = null
private var menuRestore: MenuItem? = null
private var storage: Storage? = null
private val usbFilter = IntentFilter(ACTION_USB_DEVICE_ATTACHED).apply {
addAction(ACTION_USB_DEVICE_DETACHED)
}
private val usbReceiver = object : UsbMonitor() {
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
return device.isMassStorage()
}
override fun onStatusChanged(context: Context, action: String, device: UsbDevice) {
setMenuItemStates()
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey) setPreferencesFromResource(R.xml.settings, rootKey)
setHasOptionsMenu(true) setHasOptionsMenu(true)
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java) viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
settingsManager = (requireContext().applicationContext as Backup).settingsManager
backup = findPreference<TwoStatePreference>("backup")!! backup = findPreference<TwoStatePreference>("backup")!!
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
@ -74,30 +103,31 @@ class SettingsFragment : PreferenceFragmentCompat() {
super.onStart() super.onStart()
// we need to re-set the title when returning to this fragment // we need to re-set the title when returning to this fragment
requireActivity().setTitle(R.string.app_name) activity?.setTitle(R.string.app_name)
try { storage = settingsManager.getStorage()
backup.isChecked = backupManager.isBackupEnabled setBackupState()
backup.isEnabled = true setAutoRestoreState()
} catch (e: RemoteException) { setBackupLocationSummary()
Log.e(TAG, "Error communicating with BackupManager", e) setMenuItemStates()
backup.isEnabled = false
}
val resolver = requireContext().contentResolver if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1 }
// TODO add time of last backup here override fun onStop() {
val storageName = getStorage(requireContext())?.name super.onStop()
backupLocation.summary = storageName ?: getString(R.string.settings_backup_location_none ) if (storage?.isUsb == true) context?.unregisterReceiver(usbReceiver)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater) super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.settings_menu, menu) inflater.inflate(R.menu.settings_menu, menu)
menuBackupNow = menu.findItem(R.id.action_backup)
menuRestore = menu.findItem(R.id.action_restore)
if (resources.getBoolean(R.bool.show_restore_in_settings)) { if (resources.getBoolean(R.bool.show_restore_in_settings)) {
menu.findItem(R.id.action_restore).isVisible = true menuRestore?.isVisible = true
} }
setMenuItemStates()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean = when { override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
@ -112,4 +142,45 @@ class SettingsFragment : PreferenceFragmentCompat() {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
private fun setBackupState() {
try {
backup.isChecked = backupManager.isBackupEnabled
backup.isEnabled = true
} catch (e: RemoteException) {
Log.e(TAG, "Error communicating with BackupManager", e)
backup.isEnabled = false
}
}
private fun setAutoRestoreState() {
activity?.contentResolver?.let {
autoRestore.isChecked = Settings.Secure.getInt(it, BACKUP_AUTO_RESTORE, 1) == 1
}
}
private fun setBackupLocationSummary() {
// get name of storage location
val storageName = storage?.name ?: getString(R.string.settings_backup_location_none)
// get time of last backup
val lastBackupInMillis = settingsManager.getBackupTime()
val lastBackup = if (lastBackupInMillis == 0L) {
getString(R.string.settings_backup_last_backup_never)
} else {
getRelativeTimeSpanString(lastBackupInMillis, Date().time, MINUTE_IN_MILLIS, 0)
}
backupLocation.summary = getString(R.string.settings_backup_location_summary, storageName, lastBackup)
}
private fun setMenuItemStates() {
val context = context ?: return
if (menuBackupNow != null && menuRestore != null) {
val storage = this.storage
val enabled = storage != null &&
(!storage.isUsb || storage.getDocumentFile(context).isDirectory)
menuBackupNow?.isEnabled = enabled
menuRestore?.isEnabled = enabled
}
}
} }

View file

@ -1,59 +1,139 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.content.Context import android.content.Context
import android.hardware.usb.UsbDevice
import android.net.Uri import android.net.Uri
import android.preference.PreferenceManager.getDefaultSharedPreferences import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import java.util.* import java.util.*
private const val PREF_KEY_STORAGE_URI = "storageUri" private const val PREF_KEY_STORAGE_URI = "storageUri"
private const val PREF_KEY_STORAGE_NAME = "storageName" private const val PREF_KEY_STORAGE_NAME = "storageName"
private const val PREF_KEY_STORAGE_EJECTABLE = "storageEjectable" private const val PREF_KEY_STORAGE_IS_USB = "storageIsUsb"
private const val PREF_KEY_FLASH_DRIVE_NAME = "flashDriveName"
private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber"
private const val PREF_KEY_FLASH_DRIVE_VENDOR_ID = "flashDriveVendorId"
private const val PREF_KEY_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
private const val PREF_KEY_BACKUP_TOKEN = "backupToken" private const val PREF_KEY_BACKUP_TOKEN = "backupToken"
private const val PREF_KEY_BACKUP_TIME = "backupTime"
private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword" private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"
class SettingsManager(context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
fun setStorage(storage: Storage) {
prefs.edit()
.putString(PREF_KEY_STORAGE_URI, storage.uri.toString())
.putString(PREF_KEY_STORAGE_NAME, storage.name)
.putBoolean(PREF_KEY_STORAGE_IS_USB, storage.isUsb)
.apply()
}
fun getStorage(): Storage? {
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null
val uri = Uri.parse(uriStr)
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) ?: throw IllegalStateException()
val isUsb = prefs.getBoolean(PREF_KEY_STORAGE_IS_USB, false)
return Storage(uri, name, isUsb)
}
fun setFlashDrive(usb: FlashDrive?) {
if (usb == null) {
prefs.edit()
.remove(PREF_KEY_FLASH_DRIVE_NAME)
.remove(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER)
.remove(PREF_KEY_FLASH_DRIVE_VENDOR_ID)
.remove(PREF_KEY_FLASH_DRIVE_PRODUCT_ID)
.apply()
} else {
prefs.edit()
.putString(PREF_KEY_FLASH_DRIVE_NAME, usb.name)
.putString(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER, usb.serialNumber)
.putInt(PREF_KEY_FLASH_DRIVE_VENDOR_ID, usb.vendorId)
.putInt(PREF_KEY_FLASH_DRIVE_PRODUCT_ID, usb.productId)
.apply()
}
}
fun getFlashDrive(): FlashDrive? {
val name = prefs.getString(PREF_KEY_FLASH_DRIVE_NAME, null) ?: return null
val serialNumber = prefs.getString(PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER, null)
val vendorId = prefs.getInt(PREF_KEY_FLASH_DRIVE_VENDOR_ID, -1)
val productId = prefs.getInt(PREF_KEY_FLASH_DRIVE_PRODUCT_ID, -1)
return FlashDrive(name, serialNumber, vendorId, productId)
}
/**
* Generates and returns a new backup token while saving it as well.
* Subsequent calls to [getBackupToken] will return this new token once saved.
*/
fun getAndSaveNewBackupToken(): Long = Date().time.apply {
prefs.edit()
.putLong(PREF_KEY_BACKUP_TOKEN, this)
.apply()
}
/**
* Returns the current backup token or 0 if none exists.
*/
fun getBackupToken(): Long {
return prefs.getLong(PREF_KEY_BACKUP_TOKEN, 0L)
}
/**
* Sets the last backup time to "now".
*/
fun saveNewBackupTime() {
prefs.edit()
.putLong(PREF_KEY_BACKUP_TIME, Date().time)
.apply()
}
/**
* Sets the last backup time to "never".
*/
fun resetBackupTime() {
prefs.edit()
.putLong(PREF_KEY_BACKUP_TIME, 0L)
.apply()
}
/**
* Returns the last backup time in unix epoch milli seconds.
*/
fun getBackupTime(): Long {
return prefs.getLong(PREF_KEY_BACKUP_TIME, 0L)
}
@Deprecated("Replaced by KeyManager#getBackupKey()")
fun getBackupPassword(): String? {
return prefs.getString(PREF_KEY_BACKUP_PASSWORD, null)
}
}
data class Storage( data class Storage(
val uri: Uri, val uri: Uri,
val name: String, val name: String,
val ejectable: Boolean val isUsb: Boolean) {
) fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
?: throw AssertionError("Should only happen on API < 21.")
fun setStorage(context: Context, storage: Storage) {
getDefaultSharedPreferences(context)
.edit()
.putString(PREF_KEY_STORAGE_URI, storage.uri.toString())
.putString(PREF_KEY_STORAGE_NAME, storage.name)
.putBoolean(PREF_KEY_STORAGE_EJECTABLE, storage.ejectable)
.apply()
} }
fun getStorage(context: Context): Storage? { data class FlashDrive(
val prefs = getDefaultSharedPreferences(context) val name: String,
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null val serialNumber: String?,
val uri = Uri.parse(uriStr) val vendorId: Int,
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) ?: throw IllegalStateException() val productId: Int) {
val ejectable = prefs.getBoolean(PREF_KEY_STORAGE_EJECTABLE, false) companion object {
return Storage(uri, name, ejectable) fun from(device: UsbDevice) = FlashDrive(
} name = "${device.manufacturerName} ${device.productName}",
serialNumber = device.serialNumber,
/** vendorId = device.vendorId,
* Generates and returns a new backup token while saving it as well. productId = device.productId
* Subsequent calls to [getBackupToken] will return this new token once saved. )
*/ }
fun getAndSaveNewBackupToken(context: Context): Long = Date().time.apply {
getDefaultSharedPreferences(context)
.edit()
.putLong(PREF_KEY_BACKUP_TOKEN, this)
.apply()
}
/**
* Returns the current backup token or 0 if none exists.
*/
fun getBackupToken(context: Context): Long {
return getDefaultSharedPreferences(context).getLong(PREF_KEY_BACKUP_TOKEN, 0L)
}
@Deprecated("Replaced by KeyManager#getBackupKey()")
fun getBackupPassword(context: Context): String? {
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null)
} }

View file

@ -1,20 +1,14 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.app.Application import android.app.Application
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R
import com.stevesoltys.backup.transport.requestBackup import com.stevesoltys.backup.transport.requestBackup
import com.stevesoltys.backup.ui.RequireProvisioningViewModel import com.stevesoltys.backup.ui.RequireProvisioningViewModel
private val TAG = SettingsViewModel::class.java.simpleName
class SettingsViewModel(app: Application) : RequireProvisioningViewModel(app) { class SettingsViewModel(app: Application) : RequireProvisioningViewModel(app) {
override val isRestoreOperation = false override val isRestoreOperation = false
fun backupNow() { fun backupNow() {
val nm = (app as Backup).notificationManager
nm.onBackupUpdate(app.getString(R.string.notification_backup_starting), 0, 1, true)
Thread { requestBackup(app) }.start() Thread { requestBackup(app) }.start()
} }

View file

@ -7,7 +7,6 @@ import android.app.backup.RestoreSet
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.Build.VERSION.SDK_INT
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.backup.settings.SettingsActivity import com.stevesoltys.backup.settings.SettingsActivity
@ -36,7 +35,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
} }
override fun getTransportFlags(): Int { override fun getTransportFlags(): Int {
return if (SDK_INT >= 28) FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED else 0 return FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
} }
override fun dataManagementIntent(): Intent { override fun dataManagementIntent(): Intent {

View file

@ -14,6 +14,7 @@ import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.backup.Backup import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.NotificationBackupObserver import com.stevesoltys.backup.NotificationBackupObserver
import com.stevesoltys.backup.R
import com.stevesoltys.backup.service.PackageService import com.stevesoltys.backup.service.PackageService
import com.stevesoltys.backup.session.backup.BackupMonitor import com.stevesoltys.backup.session.backup.BackupMonitor
@ -50,7 +51,10 @@ class ConfigurableBackupTransportService : Service() {
@WorkerThread @WorkerThread
fun requestBackup(context: Context) { fun requestBackup(context: Context) {
context.startService(Intent(context, ConfigurableBackupTransportService::class.java)) // show notification
val nm = (context.applicationContext as Backup).notificationManager
nm.onBackupUpdate(context.getString(R.string.notification_backup_starting), 0, 1, true)
val observer = NotificationBackupObserver(context, true) val observer = NotificationBackupObserver(context, true)
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
val packages = PackageService().eligiblePackages val packages = PackageService().eligiblePackages

View file

@ -8,8 +8,6 @@ import com.stevesoltys.backup.header.HeaderReaderImpl
import com.stevesoltys.backup.header.HeaderWriterImpl import com.stevesoltys.backup.header.HeaderWriterImpl
import com.stevesoltys.backup.metadata.MetadataReaderImpl import com.stevesoltys.backup.metadata.MetadataReaderImpl
import com.stevesoltys.backup.metadata.MetadataWriterImpl import com.stevesoltys.backup.metadata.MetadataWriterImpl
import com.stevesoltys.backup.settings.getBackupToken
import com.stevesoltys.backup.settings.getStorage
import com.stevesoltys.backup.transport.backup.BackupCoordinator import com.stevesoltys.backup.transport.backup.BackupCoordinator
import com.stevesoltys.backup.transport.backup.FullBackup import com.stevesoltys.backup.transport.backup.FullBackup
import com.stevesoltys.backup.transport.backup.InputFactory import com.stevesoltys.backup.transport.backup.InputFactory
@ -24,9 +22,10 @@ import com.stevesoltys.backup.transport.restore.plugins.DocumentsProviderRestore
class PluginManager(context: Context) { class PluginManager(context: Context) {
// We can think about using an injection framework such as Dagger to simplify this. // We can think about using an injection framework such as Dagger, Koin or Kodein to simplify this.
private val storage = DocumentsStorage(context, getStorage(context), getBackupToken(context)) private val settingsManager = (context.applicationContext as Backup).settingsManager
private val storage = DocumentsStorage(context, settingsManager)
private val headerWriter = HeaderWriterImpl() private val headerWriter = HeaderWriterImpl()
private val headerReader = HeaderReaderImpl() private val headerReader = HeaderReaderImpl()
@ -42,7 +41,7 @@ class PluginManager(context: Context) {
private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto) private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto)
private val notificationManager = (context.applicationContext as Backup).notificationManager private val notificationManager = (context.applicationContext as Backup).notificationManager
internal val backupCoordinator = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager) internal val backupCoordinator = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, settingsManager, notificationManager)
private val restorePlugin = DocumentsProviderRestorePlugin(storage) private val restorePlugin = DocumentsProviderRestorePlugin(storage)
@ -50,6 +49,6 @@ class PluginManager(context: Context) {
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto) private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, outputFactory, headerReader, crypto)
private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, outputFactory, headerReader, crypto) private val fullRestore = FullRestore(restorePlugin.fullRestorePlugin, outputFactory, headerReader, crypto)
internal val restoreCoordinator = RestoreCoordinator(context, restorePlugin, kvRestore, fullRestore, metadataReader) internal val restoreCoordinator = RestoreCoordinator(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader)
} }

View file

@ -1,15 +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.getBackupToken import com.stevesoltys.backup.settings.SettingsManager
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit.DAYS
private val TAG = BackupCoordinator::class.java.simpleName private val TAG = BackupCoordinator::class.java.simpleName
@ -23,6 +24,7 @@ class BackupCoordinator(
private val kv: KVBackup, private val kv: KVBackup,
private val full: FullBackup, private val full: FullBackup,
private val metadataWriter: MetadataWriter, private val metadataWriter: MetadataWriter,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager) { private val nm: BackupNotificationManager) {
private var calledInitialize = false private var calledInitialize = false
@ -54,14 +56,15 @@ class BackupCoordinator(
Log.i(TAG, "Initialize Device!") Log.i(TAG, "Initialize Device!")
return try { return try {
plugin.initializeDevice() plugin.initializeDevice()
writeBackupMetadata(getBackupToken(context)) writeBackupMetadata(settingsManager.getBackupToken())
// [finishBackup] will only be called when we return [TRANSPORT_OK] here // [finishBackup] will only be called when we return [TRANSPORT_OK] here
// so we remember that we initialized successfully // so we remember that we initialized successfully
calledInitialize = true calledInitialize = true
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
} }
} }
@ -83,21 +86,61 @@ class BackupCoordinator(
// Key/value incremental backup support // Key/value incremental backup support
// //
fun requestBackupTime() = kv.requestBackupTime() /**
* Verify that this is a suitable time for a key/value backup pass.
* This should return zero if a backup is reasonable right now, some positive value otherwise.
* This method will be called outside of the [performIncrementalBackup]/[finishBackup] pair.
*
* If this is not a suitable time for a backup, the transport should return a backoff delay,
* in milliseconds, after which the Backup Manager should try again.
*
* @return Zero if this is a suitable time for a backup pass, or a positive time delay
* in milliseconds to suggest deferring the backup pass for a while.
*/
fun requestBackupTime(): Long = getBackupBackoff().apply {
Log.i(TAG, "Request incremental backup time. Returned $this")
}
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int) = fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
kv.performBackup(packageInfo, data, flags) // 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)
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
return result
}
// ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------
// Full backup // Full backup
// //
fun requestFullBackupTime() = full.requestFullBackupTime() /**
* Verify that this is a suitable time for a full-data backup pass.
* This should return zero if a backup is reasonable right now, some positive value otherwise.
* This method will be called outside of the [performFullBackup]/[finishBackup] pair.
*
* If this is not a suitable time for a backup, the transport should return a backoff delay,
* in milliseconds, after which the Backup Manager should try again.
*
* @return Zero if this is a suitable time for a backup pass, or a positive time delay
* in milliseconds to suggest deferring the backup pass for a while.
*
* @see [requestBackupTime]
*/
fun requestFullBackupTime(): Long = getBackupBackoff().apply {
Log.i(TAG, "Request full backup time. Returned $this")
}
fun checkFullBackupSize(size: Long) = full.checkFullBackupSize(size) fun checkFullBackupSize(size: Long) = full.checkFullBackupSize(size)
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int) = fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
full.performFullBackup(targetPackage, fileDescriptor, flags) val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
return result
}
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes) fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
@ -156,4 +199,16 @@ class BackupCoordinator(
metadataWriter.write(outputStream, token) metadataWriter.write(outputStream, token)
} }
private fun getBackupBackoff(): Long {
val noBackoff = 0L
val defaultBackoff = 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
}
} }

View file

@ -36,11 +36,6 @@ class FullBackup(
fun hasState() = state != null fun hasState() = state != null
fun requestFullBackupTime(): Long {
Log.i(TAG, "Request full backup time")
return 0
}
fun getQuota(): Long = plugin.getQuota() fun getQuota(): Long = plugin.getQuota()
fun checkFullBackupSize(size: Long): Int { fun checkFullBackupSize(size: Long): Int {

View file

@ -27,11 +27,6 @@ class KVBackup(
fun hasState() = state != null fun hasState() = state != null
fun requestBackupTime(): Long {
Log.i(TAG, "Request K/V backup time")
return 0
}
fun getQuota(): Long = plugin.getQuota() fun getQuota(): Long = plugin.getQuota()
fun performBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { fun performBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {

View file

@ -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
} }

View file

@ -4,8 +4,8 @@ import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.backup.settings.SettingsManager
import com.stevesoltys.backup.settings.Storage import com.stevesoltys.backup.settings.Storage
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -19,12 +19,13 @@ private const val MIME_TYPE = "application/octet-stream"
private val TAG = DocumentsStorage::class.java.simpleName private val TAG = DocumentsStorage::class.java.simpleName
class DocumentsStorage(private val context: Context, storage: Storage?, token: Long) { class DocumentsStorage(private val context: Context, private val settingsManager: SettingsManager) {
private val storage: Storage? = settingsManager.getStorage()
private val token: Long = settingsManager.getBackupToken()
internal val rootBackupDir: DocumentFile? by lazy { internal val rootBackupDir: DocumentFile? by lazy {
val folderUri = storage?.uri ?: return@lazy null val parent = storage?.getDocumentFile(context) ?: return@lazy null
// [fromTreeUri] should only return null when SDK_INT < 21
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
try { try {
val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT) val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT)
// create .nomedia file to prevent Android's MediaScanner from trying to index the backup // create .nomedia file to prevent Android's MediaScanner from trying to index the backup
@ -38,7 +39,7 @@ class DocumentsStorage(private val context: Context, storage: Storage?, token: L
private val currentToken: Long by lazy { private val currentToken: Long by lazy {
if (token != 0L) token if (token != 0L) token
else getAndSaveNewBackupToken(context).apply { else settingsManager.getAndSaveNewBackupToken().apply {
Log.d(TAG, "Using a fresh backup token: $this") Log.d(TAG, "Using a fresh backup token: $this")
} }
} }
@ -71,6 +72,8 @@ class DocumentsStorage(private val context: Context, storage: Storage?, token: L
} }
} }
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())

View file

@ -5,14 +5,13 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription
import android.app.backup.RestoreDescription.* import android.app.backup.RestoreDescription.*
import android.app.backup.RestoreSet import android.app.backup.RestoreSet
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.header.UnsupportedVersionException import com.stevesoltys.backup.header.UnsupportedVersionException
import com.stevesoltys.backup.metadata.DecryptionFailedException import com.stevesoltys.backup.metadata.DecryptionFailedException
import com.stevesoltys.backup.metadata.MetadataReader import com.stevesoltys.backup.metadata.MetadataReader
import com.stevesoltys.backup.settings.getBackupToken import com.stevesoltys.backup.settings.SettingsManager
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
@ -23,7 +22,7 @@ private class RestoreCoordinatorState(
private val TAG = RestoreCoordinator::class.java.simpleName private val TAG = RestoreCoordinator::class.java.simpleName
internal class RestoreCoordinator( internal class RestoreCoordinator(
private val context: Context, private val settingsManager: SettingsManager,
private val plugin: RestorePlugin, private val plugin: RestorePlugin,
private val kv: KVRestore, private val kv: KVRestore,
private val full: FullRestore, private val full: FullRestore,
@ -75,7 +74,7 @@ internal class RestoreCoordinator(
* or 0 if there is no backup set available corresponding to the current device state. * or 0 if there is no backup set available corresponding to the current device state.
*/ */
fun getCurrentRestoreSet(): Long { fun getCurrentRestoreSet(): Long {
return getBackupToken(context) return settingsManager.getBackupToken()
.apply { Log.i(TAG, "Got current restore set token: $this") } .apply { Log.i(TAG, "Got current restore set token: $this") }
} }

View file

@ -1,18 +1,16 @@
package com.stevesoltys.backup.ui.storage package com.stevesoltys.backup.ui.storage
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.app.backup.BackupProgress import android.app.backup.BackupProgress
import android.app.backup.IBackupObserver import android.app.backup.IBackupObserver
import android.net.Uri import android.net.Uri
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.backup.Backup import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
import com.stevesoltys.backup.transport.TRANSPORT_ID import com.stevesoltys.backup.transport.TRANSPORT_ID
import com.stevesoltys.backup.transport.requestBackup
private val TAG = BackupStorageViewModel::class.java.simpleName private val TAG = BackupStorageViewModel::class.java.simpleName
@ -21,14 +19,19 @@ internal class BackupStorageViewModel(private val app: Application) : StorageVie
override val isRestoreOperation = false override val isRestoreOperation = false
override fun onLocationSet(uri: Uri) { override fun onLocationSet(uri: Uri) {
saveStorage(uri) val isUsb = saveStorage(uri)
// use a new backup token // use a new backup token
getAndSaveNewBackupToken(app) settingsManager.getAndSaveNewBackupToken()
// initialize the new location // initialize the new location
val observer = InitializationObserver() val observer = InitializationObserver()
Backup.backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer) Backup.backupManager.initializeTransportsForUser(UserHandle.myUserId(), arrayOf(TRANSPORT_ID), observer)
// if storage is on USB and this is not SetupWizard, do a backup right away
if (isUsb && !isSetupWizard) Thread {
requestBackup(app)
}.start()
} }
@WorkerThread @WorkerThread

View file

@ -31,6 +31,7 @@ class StorageActivity : BackupActivity() {
} else { } else {
ViewModelProviders.of(this).get(BackupStorageViewModel::class.java) ViewModelProviders.of(this).get(BackupStorageViewModel::class.java)
} }
viewModel.isSetupWizard = isSetupWizard()
viewModel.locationSet.observeEvent(this, LiveEventHandler { viewModel.locationSet.observeEvent(this, LiveEventHandler {
showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle()), true) showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle()), true)

View file

@ -33,7 +33,7 @@ data class StorageRoot(
internal val title: String, internal val title: String,
internal val summary: String?, internal val summary: String?,
internal val availableBytes: Long?, internal val availableBytes: Long?,
internal val supportsEject: Boolean, internal val isUsb: Boolean,
internal val enabled: Boolean = true) { internal val enabled: Boolean = true) {
internal val uri: Uri by lazy { internal val uri: Uri by lazy {
@ -41,7 +41,7 @@ data class StorageRoot(
} }
fun isInternal(): Boolean { fun isInternal(): Boolean {
return authority == AUTHORITY_STORAGE && !supportsEject return authority == AUTHORITY_STORAGE && !isUsb
} }
} }
@ -103,7 +103,7 @@ internal class StorageRootFetcher(private val context: Context) {
var cursor: Cursor? = null var cursor: Cursor? = null
try { try {
cursor = contentResolver.query(rootsUri, null, null, null, null) cursor = contentResolver.query(rootsUri, null, null, null, null)
while (cursor.moveToNext()) { while (cursor!!.moveToNext()) {
val root = getStorageRoot(authority, cursor) val root = getStorageRoot(authority, cursor)
if (root != null) roots.add(root) if (root != null) roots.add(root)
} }
@ -122,7 +122,6 @@ internal class StorageRootFetcher(private val context: Context) {
if (!supportsCreate || !supportsIsChild) return null if (!supportsCreate || !supportsIsChild) return null
val rootId = cursor.getString(COLUMN_ROOT_ID)!! val rootId = cursor.getString(COLUMN_ROOT_ID)!!
if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null
val supportsEject = flags and FLAG_SUPPORTS_EJECT != 0
return StorageRoot( return StorageRoot(
authority = authority, authority = authority,
rootId = rootId, rootId = rootId,
@ -131,13 +130,13 @@ internal class StorageRootFetcher(private val context: Context) {
title = cursor.getString(COLUMN_TITLE)!!, title = cursor.getString(COLUMN_TITLE)!!,
summary = cursor.getString(COLUMN_SUMMARY), summary = cursor.getString(COLUMN_SUMMARY),
availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES), availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES),
supportsEject = supportsEject isUsb = flags and FLAG_REMOVABLE_USB != 0
) )
} }
private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) { private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) {
for (root in roots) { for (root in roots) {
if (root.authority == AUTHORITY_STORAGE && root.supportsEject) return if (root.authority == AUTHORITY_STORAGE && root.isUsb) return
} }
val root = StorageRoot( val root = StorageRoot(
authority = AUTHORITY_STORAGE, authority = AUTHORITY_STORAGE,
@ -147,7 +146,7 @@ internal class StorageRootFetcher(private val context: Context) {
title = context.getString(R.string.storage_fake_drive_title), title = context.getString(R.string.storage_fake_drive_title),
summary = context.getString(R.string.storage_fake_drive_summary), summary = context.getString(R.string.storage_fake_drive_summary),
availableBytes = null, availableBytes = null,
supportsEject = true, isUsb = true,
enabled = false enabled = false
) )
roots.add(root) roots.add(root)
@ -198,7 +197,7 @@ internal class StorageRootFetcher(private val context: Context) {
} }
} }
private fun getPackageIcon(context: Context, authority: String?, icon: Int): Drawable? { private fun getPackageIcon(context: Context, authority: String, icon: Int): Drawable? {
if (icon != 0) { if (icon != 0) {
val pm = context.packageManager val pm = context.packageManager
val info = pm.resolveContentProvider(authority, 0) val info = pm.resolveContentProvider(authority, 0)

View file

@ -2,19 +2,22 @@ package com.stevesoltys.backup.ui.storage
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.Context.USB_SERVICE
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import android.hardware.usb.UsbManager
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.isMassStorage
import com.stevesoltys.backup.settings.BackupManagerSettings
import com.stevesoltys.backup.settings.FlashDrive
import com.stevesoltys.backup.settings.Storage import com.stevesoltys.backup.settings.Storage
import com.stevesoltys.backup.settings.getStorage
import com.stevesoltys.backup.settings.setStorage
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
import com.stevesoltys.backup.ui.LiveEvent import com.stevesoltys.backup.ui.LiveEvent
import com.stevesoltys.backup.ui.MutableLiveEvent import com.stevesoltys.backup.ui.MutableLiveEvent
@ -23,6 +26,8 @@ private val TAG = StorageViewModel::class.java.simpleName
internal abstract class StorageViewModel(private val app: Application) : AndroidViewModel(app), RemovableStorageListener { internal abstract class StorageViewModel(private val app: Application) : AndroidViewModel(app), RemovableStorageListener {
protected val settingsManager = (app as Backup).settingsManager
private val mStorageRoots = MutableLiveData<List<StorageRoot>>() private val mStorageRoots = MutableLiveData<List<StorageRoot>>()
internal val storageRoots: LiveData<List<StorageRoot>> get() = mStorageRoots internal val storageRoots: LiveData<List<StorageRoot>> get() = mStorageRoots
@ -35,14 +40,15 @@ internal abstract class StorageViewModel(private val app: Application) : Android
private val storageRootFetcher by lazy { StorageRootFetcher(app) } private val storageRootFetcher by lazy { StorageRootFetcher(app) }
private var storageRoot: StorageRoot? = null private var storageRoot: StorageRoot? = null
internal var isSetupWizard: Boolean = false
abstract val isRestoreOperation: Boolean abstract val isRestoreOperation: Boolean
companion object { companion object {
internal fun validLocationIsSet(context: Context): Boolean { internal fun validLocationIsSet(context: Context): Boolean {
val storage = getStorage(context) ?: return false val settingsManager = (context.applicationContext as Backup).settingsManager
if (storage.ejectable) return true val storage = settingsManager.getStorage() ?: return false
val file = DocumentFile.fromTreeUri(context, storage.uri) ?: return false if (storage.isUsb) return true
return file.isDirectory return storage.getDocumentFile(context).isDirectory
} }
} }
@ -74,9 +80,12 @@ internal abstract class StorageViewModel(private val app: Application) : Android
onLocationSet(uri) onLocationSet(uri)
} }
abstract fun onLocationSet(uri: Uri) /**
* Saves the storage behind the given [Uri] (and saved [storageRoot]).
protected fun saveStorage(uri: Uri) { *
* @return true if the storage is a USB flash drive, false otherwise.
*/
protected fun saveStorage(uri: Uri): Boolean {
// store backup storage location in settings // store backup storage location in settings
val root = storageRoot ?: throw IllegalStateException() val root = storageRoot ?: throw IllegalStateException()
val name = if (root.isInternal()) { val name = if (root.isInternal()) {
@ -84,15 +93,47 @@ internal abstract class StorageViewModel(private val app: Application) : Android
} else { } else {
root.title root.title
} }
val storage = Storage(uri, name, root.supportsEject) val storage = Storage(uri, name, root.isUsb)
setStorage(app, storage) settingsManager.setStorage(storage)
// reset time of last backup to "Never"
settingsManager.resetBackupTime()
if (storage.isUsb) {
Log.d(TAG, "Selected storage is a removable USB device.")
val wasSaved = saveUsbDevice()
// reset stored flash drive, if we did not update it
if (!wasSaved) settingsManager.setFlashDrive(null)
BackupManagerSettings.disableAutomaticBackups(app.contentResolver)
} else {
settingsManager.setFlashDrive(null)
BackupManagerSettings.enableAutomaticBackups(app.contentResolver)
}
// stop backup service to be sure the old location will get updated // stop backup service to be sure the old location will get updated
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java)) app.stopService(Intent(app, ConfigurableBackupTransportService::class.java))
Log.d(TAG, "New storage location saved: $uri") Log.d(TAG, "New storage location saved: $uri")
return storage.isUsb
} }
private fun saveUsbDevice(): Boolean {
val manager = app.getSystemService(USB_SERVICE) as UsbManager
manager.deviceList.values.forEach { device ->
if (device.isMassStorage()) {
val flashDrive = FlashDrive.from(device)
settingsManager.setFlashDrive(flashDrive)
Log.d(TAG, "Saved flash drive: $flashDrive")
return true
}
}
Log.e(TAG, "No USB device found even though we were expecting one.")
return false
}
abstract fun onLocationSet(uri: Uri)
override fun onCleared() { override fun onCleared() {
storageRootFetcher.setRemovableStorageListener(null) storageRootFetcher.setRemovableStorageListener(null)
super.onCleared() super.onCleared()

View file

@ -5,11 +5,13 @@
<item <item
android:id="@+id/action_backup" android:id="@+id/action_backup"
android:enabled="false"
android:title="@string/settings_backup_now" android:title="@string/settings_backup_now"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/action_restore" android:id="@+id/action_restore"
android:enabled="false"
android:title="@string/restore_backup_button" android:title="@string/restore_backup_button"
android:visible="false" android:visible="false"
app:showAsAction="never" app:showAsAction="never"

View file

@ -31,6 +31,8 @@
<string name="settings_backup_location_invalid">The chosen location can not be used.</string> <string name="settings_backup_location_invalid">The chosen location can not be used.</string>
<string name="settings_backup_location_none">None</string> <string name="settings_backup_location_none">None</string>
<string name="settings_backup_location_internal">Internal Storage</string> <string name="settings_backup_location_internal">Internal Storage</string>
<string name="settings_backup_last_backup_never">Never</string>
<string name="settings_backup_location_summary">%s · Last Backup %s</string>
<string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string> <string name="settings_info">All backups are encrypted on your phone. To restore from backup you will need your 12-word recovery code.</string>
<string name="settings_auto_restore_title">Automatic restore</string> <string name="settings_auto_restore_title">Automatic restore</string>
<string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data</string> <string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data</string>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device
class="8"
protocol="80"
subclass="6" />
</resources>

View file

@ -43,14 +43,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val fullBackupPlugin = mockk<FullBackupPlugin>() private val fullBackupPlugin = mockk<FullBackupPlugin>()
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl) private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, notificationManager) private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataWriter, settingsManager, notificationManager)
private val restorePlugin = mockk<RestorePlugin>() private val restorePlugin = mockk<RestorePlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>() private val kvRestorePlugin = mockk<KVRestorePlugin>()
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl) private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val fullRestorePlugin = mockk<FullRestorePlugin>() private val fullRestorePlugin = mockk<FullRestorePlugin>()
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl) private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(context, restorePlugin, kvRestore, fullRestore, metadataReader) private val restore = RestoreCoordinator(settingsManager, restorePlugin, kvRestore, fullRestore, metadataReader)
private val backupDataInput = mockk<BackupDataInput>() private val backupDataInput = mockk<BackupDataInput>()
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true) private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
@ -91,6 +91,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData2.size appData2.size
} }
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2 every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
every { settingsManager.saveNewBackupTime() } just Runs
// start and finish K/V backup // start and finish K/V backup
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
@ -130,6 +131,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
every { settingsManager.saveNewBackupTime() } just Runs
// perform backup to output stream // perform backup to output stream
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0)) assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import com.stevesoltys.backup.crypto.Crypto import com.stevesoltys.backup.crypto.Crypto
import com.stevesoltys.backup.settings.SettingsManager
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
@ -14,6 +15,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
abstract class TransportTest { abstract class TransportTest {
protected val crypto = mockk<Crypto>() protected val crypto = mockk<Crypto>()
protected val settingsManager = mockk<SettingsManager>()
protected val context = mockk<Context>(relaxed = true) protected val context = mockk<Context>(relaxed = true)
protected val packageInfo = PackageInfo().apply { packageName = "org.example" } protected val packageInfo = PackageInfo().apply { packageName = "org.example" }

View file

@ -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
@ -23,14 +27,15 @@ internal class BackupCoordinatorTest: BackupTest() {
private val metadataWriter = mockk<MetadataWriter>() private val metadataWriter = mockk<MetadataWriter>()
private val notificationManager = mockk<BackupNotificationManager>() private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, plugin, kv, full, metadataWriter, notificationManager) private val backup = BackupCoordinator(context, plugin, kv, full, metadataWriter, settingsManager, notificationManager)
private val metadataOutputStream = mockk<OutputStream>() private val metadataOutputStream = mockk<OutputStream>()
@Test @Test
fun `device initialization succeeds and delegates to plugin`() { fun `device initialization succeeds and delegates to plugin`() {
every { plugin.initializeDevice() } just Runs every { plugin.initializeDevice() } just Runs
expectWritingMetadata(0L) every { settingsManager.getBackupToken() } returns token
expectWritingMetadata(token)
every { kv.hasState() } returns false every { kv.hasState() } returns false
every { full.hasState() } returns false every { full.hasState() } returns false
@ -39,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())
@ -53,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()

View file

@ -20,11 +20,6 @@ internal class FullBackupTest : BackupTest() {
private val closeBytes = ByteArray(42).apply { Random.nextBytes(this) } private val closeBytes = ByteArray(42).apply { Random.nextBytes(this) }
private val inputStream = mockk<FileInputStream>() private val inputStream = mockk<FileInputStream>()
@Test
fun `now is a good time for a backup`() {
assertEquals(0, backup.requestFullBackupTime())
}
@Test @Test
fun `has no initial state`() { fun `has no initial state`() {
assertFalse(backup.hasState()) assertFalse(backup.hasState())

View file

@ -28,11 +28,6 @@ internal class KVBackupTest : BackupTest() {
private val value = ByteArray(23).apply { Random.nextBytes(this) } private val value = ByteArray(23).apply { Random.nextBytes(this) }
private val versionHeader = VersionHeader(packageName = packageInfo.packageName, key = key) private val versionHeader = VersionHeader(packageName = packageInfo.packageName, key = key)
@Test
fun `now is a good time for a backup`() {
assertEquals(0, backup.requestBackupTime())
}
@Test @Test
fun `has no initial state`() { fun `has no initial state`() {
assertFalse(backup.hasState()) assertFalse(backup.hasState())

View file

@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
private val full = mockk<FullRestore>() private val full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>() private val metadataReader = mockk<MetadataReader>()
private val restore = RestoreCoordinator(context, plugin, kv, full, metadataReader) private val restore = RestoreCoordinator(settingsManager, plugin, kv, full, metadataReader)
private val token = Random.nextLong() private val token = Random.nextLong()
private val inputStream = mockk<InputStream>() private val inputStream = mockk<InputStream>()
@ -56,8 +56,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
@Test @Test
fun `getCurrentRestoreSet() delegates to plugin`() { fun `getCurrentRestoreSet() delegates to plugin`() {
// We don't mock the SettingsManager, so the default value is returned here every { settingsManager.getBackupToken() } returns token
assertEquals(0L, restore.getCurrentRestoreSet()) assertEquals(token, restore.getCurrentRestoreSet())
} }
@Test @Test

View file

@ -2,5 +2,7 @@
<permissions> <permissions>
<privapp-permissions package="com.stevesoltys.backup"> <privapp-permissions package="com.stevesoltys.backup">
<permission name="android.permission.BACKUP"/> <permission name="android.permission.BACKUP"/>
<permission name="android.permission.MANAGE_USB"/>
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
</privapp-permissions> </privapp-permissions>
</permissions> </permissions>