From a2f5d74bfc43b5f1b7e5745f3702ee07e514a51b Mon Sep 17 00:00:00 2001 From: andrekir Date: Sat, 29 Jan 2022 10:41:24 -0300 Subject: [PATCH 1/5] fix companion pairing --- .../com/geeksville/mesh/service/BluetoothInterface.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 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 ee725f7a1..db3239ba7 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -4,6 +4,7 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager +import android.companion.CompanionDeviceManager import android.content.Context import android.content.pm.PackageManager import android.os.Build @@ -109,9 +110,13 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String /** Return true if this address is still acceptable. For BLE that means, still bonded */ override fun addressValid(context: Context, rest: String): Boolean { - val allPaired = - getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet() - + val allPaired = if (hasCompanionDeviceApi(context)) { + val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) + deviceManager.associations.map { it }.toSet() + } else { + getBluetoothAdapter(context)?.bondedDevices.orEmpty() + .map { it.address }.toSet() + } return if (!allPaired.contains(rest)) { warn("Ignoring stale bond to ${rest.anonymize}") false From dc852b97baac032f12c15cf0b5fe45d57693a4ab Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 31 Jan 2022 21:19:54 -0300 Subject: [PATCH 2/5] add bluetooth_connect permission checks --- .../java/com/geeksville/mesh/MainActivity.kt | 33 ++++++++----------- .../mesh/android/ContextServices.kt | 18 ++++++++++ .../geeksville/mesh/ui/SettingsFragment.kt | 21 +++++++----- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 02677e207..21aa626f5 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -43,11 +43,7 @@ import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.ServiceClient import com.geeksville.concurrent.handledLaunch -import com.geeksville.mesh.android.getLocationPermissions -import com.geeksville.mesh.android.getBackgroundPermissions -import com.geeksville.mesh.android.getCameraPermissions -import com.geeksville.mesh.android.getMissingPermissions -import com.geeksville.mesh.android.getScanPermissions +import com.geeksville.mesh.android.* import com.geeksville.mesh.database.entity.Packet import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.model.ChannelSet @@ -249,15 +245,8 @@ class MainActivity : AppCompatActivity(), Logging, */ private fun updateBluetoothEnabled() { var enabled = false // assume failure - val requiredPerms: MutableList = mutableListOf() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - requiredPerms.add(Manifest.permission.BLUETOOTH_CONNECT) - } else { - requiredPerms.add(Manifest.permission.BLUETOOTH) - } - - if (getMissingPermissions(requiredPerms).isEmpty()) { + if (hasConnectPermission()) { /// ask the adapter if we have access bluetoothAdapter?.apply { enabled = isEnabled @@ -309,6 +298,7 @@ class MainActivity : AppCompatActivity(), Logging, /** * @return a localized string warning user about missing permissions. Or null if everything is find */ + @SuppressLint("InlinedApi") fun getMissingMessage( missingPerms: List = getMinimumPermissions() ): String? { @@ -338,7 +328,7 @@ class MainActivity : AppCompatActivity(), Logging, } /** Possibly prompt user to grant permissions - * @param shouldShowDialog usually true, but in cases where we've already shown a dialog elsewhere we skip it. + * @param shouldShowDialog usually false in cases where we've already shown a dialog elsewhere we skip it. * * @return true if we already have the needed permissions */ @@ -640,7 +630,7 @@ class MainActivity : AppCompatActivity(), Logging, /** * Dispatch incoming result to the correct fragment. */ - @SuppressLint("InlinedApi") + @SuppressLint("InlinedApi", "MissingPermission") override fun onActivityResult( requestCode: Int, resultCode: Int, @@ -1075,18 +1065,21 @@ class MainActivity : AppCompatActivity(), Logging, super.onStop() } + @SuppressLint("MissingPermission") override fun onStart() { super.onStart() // Ask to start bluetooth if no USB devices are visible val hasUSB = SerialInterface.findDrivers(this).isNotEmpty() if (!isInTestLab && !hasUSB) { - bluetoothAdapter?.let { - if (!it.isEnabled) { - val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) + if (hasConnectPermission()) { + bluetoothAdapter?.let { + if (!it.isEnabled) { + val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) + } } - } + } else requestPermission() } try { diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt index 71e22b0b3..c3926e771 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt @@ -29,6 +29,24 @@ fun Context.getMissingPermissions(perms: List) = perms.filter { ) != PackageManager.PERMISSION_GRANTED } +/** + * Bluetooth connect permissions (or empty if we already have what we need) + */ +fun Context.getConnectPermissions(): List { + val perms = mutableListOf() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + perms.add(Manifest.permission.BLUETOOTH_CONNECT) + } else { + perms.add(Manifest.permission.BLUETOOTH) + } + + return getMissingPermissions(perms) +} + +/** @return true if the user already has Bluetooth connect permission */ +fun Context.hasConnectPermission() = getConnectPermissions().isEmpty() + /** * Bluetooth scan/discovery permissions (or empty if we already have what we need) */ 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 9a858122c..b6f6a5651 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -33,11 +33,7 @@ import com.geeksville.android.isGooglePlayAvailable import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R import com.geeksville.mesh.RadioConfigProtos -import com.geeksville.mesh.android.bluetoothManager -import com.geeksville.mesh.android.hasScanPermission -import com.geeksville.mesh.android.hasLocationPermission -import com.geeksville.mesh.android.hasBackgroundPermission -import com.geeksville.mesh.android.usbManager +import com.geeksville.mesh.android.* import com.geeksville.mesh.databinding.SettingsFragmentBinding import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.* @@ -68,6 +64,7 @@ fun changeDeviceSelection(context: MainActivity, newAddr: String?) { } /// Show the UI asking the user to bond with a device, call changeSelection() if/when bonding completes +@SuppressLint("MissingPermission") private fun requestBonding( activity: MainActivity, device: BluetoothDevice, @@ -102,7 +99,11 @@ private fun requestBonding( activity.registerReceiver(bondChangedReceiver, filter) // We ignore missing BT adapters, because it lets us run on the emulator - device.createBond() + try { + device.createBond() + } catch (ex: Throwable) { + SLogging.warn("Failed creating Bluetooth bond: ${ex.message}") + } } class BTScanModel(app: Application) : AndroidViewModel(app), Logging { @@ -180,13 +181,14 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { // For each device that appears in our scan, ask for its GATT, when the gatt arrives, // check if it is an eligable device and store it in our list of candidates // if that device later disconnects remove it as a candidate + @SuppressLint("MissingPermission") override fun onScanResult(callbackType: Int, result: ScanResult) { if ((result.device.name?.startsWith("Mesh") == true)) { val addr = result.device.address 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 isBonded = result.device.bondState == BOND_BONDED val oldDevs = devices.value!! val oldEntry = oldDevs[fullAddr] if (oldEntry == null || oldEntry.bonded != isBonded) { // Don't spam the GUI with endless updates for non changing nodes @@ -222,6 +224,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { devices.value = oldDevs // trigger gui updates } + @SuppressLint("MissingPermission") fun stopScan() { if (scanner != null) { debug("stopping scan") @@ -296,6 +299,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { } } + @SuppressLint("MissingPermission") fun startScan() { /// The following call might return null if the user doesn't have bluetooth access permissions val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner @@ -745,6 +749,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } } + @SuppressLint("MissingPermission") private fun updateDevicesButtons(devices: MutableMap?) { // Remove the old radio buttons and repopulate binding.deviceRadioGroup.removeAllViews() @@ -767,7 +772,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { // and before use val bleAddr = scanModel.selectedBluetooth - if (bleAddr != null && adapter != null) { + if (bleAddr != null && adapter != null && myActivity.hasConnectPermission()) { val bDevice = adapter.getRemoteDevice(bleAddr) if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared From 084c16bfe9cd46a721bb8be8bcfa0c1f990e78f7 Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 31 Jan 2022 21:55:24 -0300 Subject: [PATCH 3/5] clean up and reformat --- .../java/com/geeksville/mesh/MainActivity.kt | 21 ++++-------- .../mesh/service/BluetoothInterface.kt | 14 ++++---- .../geeksville/mesh/ui/SettingsFragment.kt | 34 +++++++++++-------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 21aa626f5..9839c59c7 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -36,7 +36,6 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction -import androidx.lifecycle.Observer import androidx.viewpager2.adapter.FragmentStateAdapter import com.geeksville.android.BindFailedException import com.geeksville.android.GeeksvilleApplication @@ -66,7 +65,6 @@ import com.vorlonsoft.android.rate.AppRate import com.vorlonsoft.android.rate.StoreType import kotlinx.coroutines.* import java.io.FileOutputStream -import java.lang.Runnable import java.nio.charset.Charset import java.text.DateFormat import java.util.* @@ -194,7 +192,7 @@ class MainActivity : AppCompatActivity(), Logging, } } - private val btStateReceiver = BluetoothStateReceiver { _ -> + private val btStateReceiver = BluetoothStateReceiver { updateBluetoothEnabled() } @@ -532,9 +530,9 @@ class MainActivity : AppCompatActivity(), Logging, tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon) }.attach() - model.isConnected.observe(this, Observer { connected -> + model.isConnected.observe(this) { connected -> updateConnectionStatusImage(connected) - }) + } // Handle any intent handleIntent(intent) @@ -927,10 +925,10 @@ class MainActivity : AppCompatActivity(), Logging, private var connectionJob: Job? = null private val mesh = object : - ServiceClient({ - com.geeksville.mesh.IMeshService.Stub.asInterface(it) + ServiceClient({ + IMeshService.Stub.asInterface(it) }) { - override fun onConnected(service: com.geeksville.mesh.IMeshService) { + override fun onConnected(service: IMeshService) { /* Note: we must call this callback in a coroutine. Because apparently there is only a single activity looper thread. and if that onConnected override @@ -1147,12 +1145,7 @@ class MainActivity : AppCompatActivity(), Logging, val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM) .format(Date(System.currentTimeMillis())) model.messagesState.sendMessage(str) - handler.postDelayed( - Runnable { - postPing() - }, - 30000 - ) + handler.postDelayed({ postPing() }, 30000) } item.isChecked = !item.isChecked // toggle ping test if (item.isChecked) 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 db3239ba7..50c860f71 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -1,5 +1,6 @@ package com.geeksville.mesh.service +import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattService @@ -92,13 +93,13 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String } /// this service UUID is publically visible for scanning - val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") + val BTM_SERVICE_UUID: UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd") - val BTM_FROMRADIO_CHARACTER = + val BTM_FROMRADIO_CHARACTER: UUID = UUID.fromString("8ba2bcc2-ee02-4a55-a531-c525c5e454d5") - val BTM_TORADIO_CHARACTER = + val BTM_TORADIO_CHARACTER: UUID = UUID.fromString("f75c76d2-129e-4dad-a1dd-7866124401e7") - val BTM_FROMNUM_CHARACTER = + val BTM_FROMNUM_CHARACTER: UUID = UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453") /// Get our bluetooth adapter (should always succeed except on emulator @@ -109,6 +110,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String } /** Return true if this address is still acceptable. For BLE that means, still bonded */ + @SuppressLint("NewApi", "MissingPermission") override fun addressValid(context: Context, rest: String): Boolean { val allPaired = if (hasCompanionDeviceApi(context)) { val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) @@ -196,7 +198,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String ?: throw RadioNotConnectedException("No GATT") /// Our service - note - it is possible to get back a null response for getService if the device services haven't yet been found - val bservice + private val bservice get(): BluetoothGattService = device.getService(BTM_SERVICE_UUID) ?: throw RadioNotConnectedException("BLE service not found") @@ -268,7 +270,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String /** * We had some problem, schedule a reconnection attempt (if one isn't already queued) */ - fun scheduleReconnect(reason: String) { + private fun scheduleReconnect(reason: String) { if (reconnectJob == null) { warn("Scheduling reconnect because $reason") reconnectJob = service.serviceScope.handledLaunch { retryDueToException() } 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 b6f6a5651..4c63e4123 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -619,44 +619,44 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) spinner.adapter = regionAdapter - model.bluetoothEnabled.observe(viewLifecycleOwner, { + model.bluetoothEnabled.observe(viewLifecycleOwner) { if (it) binding.changeRadioButton.show() else binding.changeRadioButton.hide() - }) + } - model.ownerName.observe(viewLifecycleOwner, { name -> + model.ownerName.observe(viewLifecycleOwner) { name -> binding.usernameEditText.setText(name) - }) + } // Only let user edit their name or set software update while connected to a radio - model.isConnected.observe(viewLifecycleOwner, { + model.isConnected.observe(viewLifecycleOwner) { updateNodeInfo() updateDevicesButtons(scanModel.devices.value) - }) + } - model.radioConfig.observe(viewLifecycleOwner, { + model.radioConfig.observe(viewLifecycleOwner) { binding.provideLocationCheckbox.isEnabled = isGooglePlayAvailable(requireContext()) && model.locationShare ?: true if (model.locationShare == false) { model.provideLocation.value = false binding.provideLocationCheckbox.isChecked = false } - }) + } // Also watch myNodeInfo because it might change later - model.myNodeInfo.observe(viewLifecycleOwner, { + model.myNodeInfo.observe(viewLifecycleOwner) { updateNodeInfo() - }) + } - scanModel.errorText.observe(viewLifecycleOwner, { errMsg -> + scanModel.errorText.observe(viewLifecycleOwner) { errMsg -> if (errMsg != null) { binding.scanStatusText.text = errMsg } - }) + } - scanModel.devices.observe(viewLifecycleOwner, { devices -> + scanModel.devices.observe(viewLifecycleOwner) { devices -> updateDevicesButtons(devices) - }) + } binding.updateFirmwareButton.setOnClickListener { doFirmwareUpdate() @@ -961,7 +961,11 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { if (!hasUSB) { // Warn user if BLE is disabled if (scanModel.bluetoothAdapter?.isEnabled != true) { - Snackbar.make(binding.changeRadioButton, R.string.error_bluetooth, Snackbar.LENGTH_INDEFINITE) + Snackbar.make( + binding.changeRadioButton, + R.string.error_bluetooth, + Snackbar.LENGTH_INDEFINITE + ) .setAction(R.string.okay) { // dismiss } From 2bd5354059573c42228287f24b4540bd34a0cf7a Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 31 Jan 2022 22:01:33 -0300 Subject: [PATCH 4/5] update gradle --- app/build.gradle | 4 ++-- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c30d5857c..dc7666135 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -88,10 +88,10 @@ android { kotlinOptions { jvmTarget = "1.8" } - - lintOptions { + lint { abortOnError false } + } play { diff --git a/build.gradle b/build.gradle index b2bfca256..c15e31f78 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 30c304d34..ba1270ea0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip From 066027c56b9b430eae455cff02315f00484a570c Mon Sep 17 00:00:00 2001 From: andrekir Date: Mon, 31 Jan 2022 23:34:12 -0300 Subject: [PATCH 5/5] 1.2.55 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index dc7666135..a9cccafc4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,8 +42,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added - versionCode 20254 // format is Mmmss (where M is 1+the numeric major number - versionName "1.2.54" + versionCode 20255 // format is Mmmss (where M is 1+the numeric major number + versionName "1.2.55" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // per https://developer.android.com/studio/write/vector-asset-studio