mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 07:24:22 +01:00
Changed profile service api style
This commit is contained in:
@@ -47,7 +47,7 @@ dependencies {
|
|||||||
implementation(project(":lib_analytics"))
|
implementation(project(":lib_analytics"))
|
||||||
implementation(project(":profile-parsers"))
|
implementation(project(":profile-parsers"))
|
||||||
implementation(project(":profile_manager"))
|
implementation(project(":profile_manager"))
|
||||||
implementation(project(":profile"))
|
api(project(":profile"))
|
||||||
implementation(project(":profile_data"))
|
implementation(project(":profile_data"))
|
||||||
implementation(project(":lib_ui"))
|
implementation(project(":lib_ui"))
|
||||||
implementation(project(":lib_utils"))
|
implementation(project(":lib_utils"))
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import no.nordicsemi.android.nrftoolbox.R
|
|||||||
import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel
|
import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel
|
||||||
import no.nordicsemi.android.nrftoolbox.viewmodel.UiEvent
|
import no.nordicsemi.android.nrftoolbox.viewmodel.UiEvent
|
||||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -82,240 +83,250 @@ internal fun HomeView() {
|
|||||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp),
|
||||||
)
|
)
|
||||||
if (state.connectedDevices.isNotEmpty()) {
|
if (state.connectedDevices.isNotEmpty()) {
|
||||||
|
Timber.tag("AAA").d("Connected devices: ${state.connectedDevices.keys}")
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
state.connectedDevices.values.forEach { (peripheral, services) ->
|
state.connectedDevices.keys.forEach {
|
||||||
// Skip if no services
|
state.connectedDevices[it]?.let { deviceData ->
|
||||||
if (services.isEmpty()) return@forEach
|
if (deviceData.connectionState.isConnected) {
|
||||||
// Case 1: If only one service, show it directly like battery service
|
// Skip if no services
|
||||||
if (services.size == 1 && services.first().profile == Profile.BATTERY) {
|
if (deviceData.services.isEmpty()) return@forEach
|
||||||
FeatureButton(
|
// Case 1: If only one service, show it directly like battery service
|
||||||
iconId = R.drawable.ic_battery,
|
if (deviceData.services.size == 1 && deviceData.services.first().profile == Profile.BATTERY) {
|
||||||
description = R.string.battery_module_full,
|
FeatureButton(
|
||||||
deviceName = peripheral.name,
|
iconId = R.drawable.ic_battery,
|
||||||
deviceAddress = peripheral.address,
|
description = R.string.battery_module_full,
|
||||||
onClick = {
|
deviceName = deviceData.peripheral.name,
|
||||||
onEvent(
|
deviceAddress = deviceData.peripheral.address,
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
services.first().profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Case 2: Show the first *non-Battery* profile.
|
|
||||||
// This ensures only one service is shown per peripheral when multiple services are available.
|
|
||||||
services.firstOrNull { it.profile != Profile.BATTERY }
|
|
||||||
?.let { serviceManager ->
|
|
||||||
when (serviceManager.profile) {
|
|
||||||
Profile.HRS -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_hrs,
|
|
||||||
description = R.string.hrs_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onEvent(
|
onEvent(
|
||||||
UiEvent.OnDeviceClick(
|
UiEvent.OnDeviceClick(
|
||||||
peripheral.address,
|
deviceData.peripheral.address,
|
||||||
serviceManager.profile
|
deviceData.services.first().profile
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Profile.HTS -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_hts,
|
|
||||||
description = R.string.hts_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.BPS -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_bps,
|
|
||||||
description = R.string.bps_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.GLS -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_gls,
|
|
||||||
description = R.string.gls_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.CGM -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_cgm,
|
|
||||||
description = R.string.cgm_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.RSCS -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_rscs,
|
|
||||||
description = R.string.rscs_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.DFS -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_distance,
|
|
||||||
description = R.string.direction_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.CSC -> FeatureButton(
|
|
||||||
iconId = R.drawable.ic_csc,
|
|
||||||
description = R.string.csc_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Profile.THROUGHPUT -> {
|
|
||||||
FeatureButton(
|
|
||||||
iconId = Icons.Default.SyncAlt,
|
|
||||||
description = R.string.throughput_module,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Profile.UART -> {
|
|
||||||
FeatureButton(
|
|
||||||
iconId = R.drawable.ic_uart,
|
|
||||||
description = R.string.uart_module_full,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Profile.CHANNEL_SOUNDING -> {
|
|
||||||
FeatureButton(
|
|
||||||
iconId = Icons.Default.SocialDistance,
|
|
||||||
description = R.string.channel_sounding_module,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Profile.LBS -> {
|
|
||||||
FeatureButton(
|
|
||||||
iconId = Icons.Default.Lightbulb,
|
|
||||||
description = R.string.lbs_blinky_module,
|
|
||||||
deviceName = peripheral.name,
|
|
||||||
deviceAddress = peripheral.address,
|
|
||||||
profileNames = services.map { it.profile.toString() },
|
|
||||||
onClick = {
|
|
||||||
onEvent(
|
|
||||||
UiEvent.OnDeviceClick(
|
|
||||||
peripheral.address,
|
|
||||||
serviceManager.profile
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
// TODO: Add more profiles
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Case 2: Show the first *non-Battery* profile.
|
||||||
|
// This ensures only one service is shown per peripheral when multiple services are available.
|
||||||
|
deviceData.services.firstOrNull { it.profile != Profile.BATTERY }
|
||||||
|
?.let { serviceManager ->
|
||||||
|
val peripheral = deviceData.peripheral
|
||||||
|
val services = deviceData.services
|
||||||
|
Timber.tag("AAA")
|
||||||
|
.d("Displaying device: ${peripheral.address} with services: ${services.map { it.profile }}")
|
||||||
|
when (serviceManager.profile) {
|
||||||
|
Profile.HRS -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_hrs,
|
||||||
|
description = R.string.hrs_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.HTS -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_hts,
|
||||||
|
description = R.string.hts_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.BPS -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_bps,
|
||||||
|
description = R.string.bps_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.GLS -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_gls,
|
||||||
|
description = R.string.gls_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.CGM -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_cgm,
|
||||||
|
description = R.string.cgm_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.RSCS -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_rscs,
|
||||||
|
description = R.string.rscs_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.DFS -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_distance,
|
||||||
|
description = R.string.direction_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.CSC -> FeatureButton(
|
||||||
|
iconId = R.drawable.ic_csc,
|
||||||
|
description = R.string.csc_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Profile.THROUGHPUT -> {
|
||||||
|
FeatureButton(
|
||||||
|
iconId = Icons.Default.SyncAlt,
|
||||||
|
description = R.string.throughput_module,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile.UART -> {
|
||||||
|
FeatureButton(
|
||||||
|
iconId = R.drawable.ic_uart,
|
||||||
|
description = R.string.uart_module_full,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile.CHANNEL_SOUNDING -> {
|
||||||
|
FeatureButton(
|
||||||
|
iconId = Icons.Default.SocialDistance,
|
||||||
|
description = R.string.channel_sounding_module,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile.LBS -> {
|
||||||
|
FeatureButton(
|
||||||
|
iconId = Icons.Default.Lightbulb,
|
||||||
|
description = R.string.lbs_blinky_module,
|
||||||
|
deviceName = peripheral.name,
|
||||||
|
deviceAddress = peripheral.address,
|
||||||
|
profileNames = services.map { it.profile.toString() },
|
||||||
|
onClick = {
|
||||||
|
onEvent(
|
||||||
|
UiEvent.OnDeviceClick(
|
||||||
|
peripheral.address,
|
||||||
|
serviceManager.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// TODO: Add more profiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ import no.nordicsemi.android.analytics.Link
|
|||||||
import no.nordicsemi.android.analytics.ProfileOpenEvent
|
import no.nordicsemi.android.analytics.ProfileOpenEvent
|
||||||
import no.nordicsemi.android.common.navigation.Navigator
|
import no.nordicsemi.android.common.navigation.Navigator
|
||||||
import no.nordicsemi.android.nrftoolbox.ScannerDestinationId
|
import no.nordicsemi.android.nrftoolbox.ScannerDestinationId
|
||||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
import no.nordicsemi.android.service.profile.ServiceApi
|
||||||
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
|
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
|
||||||
import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository
|
import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository
|
||||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal data class HomeViewState(
|
internal data class HomeViewState(
|
||||||
val connectedDevices: Map<String, Pair<Peripheral, List<ServiceManager>>> = emptyMap(),
|
val connectedDevices: Map<String, ServiceApi.DeviceData> = emptyMap(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val GITHUB_REPO_URL = "https://github.com/NordicSemiconductor/Android-nRF-Toolbox.git"
|
private const val GITHUB_REPO_URL = "https://github.com/NordicSemiconductor/Android-nRF-Toolbox.git"
|
||||||
@@ -39,6 +39,7 @@ internal class HomeViewModel @Inject constructor(
|
|||||||
init {
|
init {
|
||||||
// Observe connected devices from the repository
|
// Observe connected devices from the repository
|
||||||
deviceRepository.connectedDevices.onEach { devices ->
|
deviceRepository.connectedDevices.onEach { devices ->
|
||||||
|
Timber.tag("AAA").d("Connected devices updated: ${devices.keys}")
|
||||||
_state.update { currentState ->
|
_state.update { currentState ->
|
||||||
currentState.copy(connectedDevices = devices)
|
currentState.copy(connectedDevices = devices)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,23 @@ import android.os.Binder
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import no.nordicsemi.android.log.timber.nRFLoggerTree
|
import no.nordicsemi.android.log.timber.nRFLoggerTree
|
||||||
import no.nordicsemi.android.service.NotificationService
|
import no.nordicsemi.android.service.NotificationService
|
||||||
import no.nordicsemi.android.service.R
|
import no.nordicsemi.android.service.R
|
||||||
import no.nordicsemi.android.toolbox.lib.utils.spec.CGMS_SERVICE_UUID
|
|
||||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
||||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManagerFactory
|
import no.nordicsemi.android.toolbox.profile.manager.ServiceManagerFactory
|
||||||
import no.nordicsemi.android.ui.view.internal.DisconnectReason
|
import no.nordicsemi.android.ui.view.internal.DisconnectReason
|
||||||
|
import no.nordicsemi.kotlin.ble.client.RemoteService
|
||||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager.ConnectionOptions
|
import no.nordicsemi.kotlin.ble.client.android.CentralManager.ConnectionOptions
|
||||||
import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority
|
import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority
|
||||||
@@ -32,13 +29,10 @@ import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
|||||||
import no.nordicsemi.kotlin.ble.core.BondState
|
import no.nordicsemi.kotlin.ble.core.BondState
|
||||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||||
import no.nordicsemi.kotlin.ble.core.Manager
|
import no.nordicsemi.kotlin.ble.core.Manager
|
||||||
import no.nordicsemi.kotlin.ble.core.Phy
|
|
||||||
import no.nordicsemi.kotlin.ble.core.PhyOption
|
|
||||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
import kotlin.uuid.toKotlinUuid
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class ProfileService : NotificationService() {
|
internal class ProfileService : NotificationService() {
|
||||||
@@ -46,15 +40,13 @@ internal class ProfileService : NotificationService() {
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var centralManager: CentralManager
|
lateinit var centralManager: CentralManager
|
||||||
private var logger: nRFLoggerTree? = null
|
private var logger: nRFLoggerTree? = null
|
||||||
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
private val managedConnections = mutableMapOf<String, Job>()
|
||||||
|
|
||||||
private val _connectedDevices =
|
private val _devices = MutableStateFlow<Map<String, ServiceApi.DeviceData>>(emptyMap())
|
||||||
MutableStateFlow<Map<String, Pair<Peripheral, List<ServiceManager>>>>(emptyMap())
|
private val _isMissingServices = MutableStateFlow<Map<String, Boolean>>(emptyMap())
|
||||||
private val _isMissingServices = MutableStateFlow(false)
|
private val _disconnectionEvent = MutableStateFlow<ServiceApi.DisconnectionEvent?>(null)
|
||||||
private val _disconnectionReason = MutableStateFlow<DeviceDisconnectionReason?>(null)
|
|
||||||
|
|
||||||
private val connectionJobs = mutableMapOf<String, Job>()
|
|
||||||
private val serviceHandlingJob = mutableMapOf<String, Job>()
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
super.onBind(intent)
|
super.onBind(intent)
|
||||||
@@ -63,260 +55,244 @@ internal class ProfileService : NotificationService() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// Observe the Bluetooth state
|
// Observe the Bluetooth state to handle global disconnection reasons.
|
||||||
centralManager.state.onEach { state ->
|
centralManager.state.onEach { state ->
|
||||||
if (state == Manager.State.POWERED_OFF) {
|
if (state == Manager.State.POWERED_OFF) {
|
||||||
_disconnectionReason.tryEmit(CustomReason(DisconnectReason.BLUETOOTH_OFF))
|
_disconnectionEvent.value = ServiceApi.DisconnectionEvent(
|
||||||
|
"all_devices", // Generic address
|
||||||
|
CustomReason(DisconnectReason.BLUETOOTH_OFF)
|
||||||
|
)
|
||||||
|
// Optionally disconnect all devices
|
||||||
|
_devices.value.keys.forEach { disconnect(it) }
|
||||||
}
|
}
|
||||||
}.launchIn(lifecycleScope)
|
}.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
intent?.getStringExtra(DEVICE_ADDRESS)?.let { deviceAddress ->
|
intent?.getStringExtra(DEVICE_ADDRESS)?.let { address ->
|
||||||
initLogger(deviceAddress)
|
connect(address)
|
||||||
initiateConnection(deviceAddress)
|
|
||||||
}
|
}
|
||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class LocalBinder : Binder(), ServiceApi {
|
override fun onDestroy() {
|
||||||
override val connectedDevices: Flow<Map<String, Pair<Peripheral, List<ServiceManager>>>>
|
managedConnections.values.forEach { it.cancel() }
|
||||||
get() = _connectedDevices.asSharedFlow()
|
uprootLogger()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
override val isMissingServices: Flow<Boolean>
|
private fun connect(address: String) {
|
||||||
get() = _isMissingServices.asStateFlow()
|
// Return if already managed to avoid multiple connection jobs.
|
||||||
|
if (managedConnections.containsKey(address)) return
|
||||||
|
|
||||||
override val disconnectionReason: StateFlow<DeviceDisconnectionReason?>
|
initLogger(address) // Initialize logger for the new device.
|
||||||
get() = _disconnectionReason.asStateFlow()
|
|
||||||
|
|
||||||
override suspend fun getMaxWriteValue(address: String, writeType: WriteType): Int? {
|
val peripheral = centralManager.getPeripheralById(address) ?: run {
|
||||||
val peripheral = getPeripheralById(address) ?: return null
|
Timber.w("Peripheral with address $address not found.")
|
||||||
if (!peripheral.isConnected) return null
|
return
|
||||||
|
|
||||||
return try {
|
|
||||||
peripheral.requestHighestValueLength()
|
|
||||||
peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
|
|
||||||
peripheral.setPreferredPhy(Phy.PHY_LE_2M, Phy.PHY_LE_2M, PhyOption.S2)
|
|
||||||
peripheral.maximumWriteValueLength(writeType)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e("Failed to configure $address for MTU change with reason: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun createBonding(address: String) {
|
val job = lifecycleScope.launch {
|
||||||
val peripheral = getPeripheralById(address)
|
// Launch the initial connection attempt.
|
||||||
peripheral?.bondState
|
launch {
|
||||||
?.onEach { state ->
|
try {
|
||||||
if (state == BondState.NONE) {
|
centralManager.connect(peripheral, options = ConnectionOptions.Direct())
|
||||||
peripheral.createBond()
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to connect to $address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe connection state changes and react accordingly.
|
||||||
|
observeConnectionState(peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
managedConnections[address] = job
|
||||||
|
job.invokeOnCompletion {
|
||||||
|
// Clean up when the management coroutine is cancelled.
|
||||||
|
handleDisconnection(address, "Job cancelled")
|
||||||
|
managedConnections.remove(address)
|
||||||
|
stopServiceIfNoDevices()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun CoroutineScope.observeConnectionState(peripheral: Peripheral) {
|
||||||
|
peripheral.state
|
||||||
|
.onEach { state ->
|
||||||
|
_devices.update {
|
||||||
|
it + (peripheral.address to (it[peripheral.address]?.copy(connectionState = state)
|
||||||
|
?: ServiceApi.DeviceData(peripheral, state)))
|
||||||
|
}
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
ConnectionState.Connected -> {
|
||||||
|
try {
|
||||||
|
discoverAndObserveServices(peripheral, this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Service discovery failed for ${peripheral.address}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
is ConnectionState.Disconnected -> {
|
||||||
|
Timber.tag("AAAA").d("Disconnected State: ${peripheral.address}")
|
||||||
|
val reason = state.reason ?: DisconnectReason.UNKNOWN
|
||||||
|
_disconnectionEvent.value =
|
||||||
|
ServiceApi.DisconnectionEvent(
|
||||||
|
peripheral.address,
|
||||||
|
StateReason(reason as ConnectionState.Disconnected.Reason)
|
||||||
|
)
|
||||||
|
_devices.update { it - peripheral.address }
|
||||||
|
Timber.tag("AAA").d("Devices after disconnection: ${_devices.value.keys}")
|
||||||
|
handleDisconnection(peripheral.address, reason.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
// Handle connecting/disconnecting states if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?.filter { it == BondState.BONDED }
|
}.launchIn(this)
|
||||||
?.first() // suspend until bonded
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
private fun discoverAndObserveServices(
|
||||||
|
peripheral: Peripheral,
|
||||||
|
scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
peripheral
|
||||||
|
.services()
|
||||||
|
.onEach { service ->
|
||||||
|
var isMissing: Boolean? = null
|
||||||
|
service?.map { removeService ->
|
||||||
|
ServiceManagerFactory
|
||||||
|
.createServiceManager(removeService.uuid)
|
||||||
|
?.also { manager ->
|
||||||
|
Timber.tag("AAA")
|
||||||
|
.d("Found ServiceManager for service ${removeService.uuid}")
|
||||||
|
isMissing = false
|
||||||
|
_devices.update {
|
||||||
|
it + (peripheral.address to it[peripheral.address]!!.copy(
|
||||||
|
services = it[peripheral.address]?.services?.plus(
|
||||||
|
manager
|
||||||
|
) ?: listOf(manager)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
// _isMissingServices.update { it - peripheral.address }
|
||||||
|
scope.launch { // Launch observation for each service.
|
||||||
|
observeService(peripheral, removeService, manager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isMissing != false) {
|
||||||
|
Timber.tag("AAA").w("Peripheral ${peripheral.address} is missing services")
|
||||||
|
_isMissingServices.update { it + (peripheral.address to true) }
|
||||||
|
} else {
|
||||||
|
_isMissingServices.update { it - peripheral.address }
|
||||||
|
// If all required services are found, log it.
|
||||||
|
Timber.tag("AAA")
|
||||||
|
.d("Peripheral ${peripheral.address} has all required services")
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun observeService(
|
||||||
|
peripheral: Peripheral,
|
||||||
|
service: RemoteService,
|
||||||
|
manager: ServiceManager
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// if (manager.requiresBonding(service.uuid) && peripheral.bondingState != BondState.BONDED) {
|
||||||
|
// peripheral.ensureBonded()
|
||||||
|
// }
|
||||||
|
manager.observeServiceInteractions(peripheral.address, service, lifecycleScope)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("ObserveServices").e(e)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPeripheralById(address: String?): Peripheral? =
|
private fun disconnect(address: String) {
|
||||||
address?.let { centralManager.getPeripheralById(it) }
|
centralManager.getPeripheralById(address)?.let { peripheral ->
|
||||||
|
|
||||||
override fun disconnect(deviceAddress: String) {
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
getPeripheralById(deviceAddress)
|
peripheral.disconnect()
|
||||||
?.let { peripheral ->
|
handleDisconnection(address, "Disconnected by user")
|
||||||
if (peripheral.isConnected) peripheral.disconnect()
|
|
||||||
handleDisconnection(deviceAddress)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Couldn't disconnect from the $deviceAddress")
|
Timber.e(e, "Failed to disconnect from $address")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
managedConnections[address]?.cancel()
|
||||||
override fun connectionState(address: String): StateFlow<ConnectionState>? {
|
|
||||||
val peripheral = getPeripheralById(address) ?: return null
|
|
||||||
return peripheral.state.also { stateFlow ->
|
|
||||||
connectionJobs[address]?.cancel()
|
|
||||||
val job = stateFlow.onEach { state ->
|
|
||||||
when (state) {
|
|
||||||
ConnectionState.Connected -> {
|
|
||||||
_isMissingServices.tryEmit(false)
|
|
||||||
// Discover services if not already discovered
|
|
||||||
if (_connectedDevices.value[address] == null) {
|
|
||||||
discoverServices(peripheral)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState.Connecting, ConnectionState.Disconnecting -> {
|
|
||||||
// No action needed, just observing the state
|
|
||||||
}
|
|
||||||
|
|
||||||
is ConnectionState.Disconnected -> {
|
|
||||||
if (state.reason == null) {
|
|
||||||
_disconnectionReason.tryEmit(null)
|
|
||||||
return@onEach
|
|
||||||
} else
|
|
||||||
_disconnectionReason.tryEmit(StateReason(state.reason!!))
|
|
||||||
connectionJobs[address]?.cancel()
|
|
||||||
handleDisconnection(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onCompletion {
|
|
||||||
connectionJobs[address]?.cancel()
|
|
||||||
connectionJobs.remove(address)
|
|
||||||
}.launchIn(lifecycleScope)
|
|
||||||
connectionJobs[address] = job
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun handleDisconnection(address: String, reason: String) {
|
||||||
* Connect to the peripheral and observe its state.
|
Timber.d("Handling disconnection for $address, reason: $reason")
|
||||||
*/
|
_devices.update { it - address }
|
||||||
private fun initiateConnection(deviceAddress: String) {
|
Timber.tag("AAA").d("Devices after disconnection: ${_devices.value.keys}")
|
||||||
centralManager.getPeripheralById(deviceAddress)?.let { peripheral ->
|
_isMissingServices.update { it - address }
|
||||||
lifecycleScope.launch { connectPeripheral(peripheral) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun connectPeripheral(peripheral: Peripheral) {
|
|
||||||
try {
|
|
||||||
centralManager.connect(peripheral, options = ConnectionOptions.Direct())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to connect to the ${peripheral.address}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discover services and characteristics for the connected [peripheral].
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
|
||||||
private fun discoverServices(peripheral: Peripheral) {
|
|
||||||
val discoveredServices = mutableListOf<ServiceManager>()
|
|
||||||
serviceHandlingJob[peripheral.address]?.cancel()
|
|
||||||
val job = peripheral.services().onEach { remoteServices ->
|
|
||||||
remoteServices?.forEach { remoteService ->
|
|
||||||
val serviceManager = ServiceManagerFactory.createServiceManager(remoteService.uuid)
|
|
||||||
serviceManager?.let { manager ->
|
|
||||||
Timber.tag("DiscoverServices").i("${manager.profile}")
|
|
||||||
discoveredServices.add(manager)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
val requiresBonding =
|
|
||||||
remoteService.uuid == CGMS_SERVICE_UUID.toKotlinUuid() && peripheral.hasBondInformation
|
|
||||||
|
|
||||||
if (requiresBonding) {
|
|
||||||
peripheral.bondState
|
|
||||||
.onEach { if (it == BondState.NONE) peripheral.createBond() }
|
|
||||||
.filter { it == BondState.BONDED }
|
|
||||||
.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.observeServiceInteractions(
|
|
||||||
peripheral.address,
|
|
||||||
remoteService,
|
|
||||||
this
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("ObserveServices").e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when {
|
|
||||||
discoveredServices.isEmpty() -> {
|
|
||||||
if (remoteServices?.isNotEmpty() == true) {
|
|
||||||
_isMissingServices.tryEmit(true)
|
|
||||||
serviceHandlingJob[peripheral.address]?.cancel()
|
|
||||||
serviceHandlingJob.remove(peripheral.address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
peripheral.isConnected -> {
|
|
||||||
_isMissingServices.tryEmit(false)
|
|
||||||
updateConnectedDevices(peripheral, discoveredServices)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onCompletion {
|
|
||||||
serviceHandlingJob[peripheral.address]?.cancel()
|
|
||||||
serviceHandlingJob.remove(peripheral.address)
|
|
||||||
}
|
|
||||||
.launchIn(lifecycleScope)
|
|
||||||
serviceHandlingJob[peripheral.address] = job
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the connected devices with the latest state.
|
|
||||||
*/
|
|
||||||
private fun updateConnectedDevices(peripheral: Peripheral, handlers: List<ServiceManager>) {
|
|
||||||
_connectedDevices.update {
|
|
||||||
it.toMutableMap().apply { this[peripheral.address] = peripheral to handlers }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle disconnection and cleanup for the given peripheral.
|
|
||||||
*/
|
|
||||||
private fun handleDisconnection(device: String) {
|
|
||||||
val currentDevices = _connectedDevices.value.toMutableMap()
|
|
||||||
currentDevices[device]?.let {
|
|
||||||
currentDevices.remove(device)
|
|
||||||
_connectedDevices.tryEmit(currentDevices)
|
|
||||||
}
|
|
||||||
clearJobs(device)
|
|
||||||
clearFlags()
|
|
||||||
stopServiceIfNoDevices()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear any active jobs for connection and service handling.
|
|
||||||
*/
|
|
||||||
private fun clearJobs(peripheral: String) {
|
|
||||||
connectionJobs[peripheral]?.cancel()
|
|
||||||
connectionJobs.remove(peripheral)
|
|
||||||
|
|
||||||
serviceHandlingJob[peripheral]?.cancel()
|
|
||||||
serviceHandlingJob.remove(peripheral)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the service if no devices are connected.
|
|
||||||
*/
|
|
||||||
private fun stopServiceIfNoDevices() {
|
private fun stopServiceIfNoDevices() {
|
||||||
if (_connectedDevices.value.isEmpty()) {
|
if (_devices.value.isEmpty()) {
|
||||||
stopForegroundService()
|
stopForegroundService()
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Logger and other helper functions remain largely the same.
|
||||||
* Initialize the logger for the specified device.
|
private fun initLogger(deviceAddress: String) {
|
||||||
*/
|
if (logger != null) return
|
||||||
private fun initLogger(device: String) {
|
logger = nRFLoggerTree(this, getString(R.string.app_name), deviceAddress)
|
||||||
logger?.let { Timber.uproot(it) }
|
|
||||||
logger = nRFLoggerTree(this, this.getString(R.string.app_name), device)
|
|
||||||
.also { Timber.plant(it) }
|
.also { Timber.plant(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Uproot the logger and clear the logger instance.
|
|
||||||
*/
|
|
||||||
private fun uprootLogger() {
|
private fun uprootLogger() {
|
||||||
logger?.let { Timber.uproot(it) }
|
logger?.let { Timber.uproot(it) }
|
||||||
logger = null
|
logger = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// The Binder providing the public API.
|
||||||
* Clear the missing services and battery level flags.
|
inner class LocalBinder : Binder(), ServiceApi {
|
||||||
*/
|
override val devices: StateFlow<Map<String, ServiceApi.DeviceData>>
|
||||||
private fun clearFlags() {
|
get() = _devices.asStateFlow()
|
||||||
_isMissingServices.tryEmit(false)
|
|
||||||
uprootLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override val isMissingServices: StateFlow<Map<String, Boolean>>
|
||||||
|
get() = _isMissingServices.asStateFlow()
|
||||||
|
|
||||||
|
override val disconnectionEvent: StateFlow<ServiceApi.DisconnectionEvent?>
|
||||||
|
get() = _disconnectionEvent.asStateFlow()
|
||||||
|
|
||||||
|
override fun disconnect(address: String) = this@ProfileService.disconnect(address)
|
||||||
|
|
||||||
|
override fun getPeripheral(address: String?): Peripheral? =
|
||||||
|
address?.let { centralManager.getPeripheralById(it) }
|
||||||
|
|
||||||
|
override suspend fun getMaxWriteValue(address: String, writeType: WriteType): Int? {
|
||||||
|
val peripheral = getPeripheral(address) ?: return null
|
||||||
|
if (peripheral.state.value != ConnectionState.Connected) return null
|
||||||
|
|
||||||
|
return try {
|
||||||
|
peripheral.requestHighestValueLength() // Request highest possible MTU
|
||||||
|
peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
|
||||||
|
peripheral.readPhy()
|
||||||
|
peripheral.maximumWriteValueLength(writeType)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to configure MTU for $address")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createBond(address: String) {
|
||||||
|
getPeripheral(address)?.ensureBonded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper extension function for bonding
|
||||||
|
private suspend fun Peripheral.ensureBonded() {
|
||||||
|
if (this.bondState.value == BondState.BONDED) return
|
||||||
|
// Create bond and wait until bonded.
|
||||||
|
createBond()
|
||||||
}
|
}
|
||||||
@@ -1,59 +1,74 @@
|
|||||||
package no.nordicsemi.android.service.profile
|
package no.nordicsemi.android.service.profile
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
||||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the public-facing API for the ProfileService.
|
||||||
|
*/
|
||||||
interface ServiceApi {
|
interface ServiceApi {
|
||||||
|
|
||||||
/** Flow of connected devices. */
|
/** A data class to hold all relevant information about a connected device. */
|
||||||
val connectedDevices: Flow<Map<String, Pair<Peripheral, List<ServiceManager>>>>
|
data class DeviceData(
|
||||||
|
val peripheral: Peripheral,
|
||||||
|
val connectionState: ConnectionState = ConnectionState.Connecting,
|
||||||
|
val services: List<ServiceManager> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
/** Missing services flag. */
|
/** A data class to represent a disconnection event. */
|
||||||
val isMissingServices: Flow<Boolean>
|
data class DisconnectionEvent(val address: String, val reason: DeviceDisconnectionReason)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the peripheral by its [address].
|
* A flow that emits the current state of all managed devices.
|
||||||
*
|
* The map key is the device address.
|
||||||
* @return the peripheral instance.
|
|
||||||
*/
|
*/
|
||||||
fun getPeripheralById(address: String?): Peripheral?
|
val devices: StateFlow<Map<String, DeviceData>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect the device with the given [deviceAddress].
|
* A flow that emits whether a specific device is missing its required services.
|
||||||
*
|
* The map key is the device address.
|
||||||
* @param deviceAddress the device address.
|
|
||||||
*/
|
*/
|
||||||
fun disconnect(deviceAddress: String)
|
val isMissingServices: StateFlow<Map<String, Boolean>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the connection state of the device with the given [address].
|
* A flow that emits the reason for the last disconnection event for any device.
|
||||||
*
|
|
||||||
* @return the connection state flow.
|
|
||||||
*/
|
*/
|
||||||
fun connectionState(address: String): StateFlow<ConnectionState>?
|
val disconnectionEvent: StateFlow<DisconnectionEvent?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the disconnection reason of the device with the given address.
|
* Disconnects from a Bluetooth device and stops managing it.
|
||||||
*
|
*
|
||||||
* @return the disconnection reason flow.
|
* @param address The address of the device to disconnect from.
|
||||||
*/
|
*/
|
||||||
val disconnectionReason: StateFlow<DeviceDisconnectionReason?>
|
fun disconnect(address: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request maximum write value length.
|
* Retrieves a peripheral instance by its address.
|
||||||
* For [WriteType.WITHOUT_RESPONSE] it is equal to *ATT MTU - 3 bytes*.
|
*
|
||||||
|
* @param address The device address.
|
||||||
|
* @return The [Peripheral] instance, or null if not found.
|
||||||
|
*/
|
||||||
|
fun getPeripheral(address: String?): Peripheral?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the maximum possible value length for a write operation.
|
||||||
|
*
|
||||||
|
* @param address The device address.
|
||||||
|
* @param writeType The type of write operation.
|
||||||
|
* @return The maximum number of bytes that can be sent in a single write.
|
||||||
*/
|
*/
|
||||||
suspend fun getMaxWriteValue(
|
suspend fun getMaxWriteValue(
|
||||||
address: String,
|
address: String,
|
||||||
writeType: WriteType = WriteType.WITHOUT_RESPONSE
|
writeType: WriteType = WriteType.WITHOUT_RESPONSE
|
||||||
): Int?
|
): Int?
|
||||||
|
|
||||||
suspend fun createBonding(
|
/**
|
||||||
address: String
|
* Initiates and waits for the bonding process to complete with a device.
|
||||||
)
|
*
|
||||||
|
* @param address The device address.
|
||||||
|
*/
|
||||||
|
suspend fun createBond(address: String)
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ dependencies {
|
|||||||
implementation(project(":lib_ui"))
|
implementation(project(":lib_ui"))
|
||||||
implementation(project(":lib_utils"))
|
implementation(project(":lib_utils"))
|
||||||
implementation(project(":profile-parsers"))
|
implementation(project(":profile-parsers"))
|
||||||
implementation(project(":lib_service"))
|
api(project(":lib_service"))
|
||||||
implementation(project(":profile_manager"))
|
implementation(project(":profile_manager"))
|
||||||
implementation(project(":lib_storage"))
|
implementation(project(":lib_storage"))
|
||||||
implementation(project(":permissions-ranging"))
|
implementation(project(":permissions-ranging"))
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import no.nordicsemi.android.common.permissions.ble.RequireBluetooth
|
import no.nordicsemi.android.common.permissions.ble.RequireBluetooth
|
||||||
import no.nordicsemi.android.common.permissions.ble.RequireLocation
|
import no.nordicsemi.android.common.permissions.ble.RequireLocation
|
||||||
import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission
|
import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission
|
||||||
import no.nordicsemi.android.service.profile.CustomReason
|
|
||||||
import no.nordicsemi.android.service.profile.DeviceDisconnectionReason
|
|
||||||
import no.nordicsemi.android.service.profile.StateReason
|
|
||||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||||
import no.nordicsemi.android.toolbox.profile.data.toReason
|
|
||||||
import no.nordicsemi.android.toolbox.profile.view.battery.BatteryScreen
|
import no.nordicsemi.android.toolbox.profile.view.battery.BatteryScreen
|
||||||
import no.nordicsemi.android.toolbox.profile.view.bps.BPSScreen
|
import no.nordicsemi.android.toolbox.profile.view.bps.BPSScreen
|
||||||
import no.nordicsemi.android.toolbox.profile.view.cgms.CGMScreen
|
import no.nordicsemi.android.toolbox.profile.view.cgms.CGMScreen
|
||||||
@@ -47,45 +43,45 @@ import no.nordicsemi.android.toolbox.profile.view.rscs.RSCSScreen
|
|||||||
import no.nordicsemi.android.toolbox.profile.view.throughput.ThroughputScreen
|
import no.nordicsemi.android.toolbox.profile.view.throughput.ThroughputScreen
|
||||||
import no.nordicsemi.android.toolbox.profile.view.uart.UARTScreen
|
import no.nordicsemi.android.toolbox.profile.view.uart.UARTScreen
|
||||||
import no.nordicsemi.android.toolbox.profile.viewmodel.ConnectionEvent
|
import no.nordicsemi.android.toolbox.profile.viewmodel.ConnectionEvent
|
||||||
import no.nordicsemi.android.toolbox.profile.viewmodel.DeviceConnectionState
|
import no.nordicsemi.android.toolbox.profile.viewmodel.ProfileUiState
|
||||||
import no.nordicsemi.android.toolbox.profile.viewmodel.DeviceData
|
|
||||||
import no.nordicsemi.android.toolbox.profile.viewmodel.ProfileViewModel
|
import no.nordicsemi.android.toolbox.profile.viewmodel.ProfileViewModel
|
||||||
import no.nordicsemi.android.ui.view.internal.DeviceConnectingView
|
import no.nordicsemi.android.ui.view.internal.DeviceConnectingView
|
||||||
|
import no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView
|
||||||
import no.nordicsemi.android.ui.view.internal.DisconnectReason
|
import no.nordicsemi.android.ui.view.internal.DisconnectReason
|
||||||
import no.nordicsemi.android.ui.view.internal.LoadingView
|
|
||||||
import no.nordicsemi.android.ui.view.internal.ServiceDiscoveryView
|
import no.nordicsemi.android.ui.view.internal.ServiceDiscoveryView
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ProfileScreen() {
|
internal fun ProfileScreen() {
|
||||||
val profileViewModel: ProfileViewModel = hiltViewModel()
|
val profileViewModel: ProfileViewModel = hiltViewModel()
|
||||||
|
val uiState by profileViewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val deviceAddress = profileViewModel.address
|
val deviceAddress = profileViewModel.address
|
||||||
val deviceDataState by profileViewModel.deviceState.collectAsStateWithLifecycle()
|
|
||||||
val onClickEvent: (ConnectionEvent) -> Unit = { event ->
|
// Event handler now sends simpler, context-free events.
|
||||||
profileViewModel.onConnectionEvent(event)
|
val onEvent: (ConnectionEvent) -> Unit = { event ->
|
||||||
|
profileViewModel.onEvent(event)
|
||||||
}
|
}
|
||||||
// Handle back press to navigate up.
|
// Handle back press to navigate up.
|
||||||
BackHandler {
|
BackHandler {
|
||||||
onClickEvent(ConnectionEvent.NavigateUp)
|
onEvent(ConnectionEvent.NavigateUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
contentWindowInsets = WindowInsets.displayCutout
|
contentWindowInsets = WindowInsets.displayCutout
|
||||||
.only(WindowInsetsSides.Horizontal),
|
.only(WindowInsetsSides.Horizontal),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
// The device name is derived directly from the current state.
|
||||||
|
val deviceName = (uiState as? ProfileUiState.Connected)
|
||||||
|
?.deviceData?.peripheral?.name
|
||||||
|
?: deviceAddress
|
||||||
|
|
||||||
ProfileAppBar(
|
ProfileAppBar(
|
||||||
deviceName = when (val state = deviceDataState) {
|
deviceName = deviceName,
|
||||||
is DeviceConnectionState.Connected -> state.data.peripheral?.name
|
|
||||||
?: deviceAddress
|
|
||||||
|
|
||||||
is DeviceConnectionState.Disconnected -> state.device?.name ?: deviceAddress
|
|
||||||
|
|
||||||
else -> deviceAddress
|
|
||||||
},
|
|
||||||
title = deviceAddress,
|
title = deviceAddress,
|
||||||
connectionState = deviceDataState,
|
// The AppBar needs to be updated to accept the new ProfileUiState
|
||||||
navigateUp = { onClickEvent(ConnectionEvent.NavigateUp) },
|
connectionState = uiState,
|
||||||
disconnect = { onClickEvent(ConnectionEvent.DisconnectEvent(deviceAddress)) },
|
navigateUp = { onEvent(ConnectionEvent.NavigateUp) },
|
||||||
openLogger = { onClickEvent(ConnectionEvent.OpenLoggerEvent) }
|
disconnect = { onEvent(ConnectionEvent.DisconnectEvent) },
|
||||||
|
openLogger = { onEvent(ConnectionEvent.OpenLoggerEvent) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
@@ -101,30 +97,31 @@ internal fun ProfileScreen() {
|
|||||||
.imePadding(),
|
.imePadding(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
when (val state = deviceDataState) {
|
// The main content switches based on the UI state.
|
||||||
is DeviceConnectionState.Connected -> {
|
when (val state = uiState) {
|
||||||
DeviceConnectedView(
|
is ProfileUiState.Connected -> DeviceConnectedView(
|
||||||
state.data,
|
state = state,
|
||||||
onClickEvent
|
onEvent = onEvent
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DeviceConnectionState.Connecting -> DeviceConnectingView(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is DeviceConnectionState.Disconnected -> {
|
is ProfileUiState.Disconnected -> {
|
||||||
state.reason?.let {
|
DeviceDisconnectedView(
|
||||||
DeviceDisconnectedView(
|
disconnectedReason = state.reason.toString(),
|
||||||
it,
|
isMissingService = false,
|
||||||
deviceAddress,
|
modifier = Modifier.padding(16.dp),
|
||||||
onClickEvent
|
) {
|
||||||
)
|
Button(
|
||||||
|
onClick = { onEvent(ConnectionEvent.OnRetryClicked) },
|
||||||
|
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.reconnect))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceConnectionState.Idle, DeviceConnectionState.Disconnecting -> LoadingView()
|
ProfileUiState.Loading -> DeviceConnectingView(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,124 +130,69 @@ internal fun ProfileScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun DeviceDisconnectedView(
|
|
||||||
reason: DeviceDisconnectionReason,
|
|
||||||
deviceAddress: String,
|
|
||||||
onClickEvent: (ConnectionEvent) -> Unit
|
|
||||||
) {
|
|
||||||
when (reason) {
|
|
||||||
is CustomReason -> {
|
|
||||||
no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView(
|
|
||||||
reason = reason.reason,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = { onClickEvent(ConnectionEvent.OnRetryClicked(deviceAddress)) },
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = R.string.reconnect))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is StateReason -> {
|
|
||||||
no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView(
|
|
||||||
disconnectedReason = toReason(reason.reason),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = { onClickEvent(ConnectionEvent.OnRetryClicked(deviceAddress)) },
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = R.string.reconnect))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DeviceConnectedView(
|
internal fun DeviceConnectedView(
|
||||||
deviceData: DeviceData,
|
state: ProfileUiState.Connected,
|
||||||
onClickEvent: (ConnectionEvent) -> Unit,
|
onEvent: (ConnectionEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
// Is missing services?
|
// Check for missing services directly from the state object.
|
||||||
deviceData.peripheral?.let { peripheral ->
|
if (state.isMissingServices) {
|
||||||
when {
|
DeviceDisconnectedView(
|
||||||
deviceData.isMissingServices -> {
|
reason = DisconnectReason.MISSING_SERVICE,
|
||||||
no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView(
|
modifier = Modifier.padding(16.dp)
|
||||||
reason = DisconnectReason.MISSING_SERVICE,
|
)
|
||||||
modifier = Modifier
|
return
|
||||||
.padding(16.dp)
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
Column(
|
||||||
Column(
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.padding(16.dp)
|
||||||
.padding(16.dp)
|
.imePadding()
|
||||||
.imePadding()
|
) {
|
||||||
|
// Show service discovery view if services are not yet available.
|
||||||
|
if (state.deviceData.services.isEmpty()) {
|
||||||
|
ServiceDiscoveryView(modifier = Modifier) {
|
||||||
|
Button(
|
||||||
|
onClick = { onEvent(ConnectionEvent.DisconnectEvent) },
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
deviceData.peripheralProfileMap[deviceData.peripheral]?.forEach { profile ->
|
Text(text = stringResource(id = R.string.cancel))
|
||||||
Column(
|
}
|
||||||
modifier = Modifier
|
}
|
||||||
.imePadding()
|
} else {
|
||||||
) {
|
// Iterate through the available service managers.
|
||||||
// Requires max value length to be set.
|
state.deviceData.services.forEach { serviceManager ->
|
||||||
val needsMaxValueLength = profile.profile == Profile.THROUGHPUT ||
|
Column(modifier = Modifier.imePadding()) {
|
||||||
profile.profile == Profile.UART
|
val needsMaxValueLength = serviceManager.profile in listOf(
|
||||||
if (needsMaxValueLength) {
|
Profile.CHANNEL_SOUNDING, Profile.UART, Profile.THROUGHPUT
|
||||||
LaunchedEffect(key1 = true) {
|
)
|
||||||
if (deviceData.maxValueLength == null) {
|
|
||||||
onClickEvent(ConnectionEvent.RequestMaxValueLength)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when (profile.profile) {
|
|
||||||
Profile.HTS -> HTSScreen()
|
|
||||||
Profile.CHANNEL_SOUNDING -> ChannelSoundingScreen()
|
|
||||||
Profile.BPS -> BPSScreen()
|
|
||||||
Profile.CSC -> CSCScreen()
|
|
||||||
Profile.CGM -> CGMScreen()
|
|
||||||
Profile.DFS -> DFSScreen()
|
|
||||||
Profile.GLS -> GLSScreen()
|
|
||||||
Profile.HRS -> HRSScreen()
|
|
||||||
Profile.LBS -> BlinkyScreen()
|
|
||||||
Profile.RSCS -> RSCSScreen()
|
|
||||||
Profile.THROUGHPUT -> ThroughputScreen(deviceData.maxValueLength)
|
|
||||||
Profile.UART -> UARTScreen(deviceData.maxValueLength)
|
|
||||||
|
|
||||||
else -> {
|
// Request max value length if needed and not already set.
|
||||||
// Do nothing.
|
if (needsMaxValueLength) {
|
||||||
}
|
LaunchedEffect(Unit) {
|
||||||
}
|
if (state.maxValueLength == null) {
|
||||||
if (profile.profile == Profile.BATTERY) {
|
onEvent(ConnectionEvent.RequestMaxValueLength)
|
||||||
// Battery level will be added at the end.
|
|
||||||
BatteryScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
ServiceDiscoveryView(
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
onClickEvent(
|
|
||||||
ConnectionEvent.DisconnectEvent(
|
|
||||||
peripheral.address
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = R.string.cancel))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display the appropriate screen for each profile.
|
||||||
|
when (serviceManager.profile) {
|
||||||
|
Profile.HTS -> HTSScreen()
|
||||||
|
Profile.CHANNEL_SOUNDING -> ChannelSoundingScreen()
|
||||||
|
Profile.BPS -> BPSScreen()
|
||||||
|
Profile.CSC -> CSCScreen()
|
||||||
|
Profile.CGM -> CGMScreen()
|
||||||
|
Profile.DFS -> DFSScreen()
|
||||||
|
Profile.GLS -> GLSScreen()
|
||||||
|
Profile.HRS -> HRSScreen()
|
||||||
|
Profile.LBS -> BlinkyScreen()
|
||||||
|
Profile.RSCS -> RSCSScreen()
|
||||||
|
Profile.BATTERY -> BatteryScreen()
|
||||||
|
Profile.THROUGHPUT -> ThroughputScreen(state.maxValueLength)
|
||||||
|
Profile.UART -> UARTScreen(state.maxValueLength)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import no.nordicsemi.android.analytics.AppAnalytics
|
import no.nordicsemi.android.analytics.AppAnalytics
|
||||||
import no.nordicsemi.android.analytics.ProfileConnectedEvent
|
import no.nordicsemi.android.analytics.ProfileConnectedEvent
|
||||||
|
import no.nordicsemi.android.service.profile.ServiceApi
|
||||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
||||||
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
import no.nordicsemi.android.toolbox.lib.utils.Profile
|
||||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||||
@@ -17,7 +18,7 @@ class DeviceRepository @Inject constructor(
|
|||||||
private val analytics: AppAnalytics,
|
private val analytics: AppAnalytics,
|
||||||
) {
|
) {
|
||||||
private val _connectedDevices =
|
private val _connectedDevices =
|
||||||
MutableStateFlow<Map<String, Pair<Peripheral, List<ServiceManager>>>>(emptyMap())
|
MutableStateFlow<Map<String, ServiceApi.DeviceData>>(emptyMap())
|
||||||
val connectedDevices = _connectedDevices.asStateFlow()
|
val connectedDevices = _connectedDevices.asStateFlow()
|
||||||
|
|
||||||
private val _profilePeripheralPair =
|
private val _profilePeripheralPair =
|
||||||
@@ -26,7 +27,7 @@ class DeviceRepository @Inject constructor(
|
|||||||
|
|
||||||
private val _loggedProfiles = mutableListOf<Pair<String, String>>()
|
private val _loggedProfiles = mutableListOf<Pair<String, String>>()
|
||||||
|
|
||||||
fun updateConnectedDevices(devices: Map<String, Pair<Peripheral, List<ServiceManager>>>) {
|
fun updateConnectedDevices(devices: Map<String, ServiceApi.DeviceData>) {
|
||||||
_connectedDevices.update { devices }
|
_connectedDevices.update { devices }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package no.nordicsemi.android.toolbox.profile.view.internal
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import no.nordicsemi.android.common.theme.NordicTheme
|
import no.nordicsemi.android.common.theme.NordicTheme
|
||||||
import no.nordicsemi.android.toolbox.profile.viewmodel.DeviceConnectionState
|
import no.nordicsemi.android.toolbox.profile.viewmodel.ProfileUiState
|
||||||
import no.nordicsemi.android.ui.view.BackIconAppBar
|
import no.nordicsemi.android.ui.view.BackIconAppBar
|
||||||
import no.nordicsemi.android.ui.view.LoggerBackIconAppBar
|
import no.nordicsemi.android.ui.view.LoggerBackIconAppBar
|
||||||
import no.nordicsemi.android.ui.view.LoggerIconAppBar
|
import no.nordicsemi.android.ui.view.LoggerIconAppBar
|
||||||
@@ -12,13 +12,13 @@ import no.nordicsemi.android.ui.view.LoggerIconAppBar
|
|||||||
internal fun ProfileAppBar(
|
internal fun ProfileAppBar(
|
||||||
deviceName: String?,
|
deviceName: String?,
|
||||||
title: String,
|
title: String,
|
||||||
connectionState: DeviceConnectionState,
|
connectionState: ProfileUiState,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
disconnect: () -> Unit,
|
disconnect: () -> Unit,
|
||||||
openLogger: () -> Unit
|
openLogger: () -> Unit
|
||||||
) {
|
) {
|
||||||
if (deviceName?.isNotBlank() == true) {
|
if (deviceName?.isNotBlank() == true) {
|
||||||
if (connectionState !is DeviceConnectionState.Disconnected) {
|
if (connectionState !is ProfileUiState.Disconnected) {
|
||||||
LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger)
|
LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger)
|
||||||
} else {
|
} else {
|
||||||
LoggerBackIconAppBar(deviceName, navigateUp) { openLogger() }
|
LoggerBackIconAppBar(deviceName, navigateUp) { openLogger() }
|
||||||
@@ -35,7 +35,7 @@ private fun ProfileAppBarPreview() {
|
|||||||
ProfileAppBar(
|
ProfileAppBar(
|
||||||
deviceName = "DE",
|
deviceName = "DE",
|
||||||
title = "nRF Toolbox",
|
title = "nRF Toolbox",
|
||||||
connectionState = DeviceConnectionState.Connecting,
|
connectionState = ProfileUiState.Loading,
|
||||||
navigateUp = {},
|
navigateUp = {},
|
||||||
disconnect = {},
|
disconnect = {},
|
||||||
openLogger = {},
|
openLogger = {},
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
package no.nordicsemi.android.toolbox.profile.viewmodel
|
package no.nordicsemi.android.toolbox.profile.viewmodel
|
||||||
|
|
||||||
import no.nordicsemi.android.service.profile.DeviceDisconnectionReason
|
import no.nordicsemi.android.service.profile.DeviceDisconnectionReason
|
||||||
import no.nordicsemi.android.toolbox.profile.manager.ServiceManager
|
import no.nordicsemi.android.service.profile.ServiceApi
|
||||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
|
||||||
|
|
||||||
internal data class DeviceData(
|
/**
|
||||||
val peripheral: Peripheral? = null,
|
* Events triggered by the user from the UI.
|
||||||
val peripheralProfileMap: Map<Peripheral, List<ServiceManager>> = emptyMap(),
|
*/
|
||||||
val isMissingServices: Boolean = false,
|
internal sealed interface ConnectionEvent {
|
||||||
val maxValueLength: Int? = null,
|
data object OnRetryClicked : ConnectionEvent
|
||||||
)
|
data object NavigateUp : ConnectionEvent
|
||||||
|
data object DisconnectEvent : ConnectionEvent
|
||||||
internal sealed class DeviceConnectionState {
|
data object OpenLoggerEvent : ConnectionEvent
|
||||||
data object Idle : DeviceConnectionState()
|
data object RequestMaxValueLength : ConnectionEvent
|
||||||
data object Connecting : DeviceConnectionState()
|
}
|
||||||
data object Disconnecting : DeviceConnectionState()
|
|
||||||
data class Connected(val data: DeviceData) : DeviceConnectionState()
|
/**
|
||||||
data class Disconnected(
|
* Represents the state of the UI for the profile screen.
|
||||||
val device: Peripheral? = null,
|
*/
|
||||||
val reason: DeviceDisconnectionReason?
|
internal sealed interface ProfileUiState {
|
||||||
) : DeviceConnectionState()
|
data object Loading : ProfileUiState
|
||||||
|
data class Disconnected(val reason: DeviceDisconnectionReason?) : ProfileUiState
|
||||||
|
|
||||||
|
data class Connected(
|
||||||
|
val deviceData: ServiceApi.DeviceData,
|
||||||
|
val isMissingServices: Boolean = false,
|
||||||
|
val maxValueLength: Int? = null,
|
||||||
|
) : ProfileUiState
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,12 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -24,29 +24,12 @@ import no.nordicsemi.android.log.LogSession
|
|||||||
import no.nordicsemi.android.log.timber.nRFLoggerTree
|
import no.nordicsemi.android.log.timber.nRFLoggerTree
|
||||||
import no.nordicsemi.android.service.profile.ProfileServiceManager
|
import no.nordicsemi.android.service.profile.ProfileServiceManager
|
||||||
import no.nordicsemi.android.service.profile.ServiceApi
|
import no.nordicsemi.android.service.profile.ServiceApi
|
||||||
import no.nordicsemi.android.service.profile.StateReason
|
|
||||||
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
|
import no.nordicsemi.android.toolbox.profile.ProfileDestinationId
|
||||||
import no.nordicsemi.android.toolbox.profile.R
|
import no.nordicsemi.android.toolbox.profile.R
|
||||||
import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository
|
import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository
|
||||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
|
||||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal sealed interface ConnectionEvent {
|
|
||||||
|
|
||||||
data class OnRetryClicked(val device: String) : ConnectionEvent
|
|
||||||
|
|
||||||
data object NavigateUp : ConnectionEvent
|
|
||||||
|
|
||||||
data class DisconnectEvent(val device: String) : ConnectionEvent
|
|
||||||
|
|
||||||
data object OpenLoggerEvent : ConnectionEvent
|
|
||||||
|
|
||||||
data object RequestMaxValueLength : ConnectionEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class ProfileViewModel @Inject constructor(
|
internal class ProfileViewModel @Inject constructor(
|
||||||
private val profileServiceManager: ProfileServiceManager,
|
private val profileServiceManager: ProfileServiceManager,
|
||||||
@@ -57,239 +40,132 @@ internal class ProfileViewModel @Inject constructor(
|
|||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
) : SimpleNavigationViewModel(navigator, savedStateHandle) {
|
) : SimpleNavigationViewModel(navigator, savedStateHandle) {
|
||||||
val address: String = parameterOf(ProfileDestinationId)
|
val address: String = parameterOf(ProfileDestinationId)
|
||||||
private val _deviceState = MutableStateFlow<DeviceConnectionState>(DeviceConnectionState.Idle)
|
private var serviceApi: ServiceApi? = null
|
||||||
val deviceState = _deviceState.asStateFlow()
|
private val logger: nRFLoggerTree =
|
||||||
|
nRFLoggerTree(context, address, context.getString(R.string.app_name))
|
||||||
|
|
||||||
private var logger: nRFLoggerTree? = null
|
private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
|
||||||
private var serviceApi: WeakReference<ServiceApi>? = null
|
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
|
||||||
private var peripheral: Peripheral? = null
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
connectToPeripheral(address)
|
Timber.tag("AAA PVM").d("Initializing ViewModel for device: $address")
|
||||||
|
connectToPeripheral()
|
||||||
observeConnectedDevices()
|
observeConnectedDevices()
|
||||||
initLogger()
|
Timber.plant(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getServiceApi(): ServiceApi? {
|
private suspend fun getServiceApi() =
|
||||||
if (serviceApi == null) {
|
profileServiceManager.bindService().also { serviceApi = it }
|
||||||
serviceApi = WeakReference(profileServiceManager.bindService())
|
|
||||||
}
|
|
||||||
return serviceApi?.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initLogger() {
|
|
||||||
logger = nRFLoggerTree(context, address, context.getString(R.string.app_name)).also {
|
|
||||||
Timber.plant(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeConnectedDevices() = viewModelScope.launch {
|
private fun observeConnectedDevices() = viewModelScope.launch {
|
||||||
getServiceApi()?.let { api ->
|
// Bind the service and get the API
|
||||||
peripheral = api.getPeripheralById(address)
|
val api = getServiceApi()
|
||||||
|
|
||||||
api.connectedDevices
|
// Combine flows from the service to create a single UI state.
|
||||||
.onEach { peripheralProfileMap ->
|
combine(
|
||||||
deviceRepository.updateConnectedDevices(peripheralProfileMap)
|
api.devices,
|
||||||
|
api.isMissingServices,
|
||||||
|
api.disconnectionEvent
|
||||||
|
) { devices, missingServicesMap, disconnection ->
|
||||||
|
val deviceData = devices[address]
|
||||||
|
val isMissingServices = missingServicesMap[address] ?: false
|
||||||
|
Timber.tag("AAA PVM")
|
||||||
|
.d("DeviceData for $address: $deviceData, MissingServices: $isMissingServices, $deviceData")
|
||||||
|
|
||||||
peripheralProfileMap[peripheral?.address]?.let { pair ->
|
// Determine the UI state based on the service's state
|
||||||
deviceRepository.updateProfilePeripheralPair(pair.first, pair.second)
|
if (deviceData != null) {
|
||||||
_deviceState.update {
|
// Update connected device info in the repository
|
||||||
DeviceConnectionState.Connected(
|
deviceRepository.updateProfilePeripheralPair(
|
||||||
DeviceData(
|
deviceData.peripheral,
|
||||||
peripheral = pair.first,
|
deviceData.services
|
||||||
peripheralProfileMap = mapOf(pair.first to pair.second),
|
)
|
||||||
)
|
deviceData.services.forEach {
|
||||||
)
|
deviceRepository.updateAnalytics(
|
||||||
|
address,
|
||||||
|
it.profile
|
||||||
|
)
|
||||||
|
}
|
||||||
|
deviceRepository.updateConnectedDevices(devices)
|
||||||
|
|
||||||
}
|
// // Create the Connected state
|
||||||
}
|
val currentMaxVal =
|
||||||
|
(_uiState.value as? ProfileUiState.Connected)?.maxValueLength
|
||||||
// Send each profile handler to a shared flow that profile ViewModels can observe
|
ProfileUiState.Connected(deviceData, isMissingServices, currentMaxVal)
|
||||||
peripheralProfileMap[peripheral?.address]?.second?.forEach { handler ->
|
} else {
|
||||||
deviceRepository.updateAnalytics(address, handler.profile)
|
// If the device is not in the map, it's disconnected.
|
||||||
|
// Check if there's a specific disconnection event for this device.
|
||||||
}
|
val reason =
|
||||||
}.launchIn(viewModelScope)
|
if (disconnection?.address == address) disconnection.reason else null
|
||||||
|
deviceRepository.removeLoggedProfile(address)
|
||||||
updateConnectionState(api, address, peripheral?.isConnected == true)
|
ProfileUiState.Disconnected(reason)
|
||||||
|
}
|
||||||
|
}.catch { e ->
|
||||||
|
Timber.e(e, "Error observing profile state")
|
||||||
|
// You could emit a generic error state here if needed
|
||||||
|
}.collect { state ->
|
||||||
|
_uiState.value = state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to the peripheral with the given address. Before connecting, the service must be bound.
|
* Connect to the peripheral with the given address. Before connecting, the service must be bound.
|
||||||
* The service will be started if not already running.
|
* The service will be started if not already running.
|
||||||
* @param deviceAddress the address of the peripheral to connect to.
|
|
||||||
*/
|
*/
|
||||||
private fun connectToPeripheral(deviceAddress: String) = viewModelScope.launch {
|
private fun connectToPeripheral() = viewModelScope.launch {
|
||||||
// Connect to the peripheral
|
// Connect to the peripheral
|
||||||
getServiceApi()?.let {
|
getServiceApi().devices.onEach {
|
||||||
if (peripheral == null) peripheral = it.getPeripheralById(address)
|
if (it[address]?.connectionState?.isConnected != true) {
|
||||||
if (peripheral?.isConnected != true) {
|
Timber.tag("AAA PVM").d("Not connected to $address, connecting...")
|
||||||
profileServiceManager.connectToPeripheral(deviceAddress)
|
profileServiceManager.connectToPeripheral(address)
|
||||||
}
|
return@onEach
|
||||||
}
|
} else {
|
||||||
}
|
Timber.tag("AAA PVM").d("Already connected to $address")
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the service data, including connection state and peripheral data.
|
|
||||||
* @param api the service API.
|
|
||||||
* @param deviceAddress the address of the connected device.
|
|
||||||
* @param isAlreadyConnected true if the device is already connected, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun updateConnectionState(
|
|
||||||
api: ServiceApi,
|
|
||||||
deviceAddress: String,
|
|
||||||
isAlreadyConnected: Boolean
|
|
||||||
) {
|
|
||||||
// Drop the first default state (Closed) before connection.
|
|
||||||
job = api.connectionState(deviceAddress)
|
|
||||||
?.onEach { connectionState ->
|
|
||||||
if (peripheral == null) peripheral = api.getPeripheralById(address)
|
|
||||||
when (connectionState) {
|
|
||||||
ConnectionState.Connected -> {
|
|
||||||
_deviceState.update { currentState ->
|
|
||||||
val currentData =
|
|
||||||
(currentState as? DeviceConnectionState.Connected)?.data
|
|
||||||
DeviceConnectionState.Connected(
|
|
||||||
currentData?.copy(
|
|
||||||
peripheral = peripheral
|
|
||||||
) ?: DeviceData(peripheral = peripheral)
|
|
||||||
)
|
|
||||||
|
|
||||||
}.apply { checkForMissingServices(api) }
|
|
||||||
}
|
|
||||||
|
|
||||||
is ConnectionState.Disconnected -> {
|
|
||||||
// If disconnected reason is null, it means that the connection was never initiated.
|
|
||||||
if (connectionState.reason == null) {
|
|
||||||
_deviceState.update {
|
|
||||||
DeviceConnectionState.Idle
|
|
||||||
}
|
|
||||||
return@onEach
|
|
||||||
} else {
|
|
||||||
_deviceState.update {
|
|
||||||
DeviceConnectionState.Disconnected(
|
|
||||||
peripheral,
|
|
||||||
StateReason(connectionState.reason!!)
|
|
||||||
)
|
|
||||||
}.also {
|
|
||||||
// Remove the analytics logged profiles for the disconnected device.
|
|
||||||
deviceRepository.removeLoggedProfile(deviceAddress)
|
|
||||||
}
|
|
||||||
job?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState.Connecting -> {
|
|
||||||
_deviceState.update {
|
|
||||||
DeviceConnectionState.Connecting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionState.Disconnecting -> {
|
|
||||||
// Update the state to disconnecting.
|
|
||||||
_deviceState.update {
|
|
||||||
DeviceConnectionState.Disconnecting
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?.onCompletion {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
}?.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for missing services.
|
|
||||||
*/
|
|
||||||
private fun checkForMissingServices(api: ServiceApi) =
|
|
||||||
api.isMissingServices.onEach { isMissing ->
|
|
||||||
(_deviceState.value as? DeviceConnectionState.Connected)?.let { connectedState ->
|
|
||||||
_deviceState.update {
|
|
||||||
connectedState.copy(
|
|
||||||
data = connectedState.data.copy(isMissingServices = isMissing)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unbind the service.
|
|
||||||
*/
|
|
||||||
private fun unbindService() {
|
|
||||||
serviceApi?.let { profileServiceManager.unbindService() }
|
|
||||||
serviceApi = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onConnectionEvent(event: ConnectionEvent) {
|
|
||||||
|
fun onEvent(event: ConnectionEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConnectionEvent.DisconnectEvent -> disconnect(event.device)
|
ConnectionEvent.DisconnectEvent -> {
|
||||||
|
serviceApi?.disconnect(address)
|
||||||
|
}
|
||||||
|
|
||||||
ConnectionEvent.NavigateUp -> {
|
ConnectionEvent.NavigateUp -> {
|
||||||
// If the device is connected and missing services, disconnect it before navigating up.
|
// Disconnect only if services are missing, otherwise leave connected
|
||||||
if ((_deviceState.value as? DeviceConnectionState.Connected)?.data?.isMissingServices == true) {
|
if ((_uiState.value as? ProfileUiState.Connected)?.isMissingServices == true) {
|
||||||
disconnect(address)
|
Timber.tag("BBB").d("Disconnecting due to missing services")
|
||||||
|
serviceApi?.disconnect(address)
|
||||||
}
|
}
|
||||||
navigator.navigateUp()
|
navigator.navigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ConnectionEvent.OnRetryClicked -> reconnectDevice(event.device)
|
ConnectionEvent.OnRetryClicked -> {
|
||||||
ConnectionEvent.OpenLoggerEvent -> openLogger()
|
_uiState.value = ProfileUiState.Loading
|
||||||
ConnectionEvent.RequestMaxValueLength -> viewModelScope.launch(Dispatchers.IO) {
|
connectToPeripheral()
|
||||||
// Request maximum MTU size if it is not already set.
|
|
||||||
val mtuSize = getServiceApi()?.getMaxWriteValue(address)
|
|
||||||
_deviceState.update { currentState ->
|
|
||||||
val currentData =
|
|
||||||
(currentState as? DeviceConnectionState.Connected)?.data
|
|
||||||
if (currentData != null && currentData.maxValueLength == mtuSize) {
|
|
||||||
// No need to update if the max value length is already set.
|
|
||||||
return@update currentState
|
|
||||||
}
|
|
||||||
DeviceConnectionState.Connected(
|
|
||||||
currentData?.copy(
|
|
||||||
maxValueLength = mtuSize
|
|
||||||
) ?: DeviceData(
|
|
||||||
peripheral = peripheral,
|
|
||||||
maxValueLength = mtuSize
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConnectionEvent.OpenLoggerEvent -> openLogger()
|
||||||
|
ConnectionEvent.RequestMaxValueLength -> requestMaxWriteValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun requestMaxWriteValue() = viewModelScope.launch {
|
||||||
* Disconnect the device with the given address and navigate back.
|
val mtu = serviceApi?.getMaxWriteValue(address)
|
||||||
* @param device the address of the device to disconnect.
|
_uiState.update {
|
||||||
*/
|
(it as? ProfileUiState.Connected)?.copy(maxValueLength = mtu) ?: it
|
||||||
private fun disconnect(device: String) = viewModelScope.launch {
|
}
|
||||||
getServiceApi()?.disconnect(device)
|
|
||||||
unbindService()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Launch the logger activity.
|
|
||||||
*/
|
|
||||||
private fun openLogger() {
|
private fun openLogger() {
|
||||||
// Log the event in the analytics.
|
|
||||||
analytics.logEvent(ProfileOpenEvent(Link.LOGGER))
|
analytics.logEvent(ProfileOpenEvent(Link.LOGGER))
|
||||||
LoggerLauncher.launch(context, logger?.session as? LogSession)
|
LoggerLauncher.launch(context, logger.session as? LogSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun onCleared() {
|
||||||
* Reconnect to the device with the given address.
|
Timber.uproot(logger)
|
||||||
*
|
profileServiceManager.unbindService()
|
||||||
* @param deviceAddress the address of the device to reconnect to.
|
serviceApi = null
|
||||||
*/
|
super.onCleared()
|
||||||
private fun reconnectDevice(deviceAddress: String) = viewModelScope.launch {
|
|
||||||
getServiceApi()?.let {
|
|
||||||
connectToPeripheral(deviceAddress)
|
|
||||||
updateConnectionState(it, deviceAddress, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user