diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb993657..4959a6eb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -79,4 +79,6 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.compose.material3) implementation(libs.androidx.activity.compose) + + implementation(libs.nordic.blek.client) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 90e9fc2f..05f78497 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,7 +30,8 @@ ~ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - + @@ -44,12 +45,14 @@ + android:theme="@style/NordicTheme" + android:dataExtractionRules="@xml/data_extraction_rules" + tools:targetApi="s"> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt index 4c2a5c4b..7f224e51 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt @@ -125,7 +125,7 @@ internal class BPSViewModel @Inject constructor( private fun startGattClient(device: ServerDevice) = viewModelScope.launch { _state.value = _state.value.copy(deviceName = device.name) - logger = NordicBlekLogger(context, stringConst.APP_NAME, "BPS", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "BPS", device.address) client = device.connect(context, logger = logger) diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt index ec7f73fc..7b144e7f 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt @@ -92,7 +92,7 @@ class CGMRepository @Inject constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger(context, stringConst.APP_NAME, "CGM", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "CGM", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(CGMService::class.java, device) } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt index 61750958..62d7242d 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt @@ -92,7 +92,7 @@ class CSCRepository @Inject constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger(context, stringConst.APP_NAME, "CSC", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "CSC", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(CSCService::class.java, device) } diff --git a/profile_gls/build.gradle.kts b/profile_gls/build.gradle.kts index af42e035..40a39bac 100644 --- a/profile_gls/build.gradle.kts +++ b/profile_gls/build.gradle.kts @@ -66,5 +66,12 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.service) + testImplementation(libs.junit4) + + testImplementation("io.mockk:mockk:1.13.5") + implementation("androidx.test.ext:junit-ktx:1.1.5") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1") + testImplementation("org.slf4j:slf4j-simple:2.0.5") + testImplementation("org.robolectric:robolectric:4.10.3") } diff --git a/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt b/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt index 0b5610f6..0ca35fc1 100644 --- a/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt +++ b/profile_gls/src/debug/java/no/nordicsemi/android/gls/GlsServer.kt @@ -38,6 +38,11 @@ class GlsServer @Inject constructor( private val scope: CoroutineScope ) { + lateinit var glsCharacteristic: BleServerGattCharacteristic + lateinit var glsContextCharacteristic: BleServerGattCharacteristic + lateinit var racpCharacteristic: BleServerGattCharacteristic + lateinit var batteryLevelCharacteristic: BleServerGattCharacteristic + private val records = listOf( byteArrayOf(0x07, 0x00, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x05, 0x00, 0x00, 0x26, 0xD2.toByte(), 0x11), byteArrayOf(0x07, 0x01, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x08, 0x00, 0x00, 0x3D, 0xD2.toByte(), 0x11), @@ -48,7 +53,13 @@ class GlsServer @Inject constructor( private val racp = byteArrayOf(0x06, 0x00, 0x01, 0x01) - fun start(context: Context) = scope.launch { + fun start( + context: Context, + device: MockServerDevice = MockServerDevice( + name = "GLS Server", + address = "55:44:33:22:11" + ), + ) = scope.launch { val gmCharacteristic = BleServerGattCharacteristicConfig( GLUCOSE_MEASUREMENT_CHARACTERISTIC, listOf(BleGattProperty.PROPERTY_NOTIFY), @@ -85,11 +96,6 @@ class GlsServer @Inject constructor( listOf(batteryLevelCharacteristic) ) - val device = MockServerDevice( - name = "GLS Server", - address = "55:44:33:22:11" - ) - val server = BleGattServer.create( context = context, config = arrayOf(serviceConfig, batteryService), @@ -107,16 +113,20 @@ class GlsServer @Inject constructor( } private fun setUpConnection(connection: BluetoothGattServerConnection) { + val glsService = connection.services.findService(GLS_SERVICE_UUID)!! + glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!! + glsContextCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)!! + racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! + + val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!! + batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! + + startGlsService(connection) - startBatteryService(connection) +// startBatteryService(connection) } private fun startGlsService(connection: BluetoothGattServerConnection) { - val glsService = connection.services.findService(GLS_SERVICE_UUID)!! - val glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!! - val glsContextCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)!! - val racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! - racpCharacteristic.value .filter { it.isNotEmpty() } .onEach { @@ -150,9 +160,6 @@ class GlsServer @Inject constructor( } private fun startBatteryService(connection: BluetoothGattServerConnection) { - val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!! - val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! - scope.launch { repeat(100) { batteryLevelCharacteristic.setValue(byteArrayOf(0x61)) diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt index 703c4cef..4fcbbf09 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt @@ -105,11 +105,11 @@ internal class GLSViewModel @Inject constructor( private val stringConst: StringConst ) : ViewModel() { - private lateinit var client: BleGattClient + internal lateinit var client: BleGattClient private lateinit var logger: NordicBlekLogger - private lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic - private lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic + internal lateinit var glucoseMeasurementCharacteristic: BleGattCharacteristic + internal lateinit var recordAccessControlPointCharacteristic: BleGattCharacteristic private val _state = MutableStateFlow(GLSViewState()) val state = _state.asStateFlow() @@ -117,6 +117,8 @@ internal class GLSViewModel @Inject constructor( private val highestSequenceNumber get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1 + fun test() = 2 + init { navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID)) @@ -125,7 +127,7 @@ internal class GLSViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun handleResult(result: NavigationResult) { + internal fun handleResult(result: NavigationResult) { when (result) { is NavigationResult.Cancelled -> navigationManager.navigateUp() is NavigationResult.Success -> onDeviceSelected(result.value) @@ -166,7 +168,7 @@ internal class GLSViewModel @Inject constructor( private fun startGattClient(device: ServerDevice) = viewModelScope.launch { _state.value = _state.value.copy(deviceName = device.name) - logger = NordicBlekLogger(context, stringConst.APP_NAME, "GLS", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "GLS", device.address) client = device.connect(context, logger = logger) @@ -194,7 +196,7 @@ internal class GLSViewModel @Inject constructor( client.disconnect() } - private fun logAnalytics(connectionState: GattConnectionStateWithStatus) { + internal fun logAnalytics(connectionState: GattConnectionStateWithStatus) { if (connectionState.state == GattConnectionState.STATE_CONNECTED) { analytics.logEvent(ProfileConnectedEvent(Profile.GLS)) } @@ -318,7 +320,7 @@ internal class GLSViewModel @Inject constructor( try { recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value) } catch (e: Exception) { - e.printStackTrace() + _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) } } } diff --git a/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt b/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt deleted file mode 100644 index 885a5d38..00000000 --- a/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package no.nordicsemi.android.gls - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - - - - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt b/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt new file mode 100644 index 00000000..c76729c2 --- /dev/null +++ b/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt @@ -0,0 +1,158 @@ +package no.nordicsemi.android.gls + +import android.content.Context +import io.mockk.coJustRun +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.spyk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import no.nordicsemi.android.analytics.AppAnalytics +import no.nordicsemi.android.common.logger.NordicBlekLogger +import no.nordicsemi.android.common.navigation.NavigationResult +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.gls.data.WorkingMode +import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected +import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel +import no.nordicsemi.android.kotlin.ble.client.main.service.BleGattCharacteristic +import no.nordicsemi.android.kotlin.ble.core.MockServerDevice +import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus +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.gls.data.RequestStatus +import no.nordicsemi.android.ui.view.StringConst +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowBluetoothGatt + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(RobolectricTestRunner::class) +internal class GLSViewModelTest { + + @get:Rule + val mockkRule = MockKRule(this) + + @RelaxedMockK + lateinit var navigator: Navigator + + @RelaxedMockK + lateinit var analytics: AppAnalytics + + @MockK + lateinit var stringConst: StringConst + + @RelaxedMockK + lateinit var context: Context + + @RelaxedMockK + lateinit var logger: NordicBlekLogger + + @MockK + lateinit var characteristic: BleGattCharacteristic + + lateinit var viewModel: GLSViewModel + + lateinit var glsServer: GlsServer + + private val device = MockServerDevice( + name = "GLS Server", + address = "55:44:33:22:11" + ) + + @Before + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun release() { + Dispatchers.resetMain() + } + + @Before + fun before() { + runBlocking { + viewModel = spyk(GLSViewModel(context, navigator, analytics, stringConst)) + glsServer = GlsServer(CoroutineScope(UnconfinedTestDispatcher())) + glsServer.start(spyk(), device) + } + } + + @Before + fun prepareLogger() { + mockkObject(NordicBlekLogger.Companion) + every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk() + } + + @Test + fun addition_isCorrect() { + assertEquals(2, viewModel.test()) + } + + @Test + fun checkOnClick() = runTest { + every { viewModel.recordAccessControlPointCharacteristic } returns characteristic + coJustRun { characteristic.write(any(), any()) } + + viewModel.onEvent(OnWorkingModeSelected(WorkingMode.FIRST)) + + advanceUntilIdle() + assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) + } + + @Test + fun `when connection fails return disconnected`() { + val serverDevice = mockk() + val disconnectedState = GattConnectionStateWithStatus( + GattConnectionState.STATE_DISCONNECTED, + BleGattConnectionStatus.SUCCESS + ) +// every { viewModel.recordAccessControlPointCharacteristic } returns characteristic +// coJustRun { characteristic.write(any(), any()) } + mockkStatic("no.nordicsemi.android.kotlin.ble.client.main.ClientDeviceExtKt") + every { serverDevice.name } returns "Test" + every { serverDevice.address } returns "11:22:33:44:55" + every { stringConst.APP_NAME } returns "Test" + + viewModel.handleResult(NavigationResult.Success(serverDevice)) + + assertEquals(disconnectedState, viewModel.state.value.glsServiceData.connectionState) + } + + @Test + fun checkOnClick2() = runTest { + every { viewModel.recordAccessControlPointCharacteristic } returns characteristic + coJustRun { characteristic.write(any(), any()) } + mockkStatic("no.nordicsemi.android.kotlin.ble.client.main.ClientDeviceExtKt") + every { stringConst.APP_NAME } returns "Test" + justRun { viewModel.logAnalytics(any()) } + + viewModel.handleResult(NavigationResult.Success(device)) + viewModel.onEvent(OnWorkingModeSelected(WorkingMode.FIRST)) + + advanceUntilIdle() + assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) + } +} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt index 4153883c..1d8a85f8 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt @@ -86,7 +86,7 @@ class HRSRepository @Inject constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger(context, stringConst.APP_NAME, "HRS", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "HRS", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(HRSService::class.java, device) } 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 index e1714b94..9ef54683 100644 --- 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 @@ -87,7 +87,7 @@ class HTSRepository @Inject constructor( fun launch(device: ServerDevice) { _data.value = _data.value.copy(deviceName = device.name) - logger = NordicBlekLogger(context, stringConst.APP_NAME, "HTS", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "HTS", device.address) serviceManager.startService(HTSService::class.java, device) } diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt index d795753a..5d85fc54 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt @@ -88,7 +88,7 @@ class PRXRepository @Inject internal constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger(context, stringConst.APP_NAME, "PRX", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "PRX", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(PRXService::class.java, device) } diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt index 2889cb13..90adc396 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt @@ -85,7 +85,7 @@ class RSCSRepository @Inject constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger(context, stringConst.APP_NAME, "RSCS", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "RSCS", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(RSCSService::class.java, device) } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt index dca16a06..e6607dd0 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt @@ -97,7 +97,7 @@ class UARTRepository @Inject internal constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger(context, stringConst.APP_NAME, "UART", device.address) + logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "UART", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(UARTService::class.java, device) }