diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index e7156e2ec..b464ff9e6 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 = diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 38939a0fc..e9ea3b90d 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -100,6 +100,10 @@ class UIViewModel(app: Application) : AndroidViewModel(app), Logging { } + val bluetoothEnabled = object : MutableLiveData(false) { + } + + /// If the app was launched because we received a new channel intent, the Url will be here var requestedChannelUrl: Uri? = null diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index 5e929b2a9..e21dc7cf7 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt index 059a8ec95..f71ac6683 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SafeBluetooth.kt @@ -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 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 ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt index d707ce881..79061cbaf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ScreenFragment.kt @@ -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() { 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 9abba3f09..29ed24219 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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 = diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index 2ad55e6ed..d4cd48e75 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -41,11 +41,13 @@