From 08018fcc9bafa514201dbfcffa3512748637c540 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 19 Sep 2019 18:06:02 -0300 Subject: [PATCH] Do not allow manual backup/restore operations when removable storage is not available --- .../stevesoltys/backup/UsbIntentReceiver.kt | 68 +++++++---- .../backup/settings/SettingsFragment.kt | 108 +++++++++++++----- .../backup/ui/storage/StorageViewModel.kt | 5 +- app/src/main/res/menu/settings_menu.xml | 4 +- 4 files changed, 133 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/backup/UsbIntentReceiver.kt b/app/src/main/java/com/stevesoltys/backup/UsbIntentReceiver.kt index a9b94ebc..b4daca68 100644 --- a/app/src/main/java/com/stevesoltys/backup/UsbIntentReceiver.kt +++ b/app/src/main/java/com/stevesoltys/backup/UsbIntentReceiver.kt @@ -6,8 +6,7 @@ import android.content.Intent import android.database.ContentObserver import android.hardware.usb.UsbDevice import android.hardware.usb.UsbInterface -import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_ATTACHED -import android.hardware.usb.UsbManager.EXTRA_DEVICE +import android.hardware.usb.UsbManager.* import android.net.Uri import android.os.Handler import android.provider.DocumentsContract @@ -20,48 +19,69 @@ import java.util.concurrent.TimeUnit.HOURS private val TAG = UsbIntentReceiver::class.java.simpleName -class UsbIntentReceiver : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != ACTION_USB_DEVICE_ATTACHED) return - - val device = intent.extras?.getParcelable(EXTRA_DEVICE) ?: return - Log.d(TAG, "New USB mass-storage device attached.") - device.log() +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 + val savedFlashDrive = settingsManager.getFlashDrive() ?: return false val attachedFlashDrive = FlashDrive.from(device) - if (savedFlashDrive == attachedFlashDrive) { + 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...") - startBackupOnceMounted(context) + 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. */ -private fun startBackupOnceMounted(context: Context) { - 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) - Thread { - requestBackup(context) - }.start() - contentResolver.unregisterContentObserver(this) +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(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) } } - 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 { diff --git a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt index 7842670d..af525c52 100644 --- a/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt +++ b/app/src/main/java/com/stevesoltys/backup/settings/SettingsFragment.kt @@ -1,13 +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 -import android.text.format.DateUtils.* +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 @@ -19,6 +24,8 @@ 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.* @@ -35,6 +42,23 @@ class SettingsFragment : PreferenceFragmentCompat() { 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) @@ -79,40 +103,31 @@ class SettingsFragment : PreferenceFragmentCompat() { super.onStart() // we need to re-set the title when returning to this fragment - val activity = requireActivity() - activity.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 = activity.contentResolver - autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1 + if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter) + } - // 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 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 { @@ -127,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 + } + } + } diff --git a/app/src/main/java/com/stevesoltys/backup/ui/storage/StorageViewModel.kt b/app/src/main/java/com/stevesoltys/backup/ui/storage/StorageViewModel.kt index 2b01336c..fc6ad663 100644 --- a/app/src/main/java/com/stevesoltys/backup/ui/storage/StorageViewModel.kt +++ b/app/src/main/java/com/stevesoltys/backup/ui/storage/StorageViewModel.kt @@ -100,6 +100,7 @@ internal abstract class StorageViewModel(private val app: Application) : Android 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) @@ -121,7 +122,9 @@ 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()) { - settingsManager.setFlashDrive(FlashDrive.from(device)) + val flashDrive = FlashDrive.from(device) + settingsManager.setFlashDrive(flashDrive) + Log.d(TAG, "Saved flash drive: $flashDrive") return true } } diff --git a/app/src/main/res/menu/settings_menu.xml b/app/src/main/res/menu/settings_menu.xml index 7a055c11..71b7d5fc 100644 --- a/app/src/main/res/menu/settings_menu.xml +++ b/app/src/main/res/menu/settings_menu.xml @@ -5,14 +5,16 @@ - \ No newline at end of file +