Merge pull request #42 from grote/usb
Various optimizations for using a USB flash drive as backup storage medium
This commit is contained in:
commit
d944f985a0
37 changed files with 604 additions and 179 deletions
|
@ -3,8 +3,6 @@ package com.stevesoltys.backup
|
|||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
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.createOrGetFile
|
||||
import org.junit.After
|
||||
|
@ -21,9 +19,8 @@ private const val filename = "test-file"
|
|||
class DocumentsStorageTest {
|
||||
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val token = getBackupToken(context)
|
||||
private val folderUri = getStorage(context)
|
||||
private val storage = DocumentsStorage(context, folderUri, token)
|
||||
private val settingsManager = (context.applicationContext as Backup).settingsManager
|
||||
private val storage = DocumentsStorage(context, settingsManager)
|
||||
|
||||
private lateinit var file: DocumentFile
|
||||
|
||||
|
|
|
@ -14,6 +14,16 @@
|
|||
android:name="android.permission.MANAGE_DOCUMENTS"
|
||||
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
|
||||
android:name=".Backup"
|
||||
android:allowBackup="false"
|
||||
|
@ -72,5 +82,16 @@
|
|||
</intent-filter>
|
||||
</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>
|
||||
</manifest>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package com.stevesoltys.backup
|
||||
|
||||
import android.app.Application
|
||||
import android.app.backup.BackupManager.PACKAGE_MANAGER_SENTINEL
|
||||
import android.app.backup.IBackupManager
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ServiceManager.getService
|
||||
import com.stevesoltys.backup.crypto.KeyManager
|
||||
import com.stevesoltys.backup.crypto.KeyManagerImpl
|
||||
import com.stevesoltys.backup.ui.storage.AUTHORITY_STORAGE
|
||||
import com.stevesoltys.backup.settings.SettingsManager
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -25,10 +25,15 @@ class Backup : Application() {
|
|||
}
|
||||
}
|
||||
|
||||
val settingsManager by lazy {
|
||||
SettingsManager(this)
|
||||
}
|
||||
val notificationManager by lazy {
|
||||
BackupNotificationManager(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const val MAGIC_PACKAGE_MANAGER = PACKAGE_MANAGER_SENTINEL
|
||||
|
||||
fun isDebugBuild() = Build.TYPE == "userdebug"
|
||||
|
|
|
@ -79,9 +79,9 @@ class BackupNotificationManager(private val context: Context) {
|
|||
val notification = errorBuilder.apply {
|
||||
setContentTitle(context.getString(R.string.notification_error_title))
|
||||
setContentText(context.getString(R.string.notification_error_text))
|
||||
addAction(action)
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
mActions = arrayListOf(action)
|
||||
}.build()
|
||||
nm.notify(NOTIFICATION_ID_ERROR, notification)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class NotificationBackupObserver(context: Context, private val userInitiated: Bo
|
|||
}
|
||||
|
||||
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)
|
||||
return pm.getApplicationLabel(appInfo)
|
||||
}
|
||||
|
|
104
app/src/main/java/com/stevesoltys/backup/UsbIntentReceiver.kt
Normal file
104
app/src/main/java/com/stevesoltys/backup/UsbIntentReceiver.kt
Normal 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()}")
|
||||
}
|
|
@ -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 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.settings.SettingsManagerKt.getBackupPassword;
|
||||
import static com.stevesoltys.backup.settings.SettingsManagerKt.getStorage;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
|
@ -41,7 +39,7 @@ public class MainActivityController {
|
|||
}
|
||||
|
||||
boolean isChangeBackupLocationButtonVisible(Activity parent) {
|
||||
return getStorage(parent) != null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showChooseFolderActivity(Activity parent, boolean continueToBackup) {
|
||||
|
@ -74,15 +72,8 @@ public class MainActivityController {
|
|||
}
|
||||
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
||||
// show Toast informing the user
|
||||
Toast.makeText(parent, "REMOVED", Toast.LENGTH_SHORT).show();
|
||||
|
||||
return true;
|
||||
Toast.makeText(parent, "Please make at least one manual backup first.", Toast.LENGTH_SHORT).show();
|
||||
return false;
|
||||
}
|
||||
|
||||
void onChangeBackupLocationButtonClicked(Activity parent) {
|
||||
|
|
|
@ -21,8 +21,6 @@ import com.stevesoltys.backup.service.backup.BackupService;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword;
|
||||
|
||||
/**
|
||||
* @author Steve Soltys
|
||||
*/
|
||||
|
@ -70,12 +68,7 @@ class CreateBackupActivityController {
|
|||
}
|
||||
|
||||
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) {
|
||||
String password = getBackupPassword(parent);
|
||||
if (password == null) {
|
||||
showEnterPasswordAlert(selectedPackages, parent);
|
||||
} else {
|
||||
backupService.backupPackageData(selectedPackages, parent);
|
||||
}
|
||||
backupService.backupPackageData(selectedPackages, parent);
|
||||
}
|
||||
|
||||
private void showEnterPasswordAlert(Set<String> selectedPackages, Activity parent) {
|
||||
|
|
|
@ -10,10 +10,10 @@ import android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.getAppName
|
||||
import com.stevesoltys.backup.isDebugBuild
|
||||
import com.stevesoltys.backup.settings.getStorage
|
||||
import kotlinx.android.synthetic.main.fragment_restore_progress.*
|
||||
|
||||
class RestoreProgressFragment : Fragment() {
|
||||
|
@ -49,7 +49,8 @@ class RestoreProgressFragment : Fragment() {
|
|||
if (finished == 0) {
|
||||
// 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))
|
||||
} else {
|
||||
getString(R.string.restore_finished_warning_only_installed, null)
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.os.UserHandle
|
|||
import android.util.Log
|
||||
import com.google.android.collect.Sets.newArraySet
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.MAGIC_PACKAGE_MANAGER
|
||||
import java.util.*
|
||||
|
||||
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
|
||||
val packageArray = eligibleApps.toMutableList()
|
||||
packageArray.add("@pm@")
|
||||
packageArray.add(MAGIC_PACKAGE_MANAGER)
|
||||
|
||||
return packageArray.toTypedArray()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +1,18 @@
|
|||
package com.stevesoltys.backup.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.BACKUP_SERVICE
|
||||
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.RemoteException
|
||||
import android.provider.Settings
|
||||
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.view.Menu
|
||||
import android.view.MenuInflater
|
||||
|
@ -17,7 +24,10 @@ import androidx.preference.PreferenceFragmentCompat
|
|||
import androidx.preference.TwoStatePreference
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.UsbMonitor
|
||||
import com.stevesoltys.backup.isMassStorage
|
||||
import com.stevesoltys.backup.restore.RestoreActivity
|
||||
import java.util.*
|
||||
|
||||
private val TAG = SettingsFragment::class.java.name
|
||||
|
||||
|
@ -26,16 +36,35 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private val backupManager = Backup.backupManager
|
||||
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
private lateinit var settingsManager: SettingsManager
|
||||
|
||||
private lateinit var backup: TwoStatePreference
|
||||
private lateinit var autoRestore: TwoStatePreference
|
||||
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?) {
|
||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
|
||||
settingsManager = (requireContext().applicationContext as Backup).settingsManager
|
||||
|
||||
backup = findPreference<TwoStatePreference>("backup")!!
|
||||
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
|
||||
|
@ -74,30 +103,31 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
super.onStart()
|
||||
|
||||
// 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 {
|
||||
backup.isChecked = backupManager.isBackupEnabled
|
||||
backup.isEnabled = true
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Error communicating with BackupManager", e)
|
||||
backup.isEnabled = false
|
||||
}
|
||||
storage = settingsManager.getStorage()
|
||||
setBackupState()
|
||||
setAutoRestoreState()
|
||||
setBackupLocationSummary()
|
||||
setMenuItemStates()
|
||||
|
||||
val resolver = requireContext().contentResolver
|
||||
autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1
|
||||
if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
|
||||
}
|
||||
|
||||
// TODO add time of last backup here
|
||||
val storageName = getStorage(requireContext())?.name
|
||||
backupLocation.summary = storageName ?: getString(R.string.settings_backup_location_none )
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (storage?.isUsb == true) context?.unregisterReceiver(usbReceiver)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
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)) {
|
||||
menu.findItem(R.id.action_restore).isVisible = true
|
||||
menuRestore?.isVisible = true
|
||||
}
|
||||
setMenuItemStates()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when {
|
||||
|
@ -112,4 +142,45 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,59 +1,139 @@
|
|||
package com.stevesoltys.backup.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.usb.UsbDevice
|
||||
import android.net.Uri
|
||||
import android.preference.PreferenceManager.getDefaultSharedPreferences
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.util.*
|
||||
|
||||
private const val PREF_KEY_STORAGE_URI = "storageUri"
|
||||
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_TIME = "backupTime"
|
||||
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(
|
||||
val uri: Uri,
|
||||
val name: String,
|
||||
val ejectable: Boolean
|
||||
)
|
||||
|
||||
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()
|
||||
val isUsb: Boolean) {
|
||||
fun getDocumentFile(context: Context) = DocumentFile.fromTreeUri(context, uri)
|
||||
?: throw AssertionError("Should only happen on API < 21.")
|
||||
}
|
||||
|
||||
fun getStorage(context: Context): Storage? {
|
||||
val prefs = getDefaultSharedPreferences(context)
|
||||
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 ejectable = prefs.getBoolean(PREF_KEY_STORAGE_EJECTABLE, false)
|
||||
return Storage(uri, name, ejectable)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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)
|
||||
data class FlashDrive(
|
||||
val name: String,
|
||||
val serialNumber: String?,
|
||||
val vendorId: Int,
|
||||
val productId: Int) {
|
||||
companion object {
|
||||
fun from(device: UsbDevice) = FlashDrive(
|
||||
name = "${device.manufacturerName} ${device.productName}",
|
||||
serialNumber = device.serialNumber,
|
||||
vendorId = device.vendorId,
|
||||
productId = device.productId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
package com.stevesoltys.backup.settings
|
||||
|
||||
import android.app.Application
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.transport.requestBackup
|
||||
import com.stevesoltys.backup.ui.RequireProvisioningViewModel
|
||||
|
||||
private val TAG = SettingsViewModel::class.java.simpleName
|
||||
|
||||
class SettingsViewModel(app: Application) : RequireProvisioningViewModel(app) {
|
||||
|
||||
override val isRestoreOperation = false
|
||||
|
||||
fun backupNow() {
|
||||
val nm = (app as Backup).notificationManager
|
||||
nm.onBackupUpdate(app.getString(R.string.notification_backup_starting), 0, 1, true)
|
||||
Thread { requestBackup(app) }.start()
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.app.backup.RestoreSet
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.settings.SettingsActivity
|
||||
|
@ -36,7 +35,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.util.Log
|
|||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.NotificationBackupObserver
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.service.PackageService
|
||||
import com.stevesoltys.backup.session.backup.BackupMonitor
|
||||
|
||||
|
@ -50,7 +51,10 @@ class ConfigurableBackupTransportService : Service() {
|
|||
|
||||
@WorkerThread
|
||||
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 flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
|
||||
val packages = PackageService().eligiblePackages
|
||||
|
|
|
@ -8,8 +8,6 @@ import com.stevesoltys.backup.header.HeaderReaderImpl
|
|||
import com.stevesoltys.backup.header.HeaderWriterImpl
|
||||
import com.stevesoltys.backup.metadata.MetadataReaderImpl
|
||||
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.FullBackup
|
||||
import com.stevesoltys.backup.transport.backup.InputFactory
|
||||
|
@ -24,9 +22,10 @@ import com.stevesoltys.backup.transport.restore.plugins.DocumentsProviderRestore
|
|||
|
||||
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 headerReader = HeaderReaderImpl()
|
||||
|
@ -42,7 +41,7 @@ class PluginManager(context: Context) {
|
|||
private val fullBackup = FullBackup(backupPlugin.fullBackupPlugin, inputFactory, headerWriter, crypto)
|
||||
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)
|
||||
|
@ -50,6 +49,6 @@ class PluginManager(context: Context) {
|
|||
private val kvRestore = KVRestore(restorePlugin.kvRestorePlugin, 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)
|
||||
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
package com.stevesoltys.backup.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.BackupNotificationManager
|
||||
import com.stevesoltys.backup.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import com.stevesoltys.backup.settings.SettingsManager
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
|
||||
private val TAG = BackupCoordinator::class.java.simpleName
|
||||
|
||||
|
@ -23,6 +24,7 @@ class BackupCoordinator(
|
|||
private val kv: KVBackup,
|
||||
private val full: FullBackup,
|
||||
private val metadataWriter: MetadataWriter,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val nm: BackupNotificationManager) {
|
||||
|
||||
private var calledInitialize = false
|
||||
|
@ -54,14 +56,15 @@ class BackupCoordinator(
|
|||
Log.i(TAG, "Initialize Device!")
|
||||
return try {
|
||||
plugin.initializeDevice()
|
||||
writeBackupMetadata(getBackupToken(context))
|
||||
writeBackupMetadata(settingsManager.getBackupToken())
|
||||
// [finishBackup] will only be called when we return [TRANSPORT_OK] here
|
||||
// so we remember that we initialized successfully
|
||||
calledInitialize = true
|
||||
TRANSPORT_OK
|
||||
} catch (e: IOException) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -83,21 +86,61 @@ class BackupCoordinator(
|
|||
// 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) =
|
||||
kv.performBackup(packageInfo, data, flags)
|
||||
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)
|
||||
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
||||
return result
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// 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 performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int) =
|
||||
full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
||||
val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
||||
return result
|
||||
}
|
||||
|
||||
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
||||
|
||||
|
@ -156,4 +199,16 @@ class BackupCoordinator(
|
|||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -36,11 +36,6 @@ class FullBackup(
|
|||
|
||||
fun hasState() = state != null
|
||||
|
||||
fun requestFullBackupTime(): Long {
|
||||
Log.i(TAG, "Request full backup time")
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getQuota(): Long = plugin.getQuota()
|
||||
|
||||
fun checkFullBackupSize(size: Long): Int {
|
||||
|
|
|
@ -27,11 +27,6 @@ class KVBackup(
|
|||
|
||||
fun hasState() = state != null
|
||||
|
||||
fun requestBackupTime(): Long {
|
||||
Log.i(TAG, "Request K/V backup time")
|
||||
return 0
|
||||
}
|
||||
|
||||
fun getQuota(): Long = plugin.getQuota()
|
||||
|
||||
fun performBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
||||
|
|
|
@ -42,7 +42,7 @@ class DocumentsProviderBackupPlugin(
|
|||
}
|
||||
|
||||
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
|
||||
providerInfo.packageName
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import android.content.Context
|
|||
import android.content.pm.PackageInfo
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.backup.settings.SettingsManager
|
||||
import com.stevesoltys.backup.settings.Storage
|
||||
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
@ -19,12 +19,13 @@ private const val MIME_TYPE = "application/octet-stream"
|
|||
|
||||
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 {
|
||||
val folderUri = storage?.uri ?: return@lazy null
|
||||
// [fromTreeUri] should only return null when SDK_INT < 21
|
||||
val parent = DocumentFile.fromTreeUri(context, folderUri) ?: throw AssertionError()
|
||||
val parent = storage?.getDocumentFile(context) ?: return@lazy null
|
||||
try {
|
||||
val rootDir = parent.createOrGetDirectory(DIRECTORY_ROOT)
|
||||
// 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 {
|
||||
if (token != 0L) token
|
||||
else getAndSaveNewBackupToken(context).apply {
|
||||
else settingsManager.getAndSaveNewBackupToken().apply {
|
||||
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? {
|
||||
if (token == currentToken) return currentSetDir
|
||||
return rootBackupDir?.findFile(token.toString())
|
||||
|
|
|
@ -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.RestoreSet
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.header.UnsupportedVersionException
|
||||
import com.stevesoltys.backup.metadata.DecryptionFailedException
|
||||
import com.stevesoltys.backup.metadata.MetadataReader
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import com.stevesoltys.backup.settings.SettingsManager
|
||||
import libcore.io.IoUtils.closeQuietly
|
||||
import java.io.IOException
|
||||
|
||||
|
@ -23,7 +22,7 @@ private class RestoreCoordinatorState(
|
|||
private val TAG = RestoreCoordinator::class.java.simpleName
|
||||
|
||||
internal class RestoreCoordinator(
|
||||
private val context: Context,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val plugin: RestorePlugin,
|
||||
private val kv: KVRestore,
|
||||
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.
|
||||
*/
|
||||
fun getCurrentRestoreSet(): Long {
|
||||
return getBackupToken(context)
|
||||
return settingsManager.getBackupToken()
|
||||
.apply { Log.i(TAG, "Got current restore set token: $this") }
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
package com.stevesoltys.backup.ui.storage
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.backup.BackupProgress
|
||||
import android.app.backup.IBackupObserver
|
||||
import android.net.Uri
|
||||
import android.os.UserHandle
|
||||
import android.os.UserManager
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
|
||||
import com.stevesoltys.backup.transport.TRANSPORT_ID
|
||||
import com.stevesoltys.backup.transport.requestBackup
|
||||
|
||||
private val TAG = BackupStorageViewModel::class.java.simpleName
|
||||
|
||||
|
@ -21,14 +19,19 @@ internal class BackupStorageViewModel(private val app: Application) : StorageVie
|
|||
override val isRestoreOperation = false
|
||||
|
||||
override fun onLocationSet(uri: Uri) {
|
||||
saveStorage(uri)
|
||||
val isUsb = saveStorage(uri)
|
||||
|
||||
// use a new backup token
|
||||
getAndSaveNewBackupToken(app)
|
||||
settingsManager.getAndSaveNewBackupToken()
|
||||
|
||||
// initialize the new location
|
||||
val observer = InitializationObserver()
|
||||
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
|
||||
|
|
|
@ -31,6 +31,7 @@ class StorageActivity : BackupActivity() {
|
|||
} else {
|
||||
ViewModelProviders.of(this).get(BackupStorageViewModel::class.java)
|
||||
}
|
||||
viewModel.isSetupWizard = isSetupWizard()
|
||||
|
||||
viewModel.locationSet.observeEvent(this, LiveEventHandler {
|
||||
showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle()), true)
|
||||
|
|
|
@ -33,7 +33,7 @@ data class StorageRoot(
|
|||
internal val title: String,
|
||||
internal val summary: String?,
|
||||
internal val availableBytes: Long?,
|
||||
internal val supportsEject: Boolean,
|
||||
internal val isUsb: Boolean,
|
||||
internal val enabled: Boolean = true) {
|
||||
|
||||
internal val uri: Uri by lazy {
|
||||
|
@ -41,7 +41,7 @@ data class StorageRoot(
|
|||
}
|
||||
|
||||
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
|
||||
try {
|
||||
cursor = contentResolver.query(rootsUri, null, null, null, null)
|
||||
while (cursor.moveToNext()) {
|
||||
while (cursor!!.moveToNext()) {
|
||||
val root = getStorageRoot(authority, cursor)
|
||||
if (root != null) roots.add(root)
|
||||
}
|
||||
|
@ -122,7 +122,6 @@ internal class StorageRootFetcher(private val context: Context) {
|
|||
if (!supportsCreate || !supportsIsChild) return null
|
||||
val rootId = cursor.getString(COLUMN_ROOT_ID)!!
|
||||
if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null
|
||||
val supportsEject = flags and FLAG_SUPPORTS_EJECT != 0
|
||||
return StorageRoot(
|
||||
authority = authority,
|
||||
rootId = rootId,
|
||||
|
@ -131,13 +130,13 @@ internal class StorageRootFetcher(private val context: Context) {
|
|||
title = cursor.getString(COLUMN_TITLE)!!,
|
||||
summary = cursor.getString(COLUMN_SUMMARY),
|
||||
availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES),
|
||||
supportsEject = supportsEject
|
||||
isUsb = flags and FLAG_REMOVABLE_USB != 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkOrAddUsbRoot(roots: ArrayList<StorageRoot>) {
|
||||
for (root in roots) {
|
||||
if (root.authority == AUTHORITY_STORAGE && root.supportsEject) return
|
||||
if (root.authority == AUTHORITY_STORAGE && root.isUsb) return
|
||||
}
|
||||
val root = StorageRoot(
|
||||
authority = AUTHORITY_STORAGE,
|
||||
|
@ -147,7 +146,7 @@ internal class StorageRootFetcher(private val context: Context) {
|
|||
title = context.getString(R.string.storage_fake_drive_title),
|
||||
summary = context.getString(R.string.storage_fake_drive_summary),
|
||||
availableBytes = null,
|
||||
supportsEject = true,
|
||||
isUsb = true,
|
||||
enabled = false
|
||||
)
|
||||
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) {
|
||||
val pm = context.packageManager
|
||||
val info = pm.resolveContentProvider(authority, 0)
|
||||
|
|
|
@ -2,19 +2,22 @@ package com.stevesoltys.backup.ui.storage
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Context.USB_SERVICE
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.stevesoltys.backup.Backup
|
||||
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.getStorage
|
||||
import com.stevesoltys.backup.settings.setStorage
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
|
||||
import com.stevesoltys.backup.ui.LiveEvent
|
||||
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 {
|
||||
|
||||
protected val settingsManager = (app as Backup).settingsManager
|
||||
|
||||
private val mStorageRoots = MutableLiveData<List<StorageRoot>>()
|
||||
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 var storageRoot: StorageRoot? = null
|
||||
|
||||
internal var isSetupWizard: Boolean = false
|
||||
abstract val isRestoreOperation: Boolean
|
||||
|
||||
companion object {
|
||||
internal fun validLocationIsSet(context: Context): Boolean {
|
||||
val storage = getStorage(context) ?: return false
|
||||
if (storage.ejectable) return true
|
||||
val file = DocumentFile.fromTreeUri(context, storage.uri) ?: return false
|
||||
return file.isDirectory
|
||||
val settingsManager = (context.applicationContext as Backup).settingsManager
|
||||
val storage = settingsManager.getStorage() ?: return false
|
||||
if (storage.isUsb) return true
|
||||
return storage.getDocumentFile(context).isDirectory
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,9 +80,12 @@ internal abstract class StorageViewModel(private val app: Application) : Android
|
|||
onLocationSet(uri)
|
||||
}
|
||||
|
||||
abstract fun onLocationSet(uri: Uri)
|
||||
|
||||
protected fun saveStorage(uri: Uri) {
|
||||
/**
|
||||
* Saves the storage behind the given [Uri] (and saved [storageRoot]).
|
||||
*
|
||||
* @return true if the storage is a USB flash drive, false otherwise.
|
||||
*/
|
||||
protected fun saveStorage(uri: Uri): Boolean {
|
||||
// store backup storage location in settings
|
||||
val root = storageRoot ?: throw IllegalStateException()
|
||||
val name = if (root.isInternal()) {
|
||||
|
@ -84,15 +93,47 @@ internal abstract class StorageViewModel(private val app: Application) : Android
|
|||
} else {
|
||||
root.title
|
||||
}
|
||||
val storage = Storage(uri, name, root.supportsEject)
|
||||
setStorage(app, storage)
|
||||
val storage = Storage(uri, name, root.isUsb)
|
||||
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
|
||||
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java))
|
||||
|
||||
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() {
|
||||
storageRootFetcher.setRemovableStorageListener(null)
|
||||
super.onCleared()
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
|
||||
<item
|
||||
android:id="@+id/action_backup"
|
||||
android:enabled="false"
|
||||
android:title="@string/settings_backup_now"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_restore"
|
||||
android:enabled="false"
|
||||
android:title="@string/restore_backup_button"
|
||||
android:visible="false"
|
||||
app:showAsAction="never"
|
||||
tools:visible="true" />
|
||||
|
||||
</menu>
|
||||
</menu>
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
<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_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_auto_restore_title">Automatic restore</string>
|
||||
<string name="settings_auto_restore_summary">When reinstalling an app, restore backed up settings and data</string>
|
||||
|
|
7
app/src/main/res/xml/device_filter.xml
Normal file
7
app/src/main/res/xml/device_filter.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<usb-device
|
||||
class="8"
|
||||
protocol="80"
|
||||
subclass="6" />
|
||||
</resources>
|
|
@ -43,14 +43,14 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||
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 kvRestorePlugin = mockk<KVRestorePlugin>()
|
||||
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
|
||||
private val fullRestorePlugin = mockk<FullRestorePlugin>()
|
||||
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 fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
|
||||
|
@ -91,6 +91,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
appData2.size
|
||||
}
|
||||
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
|
||||
every { settingsManager.saveNewBackupTime() } just Runs
|
||||
|
||||
// start and finish K/V backup
|
||||
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
|
||||
|
@ -130,6 +131,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
||||
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
||||
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
||||
every { settingsManager.saveNewBackupTime() } just Runs
|
||||
|
||||
// perform backup to output stream
|
||||
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.pm.PackageInfo
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.crypto.Crypto
|
||||
import com.stevesoltys.backup.settings.SettingsManager
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
@ -14,6 +15,7 @@ import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
|
|||
abstract class TransportTest {
|
||||
|
||||
protected val crypto = mockk<Crypto>()
|
||||
protected val settingsManager = mockk<SettingsManager>()
|
||||
protected val context = mockk<Context>(relaxed = true)
|
||||
|
||||
protected val packageInfo = PackageInfo().apply { packageName = "org.example" }
|
||||
|
|
|
@ -2,8 +2,12 @@ package com.stevesoltys.backup.transport.backup
|
|||
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
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.getRandomString
|
||||
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||
import com.stevesoltys.backup.settings.Storage
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -23,14 +27,15 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
private val metadataWriter = mockk<MetadataWriter>()
|
||||
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>()
|
||||
|
||||
@Test
|
||||
fun `device initialization succeeds and delegates to plugin`() {
|
||||
every { plugin.initializeDevice() } just Runs
|
||||
expectWritingMetadata(0L)
|
||||
every { settingsManager.getBackupToken() } returns token
|
||||
expectWritingMetadata(token)
|
||||
every { kv.hasState() } returns false
|
||||
every { full.hasState() } returns false
|
||||
|
||||
|
@ -39,8 +44,11 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
}
|
||||
|
||||
@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 { settingsManager.getStorage() } returns storage
|
||||
every { notificationManager.onBackupError() } just Runs
|
||||
|
||||
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
|
||||
fun `getBackupQuota() delegates to right plugin`() {
|
||||
val isFullBackup = Random.nextBoolean()
|
||||
|
|
|
@ -20,11 +20,6 @@ internal class FullBackupTest : BackupTest() {
|
|||
private val closeBytes = ByteArray(42).apply { Random.nextBytes(this) }
|
||||
private val inputStream = mockk<FileInputStream>()
|
||||
|
||||
@Test
|
||||
fun `now is a good time for a backup`() {
|
||||
assertEquals(0, backup.requestFullBackupTime())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `has no initial state`() {
|
||||
assertFalse(backup.hasState())
|
||||
|
|
|
@ -28,11 +28,6 @@ internal class KVBackupTest : BackupTest() {
|
|||
private val value = ByteArray(23).apply { Random.nextBytes(this) }
|
||||
private val versionHeader = VersionHeader(packageName = packageInfo.packageName, key = key)
|
||||
|
||||
@Test
|
||||
fun `now is a good time for a backup`() {
|
||||
assertEquals(0, backup.requestBackupTime())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `has no initial state`() {
|
||||
assertFalse(backup.hasState())
|
||||
|
|
|
@ -27,7 +27,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
private val full = mockk<FullRestore>()
|
||||
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 inputStream = mockk<InputStream>()
|
||||
|
@ -56,8 +56,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
@Test
|
||||
fun `getCurrentRestoreSet() delegates to plugin`() {
|
||||
// We don't mock the SettingsManager, so the default value is returned here
|
||||
assertEquals(0L, restore.getCurrentRestoreSet())
|
||||
every { settingsManager.getBackupToken() } returns token
|
||||
assertEquals(token, restore.getCurrentRestoreSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -2,5 +2,7 @@
|
|||
<permissions>
|
||||
<privapp-permissions package="com.stevesoltys.backup">
|
||||
<permission name="android.permission.BACKUP"/>
|
||||
<permission name="android.permission.MANAGE_USB"/>
|
||||
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
|
||||
</privapp-permissions>
|
||||
</permissions>
|
Loading…
Reference in a new issue