Merge pull request #428 from andrekir/ble-scan

bluetooth scan & connect UI rework
pull/420/head
Andre Kirchhoff 2022-05-03 18:14:59 -03:00 zatwierdzone przez GitHub
commit 4b3427fe8c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
2 zmienionych plików z 102 dodań i 86 usunięć

Wyświetl plik

@ -10,6 +10,8 @@ import android.content.pm.PackageManager
import android.hardware.usb.UsbManager
import android.os.Build
import androidx.core.content.ContextCompat
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.mesh.MainActivity
/**
* @return null on platforms without a BlueTooth driver (i.e. the emulator)
@ -18,8 +20,11 @@ val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context
val Context.deviceManager: CompanionDeviceManager?
@SuppressLint("InlinedApi")
get() = if (hasCompanionDeviceApi()) getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager?
else null
get() {
val activity: MainActivity? = GeeksvilleApplication.currentActivity as MainActivity?
return if (hasCompanionDeviceApi()) activity?.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager?
else null
}
val Context.usbManager: UsbManager get() = requireNotNull(getSystemService(Context.USB_SERVICE) as? UsbManager?) { "USB_SERVICE is not available"}

Wyświetl plik

@ -24,6 +24,7 @@ import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
@ -246,9 +247,11 @@ class BTScanModel @Inject constructor(
scanner?.stopScan(scanCallback)
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
} finally {
scanner = null
_spinner.value = false
}
scanner = null
}
} else _spinner.value = false
}
/**
@ -314,13 +317,20 @@ class BTScanModel @Inject constructor(
}
}
fun startScan () {
if (hasCompanionDeviceApi) {
startCompanionScan()
} else startClassicScan()
}
@SuppressLint("MissingPermission")
fun startScan() {
private fun startClassicScan() {
/// The following call might return null if the user doesn't have bluetooth access permissions
val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner != null) { // could be null if bluetooth is disabled
debug("starting scan")
debug("starting classic scan")
_spinner.value = true
// filter and only accept devices that have our service
val filter =
@ -370,6 +380,63 @@ class BTScanModel @Inject constructor(
}
}
private val _spinner = MutableLiveData(false)
val spinner: LiveData<Boolean> get() = _spinner
private val _associationRequest = MutableLiveData<IntentSenderRequest?>(null)
val associationRequest: LiveData<IntentSenderRequest?> get() = _associationRequest
/**
* Called immediately after fragment observes CompanionDeviceManager activity result
*/
fun clearAssociationRequest() {
_associationRequest.value = null
}
@SuppressLint("NewApi")
private fun associationRequest(): AssociationRequest {
// To skip filtering based on name and supported feature flags (UUIDs),
// don't include calls to setNamePattern() and addServiceUuid(),
// respectively. This example uses Bluetooth.
// We only look for Mesh (rather than the full name) because NRF52 uses a very short name
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile("Mesh.*"))
// .addServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID), null)
.build()
// The argument provided in setSingleDevice() determines whether a single
// device name or a list of device names is presented to the user as
// pairing options.
return AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(false)
.build()
}
@SuppressLint("NewApi")
private fun startCompanionScan() {
debug("starting companion scan")
_spinner.value = true
deviceManager?.associate(
associationRequest(),
@SuppressLint("NewApi")
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
debug("CompanionDeviceManager - device found")
_spinner.value = false
chooserLauncher.let {
val request: IntentSenderRequest = IntentSenderRequest.Builder(it).build()
_associationRequest.value = request
}
}
override fun onFailure(error: CharSequence?) {
warn("BLE selection service failed $error")
}
}, null
)
}
val devices = object : MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf()) {
/**
@ -385,7 +452,7 @@ class BTScanModel @Inject constructor(
*/
override fun onInactive() {
super.onInactive()
if (!hasCompanionDeviceApi) stopScan()
stopScan()
}
}
@ -502,10 +569,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
@Inject
internal lateinit var usbRepository: UsbRepository
private val deviceManager: CompanionDeviceManager by lazy {
requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
}
private val myActivity get() = requireActivity() as MainActivity
override fun onDestroy() {
@ -723,6 +786,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
// show the spinner when [spinner] is true
scanModel.spinner.observe(viewLifecycleOwner) { show ->
binding.scanProgressBar.visibility = if (show) View.VISIBLE else View.GONE
}
scanModel.associationRequest.observe(viewLifecycleOwner) { request ->
request?.let {
associationResultLauncher.launch(request)
scanModel.clearAssociationRequest()
}
}
binding.updateFirmwareButton.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setMessage("${getString(R.string.update_firmware)}?")
@ -876,59 +951,24 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
private fun initClassicScan() {
binding.changeRadioButton.setOnClickListener {
debug("User clicked changeRadioButton")
if (!myActivity.hasScanPermission()) {
myActivity.requestScanPermission()
} else {
checkLocationEnabled()
scanLeDevice()
}
}
}
// per https://developer.android.com/guide/topics/connectivity/bluetooth/find-ble-devices
private fun scanLeDevice() {
var scanning = false
val SCAN_PERIOD: Long = 5000 // Stops scanning after 5 seconds
val SCAN_PERIOD: Long = 10000 // Stops scanning after 10 seconds
if (!scanning) { // Stops scanning after a pre-defined scan period.
Handler(Looper.getMainLooper()).postDelayed({
scanning = false
binding.scanProgressBar.visibility = View.GONE
scanModel.stopScan()
}, SCAN_PERIOD)
scanning = true
binding.scanProgressBar.visibility = View.VISIBLE
scanModel.startScan()
} else {
scanning = false
binding.scanProgressBar.visibility = View.GONE
scanModel.stopScan()
}
}
private fun associationRequest(): AssociationRequest {
// To skip filtering based on name and supported feature flags (UUIDs),
// don't include calls to setNamePattern() and addServiceUuid(),
// respectively. This example uses Bluetooth.
// We only look for Mesh (rather than the full name) because NRF52 uses a very short name
val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile("Mesh.*"))
// .addServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID), null)
.build()
// The argument provided in setSingleDevice() determines whether a single
// device name or a list of device names is presented to the user as
// pairing options.
return AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.setSingleDevice(false)
.build()
}
@SuppressLint("MissingPermission")
val associationResultLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
@ -947,49 +987,20 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
private fun startCompanionScan() {
// Disable the change button until our scan has some results
binding.changeRadioButton.isEnabled = false
// When the app tries to pair with the Bluetooth device, show the
// appropriate pairing request dialog to the user.
deviceManager.associate(
associationRequest(),
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
debug("Found one device - enabling changeRadioButton")
binding.changeRadioButton.isEnabled = true
binding.changeRadioButton.setOnClickListener {
chooserLauncher.let {
val request = IntentSenderRequest.Builder(it).build()
associationResultLauncher.launch(request)
}
}
}
override fun onFailure(error: CharSequence?) {
warn("BLE selection service failed $error")
// changeDeviceSelection(myActivity, null) // deselect any device
}
}, null
)
}
private fun initModernScan() {
scanModel.devices.observe(viewLifecycleOwner) {
startCompanionScan()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initCommonUI()
if (scanModel.hasCompanionDeviceApi)
initModernScan()
else
initClassicScan()
binding.changeRadioButton.setOnClickListener {
debug("User clicked changeRadioButton")
if (!myActivity.hasScanPermission()) {
myActivity.requestScanPermission()
} else {
if (!scanModel.hasCompanionDeviceApi) checkLocationEnabled()
scanLeDevice()
}
}
}
// If the user has not turned on location access throw up a toast warning