Start backup automatically when flash drive used for backup is plugged in

This commit is contained in:
Torsten Grote 2019-09-18 09:23:46 -03:00
parent 650642068e
commit b0386c8b66
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
9 changed files with 183 additions and 20 deletions

View file

@ -72,5 +72,16 @@
</intent-filter> </intent-filter>
</service> </service>
<receiver
android:name=".UsbIntentReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</receiver>
</application> </application>
</manifest> </manifest>

View file

@ -0,0 +1,78 @@
package com.stevesoltys.backup
import android.content.BroadcastReceiver
import android.content.Context
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.net.Uri
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
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<UsbDevice>(EXTRA_DEVICE) ?: return
Log.d(TAG, "New USB mass-storage device attached.")
device.log()
val savedFlashDrive = getFlashDrive(context) ?: 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
startBackupOnceMounted(context)
}
}
}
/**
* 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)
}
}
contentResolver.registerContentObserver(rootsUri, true, observer)
}
internal fun UsbDevice.isMassStorage(): Boolean {
for (i in 0 until interfaceCount) {
if (getInterface(i).isMassStorage()) return true
}
return false
}
private fun UsbInterface.isMassStorage(): Boolean {
return interfaceClass == 8 && interfaceProtocol == 80 && interfaceSubclass == 6
}
private fun UsbDevice.log() {
Log.d(TAG, " name: $manufacturerName $productName")
// Log.d(TAG, " serialNumber: $serialNumber") // requires a permission since API 29
Log.d(TAG, " productId: $productId")
Log.d(TAG, " vendorId: $vendorId")
Log.d(TAG, " isMassStorage: ${isMassStorage()}")
}

View file

@ -1,14 +1,21 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.content.Context import android.content.Context
import android.hardware.usb.UsbDevice
import android.net.Uri import android.net.Uri
import android.preference.PreferenceManager.getDefaultSharedPreferences
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import java.util.* import java.util.*
private const val PREF_KEY_STORAGE_URI = "storageUri" private const val PREF_KEY_STORAGE_URI = "storageUri"
private const val PREF_KEY_STORAGE_NAME = "storageName" private const val PREF_KEY_STORAGE_NAME = "storageName"
private const val PREF_KEY_STORAGE_EJECTABLE = "storageEjectable" private const val PREF_KEY_STORAGE_EJECTABLE = "storageEjectable"
private const val PREF_KEY_FLASH_DRIVE_NAME = "flashDriveName"
private const val PREF_KEY_FLASH_DRIVE_SERIAL_NUMBER = "flashSerialNumber"
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_TOKEN = "backupToken"
private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword" private const val PREF_KEY_BACKUP_PASSWORD = "backupLegacyPassword"
@ -21,7 +28,7 @@ data class Storage(
} }
fun setStorage(context: Context, storage: Storage) { fun setStorage(context: Context, storage: Storage) {
getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
.edit() .edit()
.putString(PREF_KEY_STORAGE_URI, storage.uri.toString()) .putString(PREF_KEY_STORAGE_URI, storage.uri.toString())
.putString(PREF_KEY_STORAGE_NAME, storage.name) .putString(PREF_KEY_STORAGE_NAME, storage.name)
@ -30,7 +37,7 @@ fun setStorage(context: Context, storage: Storage) {
} }
fun getStorage(context: Context): Storage? { fun getStorage(context: Context): Storage? {
val prefs = getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null val uriStr = prefs.getString(PREF_KEY_STORAGE_URI, null) ?: return null
val uri = Uri.parse(uriStr) val uri = Uri.parse(uriStr)
val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) ?: throw IllegalStateException() val name = prefs.getString(PREF_KEY_STORAGE_NAME, null) ?: throw IllegalStateException()
@ -38,12 +45,56 @@ fun getStorage(context: Context): Storage? {
return Storage(uri, name, ejectable) return Storage(uri, name, ejectable)
} }
data class FlashDrive(
val name: String,
val serialNumber: String?,
val vendorId: Int,
val productId: Int) {
companion object {
fun from(device: UsbDevice) = FlashDrive(
name = "${device.manufacturerName} ${device.productName}",
serialNumber = "", // device.serialNumber requires a permission since API 29
vendorId = device.vendorId,
productId = device.productId
)
}
}
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. * Generates and returns a new backup token while saving it as well.
* Subsequent calls to [getBackupToken] will return this new token once saved. * Subsequent calls to [getBackupToken] will return this new token once saved.
*/ */
fun getAndSaveNewBackupToken(context: Context): Long = Date().time.apply { fun getAndSaveNewBackupToken(context: Context): Long = Date().time.apply {
getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
.edit() .edit()
.putLong(PREF_KEY_BACKUP_TOKEN, this) .putLong(PREF_KEY_BACKUP_TOKEN, this)
.apply() .apply()
@ -53,10 +104,10 @@ fun getAndSaveNewBackupToken(context: Context): Long = Date().time.apply {
* Returns the current backup token or 0 if none exists. * Returns the current backup token or 0 if none exists.
*/ */
fun getBackupToken(context: Context): Long { fun getBackupToken(context: Context): Long {
return getDefaultSharedPreferences(context).getLong(PREF_KEY_BACKUP_TOKEN, 0L) return PreferenceManager.getDefaultSharedPreferences(context).getLong(PREF_KEY_BACKUP_TOKEN, 0L)
} }
@Deprecated("Replaced by KeyManager#getBackupKey()") @Deprecated("Replaced by KeyManager#getBackupKey()")
fun getBackupPassword(context: Context): String? { fun getBackupPassword(context: Context): String? {
return getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null) return PreferenceManager.getDefaultSharedPreferences(context).getString(PREF_KEY_BACKUP_PASSWORD, null)
} }

View file

@ -1,20 +1,14 @@
package com.stevesoltys.backup.settings package com.stevesoltys.backup.settings
import android.app.Application import android.app.Application
import com.stevesoltys.backup.Backup
import com.stevesoltys.backup.R
import com.stevesoltys.backup.transport.requestBackup import com.stevesoltys.backup.transport.requestBackup
import com.stevesoltys.backup.ui.RequireProvisioningViewModel import com.stevesoltys.backup.ui.RequireProvisioningViewModel
private val TAG = SettingsViewModel::class.java.simpleName
class SettingsViewModel(app: Application) : RequireProvisioningViewModel(app) { class SettingsViewModel(app: Application) : RequireProvisioningViewModel(app) {
override val isRestoreOperation = false override val isRestoreOperation = false
fun backupNow() { fun backupNow() {
val nm = (app as Backup).notificationManager
nm.onBackupUpdate(app.getString(R.string.notification_backup_starting), 0, 1, true)
Thread { requestBackup(app) }.start() Thread { requestBackup(app) }.start()
} }

View file

@ -7,7 +7,6 @@ import android.app.backup.RestoreSet
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.Build.VERSION.SDK_INT
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.backup.settings.SettingsActivity import com.stevesoltys.backup.settings.SettingsActivity
@ -36,7 +35,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
} }
override fun getTransportFlags(): Int { override fun getTransportFlags(): Int {
return if (SDK_INT >= 28) FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED else 0 return FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED
} }
override fun dataManagementIntent(): Intent { override fun dataManagementIntent(): Intent {

View file

@ -14,6 +14,7 @@ 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.NotificationBackupObserver import com.stevesoltys.backup.NotificationBackupObserver
import com.stevesoltys.backup.R
import com.stevesoltys.backup.service.PackageService import com.stevesoltys.backup.service.PackageService
import com.stevesoltys.backup.session.backup.BackupMonitor import com.stevesoltys.backup.session.backup.BackupMonitor
@ -50,7 +51,10 @@ class ConfigurableBackupTransportService : Service() {
@WorkerThread @WorkerThread
fun requestBackup(context: Context) { fun requestBackup(context: Context) {
context.startService(Intent(context, ConfigurableBackupTransportService::class.java)) // show notification
val nm = (context.applicationContext as Backup).notificationManager
nm.onBackupUpdate(context.getString(R.string.notification_backup_starting), 0, 1, true)
val observer = NotificationBackupObserver(context, true) val observer = NotificationBackupObserver(context, true)
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
val packages = PackageService().eligiblePackages val packages = PackageService().eligiblePackages

View file

@ -103,7 +103,7 @@ internal class StorageRootFetcher(private val context: Context) {
var cursor: Cursor? = null var cursor: Cursor? = null
try { try {
cursor = contentResolver.query(rootsUri, null, null, null, null) cursor = contentResolver.query(rootsUri, null, null, null, null)
while (cursor.moveToNext()) { while (cursor!!.moveToNext()) {
val root = getStorageRoot(authority, cursor) val root = getStorageRoot(authority, cursor)
if (root != null) roots.add(root) if (root != null) roots.add(root)
} }
@ -198,7 +198,7 @@ internal class StorageRootFetcher(private val context: Context) {
} }
} }
private fun getPackageIcon(context: Context, authority: String?, icon: Int): Drawable? { private fun getPackageIcon(context: Context, authority: String, icon: Int): Drawable? {
if (icon != 0) { if (icon != 0) {
val pm = context.packageManager val pm = context.packageManager
val info = pm.resolveContentProvider(authority, 0) val info = pm.resolveContentProvider(authority, 0)

View file

@ -2,18 +2,19 @@ package com.stevesoltys.backup.ui.storage
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.Context.USB_SERVICE
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION import android.content.Intent.FLAG_GRANT_WRITE_URI_PERMISSION
import android.hardware.usb.UsbManager
import android.net.Uri import android.net.Uri
import android.util.Log 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.R import com.stevesoltys.backup.R
import com.stevesoltys.backup.settings.Storage import com.stevesoltys.backup.isMassStorage
import com.stevesoltys.backup.settings.getStorage import com.stevesoltys.backup.settings.*
import com.stevesoltys.backup.settings.setStorage
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
@ -85,12 +86,30 @@ internal abstract class StorageViewModel(private val app: Application) : Android
val storage = Storage(uri, name, root.supportsEject) val storage = Storage(uri, name, root.supportsEject)
setStorage(app, storage) setStorage(app, storage)
if (storage.ejectable) {
val wasSaved = saveUsbDevice()
// reset stored flash drive, if we did not update it
if (!wasSaved) setFlashDrive(app, 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
app.stopService(Intent(app, ConfigurableBackupTransportService::class.java)) app.stopService(Intent(app, ConfigurableBackupTransportService::class.java))
Log.d(TAG, "New storage location saved: $uri") Log.d(TAG, "New storage location saved: $uri")
} }
private fun saveUsbDevice(): Boolean {
val manager = app.getSystemService(USB_SERVICE) as UsbManager
manager.deviceList.values.forEach { device ->
if (device.isMassStorage()) {
setFlashDrive(app, FlashDrive.from(device))
return true
}
}
Log.w(TAG, "No USB device found for ejectable storage.")
return false
}
override fun onCleared() { override fun onCleared() {
storageRootFetcher.setRemovableStorageListener(null) storageRootFetcher.setRemovableStorageListener(null)
super.onCleared() super.onCleared()

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device
class="8"
protocol="80"
subclass="6" />
</resources>