Do not allow manual backup/restore operations when removable storage is not available
This commit is contained in:
parent
cc2bb4a651
commit
08018fcc9b
4 changed files with 133 additions and 52 deletions
|
@ -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,27 +19,33 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -49,19 +54,34 @@ class UsbIntentReceiver : BroadcastReceiver() {
|
||||||
* 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() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val action = intent.action ?: return
|
||||||
|
if (intent.action == ACTION_USB_DEVICE_ATTACHED || intent.action == ACTION_USB_DEVICE_DETACHED) {
|
||||||
|
val device = intent.extras?.getParcelable<UsbDevice>(EXTRA_DEVICE) ?: return
|
||||||
|
Log.d(TAG, "New USB mass-storage device attached.")
|
||||||
|
device.log()
|
||||||
|
|
||||||
|
if (!shouldMonitorStatus(context, action, device)) return
|
||||||
|
|
||||||
val rootsUri = DocumentsContract.buildRootsUri(AUTHORITY_STORAGE)
|
val rootsUri = DocumentsContract.buildRootsUri(AUTHORITY_STORAGE)
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
val observer = object : ContentObserver(Handler()) {
|
val observer = object : ContentObserver(Handler()) {
|
||||||
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
override fun onChange(selfChange: Boolean, uri: Uri?) {
|
||||||
super.onChange(selfChange, uri)
|
super.onChange(selfChange, uri)
|
||||||
Thread {
|
onStatusChanged(context, action, device)
|
||||||
requestBackup(context)
|
|
||||||
}.start()
|
|
||||||
contentResolver.unregisterContentObserver(this)
|
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 {
|
||||||
|
|
|
@ -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
|
|
||||||
|
if (storage?.isUsb == true) context?.registerReceiver(usbReceiver, usbFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
val resolver = activity.contentResolver
|
override fun onStop() {
|
||||||
autoRestore.isChecked = Settings.Secure.getInt(resolver, BACKUP_AUTO_RESTORE, 1) == 1
|
super.onStop()
|
||||||
|
if (storage?.isUsb == true) context?.unregisterReceiver(usbReceiver)
|
||||||
// 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) {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
<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"
|
||||||
|
|
Loading…
Reference in a new issue