mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: remove Companion Device Pairing
https://developer.android.com/develop/connectivity/bluetooth/companion-device-pairing
This commit is contained in:
parent
ca537becd1
commit
bc05280988
6 changed files with 42 additions and 153 deletions
|
|
@ -53,10 +53,6 @@
|
|||
<!-- Needed to open our bluetooth connection to our paired device (after reboot) -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- For android >=26 we can use the new BLE scanning API, which allows auto launching our service when our device is seen -->
|
||||
<uses-permission android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
|
||||
<uses-permission android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" />
|
||||
|
||||
<!-- zxing library for QR Code scanning using camera -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
|
|
@ -68,11 +64,6 @@
|
|||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="false" />
|
||||
|
||||
<!-- For the modern BLE scanning API -->
|
||||
<uses-feature
|
||||
android:name="android.software.companion_device_setup"
|
||||
android:required="false" />
|
||||
|
||||
<!-- for USB serial access -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package com.geeksville.mesh.android
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
|
|
@ -21,22 +19,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
val Context.bluetoothManager: BluetoothManager?
|
||||
get() = getSystemService(Context.BLUETOOTH_SERVICE).takeIf { hasBluetoothPermission() } as? BluetoothManager?
|
||||
|
||||
val Context.companionDeviceManager: CompanionDeviceManager?
|
||||
@SuppressLint("NewApi")
|
||||
get() = getSystemService(Context.COMPANION_DEVICE_SERVICE).takeIf { hasCompanionDeviceApi() } as? CompanionDeviceManager?
|
||||
|
||||
val Context.notificationManager: NotificationManager get() = requireNotNull(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager?)
|
||||
|
||||
val Context.locationManager: LocationManager get() = requireNotNull(getSystemService(Context.LOCATION_SERVICE) as? LocationManager?)
|
||||
|
||||
/**
|
||||
* @return true if CompanionDeviceManager API is present
|
||||
*/
|
||||
fun Context.hasCompanionDeviceApi(): Boolean =
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O)
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
|
||||
else false
|
||||
|
||||
/**
|
||||
* @return true if the device has a GPS receiver
|
||||
*/
|
||||
|
|
@ -126,7 +112,7 @@ fun Context.getBluetoothPermissions(): Array<String> {
|
|||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
perms.add(Manifest.permission.BLUETOOTH_SCAN)
|
||||
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else if (!hasCompanionDeviceApi()) {
|
||||
} else {
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
return getMissingPermissions(perms)
|
||||
|
|
|
|||
|
|
@ -3,22 +3,16 @@ package com.geeksville.mesh.model
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.companion.AssociationRequest
|
||||
import android.companion.BluetoothDeviceFilter
|
||||
import android.companion.BluetoothLeDeviceFilter
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.*
|
||||
import android.content.Context
|
||||
import android.hardware.usb.UsbManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.RemoteException
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository
|
||||
import com.geeksville.mesh.repository.radio.InterfaceId
|
||||
|
|
@ -34,7 +28,6 @@ import kotlinx.coroutines.flow.catch
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -151,10 +144,11 @@ class BTScanModel @Inject constructor(
|
|||
/// Use the string for the NopInterface
|
||||
val selectedNotNull: String get() = selectedAddress ?: "n"
|
||||
|
||||
private fun addDevice(entry: DeviceListEntry) {
|
||||
val oldDevs = devices.value!!
|
||||
oldDevs[entry.fullAddress] = entry // Add/replace entry
|
||||
devices.value = oldDevs // trigger gui updates
|
||||
val scanResult = MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf())
|
||||
|
||||
fun clearScanResults() {
|
||||
stopScan()
|
||||
scanResult.value = mutableMapOf()
|
||||
}
|
||||
|
||||
fun stopScan() {
|
||||
|
|
@ -166,21 +160,16 @@ class BTScanModel @Inject constructor(
|
|||
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
|
||||
} finally {
|
||||
scanJob = null
|
||||
_spinner.value = false
|
||||
}
|
||||
} else _spinner.value = false
|
||||
}
|
||||
|
||||
fun startScan(context: Context?) {
|
||||
_spinner.value = true
|
||||
|
||||
if (context != null) startCompanionScan(context) else startClassicScan()
|
||||
}
|
||||
_spinner.value = false
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startClassicScan() {
|
||||
fun startScan() {
|
||||
debug("starting classic scan")
|
||||
|
||||
_spinner.value = true
|
||||
scanJob = bluetoothRepository.scan()
|
||||
.onEach { result ->
|
||||
val fullAddress = radioInterfaceService.toInterfaceAddress(
|
||||
|
|
@ -189,12 +178,13 @@ class BTScanModel @Inject constructor(
|
|||
)
|
||||
// prevent log spam because we'll get lots of redundant scan results
|
||||
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
|
||||
val oldDevs = devices.value ?: emptyMap()
|
||||
val oldDevs = scanResult.value!!
|
||||
val oldEntry = oldDevs[fullAddress]
|
||||
// Don't spam the GUI with endless updates for non changing nodes
|
||||
if (oldEntry == null || oldEntry.bonded != isBonded) {
|
||||
val entry = DeviceListEntry(result.device.name, fullAddress, isBonded)
|
||||
addDevice(entry)
|
||||
oldDevs[entry.fullAddress] = entry
|
||||
scanResult.value = oldDevs
|
||||
}
|
||||
}.catch { ex ->
|
||||
serviceRepository.setErrorMessage("Unexpected Bluetooth scan failure: ${ex.message}")
|
||||
|
|
@ -271,72 +261,6 @@ class BTScanModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onSelectedBle(address: String): Boolean {
|
||||
val device = bluetoothRepository.getRemoteDevice(address) ?: return false
|
||||
return onSelected(BLEDeviceListEntry(device))
|
||||
}
|
||||
|
||||
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 = AssociationRequest.Builder()
|
||||
.addDeviceFilter(
|
||||
BluetoothDeviceFilter.Builder()
|
||||
.setNamePattern(Pattern.compile(BLE_NAME_PATTERN))
|
||||
.build()
|
||||
)
|
||||
.addDeviceFilter(
|
||||
BluetoothLeDeviceFilter.Builder()
|
||||
.setNamePattern(Pattern.compile(BLE_NAME_PATTERN))
|
||||
// .setScanFilter(
|
||||
// ScanFilter.Builder()
|
||||
// .setServiceUuid(ParcelUuid(BluetoothInterface.BTM_SERVICE_UUID))
|
||||
// .build()
|
||||
// )
|
||||
.build()
|
||||
)
|
||||
.setSingleDevice(false)
|
||||
.build()
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun startCompanionScan(context: Context) {
|
||||
debug("starting companion scan")
|
||||
context.companionDeviceManager?.associate(
|
||||
associationRequest(),
|
||||
object : CompanionDeviceManager.Callback() {
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("onAssociationPending(intentSender)"))
|
||||
override fun onDeviceFound(intentSender: IntentSender) {
|
||||
onAssociationPending(intentSender)
|
||||
}
|
||||
|
||||
override fun onAssociationPending(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
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BLE_NAME_PATTERN = BluetoothRepository.BLE_NAME_PATTERN
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import android.widget.AdapterView
|
|||
import android.widget.ArrayAdapter
|
||||
import android.widget.RadioButton
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.asLiveData
|
||||
|
|
@ -29,7 +30,6 @@ import com.geeksville.mesh.model.getInitials
|
|||
import com.geeksville.mesh.repository.location.LocationRepository
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
import com.geeksville.mesh.util.exceptionToSnackbar
|
||||
import com.geeksville.mesh.util.getAssociationResult
|
||||
import com.geeksville.mesh.util.onEditorAction
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
|
@ -50,7 +50,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
internal lateinit var locationRepository: LocationRepository
|
||||
|
||||
private val hasGps by lazy { requireContext().hasGps() }
|
||||
private val hasCompanionDeviceApi by lazy { requireContext().hasCompanionDeviceApi() }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
|
|
@ -139,14 +138,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
|
||||
private fun initCommonUI() {
|
||||
|
||||
val associationResultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) {
|
||||
it.data
|
||||
?.getAssociationResult()
|
||||
?.let { address -> scanModel.onSelectedBle(address) }
|
||||
}
|
||||
|
||||
val requestBackgroundAndCheckLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.any { !it.value }) {
|
||||
|
|
@ -202,19 +193,36 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
var scanDialog: AlertDialog? = null
|
||||
scanModel.scanResult.observe(viewLifecycleOwner) { results ->
|
||||
val devices = results.values.ifEmpty { return@observe }
|
||||
scanDialog?.dismiss()
|
||||
scanDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Select a Bluetooth device")
|
||||
.setSingleChoiceItems(
|
||||
devices.map { it.name }.toTypedArray(),
|
||||
-1
|
||||
) { dialog, position ->
|
||||
val selectedDevice = devices.elementAt(position)
|
||||
scanModel.onSelected(selectedDevice)
|
||||
scanModel.clearScanResults()
|
||||
dialog.dismiss()
|
||||
scanDialog = null
|
||||
}
|
||||
.setPositiveButton(R.string.cancel) { dialog, _ ->
|
||||
scanModel.clearScanResults()
|
||||
dialog.dismiss()
|
||||
scanDialog = null
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
// show the spinner when [spinner] is true
|
||||
scanModel.spinner.observe(viewLifecycleOwner) { show ->
|
||||
binding.changeRadioButton.isEnabled = !show
|
||||
binding.scanProgressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
scanModel.associationRequest.observe(viewLifecycleOwner) { request ->
|
||||
request?.let {
|
||||
associationResultLauncher.launch(request)
|
||||
scanModel.clearAssociationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
binding.usernameEditText.onEditorAction(EditorInfo.IME_ACTION_DONE) {
|
||||
debug("received IME_ACTION_DONE")
|
||||
val n = binding.usernameEditText.text.toString().trim()
|
||||
|
|
@ -372,7 +380,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
private var scanning = false
|
||||
private fun scanLeDevice() {
|
||||
if (!checkBTEnabled()) return
|
||||
if (!hasCompanionDeviceApi) checkLocationEnabled()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) checkLocationEnabled()
|
||||
|
||||
if (!scanning) { // Stops scanning after a pre-defined scan period.
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
|
|
@ -380,7 +388,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
scanModel.stopScan()
|
||||
}, SCAN_PERIOD)
|
||||
scanning = true
|
||||
scanModel.startScan(requireActivity().takeIf { hasCompanionDeviceApi })
|
||||
scanModel.startScan()
|
||||
} else {
|
||||
scanning = false
|
||||
scanModel.stopScan()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
package com.geeksville.mesh.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.companion.AssociationInfo
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -45,19 +41,3 @@ fun Context.registerReceiverCompat(
|
|||
) {
|
||||
ContextCompat.registerReceiver(this, receiver, filter, flag)
|
||||
}
|
||||
|
||||
fun Intent.getAssociationResult(): String? = when {
|
||||
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU ->
|
||||
getParcelableExtraCompat<AssociationInfo>(CompanionDeviceManager.EXTRA_ASSOCIATION)
|
||||
?.deviceMacAddress?.toString()?.uppercase()
|
||||
|
||||
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O ->
|
||||
@Suppress("DEPRECATION")
|
||||
when (val it = getParcelableExtra<Parcelable>(CompanionDeviceManager.EXTRA_DEVICE)) {
|
||||
is BluetoothDevice -> it.address
|
||||
is ScanResult -> it.device.address
|
||||
else -> null
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
<string name="modem_config_very_long">Very Long Range / Slow</string>
|
||||
<string name="modem_config_unrecognized">UNRECOGNIZED</string>
|
||||
<string name="meshtastic_service_notifications">Service notifications</string>
|
||||
<string name="location_disabled_warning">Location must be turned on (high accuracy) to find new devices via bluetooth. You can turn it off again afterwards.</string>
|
||||
<string name="location_disabled_warning">Location must be turned on to find new devices via Bluetooth. You can turn it off again afterwards.</string>
|
||||
<string name="about">About</string>
|
||||
<string name="a_list_of_nodes_in_the_mesh">A list of nodes in the mesh</string>
|
||||
<string name="text_messages">Text messages</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue