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:
Torsten Grote 2019-09-19 11:17:16 -03:00
parent b0386c8b66
commit 007dd7759d
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
19 changed files with 196 additions and 146 deletions

View file

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

View file

@ -3,12 +3,11 @@ package com.stevesoltys.backup
import android.app.Application import android.app.Application
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.Context.BACKUP_SERVICE import android.content.Context.BACKUP_SERVICE
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.ServiceManager.getService import android.os.ServiceManager.getService
import com.stevesoltys.backup.crypto.KeyManager import com.stevesoltys.backup.crypto.KeyManager
import com.stevesoltys.backup.crypto.KeyManagerImpl import com.stevesoltys.backup.crypto.KeyManagerImpl
import com.stevesoltys.backup.ui.storage.AUTHORITY_STORAGE import com.stevesoltys.backup.settings.SettingsManager
/** /**
* @author Steve Soltys * @author Steve Soltys
@ -25,6 +24,9 @@ class Backup : Application() {
} }
} }
val settingsManager by lazy {
SettingsManager(this)
}
val notificationManager by lazy { val notificationManager by lazy {
BackupNotificationManager(this) BackupNotificationManager(this)
} }

View file

@ -13,9 +13,10 @@ import android.os.Handler
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.util.Log import android.util.Log
import com.stevesoltys.backup.settings.FlashDrive import com.stevesoltys.backup.settings.FlashDrive
import com.stevesoltys.backup.settings.getFlashDrive
import com.stevesoltys.backup.transport.requestBackup import com.stevesoltys.backup.transport.requestBackup
import com.stevesoltys.backup.ui.storage.AUTHORITY_STORAGE import com.stevesoltys.backup.ui.storage.AUTHORITY_STORAGE
import java.util.*
import java.util.concurrent.TimeUnit.HOURS
private val TAG = UsbIntentReceiver::class.java.simpleName private val TAG = UsbIntentReceiver::class.java.simpleName
@ -28,12 +29,17 @@ class UsbIntentReceiver : BroadcastReceiver() {
Log.d(TAG, "New USB mass-storage device attached.") Log.d(TAG, "New USB mass-storage device attached.")
device.log() device.log()
val savedFlashDrive = getFlashDrive(context) ?: return val settingsManager = (context.applicationContext as Backup).settingsManager
val savedFlashDrive = settingsManager.getFlashDrive() ?: return
val attachedFlashDrive = FlashDrive.from(device) val attachedFlashDrive = FlashDrive.from(device)
if (savedFlashDrive == attachedFlashDrive) { if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, requesting backup...") Log.d(TAG, "Matches stored device, checking backup time...")
// TODO only if last backup older than 24h if (Date().time - settingsManager.getBackupTime() >= HOURS.toMillis(24)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
startBackupOnceMounted(context) startBackupOnceMounted(context)
} else {
Log.d(TAG, "We have a recent backup, not requesting a new one.")
}
} }
} }

View file

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

View file

@ -21,8 +21,6 @@ import com.stevesoltys.backup.service.backup.BackupService;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static com.stevesoltys.backup.settings.SettingsManagerKt.getBackupPassword;
/** /**
* @author Steve Soltys * @author Steve Soltys
*/ */
@ -70,13 +68,8 @@ class CreateBackupActivityController {
} }
void onCreateBackupButtonClicked(Set<String> selectedPackages, Activity parent) { 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) { private void showEnterPasswordAlert(Set<String> selectedPackages, Activity parent) {
final EditText passwordTextView = new EditText(parent); final EditText passwordTextView = new EditText(parent);

View file

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

View file

@ -6,6 +6,8 @@ import android.os.Bundle
import android.os.RemoteException import android.os.RemoteException
import android.provider.Settings import android.provider.Settings
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
import android.text.format.DateUtils
import android.text.format.DateUtils.*
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -18,6 +20,7 @@ import androidx.preference.TwoStatePreference
import com.stevesoltys.backup.Backup import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.restore.RestoreActivity import com.stevesoltys.backup.restore.RestoreActivity
import java.util.*
private val TAG = SettingsFragment::class.java.name private val TAG = SettingsFragment::class.java.name
@ -26,6 +29,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private val backupManager = Backup.backupManager private val backupManager = Backup.backupManager
private lateinit var viewModel: SettingsViewModel private lateinit var viewModel: SettingsViewModel
private lateinit var settingsManager: SettingsManager
private lateinit var backup: TwoStatePreference private lateinit var backup: TwoStatePreference
private lateinit var autoRestore: TwoStatePreference private lateinit var autoRestore: TwoStatePreference
@ -36,6 +40,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
setHasOptionsMenu(true) setHasOptionsMenu(true)
viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java) viewModel = ViewModelProviders.of(requireActivity()).get(SettingsViewModel::class.java)
settingsManager = (requireContext().applicationContext as Backup).settingsManager
backup = findPreference<TwoStatePreference>("backup")!! backup = findPreference<TwoStatePreference>("backup")!!
backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue -> backup.onPreferenceChangeListener = OnPreferenceChangeListener { _, newValue ->
@ -74,7 +79,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
super.onStart() super.onStart()
// we need to re-set the title when returning to this fragment // we need to re-set the title when returning to this fragment
requireActivity().setTitle(R.string.app_name) val activity = requireActivity()
activity.setTitle(R.string.app_name)
try { try {
backup.isChecked = backupManager.isBackupEnabled backup.isChecked = backupManager.isBackupEnabled
@ -84,12 +90,21 @@ class SettingsFragment : PreferenceFragmentCompat() {
backup.isEnabled = false backup.isEnabled = false
} }
val resolver = requireContext().contentResolver val resolver = activity.contentResolver
autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1 autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1
// TODO add time of last backup here // get name of storage location
val storageName = getStorage(requireContext())?.name val storageName = settingsManager.getStorage()?.name
backupLocation.summary = storageName ?: getString(R.string.settings_backup_location_none ) ?: 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) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {

View file

@ -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_FLASH_DRIVE_PRODUCT_ID = "flashDriveProductId"
private const val PREF_KEY_BACKUP_TOKEN = "backupToken" private const val PREF_KEY_BACKUP_TOKEN = "backupToken"
private const val PREF_KEY_BACKUP_TIME = "backupTime"
private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword" private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"
class SettingsManager(context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
fun setStorage(storage: Storage) {
prefs.edit()
.putString(PREF_KEY_STORAGE_URI, storage.uri.toString())
.putString(PREF_KEY_STORAGE_NAME, storage.name)
.putBoolean(PREF_KEY_STORAGE_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( data class Storage(
val uri: Uri, val uri: Uri,
val name: String, val name: String,
@ -27,24 +123,6 @@ data class Storage(
?: throw AssertionError("Should only happen on API < 21.") ?: 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( data class FlashDrive(
val name: String, val name: String,
val serialNumber: 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)
}

View file

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

View file

@ -8,8 +8,7 @@ import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.backup.BackupNotificationManager import com.stevesoltys.backup.BackupNotificationManager
import com.stevesoltys.backup.metadata.MetadataWriter import com.stevesoltys.backup.metadata.MetadataWriter
import com.stevesoltys.backup.settings.getBackupToken import com.stevesoltys.backup.settings.SettingsManager
import com.stevesoltys.backup.settings.getStorage
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.MINUTES
@ -25,6 +24,7 @@ class BackupCoordinator(
private val kv: KVBackup, private val kv: KVBackup,
private val full: FullBackup, private val full: FullBackup,
private val metadataWriter: MetadataWriter, private val metadataWriter: MetadataWriter,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager) { private val nm: BackupNotificationManager) {
private var calledInitialize = false private var calledInitialize = false
@ -56,7 +56,7 @@ class BackupCoordinator(
Log.i(TAG, "Initialize Device!") Log.i(TAG, "Initialize Device!")
return try { return try {
plugin.initializeDevice() plugin.initializeDevice()
writeBackupMetadata(getBackupToken(context)) writeBackupMetadata(settingsManager.getBackupToken())
// [finishBackup] will only be called when we return [TRANSPORT_OK] here // [finishBackup] will only be called when we return [TRANSPORT_OK] here
// so we remember that we initialized successfully // so we remember that we initialized successfully
calledInitialize = true calledInitialize = true
@ -100,8 +100,11 @@ class BackupCoordinator(
Log.i(TAG, "Request incremental backup time. Returned $this") Log.i(TAG, "Request incremental backup time. Returned $this")
} }
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int) = fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
kv.performBackup(packageInfo, data, flags) val result = kv.performBackup(packageInfo, data, flags)
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
return result
}
// ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------
// Full backup // Full backup
@ -126,8 +129,11 @@ class BackupCoordinator(
fun checkFullBackupSize(size: Long) = full.checkFullBackupSize(size) fun checkFullBackupSize(size: Long) = full.checkFullBackupSize(size)
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int) = fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
full.performFullBackup(targetPackage, fileDescriptor, flags) val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
if (result == TRANSPORT_OK) settingsManager.saveNewBackupTime()
return result
}
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes) fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
@ -191,7 +197,7 @@ class BackupCoordinator(
val defaultBackoff = MINUTES.toMillis(10) val defaultBackoff = MINUTES.toMillis(10)
// back off if there's no storage set // 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 // don't back off if storage is not ejectable or available right now
return if (!storage.ejectable || storage.getDocumentFile(context).isDirectory) noBackoff return if (!storage.ejectable || storage.getDocumentFile(context).isDirectory) noBackoff
// otherwise back off // otherwise back off

View file

@ -4,8 +4,8 @@ import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.backup.settings.SettingsManager
import com.stevesoltys.backup.settings.Storage import com.stevesoltys.backup.settings.Storage
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -19,7 +19,10 @@ private const val MIME_TYPE = "application/octet-stream"
private val TAG = DocumentsStorage::class.java.simpleName private val TAG = DocumentsStorage::class.java.simpleName
class DocumentsStorage(private val context: Context, storage: Storage?, token: Long) { class DocumentsStorage(private val context: Context, private val settingsManager: SettingsManager) {
private val storage: Storage? = settingsManager.getStorage()
private val token: Long = settingsManager.getBackupToken()
internal val rootBackupDir: DocumentFile? by lazy { internal val rootBackupDir: DocumentFile? by lazy {
val parent = storage?.getDocumentFile(context) ?: return@lazy null 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 { private val currentToken: Long by lazy {
if (token != 0L) token if (token != 0L) token
else getAndSaveNewBackupToken(context).apply { else settingsManager.getAndSaveNewBackupToken().apply {
Log.d(TAG, "Using a fresh backup token: $this") Log.d(TAG, "Using a fresh backup token: $this")
} }
} }

View file

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

View file

@ -1,17 +1,14 @@
package com.stevesoltys.backup.ui.storage package com.stevesoltys.backup.ui.storage
import android.app.ActivityManager
import android.app.Application import android.app.Application
import android.app.backup.BackupProgress import android.app.backup.BackupProgress
import android.app.backup.IBackupObserver import android.app.backup.IBackupObserver
import android.net.Uri import android.net.Uri
import android.os.UserHandle import android.os.UserHandle
import android.os.UserManager
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.backup.Backup import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.settings.getAndSaveNewBackupToken
import com.stevesoltys.backup.transport.TRANSPORT_ID import com.stevesoltys.backup.transport.TRANSPORT_ID
private val TAG = BackupStorageViewModel::class.java.simpleName private val TAG = BackupStorageViewModel::class.java.simpleName
@ -24,7 +21,7 @@ internal class BackupStorageViewModel(private val app: Application) : StorageVie
saveStorage(uri) saveStorage(uri)
// use a new backup token // use a new backup token
getAndSaveNewBackupToken(app) settingsManager.getAndSaveNewBackupToken()
// initialize the new location // initialize the new location
val observer = InitializationObserver() val observer = InitializationObserver()

View file

@ -12,9 +12,11 @@ import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.isMassStorage import com.stevesoltys.backup.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.transport.ConfigurableBackupTransportService
import com.stevesoltys.backup.ui.LiveEvent import com.stevesoltys.backup.ui.LiveEvent
import com.stevesoltys.backup.ui.MutableLiveEvent import com.stevesoltys.backup.ui.MutableLiveEvent
@ -23,6 +25,8 @@ private val TAG = StorageViewModel::class.java.simpleName
internal abstract class StorageViewModel(private val app: Application) : AndroidViewModel(app), RemovableStorageListener { internal abstract class StorageViewModel(private val app: Application) : AndroidViewModel(app), RemovableStorageListener {
protected val settingsManager = (app as Backup).settingsManager
private val mStorageRoots = MutableLiveData<List<StorageRoot>>() private val mStorageRoots = MutableLiveData<List<StorageRoot>>()
internal val storageRoots: LiveData<List<StorageRoot>> get() = mStorageRoots internal val storageRoots: LiveData<List<StorageRoot>> get() = mStorageRoots
@ -39,7 +43,8 @@ internal abstract class StorageViewModel(private val app: Application) : Android
companion object { companion object {
internal fun validLocationIsSet(context: Context): Boolean { internal fun validLocationIsSet(context: Context): Boolean {
val storage = getStorage(context) ?: return false val settingsManager = (context.applicationContext as Backup).settingsManager
val storage = settingsManager.getStorage() ?: return false
if (storage.ejectable) return true if (storage.ejectable) return true
return storage.getDocumentFile(context).isDirectory return storage.getDocumentFile(context).isDirectory
} }
@ -84,12 +89,15 @@ internal abstract class StorageViewModel(private val app: Application) : Android
root.title root.title
} }
val storage = Storage(uri, name, root.supportsEject) 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) { if (storage.ejectable) {
val wasSaved = saveUsbDevice() val wasSaved = saveUsbDevice()
// reset stored flash drive, if we did not update it // 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 // 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 val manager = app.getSystemService(USB_SERVICE) as UsbManager
manager.deviceList.values.forEach { device -> manager.deviceList.values.forEach { device ->
if (device.isMassStorage()) { if (device.isMassStorage()) {
setFlashDrive(app, FlashDrive.from(device)) settingsManager.setFlashDrive(FlashDrive.from(device))
return true return true
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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