refactor: extract Bluetooth and USB API methods to repositories

This commit is contained in:
andrekir 2023-11-17 08:46:54 -03:00
parent c6ad5dcfdf
commit f29d4e2309
12 changed files with 288 additions and 324 deletions

View file

@ -1,18 +1,14 @@
package com.geeksville.mesh.ui
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.bluetooth.BluetoothDevice
import android.companion.CompanionDeviceManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
@ -24,12 +20,8 @@ import android.widget.RadioButton
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.ModuleConfigProtos
@ -42,15 +34,11 @@ import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.SoftwareUpdateService
import com.geeksville.mesh.util.PendingIntentCompat
import com.geeksville.mesh.util.anonymize
import com.geeksville.mesh.util.exceptionReporter
import com.geeksville.mesh.util.exceptionToSnackbar
import com.geeksville.mesh.util.getParcelableExtraCompat
import com.geeksville.mesh.util.onEditorAction
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@ -67,8 +55,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
@Inject
internal lateinit var locationRepository: LocationRepository
private val myActivity get() = requireActivity() as MainActivity
private val hasGps by lazy { requireContext().hasGps() }
private val hasCompanionDeviceApi by lazy { requireContext().hasCompanionDeviceApi() }
private val useCompanionDeviceApi by lazy {
@ -239,14 +225,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
) {
it.data
?.getParcelableExtraCompat<BluetoothDevice>(CompanionDeviceManager.EXTRA_DEVICE)
?.let { device -> onSelected(BTScanModel.BLEDeviceListEntry(device)) }
?.let { device -> scanModel.onSelected(BTScanModel.BLEDeviceListEntry(device)) }
}
val requestBackgroundAndCheckLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.entries.all { it.value }) {
binding.provideLocationCheckbox.isChecked = true
} else {
if (permissions.entries.any { !it.value }) {
debug("User denied background permission")
model.showSnackbar(getString(R.string.why_background_required))
}
@ -256,9 +240,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.entries.all { it.value }) {
// Older versions of android only need Location permission
if (requireContext().hasBackgroundPermission()) {
binding.provideLocationCheckbox.isChecked = true
} else requestBackgroundAndCheckLauncher.launch(requireContext().getBackgroundPermissions())
if (!requireContext().hasBackgroundPermission())
requestBackgroundAndCheckLauncher.launch(requireContext().getBackgroundPermissions())
} else {
debug("User denied location permission")
model.showSnackbar(getString(R.string.why_background_required))
@ -357,12 +340,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
// Observe receivingLocationUpdates state and update provideLocationCheckbox
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
locationRepository.receivingLocationUpdates.collect {
binding.provideLocationCheckbox.isChecked = it
}
}
locationRepository.receivingLocationUpdates.asLiveData().observe(viewLifecycleOwner) {
binding.provideLocationCheckbox.isChecked = it
}
binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked ->
@ -439,8 +418,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
b.setOnClickListener {
if (!device.bonded) // If user just clicked on us, try to bond
binding.scanStatusText.setText(R.string.starting_pairing)
b.isChecked = onSelected(device)
b.isChecked = scanModel.onSelected(device)
}
}
@ -452,12 +430,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.deviceRadioGroup.addView(b)
b.setOnClickListener {
b.isChecked = onSelected(BTScanModel.DeviceListEntry("", "t" + e.text, true))
b.isChecked = scanModel.onSelected(BTScanModel.DeviceListEntry("", "t" + e.text, true))
}
binding.deviceRadioGroup.addView(e)
e.doAfterTextChanged {
@ -486,7 +460,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// and before use
val curAddr = scanModel.selectedAddress
if (curAddr != null) {
val curDevice = scanModel.getDeviceListEntry(curAddr)
val curDevice = BTScanModel.DeviceListEntry(curAddr.substring(1), curAddr, false)
addDeviceButton(curDevice, model.isConnected())
}
}
@ -522,115 +496,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
private fun changeDeviceAddress(address: String) {
try {
model.meshService?.let { service ->
MeshService.changeDeviceAddress(requireActivity(), service, address)
}
scanModel.changeSelectedAddress(address) // if it throws the change will be discarded
} catch (ex: RemoteException) {
errormsg("changeDeviceSelection failed, probably it is shutting down $ex.message")
// ignore the failure and the GUI won't be updating anyways
}
}
/// Called by the GUI when a new device has been selected by the user
/// Returns true if we were able to change to that item
private fun onSelected(it: BTScanModel.DeviceListEntry): Boolean {
// If the device is paired, let user select it, otherwise start the pairing flow
if (it.bonded) {
changeDeviceAddress(it.fullAddress)
return true
} else {
// Handle requesting USB or bluetooth permissions for the device
debug("Requesting permissions for the device")
exceptionReporter {
if (it.isBLE) {
// Request bonding for bluetooth
// We ignore missing BT adapters, because it lets us run on the emulator
scanModel.getRemoteDevice(it.address)?.let { device ->
requestBonding(device) { state ->
if (state == BluetoothDevice.BOND_BONDED) {
scanModel.setErrorText(getString(R.string.pairing_completed))
changeDeviceAddress(it.fullAddress)
} else {
scanModel.setErrorText(getString(R.string.pairing_failed_try_again))
}
}
}
}
}
if (it.isUSB) {
it as BTScanModel.USBDeviceListEntry
val usbReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (BTScanModel.ACTION_USB_PERMISSION != intent.action) return
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
info("User approved USB access")
changeDeviceAddress(it.fullAddress)
} else {
errormsg("USB permission denied for device ${it.address}")
}
// We don't need to stay registered
requireActivity().unregisterReceiver(this)
}
}
val permissionIntent = PendingIntent.getBroadcast(
activity,
0,
Intent(BTScanModel.ACTION_USB_PERMISSION),
PendingIntentCompat.FLAG_MUTABLE
)
val filter = IntentFilter(BTScanModel.ACTION_USB_PERMISSION)
requireActivity().registerReceiver(usbReceiver, filter)
requireContext().usbManager.requestPermission(it.usb.device, permissionIntent)
}
return false
}
}
/// Show the UI asking the user to bond with a device, call changeSelection() if/when bonding completes
@SuppressLint("MissingPermission")
private fun requestBonding(
device: BluetoothDevice,
onComplete: (Int) -> Unit
) {
info("Starting bonding for ${device.anonymize}")
// We need this receiver to get informed when the bond attempt finished
val bondChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
val state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1)
debug("Received bond state changed $state")
if (state != BluetoothDevice.BOND_BONDING) {
context.unregisterReceiver(this) // we stay registered until bonding completes (either with BONDED or NONE)
debug("Bonding completed, state=$state")
onComplete(state)
}
}
}
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
requireActivity().registerReceiver(bondChangedReceiver, filter)
// We ignore missing BT adapters, because it lets us run on the emulator
try {
device.createBond()
} catch (ex: Throwable) {
warn("Failed creating Bluetooth bond: ${ex.message}")
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -702,10 +567,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
override fun onResume() {
super.onResume()
// system permissions might have changed while we were away
binding.provideLocationCheckbox.isChecked = requireContext().hasBackgroundPermission() && (model.provideLocation.value ?: false)
myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter)
requireActivity().registerReceiver(updateProgressReceiver, updateProgressFilter)
// Warn user if BLE device is selected but BLE disabled
if (scanModel.selectedBluetooth) checkBTEnabled()