From e8999712d288c22618ecd46feb07f3afefb4fbdc Mon Sep 17 00:00:00 2001 From: andrekir Date: Fri, 7 Jan 2022 18:51:20 -0300 Subject: [PATCH 1/9] fix companion device pairing --- app/src/main/AndroidManifest.xml | 3 + .../java/com/geeksville/mesh/MainActivity.kt | 70 ++++++++++++++----- .../geeksville/mesh/ui/SettingsFragment.kt | 70 ++----------------- 3 files changed, 61 insertions(+), 82 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 940195c5d..91cfdb93d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,9 @@ + + diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index e380da5ae..dd91a3397 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -6,6 +6,8 @@ import android.app.Activity import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager +import android.companion.AssociationRequest +import android.companion.BluetoothDeviceFilter import android.companion.CompanionDeviceManager import android.content.* import android.content.pm.PackageInfo @@ -70,6 +72,7 @@ import java.lang.Runnable import java.nio.charset.Charset import java.text.DateFormat import java.util.* +import java.util.regex.Pattern import kotlin.math.roundToInt @@ -131,8 +134,7 @@ class MainActivity : AppCompatActivity(), Logging, const val REQUEST_ENABLE_BT = 10 const val DID_REQUEST_PERM = 11 const val RC_SIGN_IN = 12 // google signin completed - const val RC_SELECT_DEVICE = - 13 // seems to be hardwired in CompanionDeviceManager to add 65536 + const val SELECT_DEVICE_REQUEST_CODE = 13 const val CREATE_CSV_FILE = 14 } @@ -194,11 +196,52 @@ class MainActivity : AppCompatActivity(), Logging, } } - private val btStateReceiver = BluetoothStateReceiver { _ -> updateBluetoothEnabled() } + fun startCompanionScan() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val deviceManager: CompanionDeviceManager by lazy { + getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager + } + + // 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. + val pairingRequest: AssociationRequest = AssociationRequest.Builder() + .addDeviceFilter(deviceFilter) + .setSingleDevice(false) + .build() + + // When the app tries to pair with the Bluetooth device, show the + // appropriate pairing request dialog to the user. + deviceManager.associate(pairingRequest, + object : CompanionDeviceManager.Callback() { + override fun onDeviceFound(chooserLauncher: IntentSender) { + startIntentSenderForResult(chooserLauncher, + SELECT_DEVICE_REQUEST_CODE, null, 0, 0, 0) + } + + override fun onFailure(error: CharSequence?) { + warn("BLE selection service failed $error") + // changeDeviceSelection(mainActivity, null) // deselect any device + } + }, null + ) + } + else warn("startCompanionScan should not run on SDK < 26") + } + /** * Don't tell our app we have bluetooth until we have bluetooth _and_ location access */ @@ -410,7 +453,6 @@ class MainActivity : AppCompatActivity(), Logging, } } - /// Ask user to rate in play store private fun askToRate() { exceptionReporter { // Got one IllegalArgumentException from inside this lib, but we don't want to crash our app because of bugs in this optional feature @@ -497,7 +539,6 @@ class MainActivity : AppCompatActivity(), Logging, requestPermission() } - private fun initToolbar() { val toolbar = findViewById(R.id.toolbar) as Toolbar @@ -601,25 +642,18 @@ class MainActivity : AppCompatActivity(), Logging, GoogleSignIn.getSignedInAccountFromIntent(data) handleSignInResult(task) } - (65536 + RC_SELECT_DEVICE) -> when (resultCode) { + (SELECT_DEVICE_REQUEST_CODE) -> when (resultCode) { Activity.RESULT_OK -> { - // User has chosen to pair with the Bluetooth device. - val device: BluetoothDevice = + val deviceToPair: BluetoothDevice = data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)!! - debug("Received BLE pairing ${device.address}") - if (device.bondState != BluetoothDevice.BOND_BONDED) { - device.createBond() - // FIXME - wait for bond to complete + if (deviceToPair.bondState != BluetoothDevice.BOND_BONDED) { + deviceToPair.createBond() } - - // ... Continue interacting with the paired device. model.meshService?.let { service -> - MeshService.changeDeviceAddress(this@MainActivity, service, device.address) + MeshService.changeDeviceAddress(this@MainActivity, service, "x${deviceToPair.address}") } } - - else -> - warn("BLE device select intent failed") + else -> warn("BLE device select intent failed") } CREATE_CSV_FILE -> { if (resultCode == Activity.RESULT_OK) { 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 3d9a4feb5..bb8f0a9ff 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -7,9 +7,6 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothDevice.BOND_BONDED import android.bluetooth.BluetoothDevice.BOND_BONDING import android.bluetooth.le.* -import android.companion.AssociationRequest -import android.companion.BluetoothDeviceFilter -import android.companion.CompanionDeviceManager import android.content.* import android.content.pm.PackageManager import android.hardware.usb.UsbDevice @@ -56,7 +53,6 @@ import com.hoho.android.usbserial.driver.UsbSerialDriver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import java.util.regex.Pattern object SLogging : Logging @@ -106,7 +102,6 @@ private fun requestBonding( device.createBond() } - class BTScanModel(app: Application) : AndroidViewModel(app), Logging { private val context: Context get() = getApplication().applicationContext @@ -123,7 +118,6 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { else null - override fun toString(): String { return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize})" } @@ -187,7 +181,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { if ((result.device.name?.startsWith("Mesh") == true)) { val addr = result.device.address - val fullAddr = "x$addr" // full address with the bluetooh prefix + val fullAddr = "x$addr" // full address with the bluetooth prefix added // prevent logspam because weill get get lots of redundant scan results val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED val oldDevs = devices.value!! @@ -456,10 +450,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { BluetoothInterface.hasCompanionDeviceApi(requireContext()) } - private val deviceManager: CompanionDeviceManager by lazy { - requireContext().getSystemService(CompanionDeviceManager::class.java) - } - private val myActivity get() = requireActivity() as MainActivity override fun onDestroy() { @@ -540,6 +530,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { val isConnected = connected == MeshService.ConnectionState.CONNECTED binding.nodeSettings.visibility = if (isConnected) View.VISIBLE else View.GONE + binding.provideLocationCheckbox.visibility = if (isConnected) View.VISIBLE else View.GONE if (connected == MeshService.ConnectionState.DISCONNECTED) model.ownerName.value = "" @@ -720,7 +711,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired binding.deviceRadioGroup.addView(b) - // Once we have at least one device, don't show the "looking for" animation - it makes uers think + // Once we have at least one device, don't show the "looking for" animation - it makes users think // something is busted binding.scanProgressBar.visibility = View.INVISIBLE @@ -830,56 +821,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { { updateDevicesButtons(scanModel.devices.value) }) } - /// Start running the modern scan, once it has one result we enable the - private fun startBackgroundScan() { - // Disable the change button until our scan has some results - binding.changeRadioButton.isEnabled = false - - // 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. - val pairingRequest: AssociationRequest = AssociationRequest.Builder() - .addDeviceFilter(deviceFilter) - .setSingleDevice(false) - .build() - - // When the app tries to pair with the Bluetooth device, show the - // appropriate pairing request dialog to the user. - deviceManager.associate( - pairingRequest, - object : CompanionDeviceManager.Callback() { - - override fun onDeviceFound(chooserLauncher: IntentSender) { - debug("Found one device - enabling button") - binding.changeRadioButton.isEnabled = true - binding.changeRadioButton.setOnClickListener { - debug("User clicked BLE change button") - - // Request code seems to be ignored anyways - startIntentSenderForResult( - chooserLauncher, - MainActivity.RC_SELECT_DEVICE, null, 0, 0, 0, null - ) - } - } - - override fun onFailure(error: CharSequence?) { - warn("BLE selection service failed $error") - // changeDeviceSelection(mainActivity, null) // deselect any device - } - }, null - ) - } - private fun initModernScan() { // Turn off the widgets for the classic API binding.scanProgressBar.visibility = View.GONE @@ -895,8 +836,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { binding.scanStatusText.text = getString(R.string.not_paired_yet) binding.changeRadioButton.setText(R.string.select_radio) } - - startBackgroundScan() + binding.changeRadioButton.setOnClickListener { + myActivity.startCompanionScan() + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { From bc57946aec59469946f4af580c711bd8321e2f94 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 8 Jan 2022 16:18:36 -0300 Subject: [PATCH 2/9] convert changeRadioButton to fab --- .../java/com/geeksville/mesh/ui/SettingsFragment.kt | 2 -- app/src/main/res/drawable/ic_twotone_add_24.xml | 10 ++++++++++ app/src/main/res/layout/settings_fragment.xml | 12 ++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/ic_twotone_add_24.xml 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 bb8f0a9ff..9888caf57 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -831,10 +831,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { if (curRadio != null) { binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio) - binding.changeRadioButton.text = getString(R.string.change_radio) } else { binding.scanStatusText.text = getString(R.string.not_paired_yet) - binding.changeRadioButton.setText(R.string.select_radio) } binding.changeRadioButton.setOnClickListener { myActivity.startCompanionScan() diff --git a/app/src/main/res/drawable/ic_twotone_add_24.xml b/app/src/main/res/drawable/ic_twotone_add_24.xml new file mode 100644 index 000000000..eb232541d --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_add_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index fefd3e54e..c9d1372b3 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -96,7 +96,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="16dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/changeRadioButton"> + app:layout_constraintTop_toBottomOf="@+id/scanStatusText"> - + app:layout_constraintBottom_toTopOf="@+id/reportBugButton" /> Date: Sat, 8 Jan 2022 16:30:06 -0300 Subject: [PATCH 3/9] update initCommonUI --- .../geeksville/mesh/ui/SettingsFragment.kt | 70 ++++++++----------- app/src/main/res/layout/settings_fragment.xml | 1 + 2 files changed, 29 insertions(+), 42 deletions(-) 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 9888caf57..ce5b6dd8b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -627,6 +627,20 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { updateNodeInfo() }) + scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg -> + if (errMsg != null) { + binding.scanStatusText.text = errMsg + } + }) + + scanModel.devices.observe( + viewLifecycleOwner, + Observer { devices -> updateDevicesButtons(devices) }) + + model.isConnected.observe( + viewLifecycleOwner, + { updateDevicesButtons(scanModel.devices.value) }) + binding.updateFirmwareButton.setOnClickListener { doFirmwareUpdate() } @@ -727,12 +741,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } } - /// Show the GUI for classic scanning - private fun showClassicWidgets(visible: Int) { - binding.scanProgressBar.visibility = visible - binding.deviceRadioGroup.visibility = visible - } - private fun updateDevicesButtons(devices: MutableMap?) { // Remove the old radio buttons and repopulate binding.deviceRadioGroup.removeAllViews() @@ -790,42 +798,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { View.GONE else View.VISIBLE - } - - /// Setup the GUI to do a classic (pre SDK 26 BLE scan) - private fun initClassicScan() { - // Turn off the widgets for the new API (we turn on/off hte classic widgets when we start scanning - binding.changeRadioButton.visibility = View.GONE - - showClassicWidgets(View.VISIBLE) - - model.bluetoothEnabled.observe(viewLifecycleOwner, Observer { enabled -> - if (enabled) - scanModel.startScan() - else - scanModel.stopScan() - }) - - scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg -> - if (errMsg != null) { - binding.scanStatusText.text = errMsg - } - }) - - scanModel.devices.observe( - viewLifecycleOwner, - Observer { devices -> updateDevicesButtons(devices) }) - - model.isConnected.observe( - viewLifecycleOwner, - { updateDevicesButtons(scanModel.devices.value) }) - } - - private fun initModernScan() { - // Turn off the widgets for the classic API - binding.scanProgressBar.visibility = View.GONE - binding.deviceRadioGroup.visibility = View.GONE - binding.changeRadioButton.visibility = View.VISIBLE val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext()) @@ -834,6 +806,20 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } else { binding.scanStatusText.text = getString(R.string.not_paired_yet) } + } + + private fun initClassicScan() { + + model.bluetoothEnabled.observe(viewLifecycleOwner, Observer { enabled -> + if (enabled) + scanModel.startScan() + else + scanModel.stopScan() + }) + } + + private fun initModernScan() { + binding.changeRadioButton.setOnClickListener { myActivity.startCompanionScan() } diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index c9d1372b3..48190d6a2 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -85,6 +85,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/deviceRadioGroup" /> From c0a5c4dd3cd26df7c2c181be23c6c2ce3fc26022 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 8 Jan 2022 16:43:52 -0300 Subject: [PATCH 4/9] no permissions needed if hasCompanionDeviceApi --- .../geeksville/mesh/ui/SettingsFragment.kt | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) 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 ce5b6dd8b..9842dcdd9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -559,7 +559,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { val statusText = binding.scanStatusText val permissionsWarning = myActivity.getMissingMessage() when { - permissionsWarning != null -> + (!hasCompanionDeviceApi && permissionsWarning != null) -> statusText.text = permissionsWarning region == RadioConfigProtos.RegionCode.Unset -> @@ -927,17 +927,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { // Keep reminding user BLE is still off val hasUSB = SerialInterface.findDrivers(myActivity).isNotEmpty() if (!hasUSB) { - // First warn about permissions, and then if needed warn about settings - if (!myActivity.warnMissingPermissions()) { - // Warn user if BLE is disabled - if (scanModel.bluetoothAdapter?.isEnabled != true) { - Toast.makeText( - requireContext(), - R.string.error_bluetooth, - Toast.LENGTH_SHORT - ).show() - } else { - checkLocationEnabled() + // Warn user if BLE is disabled + if (scanModel.bluetoothAdapter?.isEnabled != true) { + Toast.makeText( + requireContext(), + R.string.error_bluetooth, + Toast.LENGTH_SHORT + ).show() + } else { + if (!hasCompanionDeviceApi) { + if (!myActivity.warnMissingPermissions()) { + checkLocationEnabled() + } } } } From a3bd9564f501eb06dbcd589ff1bb0f9e9bce456a Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 8 Jan 2022 16:56:41 -0300 Subject: [PATCH 5/9] split startScan into Setup/Start --- .../geeksville/mesh/ui/SettingsFragment.kt | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) 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 9842dcdd9..3e08d3a1a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -234,7 +234,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { /** * returns true if we could start scanning, false otherwise */ - fun startScan(): Boolean { + fun setupScan(): Boolean { debug("BTScan component active") selectedAddress = RadioInterfaceService.getDeviceAddress(context) @@ -263,14 +263,11 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { true } else { - /// The following call might return null if the user doesn't have bluetooth access permissions - val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner - val usbDrivers = SerialInterface.findDrivers(context) /* model.bluetoothEnabled.value */ - if (s == null && usbDrivers.isEmpty()) { + if (bluetoothAdapter.bluetoothLeScanner == null && usbDrivers.isEmpty()) { errorText.value = context.getString(R.string.requires_bluetooth) @@ -289,34 +286,38 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { USBDeviceListEntry(usbManager, d) ) } - - if (s != null) { // could be null if bluetooth is disabled - debug("starting scan") - - // filter and only accept devices that have our service - val filter = - ScanFilter.Builder() - // Samsung doesn't seem to filter properly by service so this can't work - // see https://stackoverflow.com/questions/57981986/altbeacon-android-beacon-library-not-working-after-device-has-screen-off-for-a-s/57995960#57995960 - // and https://stackoverflow.com/a/45590493 - // .setServiceUuid(ParcelUuid(BluetoothInterface.BTM_SERVICE_UUID)) - .build() - - val settings = - ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) - .build() - s.startScan(listOf(filter), settings, scanCallback) - scanner = s - } } else { debug("scan already running") } - true } } } + fun startScan() { + /// 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") + + // filter and only accept devices that have our service + val filter = + ScanFilter.Builder() + // Samsung doesn't seem to filter properly by service so this can't work + // see https://stackoverflow.com/questions/57981986/altbeacon-android-beacon-library-not-working-after-device-has-screen-off-for-a-s/57995960#57995960 + // and https://stackoverflow.com/a/45590493 + // .setServiceUuid(ParcelUuid(BluetoothInterface.BTM_SERVICE_UUID)) + .build() + + val settings = + ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build() + bluetoothLeScanner.startScan(listOf(filter), settings, scanCallback) + scanner = bluetoothLeScanner + } + } + val devices = object : MutableLiveData>(mutableMapOf()) { /** @@ -603,8 +604,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { it.name }.sorted() - /// Setup the ui widgets unrelated to BLE scanning private fun initCommonUI() { + scanModel.setupScan() // init our region spinner val spinner = binding.regionSpinner @@ -908,7 +909,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { override fun onPause() { super.onPause() - scanModel.stopScan() requireActivity().unregisterReceiver(updateProgressReceiver) } @@ -916,8 +916,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { override fun onResume() { super.onResume() - if (!hasCompanionDeviceApi) - scanModel.startScan() + scanModel.setupScan() // system permissions might have changed while we were away binding.provideLocationCheckbox.isChecked = myActivity.hasLocationPermission() && myActivity.hasBackgroundPermission() && (model.provideLocation.value ?: false) && isGooglePlayAvailable(requireContext()) From dd4fbc12d5c5c028e7c40b02c13c49a63f91d712 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 8 Jan 2022 17:33:20 -0300 Subject: [PATCH 6/9] add ClassicScan button, timer & permissions check --- .../java/com/geeksville/mesh/MainActivity.kt | 8 ++--- .../geeksville/mesh/ui/SettingsFragment.kt | 32 ++++++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index dd91a3397..96a84db5d 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -289,6 +289,9 @@ class MainActivity : AppCompatActivity(), Logging, return getMissingPermissions(perms) } + /** Ask the user to grant camera permission */ + fun requestBTScanPermission() = requestPermission(getCameraPermissions(), false) + /** Ask the user to grant camera permission */ fun requestCameraPermission() = requestPermission(getCameraPermissions(), false) @@ -331,7 +334,7 @@ class MainActivity : AppCompatActivity(), Logging, * * @return true if we already have the needed permissions */ - private fun requestPermission( + fun requestPermission( missingPerms: List = getMinimumPermissions(), shouldShowDialog: Boolean = true ): Boolean = @@ -534,9 +537,6 @@ class MainActivity : AppCompatActivity(), Logging, handleIntent(intent) askToRate() - - // if (!isInTestLab) - very important - even in test lab we must request permissions because we need location perms for some of our tests to pass - requestPermission() } private fun initToolbar() { 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 3e08d3a1a..4a640dfca 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -12,6 +12,8 @@ import android.content.pm.PackageManager import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.os.RemoteException import android.view.LayoutInflater import android.view.View @@ -811,12 +813,32 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { private fun initClassicScan() { - model.bluetoothEnabled.observe(viewLifecycleOwner, Observer { enabled -> - if (enabled) - scanModel.startScan() - else + binding.changeRadioButton.setOnClickListener { + if (myActivity.warnMissingPermissions()) { + myActivity.requestPermission() + } else 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 + + 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 initModernScan() { From b95dcbb26eab9dc3a139401c7a62a7466c9ded5f Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 8 Jan 2022 17:50:48 -0300 Subject: [PATCH 7/9] update common ui logic --- .../geeksville/mesh/ui/SettingsFragment.kt | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) 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 4a640dfca..4734956da 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -26,7 +26,6 @@ import android.widget.Toast import androidx.fragment.app.activityViewModels import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.hideKeyboard @@ -335,7 +334,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { */ override fun onInactive() { super.onInactive() - stopScan() + // stopScan() } } @@ -621,16 +620,18 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { }) // Only let user edit their name or set software update while connected to a radio - model.isConnected.observe(viewLifecycleOwner, Observer { _ -> - updateNodeInfo() - }) + model.isConnected.observe( + viewLifecycleOwner, { + updateNodeInfo() + updateDevicesButtons(scanModel.devices.value) + }) // Also watch myNodeInfo because it might change later - model.myNodeInfo.observe(viewLifecycleOwner, Observer { + model.myNodeInfo.observe(viewLifecycleOwner, { updateNodeInfo() }) - scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg -> + scanModel.errorText.observe(viewLifecycleOwner, { errMsg -> if (errMsg != null) { binding.scanStatusText.text = errMsg } @@ -638,11 +639,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { scanModel.devices.observe( viewLifecycleOwner, - Observer { devices -> updateDevicesButtons(devices) }) - - model.isConnected.observe( - viewLifecycleOwner, - { updateDevicesButtons(scanModel.devices.value) }) + { devices -> updateDevicesButtons(devices) }) binding.updateFirmwareButton.setOnClickListener { doFirmwareUpdate() @@ -791,22 +788,15 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } } - val hasBonded = - RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null - // get rid of the warning text once at least one device is paired. // If we are running on an emulator, always leave this message showing so we can test the worst case layout - binding.warningNotPaired.visibility = - if (hasBonded && !MockInterface.addressValid(requireContext(), "")) - View.GONE - else - View.VISIBLE - val curRadio = RadioInterfaceService.getBondedDeviceAddress(requireContext()) - if (curRadio != null) { + if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) { + binding.warningNotPaired.visibility = View.GONE binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio) } else { + binding.warningNotPaired.visibility = View.VISIBLE binding.scanStatusText.text = getString(R.string.not_paired_yet) } } From 45ce83db99c2524a3dd075700fce3b2c46b8b3a7 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sun, 9 Jan 2022 00:25:40 -0300 Subject: [PATCH 8/9] clean up companion device pairing --- .../java/com/geeksville/mesh/MainActivity.kt | 3 -- .../geeksville/mesh/ui/SettingsFragment.kt | 43 +++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 96a84db5d..b78999949 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -289,9 +289,6 @@ class MainActivity : AppCompatActivity(), Logging, return getMissingPermissions(perms) } - /** Ask the user to grant camera permission */ - fun requestBTScanPermission() = requestPermission(getCameraPermissions(), false) - /** Ask the user to grant camera permission */ fun requestCameraPermission() = requestPermission(getCameraPermissions(), false) 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 4734956da..1a0685799 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -236,7 +236,6 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { * returns true if we could start scanning, false otherwise */ fun setupScan(): Boolean { - debug("BTScan component active") selectedAddress = RadioInterfaceService.getDeviceAddress(context) return if (bluetoothAdapter == null || MockInterface.addressValid(context, "")) { @@ -678,12 +677,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } .show() - if (view.isChecked) + if (view.isChecked) { model.provideLocation.value = isChecked model.meshService?.setupProvideLocation() + } } - } - else { + } else { model.provideLocation.value = isChecked model.meshService?.stopProvideLocation() } @@ -725,10 +724,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { device.address == scanModel.selectedNotNull && device.bonded // Only show checkbox if device is still paired binding.deviceRadioGroup.addView(b) - // Once we have at least one device, don't show the "looking for" animation - it makes users think - // something is busted - binding.scanProgressBar.visibility = View.INVISIBLE - b.setOnClickListener { if (!device.bonded) // If user just clicked on us, try to bond binding.scanStatusText.setText(R.string.starting_pairing) @@ -794,7 +789,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { if (curRadio != null && !MockInterface.addressValid(requireContext(), "")) { binding.warningNotPaired.visibility = View.GONE - binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio) + // binding.scanStatusText.text = getString(R.string.current_pair).format(curRadio) } else { binding.warningNotPaired.visibility = View.VISIBLE binding.scanStatusText.text = getString(R.string.not_paired_yet) @@ -851,11 +846,11 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { // If the user has not turned on location access throw up a toast warning private fun checkLocationEnabled() { - fun hasGps(): Boolean = + val hasGps: Boolean = myActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) // FIXME If they don't have google play for now we don't check for location enabled - if (hasGps() && isGooglePlayAvailable(requireContext())) { + if (hasGps && isGooglePlayAvailable(requireContext())) { // We do this painful process because LocationManager.isEnabled is only SDK28 or latet val builder = LocationSettingsRequest.Builder() builder.setNeedBle(true) @@ -869,14 +864,17 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { .checkLocationSettings(builder.build()) fun weNeedAccess() { - context?.let { c -> - warn("Telling user we need need location accesss") - Toast.makeText( - c, - getString(R.string.location_disabled_warning), - Toast.LENGTH_SHORT - ).show() - } + warn("Telling user we need need location access") + + var warningReason = getString(R.string.location_disabled) + if (!hasCompanionDeviceApi) + warningReason = getString(R.string.location_disabled_warning) + + Toast.makeText( + requireContext(), + warningReason, + Toast.LENGTH_LONG + ).show() } locationSettingsResponse.addOnSuccessListener { @@ -947,10 +945,11 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { ).show() } else { if (!hasCompanionDeviceApi) { - if (!myActivity.warnMissingPermissions()) { + if (!myActivity.warnMissingPermissions()) + checkLocationEnabled() + } else + if (binding.provideLocationCheckbox.isChecked) checkLocationEnabled() - } - } } } } From 4bd5ea5aa403965633f962fad650719b1d774b46 Mon Sep 17 00:00:00 2001 From: andrekir Date: Sun, 9 Jan 2022 00:26:19 -0300 Subject: [PATCH 9/9] enable hasCompanionDeviceApi --- .../java/com/geeksville/mesh/service/BluetoothInterface.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt index 85e4fc091..ee725f7a1 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -5,6 +5,8 @@ import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.content.Context +import android.content.pm.PackageManager +import android.os.Build import com.geeksville.android.Logging import com.geeksville.concurrent.handledLaunch import com.geeksville.util.anonymize @@ -148,7 +150,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String */ /// Can we use the modern BLE scan API? - fun hasCompanionDeviceApi(context: Context): Boolean = false /* ALAS - not ready for production yet + fun hasCompanionDeviceApi(context: Context): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val res = context.packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP) @@ -157,7 +159,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String } else { warn("CompanionDevice API not available, falling back to classic scan") false - } */ + } /** FIXME - when adding companion device support back in, use this code to set companion device from setBondedDevice * if (BluetoothInterface.hasCompanionDeviceApi(this)) {