diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2799b7758..0b878ee71 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,10 +53,6 @@ - - - - @@ -68,11 +64,6 @@ android:name="android.hardware.bluetooth_le" android:required="false" /> - - - = 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 { 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) diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index 172338d12..12088bb3e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -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>(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 get() = _spinner - - private val _associationRequest = MutableLiveData(null) - val associationRequest: LiveData 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 - } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 2edfa427e..7b253b7fc 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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() diff --git a/app/src/main/java/com/geeksville/mesh/util/CompatExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/CompatExtensions.kt index fa6540e1b..07b2f361d 100644 --- a/app/src/main/java/com/geeksville/mesh/util/CompatExtensions.kt +++ b/app/src/main/java/com/geeksville/mesh/util/CompatExtensions.kt @@ -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(CompanionDeviceManager.EXTRA_ASSOCIATION) - ?.deviceMacAddress?.toString()?.uppercase() - - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O -> - @Suppress("DEPRECATION") - when (val it = getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)) { - is BluetoothDevice -> it.address - is ScanResult -> it.device.address - else -> null - } - - else -> null -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 265341ae3..f889f67c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ Very Long Range / Slow UNRECOGNIZED Service notifications - Location must be turned on (high accuracy) to find new devices via bluetooth. You can turn it off again afterwards. + Location must be turned on to find new devices via Bluetooth. You can turn it off again afterwards. About A list of nodes in the mesh Text messages