1
0
Fork 0
seedvault/app/src/main/java/com/stevesoltys/seedvault/UsbIntentReceiver.kt
Torsten Grote e7e489e091
Only reschedule next app backup when not on USB storage
Currently, after a manual run, we need to schedule the background backups again, because the scheduling gets lost. However, we need to be careful not to do that when the backup destination is on removable storage. Then we don't want to run.
2024-03-08 09:52:06 -03:00

127 lines
5.1 KiB
Kotlin

package com.stevesoltys.seedvault
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.ACTION_USB_DEVICE_DETACHED
import android.hardware.usb.UsbManager.EXTRA_DEVICE
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.DocumentsContract
import android.util.Log
import androidx.core.content.ContextCompat.startForegroundService
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.storage.StorageBackupService
import com.stevesoltys.seedvault.storage.StorageBackupService.Companion.EXTRA_START_APP_BACKUP
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
import com.stevesoltys.seedvault.worker.AppBackupWorker
import org.koin.core.context.GlobalContext.get
import java.util.concurrent.TimeUnit.HOURS
private val TAG = UsbIntentReceiver::class.java.simpleName
private const val HOURS_AUTO_BACKUP: Long = 24
class UsbIntentReceiver : UsbMonitor() {
// using KoinComponent would crash robolectric tests :(
private val settingsManager: SettingsManager by lazy { get().get() }
private val metadataManager: MetadataManager by lazy { get().get() }
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 savedFlashDrive = settingsManager.getFlashDrive() ?: return false
val attachedFlashDrive = FlashDrive.from(device)
return if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, checking backup time...")
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
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) {
if (settingsManager.isStorageBackupEnabled()) {
val i = Intent(context, StorageBackupService::class.java)
// this starts an app backup afterwards
i.putExtra(EXTRA_START_APP_BACKUP, true)
startForegroundService(context, i)
} else {
AppBackupWorker.scheduleNow(context, reschedule = false)
}
}
}
/**
* 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.
*/
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 contentResolver = context.contentResolver
val handler = Handler(Looper.getMainLooper())
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)
}
}
abstract fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean
abstract fun onStatusChanged(context: Context, action: String, device: UsbDevice)
}
internal fun UsbDevice.isMassStorage(): Boolean {
for (i in 0 until interfaceCount) {
if (getInterface(i).isMassStorage()) return true
}
return false
}
private fun UsbInterface.isMassStorage(): Boolean {
@Suppress("MagicNumber")
return interfaceClass == 8 && interfaceProtocol == 80 && interfaceSubclass == 6
}
private fun UsbDevice.log() {
Log.d(TAG, " name: $manufacturerName $productName")
Log.d(TAG, " serialNumber: $serialNumber")
Log.d(TAG, " productId: $productId")
Log.d(TAG, " vendorId: $vendorId")
Log.d(TAG, " isMassStorage: ${isMassStorage()}")
}