Issue #369 - Expand bluetooth repository use cases

Changes:
- Adds support for obtaining bonded devices
- Adds support for obtaining BLE scanner
- Consolidates state into a single, immutable data class instance
- Simplified and renamed broadcast receiver
- Renamed view model permissionsUpdated fun to identify the intended use
master
Mike Cumings 2022-02-27 11:35:22 -08:00
rodzic f961f2e07e
commit 9592fd68de
7 zmienionych plików z 99 dodań i 44 usunięć

Wyświetl plik

@ -134,7 +134,7 @@ class MainActivity : AppCompatActivity(), Logging,
// Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
val bluetoothViewModel: BluetoothViewModel by viewModels()
private val bluetoothViewModel: BluetoothViewModel by viewModels()
val model: UIViewModel by viewModels()
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
@ -355,7 +355,7 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
bluetoothViewModel.refreshState()
bluetoothViewModel.permissionsUpdated()
}

Wyświetl plik

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.map
import javax.inject.Inject
/**
@ -13,7 +14,11 @@ import javax.inject.Inject
class BluetoothViewModel @Inject constructor(
private val bluetoothRepository: BluetoothRepository,
) : ViewModel() {
fun refreshState() = bluetoothRepository.refreshState()
/**
* Called when permissions have been updated. This causes an explicit refresh of the
* bluetooth state.
*/
fun permissionsUpdated() = bluetoothRepository.refreshState()
val enabled = bluetoothRepository.enabled.asLiveData()
val enabled = bluetoothRepository.state.map { it.enabled }.asLiveData()
}

Wyświetl plik

@ -5,20 +5,14 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.util.exceptionReporter
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* A helper class to call onChanged when bluetooth is enabled or disabled
*/
class BluetoothStateReceiver @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val bluetoothRepository: BluetoothRepository,
private val processLifecycle: Lifecycle,
class BluetoothBroadcastReceiver @Inject constructor(
private val bluetoothRepository: BluetoothRepository
) : BroadcastReceiver() {
internal val intentFilter get() = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) // Can be used for registering
@ -26,18 +20,12 @@ class BluetoothStateReceiver @Inject constructor(
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
when (intent.bluetoothAdapterState) {
// Simulate a disconnection if the user disables bluetooth entirely
BluetoothAdapter.STATE_OFF -> emitState(false)
BluetoothAdapter.STATE_ON -> emitState(true)
BluetoothAdapter.STATE_OFF -> bluetoothRepository.refreshState()
BluetoothAdapter.STATE_ON -> bluetoothRepository.refreshState()
}
}
}
private fun emitState(newState: Boolean) {
processLifecycle.coroutineScope.launch(dispatchers.default) {
bluetoothRepository.enabledInternal.emit(newState)
}
}
private val Intent.bluetoothAdapterState: Int
get() = getIntExtra(BluetoothAdapter.EXTRA_STATE,-1)
}

Wyświetl plik

@ -1,15 +1,19 @@
package com.geeksville.mesh.repository.bluetooth
import android.annotation.SuppressLint
import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.le.BluetoothLeScanner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.android.Logging
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.android.hasConnectPermission
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@ -19,18 +23,22 @@ import javax.inject.Singleton
@Singleton
class BluetoothRepository @Inject constructor(
private val application: Application,
private val bluetoothAdapterLazy: dagger.Lazy<BluetoothAdapter>,
private val bluetoothStateReceiverLazy: dagger.Lazy<BluetoothStateReceiver>,
private val bluetoothAdapterLazy: dagger.Lazy<BluetoothAdapter?>,
private val bluetoothBroadcastReceiverLazy: dagger.Lazy<BluetoothBroadcastReceiver>,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
) : Logging {
internal val enabledInternal = MutableStateFlow(false)
val enabled: StateFlow<Boolean> = enabledInternal
private val _state = MutableStateFlow(BluetoothState(
// Assume we have permission until we get our initial state update to prevent premature
// notifications to the user.
hasPermissions = true
))
val state: StateFlow<BluetoothState> = _state.asStateFlow()
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
updateBluetoothEnabled()
bluetoothStateReceiverLazy.get().let { receiver ->
updateBluetoothState()
bluetoothBroadcastReceiverLazy.get().let { receiver ->
application.registerReceiver(receiver, receiver.intentFilter)
}
}
@ -38,19 +46,57 @@ class BluetoothRepository @Inject constructor(
fun refreshState() {
processLifecycle.coroutineScope.launch(dispatchers.default) {
updateBluetoothEnabled()
updateBluetoothState()
}
}
private suspend fun updateBluetoothEnabled() {
if (application.hasConnectPermission()) {
/// ask the adapter if we have access
bluetoothAdapterLazy.get()?.let { adapter ->
enabledInternal.emit(adapter.isEnabled)
}
} else
errormsg("Still missing needed bluetooth permissions")
fun getRemoteDevice(address: String): BluetoothDevice? {
return bluetoothAdapterLazy.get()?.getRemoteDevice(address)
}
debug("Detected our bluetooth access=${enabled.value}")
fun getBluetoothLeScanner(): BluetoothLeScanner? {
return bluetoothAdapterLazy.get()?.bluetoothLeScanner
}
@SuppressLint("MissingPermission")
internal suspend fun updateBluetoothState() {
val newState: BluetoothState = bluetoothAdapterLazy.get()?.takeIf {
application.hasConnectPermission().also { hasPerms ->
if (!hasPerms) errormsg("Still missing needed bluetooth permissions")
}
}?.let { adapter ->
/// ask the adapter if we have access
BluetoothState(
hasPermissions = true,
enabled = adapter.isEnabled,
bondedDevices = createBondedDevicesFlow(adapter),
)
} ?: BluetoothState()
_state.emit(newState)
debug("Detected our bluetooth access=$newState")
}
/**
* Creates a cold Flow used to obtain the set of bonded devices.
*/
@SuppressLint("MissingPermission") // Already checked prior to calling
private suspend fun createBondedDevicesFlow(adapter: BluetoothAdapter): Flow<Set<BluetoothDevice>>? {
return if (adapter.isEnabled) {
flow<Set<BluetoothDevice>> {
withContext(dispatchers.default) {
while (true) {
emit(adapter.bondedDevices)
delay(REFRESH_DELAY_MS)
}
}
}.flowOn(dispatchers.default)
} else {
null
}
}
companion object {
const val REFRESH_DELAY_MS = 1000L
}
}

Wyświetl plik

@ -14,13 +14,13 @@ import dagger.hilt.components.SingletonComponent
interface BluetoothRepositoryModule {
companion object {
@Provides
fun provideBluetoothManager(application: Application): BluetoothManager {
return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
fun provideBluetoothManager(application: Application): BluetoothManager? {
return application.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
}
@Provides
fun provideBluetoothAdapter(service: BluetoothManager): BluetoothAdapter {
return service.adapter
fun provideBluetoothAdapter(service: BluetoothManager?): BluetoothAdapter? {
return service?.adapter
}
}
}

Wyświetl plik

@ -0,0 +1,16 @@
package com.geeksville.mesh.repository.bluetooth
import android.bluetooth.BluetoothDevice
import kotlinx.coroutines.flow.Flow
/**
* A snapshot in time of the state of the bluetooth subsystem.
*/
data class BluetoothState(
/** Whether we have adequate permissions to query bluetooth state */
val hasPermissions: Boolean = false,
/** If we have adequate permissions and bluetooth is enabled */
val enabled: Boolean = false,
/** If enabled, a cold flow of the currently bonded devices */
val bondedDevices: Flow<Set<BluetoothDevice>>? = null
)

Wyświetl plik

@ -204,8 +204,8 @@ class RadioInterfaceService : Service(), Logging {
super.onCreate()
lifecycleOwner.lifecycle.coroutineScope.launch {
bluetoothRepository.enabled.collect { enabled ->
if (enabled) {
bluetoothRepository.state.collect { state ->
if (state.enabled) {
startInterface()
} else {
stopInterface()