Do not allow manual backup/restore operations when removable storage is not available

This commit is contained in:
Torsten Grote 2019-09-19 18:06:02 -03:00
parent cc2bb4a651
commit 08018fcc9b
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
4 changed files with 133 additions and 52 deletions

View file

@ -6,8 +6,7 @@ import android.content.Intent
import android.database.ContentObserver import android.database.ContentObserver
import android.hardware.usb.UsbDevice import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbInterface import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_ATTACHED import android.hardware.usb.UsbManager.*
import android.hardware.usb.UsbManager.EXTRA_DEVICE
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
import android.provider.DocumentsContract import android.provider.DocumentsContract
@ -20,48 +19,69 @@ import java.util.concurrent.TimeUnit.HOURS
private val TAG = UsbIntentReceiver::class.java.simpleName private val TAG = UsbIntentReceiver::class.java.simpleName
class UsbIntentReceiver : BroadcastReceiver() { class UsbIntentReceiver : UsbMonitor() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION_USB_DEVICE_ATTACHED) return
val device = intent.extras?.getParcelable<UsbDevice>(EXTRA_DEVICE) ?: return
Log.d(TAG, "New USB mass-storage device attached.")
device.log()
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 settingsManager = (context.applicationContext as Backup).settingsManager
val savedFlashDrive = settingsManager.getFlashDrive() ?: return val savedFlashDrive = settingsManager.getFlashDrive() ?: return false
val attachedFlashDrive = FlashDrive.from(device) val attachedFlashDrive = FlashDrive.from(device)
if (savedFlashDrive == attachedFlashDrive) { return if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, checking backup time...") Log.d(TAG, "Matches stored device, checking backup time...")
if (Date().time - settingsManager.getBackupTime() >= HOURS.toMillis(24)) { if (Date().time - settingsManager.getBackupTime() >= HOURS.toMillis(24)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...") Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
startBackupOnceMounted(context) true
} else { } else {
Log.d(TAG, "We have a recent backup, not requesting a new one.") 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. * 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. * So we need to use a ContentObserver to request a backup only once available.
*/ */
private fun startBackupOnceMounted(context: Context) { abstract class UsbMonitor : BroadcastReceiver() {
val rootsUri = DocumentsContract.buildRootsUri(AUTHORITY_STORAGE)
val contentResolver = context.contentResolver override fun onReceive(context: Context, intent: Intent) {
val observer = object : ContentObserver(Handler()) { val action = intent.action ?: return
override fun onChange(selfChange: Boolean, uri: Uri?) { if (intent.action == ACTION_USB_DEVICE_ATTACHED || intent.action == ACTION_USB_DEVICE_DETACHED) {
super.onChange(selfChange, uri) val device = intent.extras?.getParcelable<UsbDevice>(EXTRA_DEVICE) ?: return
Thread { Log.d(TAG, "New USB mass-storage device attached.")
requestBackup(context) device.log()
}.start()
contentResolver.unregisterContentObserver(this) 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 { internal fun UsbDevice.isMassStorage(): Boolean {

View file

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

View file

@ -100,6 +100,7 @@ internal abstract class StorageViewModel(private val app: Application) : Android
settingsManager.resetBackupTime() settingsManager.resetBackupTime()
if (storage.isUsb) { if (storage.isUsb) {
Log.d(TAG, "Selected storage is a removable USB device.")
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) settingsManager.setFlashDrive(null) 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 val manager = app.getSystemService(USB_SERVICE) as UsbManager
manager.deviceList.values.forEach { device -> manager.deviceList.values.forEach { device ->
if (device.isMassStorage()) { 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 return true
} }
} }

View file

@ -5,14 +5,16 @@
<item <item
android:id="@+id/action_backup" android:id="@+id/action_backup"
android:enabled="false"
android:title="@string/settings_backup_now" android:title="@string/settings_backup_now"
app:showAsAction="never" /> app:showAsAction="never" />
<item <item
android:id="@+id/action_restore" android:id="@+id/action_restore"
android:enabled="false"
android:title="@string/restore_backup_button" android:title="@string/restore_backup_button"
android:visible="false" android:visible="false"
app:showAsAction="never" app:showAsAction="never"
tools:visible="true" /> tools:visible="true" />
</menu> </menu>