diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab4e1bc3c..2b219a7bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,6 +31,7 @@ plugins { alias(libs.plugins.nordic.application.compose) alias(libs.plugins.nordic.hilt) + alias(libs.plugins.kotlin.parcelize) } if (getGradle().getStartParameter().getTaskRequests().toString().contains("Release")) { @@ -43,18 +44,7 @@ android { } dependencies { - //Hilt requires to implement every module in the main app module - //https://github.com/google/dagger/issues/2123 - implementation(project(":profile_bps")) - implementation(project(":profile_csc")) - implementation(project(":profile_cgms")) - implementation(project(":profile_gls")) - implementation(project(":profile_hrs")) implementation(project(":profile_hts")) - implementation(project(":profile_prx")) - implementation(project(":profile_rscs")) - - implementation(project(":profile_uart")) implementation(project(":lib_analytics")) implementation(project(":lib_ui")) @@ -64,13 +54,11 @@ dependencies { implementation(libs.nordic.core) implementation(libs.nordic.theme) + implementation(libs.nordic.ui) implementation(libs.nordic.navigation) - implementation(libs.nordic.blek.uiscanner) implementation(libs.nordic.logger) implementation(libs.nordic.permissions.ble) implementation(libs.nordic.analytics) - - implementation(libs.nordic.blek.client) implementation(libs.androidx.core.ktx) implementation(libs.androidx.compose.material3) @@ -83,4 +71,6 @@ dependencies { // Timber & SLF4J implementation (libs.slf4j.timber) implementation(libs.nordic.log.timber) + + implementation("no.nordicsemi.kotlin.ble:client-android") } diff --git a/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt b/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt index 9379a68bd..1b4b5d4eb 100644 --- a/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt +++ b/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt @@ -35,8 +35,6 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppOpenEvent -import no.nordicsemi.android.gls.GLSServer -import no.nordicsemi.android.uart.UartServer import timber.log.Timber import javax.inject.Inject @@ -46,20 +44,11 @@ class NrfToolboxApplication : Application() { @Inject lateinit var analytics: AppAnalytics - @Inject - lateinit var uartServer: UartServer - - @Inject - lateinit var glsServer: GLSServer - override fun onCreate() { super.onCreate() analytics.logEvent(AppOpenEvent) - uartServer.start(this) - glsServer.start(this) - Timber.plant(Timber.DebugTree()) } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt index d76a3e6d0..e1217da47 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt @@ -31,45 +31,10 @@ package no.nordicsemi.android.nrftoolbox -import no.nordicsemi.android.bps.view.BPSScreen -import no.nordicsemi.android.cgms.view.CGMScreen import no.nordicsemi.android.common.navigation.createSimpleDestination import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.csc.view.CSCScreen -import no.nordicsemi.android.gls.main.view.GLSScreen -import no.nordicsemi.android.hrs.view.HRSScreen -import no.nordicsemi.android.hts.view.HTSScreen -import no.nordicsemi.android.nrftoolbox.view.HomeScreen -import no.nordicsemi.android.prx.view.PRXScreen -import no.nordicsemi.android.rscs.view.RSCSScreen -import no.nordicsemi.android.toolbox.scanner.ScannerDestination -import no.nordicsemi.android.uart.view.UARTScreen +import no.nordicsemi.android.nrftoolbox.view.HomeView val HomeDestinationId = createSimpleDestination("home-destination") -val HomeDestinations = listOf( - defineDestination(HomeDestinationId) { HomeScreen() }, - ScannerDestination -) - -val CSCDestinationId = createSimpleDestination("csc-destination") -val HRSDestinationId = createSimpleDestination("hrs-destination") -val HTSDestinationId = createSimpleDestination("hts-destination") -val GLSDestinationId = createSimpleDestination("gls-destination") -val BPSDestinationId = createSimpleDestination("bps-destination") -val PRXDestinationId = createSimpleDestination("prx-destination") -val RSCSDestinationId = createSimpleDestination("rscs-destination") -val CGMSDestinationId = createSimpleDestination("cgms-destination") -val UARTDestinationId = createSimpleDestination("uart-destination") - -val ProfileDestinations = listOf( - defineDestination(CSCDestinationId) { CSCScreen() }, - defineDestination(HRSDestinationId) { HRSScreen() }, - defineDestination(HTSDestinationId) { HTSScreen() }, - defineDestination(GLSDestinationId) { GLSScreen() }, - defineDestination(BPSDestinationId) { BPSScreen() }, - defineDestination(PRXDestinationId) { PRXScreen() }, - defineDestination(RSCSDestinationId) { RSCSScreen() }, - defineDestination(CGMSDestinationId) { CGMScreen() }, - defineDestination(UARTDestinationId) { UARTScreen() }, -) +val HomeDestinations = defineDestination(HomeDestinationId) { HomeView() } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt index e5a8682b2..532c39ea1 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt @@ -42,9 +42,8 @@ import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionRequestDia import no.nordicsemi.android.common.navigation.NavigationView import no.nordicsemi.android.common.theme.NordicActivity import no.nordicsemi.android.common.theme.NordicTheme -import no.nordicsemi.android.gls.GLSDestination +import no.nordicsemi.android.hts.HTSDestination import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals -import no.nordicsemi.android.toolbox.scanner.ScannerDestination import javax.inject.Inject @AndroidEntryPoint @@ -62,7 +61,7 @@ class MainActivity : NordicActivity() { color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxSize() ) { - NavigationView(HomeDestinations + ProfileDestinations + ScannerDestination + GLSDestination) + NavigationView(HomeDestinations + HTSDestination) } AnalyticsPermissionRequestDialog() diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/data/Spec.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/data/Spec.kt new file mode 100644 index 000000000..3e3e39760 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/data/Spec.kt @@ -0,0 +1,6 @@ +package no.nordicsemi.android.nrftoolbox.data + +import java.util.UUID + +val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") +val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/ApplicationScopeModule.kt similarity index 89% rename from app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt rename to app/src/main/java/no/nordicsemi/android/nrftoolbox/di/ApplicationScopeModule.kt index 0e1e58c81..e920a51df 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/ApplicationScopeModule.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.nrftoolbox +package no.nordicsemi.android.nrftoolbox.di import dagger.Module import dagger.Provides diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt new file mode 100644 index 000000000..7f0e417d6 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.android.nrftoolbox.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import no.nordicsemi.kotlin.ble.client.android.CentralManager +import no.nordicsemi.kotlin.ble.client.android.native + +@Module +@InstallIn(ViewModelComponent::class) +object CentralManagerModule { + + @Provides + fun provideCentralManager( + @ApplicationContext context: Context, + scope: CoroutineScope + ): CentralManager { + return CentralManager.Factory.native(context, scope) + } +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt deleted file mode 100644 index 27fcd0f02..000000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.nrftoolbox.view - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.nrftoolbox.R - -@Composable -fun FeatureButton( - @DrawableRes iconId: Int, - @StringRes nameCode: Int, - @StringRes name: Int, - isRunning: Boolean? = null, - @StringRes description: Int? = null, - onClick: () -> Unit -) { - OutlinedCard(onClick = onClick) { - Row( - modifier = Modifier.padding(16.dp).fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - val color = if (isRunning == true) { - colorResource(id = R.color.nordicGrass) - } else { - MaterialTheme.colorScheme.secondary - } - - Image( - painter = painterResource(iconId), - contentDescription = stringResource(id = name), - contentScale = ContentScale.Crop, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondary), - modifier = Modifier - .size(64.dp) - .clip(CircleShape) - .background(color) - .padding(16.dp) - ) - - Spacer(modifier = Modifier.size(16.dp)) - - Column(modifier = Modifier.weight(1f)) { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = name), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center - ) - - description?.let { - Spacer(modifier = Modifier.size(4.dp)) - - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = it), - style = MaterialTheme.typography.labelMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } -} - -@Preview -@Composable -private fun FeatureButtonPreview() { - FeatureButton(R.drawable.ic_csc, R.string.csc_module, R.string.csc_module_full) { } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt index f3765adab..97f729fe5 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt @@ -1,263 +1,59 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.nrftoolbox.view -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.analytics.Link -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileOpenEvent -import no.nordicsemi.android.nrftoolbox.BPSDestinationId -import no.nordicsemi.android.nrftoolbox.BuildConfig -import no.nordicsemi.android.nrftoolbox.CGMSDestinationId -import no.nordicsemi.android.nrftoolbox.CSCDestinationId -import no.nordicsemi.android.nrftoolbox.GLSDestinationId -import no.nordicsemi.android.nrftoolbox.HRSDestinationId -import no.nordicsemi.android.nrftoolbox.HTSDestinationId -import no.nordicsemi.android.nrftoolbox.PRXDestinationId +import no.nordicsemi.android.common.permissions.ble.RequireBluetooth +import no.nordicsemi.android.common.permissions.ble.RequireLocation import no.nordicsemi.android.nrftoolbox.R -import no.nordicsemi.android.nrftoolbox.RSCSDestinationId -import no.nordicsemi.android.nrftoolbox.UARTDestinationId import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel -private const val DFU_PACKAGE_NAME = "no.nordicsemi.android.dfu" -private const val DFU_LINK = "https://play.google.com/store/apps/details?id=no.nordicsemi.android.dfu" - -private const val LOGGER_PACKAGE_NAME = "no.nordicsemi.android.log" - @Composable -fun HomeScreen() { - val viewModel: HomeViewModel = hiltViewModel() - val state by viewModel.state.collectAsStateWithLifecycle() - - Scaffold( - topBar = { - TitleAppBar(stringResource(id = R.string.app_name)) - } +internal fun HomeView() { + val viewModel = hiltViewModel() + val state by viewModel.viewState.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Column( - modifier = Modifier - .padding(it) - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.viewmodel_profiles), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton(R.drawable.ic_gls, R.string.gls_module, R.string.gls_module_full) { - viewModel.openProfile(GLSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.GLS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton(R.drawable.ic_bps, R.string.bps_module, R.string.bps_module_full) { - viewModel.openProfile(BPSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.BPS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.service_profiles), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_csc, - R.string.csc_module, - R.string.csc_module_full, - state.isCSCModuleRunning - ) { - viewModel.openProfile(CSCDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.CSC)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_hrs, - R.string.hrs_module, - R.string.hrs_module_full, - state.isHRSModuleRunning - ) { - viewModel.openProfile(HRSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.HRS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_hts, - R.string.hts_module, - R.string.hts_module_full, - state.isHTSModuleRunning - ) { - viewModel.openProfile(HTSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.HTS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_rscs, - R.string.rscs_module, - R.string.rscs_module_full, - state.isRSCSModuleRunning - ) { - viewModel.openProfile(RSCSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.RSCS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_cgm, - R.string.cgm_module, - R.string.cgm_module_full, - state.isCGMModuleRunning - ) { - viewModel.openProfile(CGMSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.CGMS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_prx, - R.string.prx_module, - R.string.prx_module_full, - state.isPRXModuleRunning - ) { - viewModel.openProfile(PRXDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.PRX)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.utils_services), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_uart, - R.string.uart_module, - R.string.uart_module_full, - state.isUARTModuleRunning - ) { - viewModel.openProfile(UARTDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.UART)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - val uriHandler = LocalUriHandler.current - val context = LocalContext.current - val packageManger = context.packageManager - - val description = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME)?.let { - R.string.dfu_module_info - } ?: R.string.dfu_module_install - - FeatureButton(R.drawable.ic_dfu, R.string.dfu_module, R.string.dfu_module_full, null, description) { - val intent = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME) - if (intent != null) { - context.startActivity(intent) - } else { - uriHandler.openUri(DFU_LINK) + RequireBluetooth { + RequireLocation { isLocationRequiredAndDisabled -> + // Both Bluetooth and Location permissions are granted. + // We can now start scanning. + Scaffold( + topBar = { + TitleAppBar(stringResource(id = R.string.app_name), state.isScanning) } - viewModel.logEvent(ProfileOpenEvent(Link.DFU)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - val loggerDescription = packageManger.getLaunchIntentForPackage(LOGGER_PACKAGE_NAME)?.let { - R.string.logger_module_info - } ?: R.string.dfu_module_install - - FeatureButton( - R.drawable.ic_logger, - R.string.logger_module, - R.string.logger_module_full, - null, - loggerDescription ) { - viewModel.openLogger() - viewModel.logEvent(ProfileOpenEvent(Link.LOGGER)) + Column( + modifier = Modifier + .padding(it) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + state.devices.forEach { device -> + ScannedDeviceRow(device = device) { + viewModel.connect(device) + } + } + } } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt deleted file mode 100644 index 5fc11ff4f..000000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.nrftoolbox.view - -data class HomeViewState( - val isCSCModuleRunning: Boolean = false, - val isHRSModuleRunning: Boolean = false, - val isHTSModuleRunning: Boolean = false, - val isRSCSModuleRunning: Boolean = false, - val isPRXModuleRunning: Boolean = false, - val isCGMModuleRunning: Boolean = false, - val isUARTModuleRunning: Boolean = false, - val refreshToggle: Boolean = false -) { - - fun copyWithRefresh(): HomeViewState { - return copy(refreshToggle = !refreshToggle) - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/ScannedDeviceRow.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/ScannedDeviceRow.kt new file mode 100644 index 000000000..703de26eb --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/ScannedDeviceRow.kt @@ -0,0 +1,52 @@ +package no.nordicsemi.android.nrftoolbox.view + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.nordicBlue +import no.nordicsemi.kotlin.ble.client.android.Peripheral + +@Composable +internal fun ScannedDeviceRow( + device: Peripheral, + onDeviceClick: (Peripheral) -> Unit +) { + if (device.name == null) return + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .clickable { onDeviceClick(device) } + ) { + Icon( + imageVector = Icons.Default.Bluetooth, + contentDescription = "Bluetooth", + modifier = Modifier + .padding(8.dp) + .size(24.dp), + tint = MaterialTheme.colorScheme.nordicBlue, + ) + Column( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + ) { + Text(text = device.name ?: "Unknown") + Text(text = device.address) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt index ac65aa3b7..a58eb751f 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt @@ -31,6 +31,7 @@ package no.nordicsemi.android.nrftoolbox.view +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -43,7 +44,7 @@ import no.nordicsemi.android.nrftoolbox.R @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TitleAppBar(text: String) { +fun TitleAppBar(text: String, isScanning: Boolean) { TopAppBar( title = { Text(text, maxLines = 2) }, colors = TopAppBarDefaults.topAppBarColors( @@ -54,6 +55,8 @@ fun TitleAppBar(text: String) { navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, ), actions = { + if (isScanning) CircularProgressIndicator() + AnalyticsPermissionButton() } ) diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt index 94b1434a8..2b8e356af 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt @@ -1,123 +1,153 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.nrftoolbox.viewmodel -import android.content.Context import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.ProfileOpenEvent -import no.nordicsemi.android.cgms.repository.CGMRepository -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.common.navigation.DestinationId +import kotlinx.coroutines.launch import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.csc.repository.CSCRepository -import no.nordicsemi.android.hrs.service.HRSRepository -import no.nordicsemi.android.hts.repository.HTSRepository -import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals -import no.nordicsemi.android.nrftoolbox.view.HomeViewState -import no.nordicsemi.android.prx.repository.PRXRepository -import no.nordicsemi.android.rscs.repository.RSCSRepository -import no.nordicsemi.android.uart.repository.UARTRepository +import no.nordicsemi.android.hts.HTSDestinationId +import no.nordicsemi.android.nrftoolbox.data.BPS_SERVICE_UUID +import no.nordicsemi.android.nrftoolbox.data.HTS_SERVICE_UUID +import no.nordicsemi.android.toolbox.scanner.MockRemoteService +import no.nordicsemi.android.toolbox.scanner.Profile +import no.nordicsemi.kotlin.ble.client.android.CentralManager +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import no.nordicsemi.kotlin.ble.client.distinctByPeripheral +import no.nordicsemi.kotlin.ble.core.ConnectionState +import no.nordicsemi.kotlin.ble.core.util.distinct +import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +data class HomeViewState( + val isScanning: Boolean = false, + val devices: List = emptyList(), +) @HiltViewModel -class HomeViewModel @Inject constructor( - @ApplicationContext - private val context: Context, - private val navigationManager: Navigator, - private val activitySignals: ActivitySignals, - cgmRepository: CGMRepository, - cscRepository: CSCRepository, - hrsRepository: HRSRepository, - htsRepository: HTSRepository, - prxRepository: PRXRepository, - rscsRepository: RSCSRepository, - uartRepository: UARTRepository, - private val analytics: AppAnalytics +internal class HomeViewModel @Inject constructor( + private val centralManager: CentralManager, + private val navigator: Navigator, + private val scope: CoroutineScope, ) : ViewModel() { - private val _state = MutableStateFlow(HomeViewState()) - val state = _state.asStateFlow() + val viewState = _state.asStateFlow() + private var job: Job? = null + private var connectionScope: CoroutineScope? = null init { - cgmRepository.isRunning.onEach { - _state.value = _state.value.copy(isCGMModuleRunning = it) - }.launchIn(viewModelScope) - - cscRepository.isRunning.onEach { - _state.value = _state.value.copy(isCSCModuleRunning = it) - }.launchIn(viewModelScope) - - hrsRepository.isRunning.onEach { - _state.value = _state.value.copy(isHRSModuleRunning = it) - }.launchIn(viewModelScope) + startScanning() + } - htsRepository.isRunning.onEach { - _state.value = _state.value.copy(isHTSModuleRunning = it) - }.launchIn(viewModelScope) + private fun startScanning() { + _state.value = _state.value.copy(isScanning = true) + job = centralManager.scan(5000.milliseconds) + .distinctByPeripheral() + .map { + it.peripheral + } + .distinct() + .onEach { device -> + checkForExistingDevice(device) + } + .catch { e -> + Timber.e(e) + } + .onCompletion { + _state.value = _state.value.copy( + isScanning = false, + ) + } + .launchIn(scope) + } - prxRepository.isRunning.onEach { - _state.value = _state.value.copy(isPRXModuleRunning = it) - }.launchIn(viewModelScope) + private fun checkForExistingDevice(peripheral: Peripheral) { + val devices = _state.value.devices.toMutableList() + val existingDevice = devices.firstOrNull { it.address == peripheral.address } + if (existingDevice != null) { + val index = devices.indexOf(existingDevice) + devices[index] = peripheral + _state.value = _state.value.copy(devices = devices) + } else { + devices.add(peripheral) + _state.value = _state.value.copy(devices = devices) + } + } - rscsRepository.isRunning.onEach { - _state.value = _state.value.copy(isRSCSModuleRunning = it) - }.launchIn(viewModelScope) + private fun stopScanning() { + if (_state.value.isScanning) { + job?.cancel() + _state.value = _state.value.copy(isScanning = false) + } + } - uartRepository.isRunning.onEach { - _state.value = _state.value.copy(isUARTModuleRunning = it) - }.launchIn(viewModelScope) + fun connect(peripheral: Peripheral, autoConnect: Boolean = false) { + stopScanning() + connectionScope = CoroutineScope(context = Dispatchers.IO).apply { + launch { + centralManager.connect( + peripheral = peripheral, + options = if (autoConnect) { + CentralManager.ConnectionOptions.AutoConnect + } else CentralManager.ConnectionOptions.Default + ) + peripheral.state + .onEach { + if (it == ConnectionState.Connected) { + peripheral.services().onEach { remoteServices -> + remoteServices.forEach { remoteService -> + when (remoteService.uuid) { + HTS_SERVICE_UUID -> { + // HTS service found + // Navigate to the HTS screen. + navigator.navigateTo( + HTSDestinationId, Profile.HTS( + MockRemoteService( + remoteService, + peripheral.state, + peripheral, + ) + ) + ) + connectionScope?.cancel() + } - activitySignals.state.onEach { - _state.value = _state.value.copyWithRefresh() - }.launchIn(viewModelScope) + BPS_SERVICE_UUID -> { + Timber.tag("BBB").d("BPS Service found.") + // BPS service found + // Navigate to the BPS screen. + } + } + } + }.launchIn(this) + } + } + .onCompletion { + connectionScope?.cancel() + } + .launchIn(this) + } + } } - fun openProfile(destination: DestinationId) { - navigationManager.navigateTo(destination) + private fun closeCentralManager() { + if (job?.isActive == true) job?.cancel() + centralManager.close() } - fun openLogger() { - LoggerLauncher.launch(context, null) + public override fun onCleared() { + super.onCleared() + closeCentralManager() } - fun logEvent(event: ProfileOpenEvent) { - analytics.logEvent(event) - } -} +} \ No newline at end of file diff --git a/lib_scanner/build.gradle.kts b/lib_scanner/build.gradle.kts index 9a3684cec..4d1fd7304 100644 --- a/lib_scanner/build.gradle.kts +++ b/lib_scanner/build.gradle.kts @@ -31,6 +31,7 @@ plugins { alias(libs.plugins.nordic.feature) + alias(libs.plugins.kotlin.parcelize) } android { @@ -38,13 +39,6 @@ android { } dependencies { - implementation(libs.nordic.navigation) + implementation("no.nordicsemi.kotlin.ble:client-android") - implementation(libs.nordic.blek.uiscanner) - implementation(libs.nordic.blek.scanner) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) } diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/Profile.kt b/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/Profile.kt new file mode 100644 index 000000000..383eb0380 --- /dev/null +++ b/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/Profile.kt @@ -0,0 +1,37 @@ +package no.nordicsemi.android.toolbox.scanner + +import android.os.Parcelable +import kotlinx.coroutines.flow.StateFlow +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import no.nordicsemi.kotlin.ble.core.ConnectionState + +@Parcelize +data class MockRemoteService( + val serviceData: @RawValue RemoteService? = null, + val connectionState: @RawValue StateFlow? = null, + val peripheral: @RawValue Peripheral?=null, +) : Parcelable + +@Parcelize +sealed interface Profile: Parcelable { + val remoteService: MockRemoteService + get() = MockRemoteService() + + @Parcelize + data class HTS( + override val remoteService: MockRemoteService = MockRemoteService() + ) : Profile + + @Parcelize + data class HRS( + override val remoteService: MockRemoteService = MockRemoteService() + ) : Profile + + @Parcelize + data class BPS( + override val remoteService: MockRemoteService = MockRemoteService() + ) : Profile +} diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt b/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt deleted file mode 100644 index 9c44d07c1..000000000 --- a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt +++ /dev/null @@ -1,29 +0,0 @@ -package no.nordicsemi.android.toolbox.scanner - -import android.os.ParcelUuid -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.common.navigation.createDestination -import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.ui.scanner.DeviceSelected -import no.nordicsemi.android.kotlin.ble.ui.scanner.ScannerScreen -import no.nordicsemi.android.kotlin.ble.ui.scanner.ScanningCancelled - -val ScannerDestinationId = createDestination("uiscanner-destination") - -val ScannerDestination = defineDestination(ScannerDestinationId) { - val navigationViewModel = hiltViewModel() - - val arg = navigationViewModel.parameterOf(ScannerDestinationId) - - ScannerScreen( - uuid = arg, - onResult = { - when (it) { - is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.scanResults.device) - ScanningCancelled -> navigationViewModel.navigateUp() - } - } - ) -} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt index bf06b6082..a84a2ed7b 100644 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt @@ -34,7 +34,7 @@ package no.nordicsemi.android.ui.view import androidx.annotation.StringRes import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -51,8 +51,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus import no.nordicsemi.android.ui.R @OptIn(ExperimentalMaterial3Api::class) @@ -93,7 +91,7 @@ fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) { navigationIcon = { IconButton(onClick = { onClick() }) { Icon( - Icons.Default.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onPrimary, contentDescription = stringResource(id = R.string.back_screen), ) @@ -127,7 +125,7 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) { navigationIcon = { IconButton(onClick = { onClick() }) { Icon( - Icons.Default.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onPrimary, contentDescription = stringResource(id = R.string.back_screen), ) @@ -156,7 +154,7 @@ fun LoggerIconAppBar( navigationIcon = { IconButton(onClick = { onClick() }) { Icon( - Icons.Default.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onPrimary, contentDescription = stringResource(id = R.string.back_screen), ) @@ -187,19 +185,14 @@ fun LoggerIconAppBar( @Composable fun ProfileAppBar( deviceName: String?, - connectionState: GattConnectionStateWithStatus?, @StringRes title: Int, navigateUp: () -> Unit, disconnect: () -> Unit, openLogger: () -> Unit ) { - if (deviceName?.isNotBlank() == true) { - if (connectionState?.state == GattConnectionState.STATE_DISCONNECTING || connectionState?.state == GattConnectionState.STATE_DISCONNECTED) { - LoggerBackIconAppBar(deviceName, openLogger) - } else { - LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger) - } + if (deviceName != null) { + LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger) } else { BackIconAppBar(stringResource(id = title), navigateUp) } diff --git a/profile_gls/build.gradle.kts b/profile_gls/build.gradle.kts index f49b3b98c..dbc3affb0 100644 --- a/profile_gls/build.gradle.kts +++ b/profile_gls/build.gradle.kts @@ -78,7 +78,6 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(libs.androidx.test.ext) testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.slf4j.simple) testImplementation(libs.test.robolectric) testImplementation(libs.kotlin.junit) } diff --git a/profile_hts/build.gradle.kts b/profile_hts/build.gradle.kts index a4dde0012..5524277f2 100644 --- a/profile_hts/build.gradle.kts +++ b/profile_hts/build.gradle.kts @@ -32,6 +32,7 @@ plugins { alias(libs.plugins.nordic.feature) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.parcelize) } android { @@ -66,4 +67,5 @@ dependencies { // Timber & SLF4J implementation (libs.slf4j.timber) implementation(libs.nordic.log.timber) + implementation("no.nordicsemi.kotlin.ble:client-android") } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/HTSDestination.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/HTSDestination.kt new file mode 100644 index 000000000..e5307fa56 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/HTSDestination.kt @@ -0,0 +1,10 @@ +package no.nordicsemi.android.hts + +import no.nordicsemi.android.common.navigation.createDestination +import no.nordicsemi.android.common.navigation.defineDestination +import no.nordicsemi.android.hts.view.HtsHomeView +import no.nordicsemi.android.toolbox.scanner.Profile + +val HTSDestinationId = + createDestination("health-thermometer-service-destination") +val HTSDestination = defineDestination(HTSDestinationId) { HtsHomeView() } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/BatteryLavelParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/BatteryLavelParser.kt new file mode 100644 index 000000000..c4e84e472 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/BatteryLavelParser.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.hts.data + +import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray +import no.nordicsemi.android.kotlin.ble.core.data.util.IntFormat + +object BatteryLevelParser { + + fun parse(byte: ByteArray): Int? { + val bytes = DataByteArray(byte) + if (bytes.size == 1) { + return bytes.getIntValue(IntFormat.FORMAT_UINT8, 0) + } + return null + } +} \ No newline at end of file diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/DateTimeParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/DateTimeParser.kt new file mode 100644 index 000000000..6a1854c47 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/DateTimeParser.kt @@ -0,0 +1,44 @@ +package no.nordicsemi.android.hts.data + +import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray +import no.nordicsemi.android.kotlin.ble.core.data.util.IntFormat +import java.util.Calendar + +internal object DateTimeParser { + + fun parse(bytes: DataByteArray, offset: Int): Calendar? { + if (bytes.size < offset + 7) return null + val calendar = Calendar.getInstance() + val year: Int = bytes.getIntValue(IntFormat.FORMAT_UINT16_LE, offset) ?: return null + val month: Int = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset + 2) ?: return null + val day: Int = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset + 3) ?: return null + val hourOfDay: Int = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset + 4) ?: return null + val minute: Int = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset + 5) ?: return null + val second: Int = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset + 6) ?: return null + + if (year > 0) { + calendar[Calendar.YEAR] = year + } else { + calendar.clear(Calendar.YEAR) + } + + if (month > 0) { + calendar[Calendar.MONTH] = month - 1 + } else { + calendar.clear(Calendar.MONTH) + } + + if (day > 0) { + calendar[Calendar.DATE] = day + } else { + calendar.clear(Calendar.DATE) + } + + calendar[Calendar.HOUR_OF_DAY] = hourOfDay + calendar[Calendar.MINUTE] = minute + calendar[Calendar.SECOND] = second + calendar[Calendar.MILLISECOND] = 0 + + return calendar + } +} \ No newline at end of file diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSDataParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSDataParser.kt new file mode 100644 index 000000000..2cdb96fe7 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSDataParser.kt @@ -0,0 +1,48 @@ +package no.nordicsemi.android.hts.data + +import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray +import no.nordicsemi.android.kotlin.ble.core.data.util.FloatFormat +import no.nordicsemi.android.kotlin.ble.core.data.util.IntFormat +import java.util.Calendar + +object HTSDataParser { + + fun parse(byte: ByteArray): HtsData? { + val bytes = DataByteArray(byte) + + if (bytes.size < 5) { + return null + } + + var offset = 0 + val flags: Int = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset) ?: return null + + val unit: TemperatureUnitData = TemperatureUnitData.create(flags and 0x01) ?: return null + + val timestampPresent = flags and 0x02 != 0 + val temperatureTypePresent = flags and 0x04 != 0 + offset += 1 + + if (bytes.size < 5 + (if (timestampPresent) 7 else 0) + (if (temperatureTypePresent) 1 else 0)) { + return null + } + + val temperature: Float = + bytes.getFloatValue(FloatFormat.FORMAT_FLOAT, offset) ?: return null + offset += 4 + + var calendar: Calendar? = null + if (timestampPresent) { + calendar = DateTimeParser.parse(bytes, offset) + offset += 7 + } + + var type: Int? = null + if (temperatureTypePresent) { + type = bytes.getIntValue(IntFormat.FORMAT_UINT8, offset) + // offset += 1; + } + + return HtsData(temperature, unit, calendar, type) + } +} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt index d56e3f859..6dfadda68 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt @@ -1,53 +1,32 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.hts.data import no.nordicsemi.android.hts.view.TemperatureUnit -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.hts.data.HTSData +import no.nordicsemi.kotlin.ble.core.ConnectionState +import java.util.Calendar + +data class HtsData( + val temperature: Float = 0f, + val unit: TemperatureUnitData = TemperatureUnitData.CELSIUS, + val timestamp: Calendar? = null, + val type: Int? = null +) + +enum class TemperatureUnitData(private val value: Int) { + CELSIUS(0), + FAHRENHEIT(1); + + companion object { + fun create(value: Int): TemperatureUnitData? { + return entries.firstOrNull { it.value == value } + } + } +} internal data class HTSServiceData( - val data: HTSData = HTSData(), + val data: HtsData = HtsData(), val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } -} + val missingServices: Boolean = false, + val connectionState: ConnectionState = ConnectionState.Connecting, +) diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt deleted file mode 100644 index 6942b27b1..000000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.hts.data.HTSServiceData -import no.nordicsemi.android.hts.view.TemperatureUnit -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.hts.data.HTSData -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HTSRepository @Inject constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(HTSServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "HTS", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - _data.value = _data.value.copy(deviceName = device.name) - initLogger(device) - serviceManager.startService(HTSService::class.java, device) - } - - internal fun setTemperatureUnit(temperatureUnit: TemperatureUnit) { - _data.value = _data.value.copy(temperatureUnit = temperatureUnit) - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onHTSDataChanged(data: HTSData) { - _data.value = _data.value.copy(data = data) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = HTSServiceData() - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt deleted file mode 100644 index 1b40888c1..000000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.hts.HTSDataParser -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import java.util.* -import javax.inject.Inject - -val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") -private val HTS_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class HTSService : NotificationService() { - - @Inject - lateinit var repository: HTSRepository - - private var client: ClientBleGatt? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@HTSService, device, lifecycleScope) - this@HTSService.client = client - - client.connectionStateWithStatus - .onEach { repository.onConnectionStateChanged(it) } - .filterNotNull() - .onEach { stopIfDisconnected(it) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val htsService = services.findService(HTS_SERVICE_UUID)!! - val htsMeasurementCharacteristic = htsService.findCharacteristic(HTS_MEASUREMENT_CHARACTERISTIC_UUID)!! - - htsMeasurementCharacteristic.getNotifications() - .mapNotNull { HTSDataParser.parse(it) } - .onEach { repository.onHTSDataChanged(it) } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - } - - private fun stopIfDisconnected(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt deleted file mode 100644 index a37944394..000000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.common.ui.view.RadioButtonGroup -import no.nordicsemi.android.hts.R -import no.nordicsemi.android.hts.data.HTSServiceData -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.KeyValueField -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun HTSContentView(state: HTSServiceData, onEvent: (HTSScreenViewEvent) -> Unit) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - ScreenSection { - SectionTitle(resId = R.drawable.ic_thermometer, title = "Settings") - - Spacer(modifier = Modifier.height(16.dp)) - - RadioButtonGroup(viewEntity = state.temperatureUnit.temperatureSettingsItems()) { - onEvent(OnTemperatureUnitSelected(it.label.toTemperatureUnit())) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - ScreenSection { - SectionTitle(resId = R.drawable.ic_records, title = stringResource(id = R.string.hts_records_section)) - - Spacer(modifier = Modifier.height(16.dp)) - - KeyValueField( - stringResource(id = R.string.hts_temperature), - displayTemperature(state.data.temperature, state.temperatureUnit) - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} - -@Preview -@Composable -private fun Preview() { - HTSContentView(state = HTSServiceData()) { } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt index 4e6cf825f..66801d813 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt @@ -1,88 +1,132 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.hts.view +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.ui.view.RadioButtonGroup import no.nordicsemi.android.hts.R +import no.nordicsemi.android.hts.viewmodel.DisconnectClickEvent import no.nordicsemi.android.hts.viewmodel.HTSViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton +import no.nordicsemi.android.hts.viewmodel.HtsClickEvent +import no.nordicsemi.android.hts.viewmodel.HtsClickEventBack +import no.nordicsemi.android.hts.viewmodel.OnTemperatureUnitSelectedEvent +import no.nordicsemi.android.ui.view.BatteryLevelView +import no.nordicsemi.android.ui.view.KeyValueField import no.nordicsemi.android.ui.view.ProfileAppBar +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionTitle +import no.nordicsemi.kotlin.ble.core.ConnectionState @Composable -fun HTSScreen() { - val viewModel: HTSViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUp) } +internal fun HtsHomeView() { + val htsVM = hiltViewModel() + val healthThermometerServiceData by htsVM.viewState.collectAsStateWithLifecycle() + val onClickEvent: (HtsClickEvent) -> Unit = { htsVM.onEvent(it) } Scaffold( topBar = { ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, + deviceName = healthThermometerServiceData.deviceName, title = R.string.hts_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } + navigateUp = { onClickEvent(HtsClickEventBack) }, + disconnect = { onClickEvent(DisconnectClickEvent) }, + openLogger = { } ) } - ) { + ) { paddingValues -> Column( modifier = Modifier - .padding(it) + .padding(paddingValues) .verticalScroll(rememberScrollState()) .padding(16.dp) ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) + when (healthThermometerServiceData.connectionState) { + + ConnectionState.Connecting, ConnectionState.Disconnecting -> { + LoadingView() + } + + is ConnectionState.Disconnected -> {} + + ConnectionState.Connected -> { + + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ScreenSection { + SectionTitle(resId = R.drawable.ic_thermometer, title = "Settings") + + Spacer(modifier = Modifier.height(16.dp)) + + RadioButtonGroup(viewEntity = healthThermometerServiceData.temperatureUnit.temperatureSettingsItems()) { + onClickEvent(OnTemperatureUnitSelectedEvent(it.label.toTemperatureUnit())) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + ScreenSection { + SectionTitle( + resId = R.drawable.ic_records, + title = stringResource(id = R.string.hts_records_section) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + KeyValueField( + stringResource(id = R.string.hts_temperature), + displayTemperature( + healthThermometerServiceData.data.temperature, + healthThermometerServiceData.temperatureUnit + ) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + healthThermometerServiceData.batteryLevel?.let { batteryLevel -> + BatteryLevelView(batteryLevel) + + Spacer(modifier = Modifier.height(16.dp)) + } + + Button( + onClick = { } + ) { + Text(text = stringResource(id = R.string.disconnect)) + } + } } - GattConnectionState.STATE_CONNECTED -> HTSContentView(state) { viewModel.onEvent(it) } } } } } + +@Composable +internal fun LoadingView() { + Box( + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } +} \ No newline at end of file diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt deleted file mode 100644 index eb12feaaf..000000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.view - -internal sealed class HTSScreenViewEvent - -internal data class OnTemperatureUnitSelected(val value: TemperatureUnit) : HTSScreenViewEvent() - -internal object DisconnectEvent : HTSScreenViewEvent() - -internal object NavigateUp : HTSScreenViewEvent() - -internal object OpenLoggerEvent : HTSScreenViewEvent() diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt index c7d64d71b..ff00dd0c3 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt @@ -1,125 +1,171 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.hts.viewmodel -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.navigation.NavigationResult +import kotlinx.parcelize.RawValue import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.hts.repository.HTSRepository -import no.nordicsemi.android.hts.repository.HTS_SERVICE_UUID -import no.nordicsemi.android.hts.view.DisconnectEvent -import no.nordicsemi.android.hts.view.HTSScreenViewEvent -import no.nordicsemi.android.hts.view.NavigateUp -import no.nordicsemi.android.hts.view.OnTemperatureUnitSelected -import no.nordicsemi.android.hts.view.OpenLoggerEvent -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.hts.HTSDestinationId +import no.nordicsemi.android.hts.data.BatteryLevelParser +import no.nordicsemi.android.hts.data.HTSDataParser +import no.nordicsemi.android.hts.data.HTSServiceData +import no.nordicsemi.android.hts.view.TemperatureUnit +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import timber.log.Timber +import java.util.UUID import javax.inject.Inject +private val HTS_MEASUREMENT_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") + +private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") +private val BATTERY_LEVEL_CHARACTERISTIC_UUID = + UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") + +internal interface HtsClickEvent + +internal data object HtsClickEventBack : HtsClickEvent + +internal data object DisconnectClickEvent : HtsClickEvent + +internal data object LoggersClickEvent : HtsClickEvent + +internal data class OnTemperatureUnitSelectedEvent( + val value: TemperatureUnit = TemperatureUnit.CELSIUS, +) : HtsClickEvent + +/** + * ViewModel for the Health Thermometer Service. + */ @HiltViewModel internal class HTSViewModel @Inject constructor( - private val repository: HTSRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics -) : ViewModel() { - - val state = repository.data + private val navigator: Navigator, + savedStateHandle: SavedStateHandle, + private val scope: CoroutineScope, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val htsParam = parameterOf(HTSDestinationId).remoteService + private val _viewState: MutableStateFlow = + MutableStateFlow(HTSServiceData()) + val viewState = _viewState.asStateFlow() init { - repository.setOnScreen(true) - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } + htsParam + .serviceData?.let { remoteService -> + discoverService(remoteService) + } + htsParam.connectionState + ?.onEach { + _viewState.value = _viewState.value.copy(connectionState = it) + } + ?.launchIn(viewModelScope) } - - repository.data.onEach { - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.HTS)) - } - }.launchIn(viewModelScope) } - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(HTS_SERVICE_UUID)) + fun onEvent(event: HtsClickEvent) { + when (event) { + is HtsClickEventBack, is DisconnectClickEvent -> { + scope.launch { + // Disconnect from the device + htsParam.serviceData?.owner?.disconnect() + // Navigate back + disconnect(htsParam.peripheral!!) + navigator.navigateUp() + scope.cancel() + } + } - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } + LoggersClickEvent -> { + // Open the loggers screen. + } - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) + is OnTemperatureUnitSelectedEvent -> { + _viewState.value = _viewState.value.copy( + temperatureUnit = event.value + ) + } } } - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) + private suspend fun discoverService(remoteService: @RawValue RemoteService) { + remoteService.owner?.services() + ?.onEach { services -> + handleServiceDiscovery( + services, + HTS_MEASUREMENT_CHARACTERISTIC_UUID, + ::handleHTSData + ) + handleServiceDiscovery( + services, + BATTERY_LEVEL_CHARACTERISTIC_UUID, + ::handleBatteryLevel + ) + } + ?.onCompletion { + scope.cancel() + } + ?.catch { e -> Timber.e(e) } + ?.launchIn(scope) } - fun onEvent(event: HTSScreenViewEvent) { - when (event) { - DisconnectEvent -> disconnect() - is OnTemperatureUnitSelected -> onTemperatureUnitSelected(event) - NavigateUp -> navigationManager.navigateUp() - OpenLoggerEvent -> repository.openLogger() + private suspend fun handleServiceDiscovery( + services: List, + characteristicUuid: UUID, + handleData: suspend (characteristic: RemoteCharacteristic) -> Unit + ) { + services.forEach { service -> + service.characteristics.firstOrNull { it.uuid == characteristicUuid }?.let { + handleData(it) + } } } - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() + private suspend fun handleHTSData(characteristic: RemoteCharacteristic) { + characteristic.subscribe() + .mapNotNull { HTSDataParser.parse(it) } + .onEach { htsData -> + _viewState.value = _viewState.value.copy( + deviceName = characteristic.service.owner?.name, + data = htsData + ) + } + .catch { e -> Timber.e(e) } + .launchIn(viewModelScope) } - private fun onTemperatureUnitSelected(event: OnTemperatureUnitSelected) { - repository.setTemperatureUnit(event.value) + private suspend fun handleBatteryLevel(characteristic: RemoteCharacteristic) { + characteristic.subscribe() + .mapNotNull { BatteryLevelParser.parse(it) } + .onEach { batteryLevel -> + _viewState.value = _viewState.value.copy( + batteryLevel = batteryLevel + ) + } + .catch { e -> Timber.e(e) } + .launchIn(viewModelScope) } override fun onCleared() { super.onCleared() - repository.setOnScreen(false) + viewModelScope.cancel() } -} + + + private suspend fun disconnect(peripheral: Peripheral) { + peripheral.disconnect() + } + +} \ No newline at end of file diff --git a/profile_uart/build.gradle.kts b/profile_uart/build.gradle.kts index 1d88c4d64..6705f1195 100644 --- a/profile_uart/build.gradle.kts +++ b/profile_uart/build.gradle.kts @@ -89,7 +89,6 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(libs.androidx.test.ext) testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.slf4j.simple) testImplementation(libs.test.robolectric) testImplementation(libs.kotlin.junit) diff --git a/settings.gradle.kts b/settings.gradle.kts index 76db64550..761730f6b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,15 +59,7 @@ rootProject.name = "Android-nRF-Toolbox" include(":app") -include(":profile_bps") -include(":profile_cgms") -include(":profile_csc") -include(":profile_gls") -include(":profile_hrs") include(":profile_hts") -include(":profile_prx") -include(":profile_rscs") -include(":profile_uart") include(":lib_analytics") include(":lib_scanner") @@ -79,6 +71,6 @@ include(":lib_utils") // includeBuild("../Android-Common-Libraries") //} // -//if (file("../Kotlin-BLE-Library").exists()) { -// includeBuild("../Kotlin-BLE-Library") -//} +if (file("../Kotlin-BLE-Library").exists()) { + includeBuild("../Kotlin-BLE-Library") +}