diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 0782ec62..d7bf8c0d 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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() } diff --git a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt index 87631a68..6562f41b 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BluetoothViewModel.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothBroadcastReceiver.kt similarity index 58% rename from app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt rename to app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothBroadcastReceiver.kt index 2c288ef3..3f4c2ee7 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothStateReceiver.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothBroadcastReceiver.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt index 5f9a6d15..4502783a 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt @@ -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, - private val bluetoothStateReceiverLazy: dagger.Lazy, + private val bluetoothAdapterLazy: dagger.Lazy, + private val bluetoothBroadcastReceiverLazy: dagger.Lazy, private val dispatchers: CoroutineDispatchers, private val processLifecycle: Lifecycle, ) : Logging { - internal val enabledInternal = MutableStateFlow(false) - val enabled: StateFlow = 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 = _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>? { + return if (adapter.isEnabled) { + flow> { + 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt index 9aa992bc..7974b9b3 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepositoryModule.kt @@ -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 } } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt new file mode 100644 index 00000000..895afc9d --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothState.kt @@ -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>? = 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 7ca4ea79..9b636669 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -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()