sforkowany z mirror/meshtastic-android
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 usemaster
rodzic
f961f2e07e
commit
9592fd68de
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
|
|
Ładowanie…
Reference in New Issue