2020-02-13 09:25:39 -08:00
package com.geeksville.mesh.ui
2020-02-13 19:54:05 -08:00
import android.bluetooth.BluetoothDevice
2020-02-13 09:25:39 -08:00
import android.bluetooth.BluetoothManager
2020-02-18 08:56:53 -08:00
import android.bluetooth.le.*
2020-02-13 09:25:39 -08:00
import android.os.ParcelUuid
2020-02-13 19:02:40 -08:00
import androidx.compose.*
import androidx.compose.frames.modelMapOf
2020-02-13 09:25:39 -08:00
import androidx.ui.core.ContextAmbient
2020-02-28 20:09:00 -08:00
import androidx.ui.core.LayoutModifier
2020-02-13 09:25:39 -08:00
import androidx.ui.core.Text
import androidx.ui.layout.Column
2020-02-28 20:09:00 -08:00
import androidx.ui.layout.LayoutGravity
2020-02-13 19:02:40 -08:00
import androidx.ui.material.CircularProgressIndicator
2020-02-13 19:54:05 -08:00
import androidx.ui.material.EmphasisLevels
import androidx.ui.material.ProvideEmphasis
2020-02-13 19:02:40 -08:00
import androidx.ui.material.RadioGroup
2020-02-13 09:25:39 -08:00
import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
import com.geeksville.mesh.service.RadioInterfaceService
2020-02-13 19:02:40 -08:00
@Model
2020-02-18 08:56:53 -08:00
object ScanUIState {
2020-02-13 19:02:40 -08:00
var selectedMacAddr : String ? = null
var errorText : String ? = null
2020-02-18 08:56:53 -08:00
val devices = modelMapOf < String , BTScanEntry > ( )
/// Change to a new macaddr selection, updating GUI and radio
2020-02-18 09:09:49 -08:00
fun changeSelection ( context : Context , newAddr : String ) {
2020-02-18 08:56:53 -08:00
ScanState . info ( " Changing BT device to $newAddr " )
selectedMacAddr = newAddr
RadioInterfaceService . setBondedDeviceAddress ( context , newAddr )
}
}
/// FIXME, remove once compose has better lifecycle management
object ScanState : Logging {
var scanner : BluetoothLeScanner ? = null
var callback : ScanCallback ? = null // SUPER NASTY FIXME
fun stopScan ( ) {
if ( callback != null ) {
debug ( " stopping scan " )
2020-03-03 11:00:01 -08:00
try {
scanner !! . stopScan ( callback )
} catch ( ex : Throwable ) {
warn ( " Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message} " )
}
2020-02-18 10:40:02 -08:00
callback = null
2020-02-25 09:28:47 -08:00
}
2020-02-18 08:56:53 -08:00
}
2020-02-13 19:02:40 -08:00
}
@Model
2020-02-13 19:54:05 -08:00
data class BTScanEntry ( val name : String , val macAddress : String , val bonded : Boolean ) {
2020-02-18 08:56:53 -08:00
val isSelected get ( ) = macAddress == ScanUIState . selectedMacAddr
2020-02-13 19:02:40 -08:00
}
2020-02-13 09:25:39 -08:00
@Composable
fun BTScanScreen ( ) {
2020-03-02 10:30:32 -08:00
val context = ContextAmbient . current
2020-02-13 09:25:39 -08:00
/// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
val bluetoothAdapter =
2020-02-18 10:40:02 -08:00
( context . getSystemService ( Context . BLUETOOTH _SERVICE ) as BluetoothManager ? ) ?. adapter
2020-02-13 19:02:40 -08:00
2020-02-13 09:25:39 -08:00
onActive {
2020-02-18 08:56:53 -08:00
ScanUIState . selectedMacAddr = RadioInterfaceService . getBondedDeviceAddress ( context )
2020-02-13 09:25:39 -08:00
2020-02-18 09:09:49 -08:00
val scanCallback = object : ScanCallback ( ) {
override fun onScanFailed ( errorCode : Int ) {
val msg = " Unexpected bluetooth scan failure: $errorCode "
2020-02-18 10:40:02 -08:00
// error code2 seeems to be indicate hung bluetooth stack
2020-02-18 09:09:49 -08:00
ScanUIState . errorText = msg
ScanState . reportError ( msg )
}
// 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
override fun onScanResult ( callbackType : Int , result : ScanResult ) {
val addr = result . device . address
// prevent logspam because weill get get lots of redundant scan results
if ( ! ScanUIState . devices . contains ( addr ) ) {
val entry = BTScanEntry (
result . device . name ,
addr ,
result . device . bondState == BluetoothDevice . BOND _BONDED
)
ScanState . debug ( " onScanResult ${entry} " )
ScanUIState . devices [ addr ] = entry
// If nothing was selected, by default select the first thing we see
if ( ScanUIState . selectedMacAddr == null && entry . bonded )
ScanUIState . changeSelection ( context , addr )
}
}
}
2020-02-13 19:02:40 -08:00
if ( bluetoothAdapter == null ) {
2020-02-18 08:56:53 -08:00
ScanState . warn ( " No bluetooth adapter. Running under emulation? " )
2020-02-13 19:02:40 -08:00
val testnodes = listOf (
2020-02-13 19:54:05 -08:00
BTScanEntry ( " Meshtastic_ab12 " , " xx " , false ) ,
BTScanEntry ( " Meshtastic_32ac " , " xb " , true )
2020-02-13 19:02:40 -08:00
)
2020-02-18 08:56:53 -08:00
ScanUIState . devices . putAll ( testnodes . map { it . macAddress to it } )
2020-02-13 19:02:40 -08:00
// If nothing was selected, by default select the first thing we see
2020-02-18 08:56:53 -08:00
if ( ScanUIState . selectedMacAddr == null )
2020-02-18 09:09:49 -08:00
ScanUIState . changeSelection ( context , testnodes . first ( ) . macAddress )
2020-02-13 19:02:40 -08:00
} else {
2020-02-29 14:14:52 -08:00
/// The following call might return null if the user doesn't have bluetooth access permissions
val s : BluetoothLeScanner ? = bluetoothAdapter . bluetoothLeScanner
if ( s == null ) {
ScanUIState . errorText = " This application requires bluetooth access. Please grant access in android settings. "
}
else {
ScanState . 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 ( )
val settings =
ScanSettings . Builder ( ) . setScanMode ( ScanSettings . SCAN _MODE _LOW _LATENCY ) . build ( )
s . startScan ( listOf ( filter ) , settings , scanCallback )
ScanState . scanner = s
ScanState . callback = scanCallback
}
2020-02-18 08:56:53 -08:00
}
2020-02-13 09:25:39 -08:00
2020-02-18 08:56:53 -08:00
onDispose {
ScanState . stopScan ( )
2020-02-13 09:25:39 -08:00
}
}
Column {
2020-02-18 08:56:53 -08:00
if ( ScanUIState . errorText != null ) {
Text ( " An unexpected error was encountered. Please file a bug on our github: ${ScanUIState.errorText} " )
2020-02-13 19:02:40 -08:00
} else {
2020-02-18 08:56:53 -08:00
if ( ScanUIState . devices . isEmpty ( ) ) {
2020-02-28 20:09:00 -08:00
Text ( text = " Looking for Meshtastic devices... (zero found) " , modifier = LayoutGravity . Center )
2020-02-13 19:02:40 -08:00
2020-02-13 19:54:05 -08:00
CircularProgressIndicator ( ) // Show that we are searching still
} else {
// val allPaired = bluetoothAdapter?.bondedDevices.orEmpty().map { it.address }.toSet()
/ * Only let user select paired devices
2020-02-13 19:02:40 -08:00
val paired = devices . values . filter { allPaired . contains ( it . macAddress ) }
if ( paired . size < devices . size ) {
Text (
" Warning: there are nearby Meshtastic devices that are not paired with this phone. Before you can select a device, you will need to pair it in Bluetooth Settings. "
)
2020-02-13 19:54:05 -08:00
} * /
2020-02-13 19:02:40 -08:00
RadioGroup {
Column {
2020-02-18 08:56:53 -08:00
ScanUIState . devices . values . forEach {
2020-02-13 19:02:40 -08:00
// disabled pending https://issuetracker.google.com/issues/149528535
2020-02-13 19:54:05 -08:00
ProvideEmphasis ( emphasis = if ( it . bonded ) EmphasisLevels ( ) . high else EmphasisLevels ( ) . disabled ) {
RadioGroupTextItem (
selected = ( it . isSelected ) ,
onSelect = {
// If the device is paired, let user select it, otherwise start the pairing flow
2020-02-18 09:09:49 -08:00
if ( it . bonded ) {
ScanUIState . changeSelection ( context , it . macAddress )
} else {
2020-02-18 08:56:53 -08:00
ScanState . info ( " Starting bonding for $it " )
2020-02-13 19:54:05 -08:00
// We ignore missing BT adapters, because it lets us run on the emulator
2020-02-18 08:56:53 -08:00
bluetoothAdapter
?. getRemoteDevice ( it . macAddress )
2020-02-13 19:54:05 -08:00
?. createBond ( )
}
} ,
text = it . name
)
}
2020-02-13 19:02:40 -08:00
}
}
}
}
}
2020-02-13 09:25:39 -08:00
}
}
@Preview
@Composable
fun btScanScreenPreview ( ) {
BTScanScreen ( )
}