mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: extract Bluetooth and USB API methods to repositories
This commit is contained in:
parent
c6ad5dcfdf
commit
f29d4e2309
12 changed files with 288 additions and 324 deletions
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue