Save the time of the last backup and only do automatic flash drive backups once a day
This commit also turns SettingsManager into a class, so we can mock and later also inject it.
This commit is contained in:
parent
b0386c8b66
commit
007dd7759d
19 changed files with 196 additions and 146 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
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@ package com.stevesoltys.backup
|
|||
import android.app.Application
|
||||
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,6 +24,9 @@ class Backup : Application() {
|
|||
}
|
||||
}
|
||||
|
||||
val settingsManager by lazy {
|
||||
SettingsManager(this)
|
||||
}
|
||||
val notificationManager by lazy {
|
||||
BackupNotificationManager(this)
|
||||
}
|
||||
|
|
|
@ -13,9 +13,10 @@ import android.os.Handler
|
|||
import android.provider.DocumentsContract
|
||||
import android.util.Log
|
||||
import com.stevesoltys.backup.settings.FlashDrive
|
||||
import com.stevesoltys.backup.settings.getFlashDrive
|
||||
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
|
||||
|
||||
|
@ -28,12 +29,17 @@ class UsbIntentReceiver : BroadcastReceiver() {
|
|||
Log.d(TAG, "New USB mass-storage device attached.")
|
||||
device.log()
|
||||
|
||||
val savedFlashDrive = getFlashDrive(context) ?: return
|
||||
val settingsManager = (context.applicationContext as Backup).settingsManager
|
||||
val savedFlashDrive = settingsManager.getFlashDrive() ?: return
|
||||
val attachedFlashDrive = FlashDrive.from(device)
|
||||
if (savedFlashDrive == attachedFlashDrive) {
|
||||
Log.d(TAG, "Matches stored device, requesting backup...")
|
||||
// TODO only if last backup older than 24h
|
||||
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...")
|
||||
startBackupOnceMounted(context)
|
||||
} else {
|
||||
Log.d(TAG, "We have a recent backup, not requesting a new one.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,17 +72,10 @@ 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;
|
||||
}
|
||||
|
||||
void onChangeBackupLocationButtonClicked(Activity parent) {
|
||||
showChooseFolderActivity(parent, false);
|
||||
}
|
||||
|
|
|
@ -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,13 +68,8 @@ class CreateBackupActivityController {
|
|||
}
|
||||
|
||||
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) {
|
||||
String password = getBackupPassword(parent);
|
||||
if (password == null) {
|
||||
showEnterPasswordAlert(selectedPackages, parent);
|
||||
} else {
|
||||
backupService.backupPackageData(selectedPackages, parent);
|
||||
}
|
||||
}
|
||||
|
||||
private void showEnterPasswordAlert(Set<String> selectedPackages, Activity parent) {
|
||||
final EditText passwordTextView = new EditText(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()?.ejectable == 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)
|
||||
|
|
|
@ -6,6 +6,8 @@ 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
|
||||
import android.text.format.DateUtils.*
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
|
@ -18,6 +20,7 @@ import androidx.preference.TwoStatePreference
|
|||
import com.stevesoltys.backup.Backup
|
||||
import com.stevesoltys.backup.R
|
||||
import com.stevesoltys.backup.restore.RestoreActivity
|
||||
import java.util.*
|
||||
|
||||
private val TAG = SettingsFragment::class.java.name
|
||||
|
||||
|
@ -26,6 +29,7 @@ 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
|
||||
|
@ -36,6 +40,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
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,7 +79,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
super.onStart()
|
||||
|
||||
// we need to re-set the title when returning to this fragment
|
||||
requireActivity().setTitle(R.string.app_name)
|
||||
val activity = requireActivity()
|
||||
activity.setTitle(R.string.app_name)
|
||||
|
||||
try {
|
||||
backup.isChecked = backupManager.isBackupEnabled
|
||||
|
@ -84,12 +90,21 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
backup.isEnabled = false
|
||||
}
|
||||
|
||||
val resolver = requireContext().contentResolver
|
||||
val resolver = activity.contentResolver
|
||||
autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1
|
||||
|
||||
// TODO add time of last backup here
|
||||
val storageName = getStorage(requireContext())?.name
|
||||
backupLocation.summary = storageName ?: getString(R.string.settings_backup_location_none )
|
||||
// get name of storage location
|
||||
val storageName = settingsManager.getStorage()?.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)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
|
|
|
@ -17,8 +17,104 @@ 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_EJECTABLE, storage.ejectable)
|
||||
.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 ejectable = prefs.getBoolean(PREF_KEY_STORAGE_EJECTABLE, false)
|
||||
return Storage(uri, name, ejectable)
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -27,24 +123,6 @@ data class Storage(
|
|||
?: throw AssertionError("Should only happen on API < 21.")
|
||||
}
|
||||
|
||||
fun setStorage(context: Context, storage: Storage) {
|
||||
PreferenceManager.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? {
|
||||
val prefs = PreferenceManager.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)
|
||||
}
|
||||
|
||||
data class FlashDrive(
|
||||
val name: String,
|
||||
val serialNumber: String?,
|
||||
|
@ -59,55 +137,3 @@ data class FlashDrive(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFlashDrive(context: Context, usb: FlashDrive?) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
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(context: Context): FlashDrive? {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
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(context: Context): Long = Date().time.apply {
|
||||
PreferenceManager.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 PreferenceManager.getDefaultSharedPreferences(context).getLong(PREF_KEY_BACKUP_TOKEN, 0L)
|
||||
}
|
||||
|
||||
@Deprecated("Replaced by KeyManager#getBackupKey()")
|
||||
fun getBackupPassword(context: Context): String? {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -8,8 +8,7 @@ import android.os.ParcelFileDescriptor
|
|||
import android.util.Log
|
||||
import com.stevesoltys.backup.BackupNotificationManager
|
||||
import com.stevesoltys.backup.metadata.MetadataWriter
|
||||
import com.stevesoltys.backup.settings.getBackupToken
|
||||
import com.stevesoltys.backup.settings.getStorage
|
||||
import com.stevesoltys.backup.settings.SettingsManager
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
|
||||
|
@ -25,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
|
||||
|
@ -56,7 +56,7 @@ 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
|
||||
|
@ -100,8 +100,11 @@ class BackupCoordinator(
|
|||
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 {
|
||||
val result = kv.performBackup(packageInfo, data, flags)
|
||||
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
|
||||
return result
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Full backup
|
||||
|
@ -126,8 +129,11 @@ class BackupCoordinator(
|
|||
|
||||
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)
|
||||
|
||||
|
@ -191,7 +197,7 @@ class BackupCoordinator(
|
|||
val defaultBackoff = MINUTES.toMillis(10)
|
||||
|
||||
// back off if there's no storage set
|
||||
val storage = getStorage(context) ?: return defaultBackoff
|
||||
val storage = settingsManager.getStorage() ?: return defaultBackoff
|
||||
// don't back off if storage is not ejectable or available right now
|
||||
return if (!storage.ejectable || storage.getDocumentFile(context).isDirectory) noBackoff
|
||||
// otherwise back off
|
||||
|
|
|
@ -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,7 +19,10 @@ 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 parent = storage?.getDocumentFile(context) ?: return@lazy null
|
||||
|
@ -36,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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,17 +1,14 @@
|
|||
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
|
||||
|
||||
private val TAG = BackupStorageViewModel::class.java.simpleName
|
||||
|
@ -24,7 +21,7 @@ internal class BackupStorageViewModel(private val app: Application) : StorageVie
|
|||
saveStorage(uri)
|
||||
|
||||
// use a new backup token
|
||||
getAndSaveNewBackupToken(app)
|
||||
settingsManager.getAndSaveNewBackupToken()
|
||||
|
||||
// initialize the new location
|
||||
val observer = InitializationObserver()
|
||||
|
|
|
@ -12,9 +12,11 @@ import android.util.Log
|
|||
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.*
|
||||
import com.stevesoltys.backup.settings.FlashDrive
|
||||
import com.stevesoltys.backup.settings.Storage
|
||||
import com.stevesoltys.backup.transport.ConfigurableBackupTransportService
|
||||
import com.stevesoltys.backup.ui.LiveEvent
|
||||
import com.stevesoltys.backup.ui.MutableLiveEvent
|
||||
|
@ -23,6 +25,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
|
||||
|
||||
|
@ -39,7 +43,8 @@ internal abstract class StorageViewModel(private val app: Application) : Android
|
|||
|
||||
companion object {
|
||||
internal fun validLocationIsSet(context: Context): Boolean {
|
||||
val storage = getStorage(context) ?: return false
|
||||
val settingsManager = (context.applicationContext as Backup).settingsManager
|
||||
val storage = settingsManager.getStorage() ?: return false
|
||||
if (storage.ejectable) return true
|
||||
return storage.getDocumentFile(context).isDirectory
|
||||
}
|
||||
|
@ -84,12 +89,15 @@ internal abstract class StorageViewModel(private val app: Application) : Android
|
|||
root.title
|
||||
}
|
||||
val storage = Storage(uri, name, root.supportsEject)
|
||||
setStorage(app, storage)
|
||||
settingsManager.setStorage(storage)
|
||||
|
||||
// reset time of last backup to "Never"
|
||||
settingsManager.resetBackupTime()
|
||||
|
||||
if (storage.ejectable) {
|
||||
val wasSaved = saveUsbDevice()
|
||||
// reset stored flash drive, if we did not update it
|
||||
if (!wasSaved) setFlashDrive(app, null)
|
||||
if (!wasSaved) settingsManager.setFlashDrive(null)
|
||||
}
|
||||
|
||||
// stop backup service to be sure the old location will get updated
|
||||
|
@ -102,7 +110,7 @@ internal abstract class StorageViewModel(private val app: Application) : Android
|
|||
val manager = app.getSystemService(USB_SERVICE) as UsbManager
|
||||
manager.deviceList.values.forEach { device ->
|
||||
if (device.isMassStorage()) {
|
||||
setFlashDrive(app, FlashDrive.from(device))
|
||||
settingsManager.setFlashDrive(FlashDrive.from(device))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -23,14 +23,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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue