if bluetooth was disabled when we started, reattempt device connect later

This commit is contained in:
geeksville 2020-04-20 09:56:38 -07:00
parent 25740c4fe4
commit 34aa4cde05
7 changed files with 156 additions and 73 deletions

View file

@ -167,6 +167,10 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
private val btStateReceiver = BluetoothStateReceiver { enabled ->
model.bluetoothEnabled.value = enabled
}
private fun requestPermission() {
debug("Checking permissions")
@ -298,6 +302,9 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
private val isInTestLab: Boolean by lazy {
(application as GeeksvilleApplication).isInTestLab
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -305,24 +312,14 @@ class MainActivity : AppCompatActivity(), Logging,
val prefs = UIViewModel.getPreferences(this)
model.ownerName.value = prefs.getString("owner", "")!!
val isInTestLab = (application as GeeksvilleApplication).isInTestLab
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (bluetoothAdapter != null && !isInTestLab) {
bluetoothAdapter!!.takeIf { !it.isEnabled }?.apply {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
} else {
Toast.makeText(
this,
R.string.error_bluetooth,
Toast.LENGTH_LONG
)
.show()
/// Set initial bluetooth state
bluetoothAdapter?.apply {
model.bluetoothEnabled.value = isEnabled
}
/// We now want to be informed of bluetooth state
registerReceiver(btStateReceiver, btStateReceiver.intent)
// 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()
@ -399,6 +396,8 @@ class MainActivity : AppCompatActivity(), Logging,
}
override fun onDestroy() {
unregisterReceiver(btStateReceiver)
unregisterMeshReceiver()
super.onDestroy()
}
@ -669,6 +668,15 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onStart() {
super.onStart()
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (!isInTestLab) {
bluetoothAdapter?.takeIf { !it.isEnabled }?.apply {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
}
bindMeshService()
val bonded =

View file

@ -100,6 +100,10 @@ class UIViewModel(app: Application) : AndroidViewModel(app), Logging {
}
val bluetoothEnabled = object : MutableLiveData<Boolean>(false) {
}
/// If the app was launched because we received a new channel intent, the Url will be here
var requestedChannelUrl: Uri? = null

View file

@ -395,13 +395,23 @@ class RadioInterfaceService : Service(), Logging {
}
}
/**
* If the user turns on bluetooth after we start, make sure to try and reconnected then
*/
private val bluetoothStateReceiver = BluetoothStateReceiver { enabled ->
if (enabled)
setEnabled(true)
}
override fun onCreate() {
runningService = this
super.onCreate()
setEnabled(true)
registerReceiver(bluetoothStateReceiver, bluetoothStateReceiver.intent)
}
override fun onDestroy() {
unregisterReceiver(bluetoothStateReceiver)
setEnabled(false)
serviceJob.cancel()
runningService = null

View file

@ -21,6 +21,24 @@ import java.util.*
/// Return a standard BLE 128 bit UUID from the short 16 bit versions
fun longBLEUUID(hexFour: String) = UUID.fromString("0000$hexFour-0000-1000-8000-00805f9b34fb")
/**
* A helper class to call onChanged when bluetooth is enabled or disabled
*/
class BluetoothStateReceiver(val onChanged: (Boolean) -> Unit) : BroadcastReceiver() {
val intent = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
// Simulate a disconnection if the user disables bluetooth entirely
BluetoothAdapter.STATE_OFF -> onChanged(false)
BluetoothAdapter.STATE_ON -> onChanged(true)
}
}
}
}
/**
* Uses coroutines to safely access a bluetooth GATT device with a synchronous API
*
@ -51,28 +69,19 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
private val notifyHandlers = mutableMapOf<UUID, (BluetoothGattCharacteristic) -> Unit>()
/// When we see the BT stack getting disabled/renabled we handle that as a connect/disconnect event
private val btStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
val newstate = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
when (newstate) {
// Simulate a disconnection if the user disables bluetooth entirely
BluetoothAdapter.STATE_OFF -> {
if (state == BluetoothProfile.STATE_CONNECTED)
gattCallback.onConnectionStateChange(
gatt!!,
0,
BluetoothProfile.STATE_DISCONNECTED
)
else
debug("We were not connected, so ignoring bluetooth shutdown")
}
BluetoothAdapter.STATE_ON -> {
warn("requeue a connect anytime bluetooth is reenabled")
reconnect()
}
}
}
private val btStateReceiver = BluetoothStateReceiver { enabled ->
if (!enabled) {
if (state == BluetoothProfile.STATE_CONNECTED)
gattCallback.onConnectionStateChange(
gatt!!,
0,
BluetoothProfile.STATE_DISCONNECTED
)
else
debug("We were not connected, so ignoring bluetooth shutdown")
} else {
warn("requeue a connect anytime bluetooth is reenabled")
reconnect()
}
}
@ -83,7 +92,7 @@ class SafeBluetooth(private val context: Context, private val device: BluetoothD
init {
context.registerReceiver(
btStateReceiver,
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
btStateReceiver.intent
)
}

View file

@ -1,7 +1,12 @@
package com.geeksville.mesh.ui
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.mesh.R
/**
* A fragment that represents a current 'screen' in our app.
@ -9,9 +14,24 @@ import com.geeksville.android.GeeksvilleApplication
* Useful for tracking analytics
*/
open class ScreenFragment(private val screenName: String) : Fragment() {
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager =
requireContext().getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
override fun onResume() {
super.onResume()
GeeksvilleApplication.analytics.sendScreenView(screenName)
// Keep reminding user BLE is still off
if (bluetoothAdapter?.isEnabled != true) {
Toast.makeText(
requireContext(),
R.string.error_bluetooth,
Toast.LENGTH_SHORT
).show()
}
}
override fun onPause() {

View file

@ -158,11 +158,14 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
}
}
fun startScan() {
/**
* returns true if we could start scanning, false otherwise
*/
fun startScan(): Boolean {
debug("BTScan component active")
selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
if (bluetoothAdapter == null) {
return if (bluetoothAdapter == null) {
warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
@ -178,6 +181,8 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
GeeksvilleApplication.currentActivity as MainActivity,
testnodes.first().macAddress
)
true
} else {
/// The following call might return null if the user doesn't have bluetooth access permissions
val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
@ -185,20 +190,28 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
if (s == null) {
errorText.value =
context.getString(R.string.requires_bluetooth)
false
} else {
debug("starting scan")
if (scanner == null) {
debug("starting scan")
// filter and only accept devices that have a sw update service
val filter =
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
.build()
// filter and only accept devices that have a sw update service
val filter =
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
.build()
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
s.startScan(listOf(filter), settings, scanCallback)
scanner = s
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
}
}
}
@ -342,13 +355,25 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
}
/// Show the GUI for classic scanning
private fun showClassicWidgets(visible: Int) {
scanProgressBar.visibility = visible
deviceRadioGroup.visibility = visible
}
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
private fun initClassicScan() {
// Turn off the widgets for the new API
scanProgressBar.visibility = View.VISIBLE
deviceRadioGroup.visibility = View.VISIBLE
// Turn off the widgets for the new API (we turn on/off hte classic widgets when we start scanning
changeRadioButton.visibility = View.GONE
model.bluetoothEnabled.observe(viewLifecycleOwner, Observer { enabled ->
showClassicWidgets(if (enabled) View.VISIBLE else View.GONE)
if (enabled)
scanModel.startScan()
else
scanModel.stopScan()
})
scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
if (errMsg != null) {
scanStatusText.text = errMsg
@ -359,24 +384,29 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// Remove the old radio buttons and repopulate
deviceRadioGroup.removeAllViews()
var hasShownOurDevice = false
devices.values.forEach { device ->
hasShownOurDevice =
hasShownOurDevice || device.macAddress == scanModel.selectedMacAddr
addDeviceButton(device, true)
}
val adapter = scanModel.bluetoothAdapter!!
if (adapter.isEnabled) {
// This code requres BLE to be enabled
var hasShownOurDevice = false
devices.values.forEach { device ->
hasShownOurDevice =
hasShownOurDevice || device.macAddress == scanModel.selectedMacAddr
addDeviceButton(device, true)
}
// The device the user is already paired with is offline currently, still show it
// it in the list, but greyed out
val selectedAddr = scanModel.selectedMacAddr
if (!hasShownOurDevice && selectedAddr != null) {
val bDevice = scanModel.bluetoothAdapter!!.getRemoteDevice(selectedAddr)
val curDevice = BTScanModel.BTScanEntry(
bDevice.name,
bDevice.address,
bDevice.bondState == BOND_BONDED
)
addDeviceButton(curDevice, false)
// The device the user is already paired with is offline currently, still show it
// it in the list, but greyed out
val selectedAddr = scanModel.selectedMacAddr
if (!hasShownOurDevice && selectedAddr != null) {
val bDevice = scanModel.bluetoothAdapter!!.getRemoteDevice(selectedAddr)
val curDevice = BTScanModel.BTScanEntry(
bDevice.name,
bDevice.address,
bDevice.bondState == BOND_BONDED
)
addDeviceButton(curDevice, false)
}
}
val hasBonded =