From 2870ef109ef5409c7f05408f1cf264fb80dfdf1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylwester=20Zieli=C5=84ski?= Date: Fri, 23 Jun 2023 10:55:26 +0200 Subject: [PATCH] Make test with service working --- .../nrftoolbox/NrfToolboxApplication.kt | 53 ++--- .../nrftoolbox/NrfToolboxApplication.kt | 6 - .../android/service/ServiceManager.kt | 34 +-- .../service/ServiceManagerHiltModule.kt | 21 ++ .../android/service/ServiceManagerImpl.kt | 20 ++ lib_ui/build.gradle.kts | 4 + ...le.kt => NordicLoggerFactoryHiltModule.kt} | 3 +- .../gls/main/viewmodel/GLSViewModel.kt | 2 - .../android/gls/GLSViewModelTest.kt | 6 - profile_uart/build.gradle.kts | 21 ++ .../no/nordicsemi/android/gls/UartServer.kt | 184 ++++++++++++++++ .../nordicsemi/android/uart/DaoHiltModule.kt | 20 ++ .../nordicsemi/android/uart/DbHiltModule.kt | 26 +++ .../android/uart/repository/UARTRepository.kt | 7 +- .../android/uart/repository/UARTService.kt | 8 +- .../android/uart/viewmodel/UARTViewModel.kt | 6 +- .../gls/NordicLoggerFactoryTestModule.kt | 40 ++++ .../android/gls/ServiceManagerTestModule.kt | 57 +++++ .../android/gls/TestDbHiltModule.kt | 29 +++ .../nordicsemi/android/gls/TestHiltModule.kt | 13 ++ .../android/gls/UARTViewModelTest.kt | 208 ++++++++++++++++++ 21 files changed, 682 insertions(+), 86 deletions(-) rename profile_uart/src/main/java/no/nordicsemi/android/uart/HiltModule.kt => app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt (62%) rename app/src/{main => release}/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt (94%) create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt rename lib_ui/src/main/java/no/nordicsemi/android/ui/view/{HiltModule.kt => NordicLoggerFactoryHiltModule.kt} (91%) create mode 100644 profile_uart/src/debug/java/no/nordicsemi/android/gls/UartServer.kt create mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt create mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt create mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt create mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt create mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt create mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt create mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/HiltModule.kt b/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt similarity index 62% rename from profile_uart/src/main/java/no/nordicsemi/android/uart/HiltModule.kt rename to app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt index e3a8dcc3..7919cd5d 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/HiltModule.kt +++ b/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt @@ -29,37 +29,32 @@ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package no.nordicsemi.android.uart +package no.nordicsemi.android.nrftoolbox -import android.content.Context -import androidx.room.Room -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import no.nordicsemi.android.uart.db.ConfigurationsDao -import no.nordicsemi.android.uart.db.ConfigurationsDatabase -import no.nordicsemi.android.uart.db.MIGRATION_1_2 -import javax.inject.Singleton +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.UartServer +import javax.inject.Inject -@Module -@InstallIn(SingletonComponent::class) -class HiltModule { +@HiltAndroidApp +class NrfToolboxApplication : Application() { - @Provides - @Singleton - internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase { - return Room.databaseBuilder( - context, - ConfigurationsDatabase::class.java, "toolbox_uart.db" - ).addMigrations(MIGRATION_1_2).build() + @Inject + lateinit var analytics: AppAnalytics + + @Inject + lateinit var glsServer: UartServer + + @Inject + lateinit var uartServer: UartServer + + override fun onCreate() { + super.onCreate() + + analytics.logEvent(AppOpenEvent) + + uartServer.start(this) } - - @Provides - @Singleton - internal fun provideDao(db: ConfigurationsDatabase): ConfigurationsDao { - return db.dao() - } - } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt b/app/src/release/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt similarity index 94% rename from app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt rename to app/src/release/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt index fce2b821..05cb85b9 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt +++ b/app/src/release/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt @@ -35,7 +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 javax.inject.Inject @HiltAndroidApp @@ -44,14 +43,9 @@ class NrfToolboxApplication : Application() { @Inject lateinit var analytics: AppAnalytics - @Inject - lateinit var glsServer: GlsServer - override fun onCreate() { super.onCreate() analytics.logEvent(AppOpenEvent) - - glsServer.start(this) } } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt index faf0c063..c9ae50fc 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt @@ -31,41 +31,11 @@ package no.nordicsemi.android.service -import android.bluetooth.BluetoothDevice -import android.content.Context -import android.content.Intent -import dagger.hilt.android.qualifiers.ApplicationContext import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import javax.inject.Inject const val DEVICE_DATA = "device-data" -class ServiceManager @Inject constructor( - @ApplicationContext - private val context: Context -) { +interface ServiceManager { - fun startService(service: Class, device: ServerDevice) { - val intent = Intent(context, service).apply { - putExtra(DEVICE_DATA, device) - } - context.startService(intent) - } - - fun startService(service: Class, device: BluetoothDevice) { - val intent = Intent(context, service).apply { - putExtra(DEVICE_DATA, device) - } - context.startService(intent) - } - - fun startService(service: Class) { - val intent = Intent(context, service) - context.startService(intent) - } - - fun stopService(service: Class) { - val intent = Intent(context, service) - context.stopService(intent) - } + fun startService(service: Class, device: ServerDevice) } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt new file mode 100644 index 00000000..b9b0eba9 --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt @@ -0,0 +1,21 @@ +package no.nordicsemi.android.service + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class ServiceManagerHiltModule { + + @Provides + fun createServiceManager( + @ApplicationContext + context: Context, + ): ServiceManager { + return ServiceManagerImpl(context) + } +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt new file mode 100644 index 00000000..1872a84d --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.service + +import android.content.Context +import android.content.Intent +import dagger.hilt.android.qualifiers.ApplicationContext +import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import javax.inject.Inject + +class ServiceManagerImpl @Inject constructor( + @ApplicationContext + private val context: Context +): ServiceManager { + + override fun startService(service: Class, device: ServerDevice) { + val intent = Intent(context, service).apply { + putExtra(DEVICE_DATA, device) + } + context.startService(intent) + } +} diff --git a/lib_ui/build.gradle.kts b/lib_ui/build.gradle.kts index fd628e96..e8a0ff7a 100644 --- a/lib_ui/build.gradle.kts +++ b/lib_ui/build.gradle.kts @@ -35,6 +35,10 @@ plugins { android { namespace = "no.nordicsemi.android.ui" + + testOptions { + unitTests.isIncludeAndroidResources = true + } } dependencies { diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/HiltModule.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/NordicLoggerFactoryHiltModule.kt similarity index 91% rename from lib_ui/src/main/java/no/nordicsemi/android/ui/view/HiltModule.kt rename to lib_ui/src/main/java/no/nordicsemi/android/ui/view/NordicLoggerFactoryHiltModule.kt index 679c69b5..8550b29d 100644 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/HiltModule.kt +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/NordicLoggerFactoryHiltModule.kt @@ -6,12 +6,11 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import no.nordicsemi.android.common.logger.NordicBlekLogger -import no.nordicsemi.android.common.logger.BlekLogger import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher @Module @InstallIn(SingletonComponent::class) -class HiltModule { +class NordicLoggerFactoryHiltModule { @Provides fun createLogger(): NordicLoggerFactory { 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 b9b6235d..09a3352c 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 @@ -119,8 +119,6 @@ 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)) 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 index c2eb1b56..22f4c8ed 100644 --- a/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt +++ b/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt @@ -37,7 +37,6 @@ import no.nordicsemi.android.ui.view.NordicLoggerFactory import no.nordicsemi.android.ui.view.StringConst import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -121,11 +120,6 @@ internal class GLSViewModelTest { every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk() } - @Test - fun addition_isCorrect() { - assertEquals(2, viewModel.test()) - } - @Test fun `when connection fails return disconnected`() = runTest { val disconnectedState = GattConnectionStateWithStatus( diff --git a/profile_uart/build.gradle.kts b/profile_uart/build.gradle.kts index 1662b6ce..bd8230c1 100644 --- a/profile_uart/build.gradle.kts +++ b/profile_uart/build.gradle.kts @@ -38,6 +38,10 @@ plugins { android { namespace = "no.nordicsemi.android.uart" + + testOptions { + unitTests.isIncludeAndroidResources = true + } } wire { @@ -54,6 +58,8 @@ dependencies { implementation(libs.nordic.blek.client) implementation(libs.nordic.blek.profile) implementation(libs.nordic.blek.core) + implementation(libs.nordic.blek.server) + implementation(libs.nordic.blek.advertiser) implementation(libs.room.runtime) implementation(libs.room.ktx) @@ -81,6 +87,21 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.service) + // For Robolectric tests. + testImplementation("com.google.dagger:hilt-android-testing:2.44") + // ...with Kotlin. + kaptTest("com.google.dagger:hilt-android-compiler:2.46.1") + + testImplementation("androidx.test:rules:1.5.0") + + testImplementation(libs.junit4) + testImplementation(libs.test.mockk) + testImplementation(libs.androidx.test.ext) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.test.slf4j.simple) + testImplementation(libs.test.robolectric) + testImplementation(libs.kotlin.junit) + implementation("org.simpleframework:simple-xml:2.7.1") { exclude(group = "stax", module = "stax-api") exclude(group = "xpp3", module = "xpp3") diff --git a/profile_uart/src/debug/java/no/nordicsemi/android/gls/UartServer.kt b/profile_uart/src/debug/java/no/nordicsemi/android/gls/UartServer.kt new file mode 100644 index 00000000..bf041074 --- /dev/null +++ b/profile_uart/src/debug/java/no/nordicsemi/android/gls/UartServer.kt @@ -0,0 +1,184 @@ +package no.nordicsemi.android.gls + +import android.annotation.SuppressLint +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +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.advertiser.BleAdvertiser +import no.nordicsemi.android.kotlin.ble.core.MockServerDevice +import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertiseConfig +import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission +import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty +import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser +import no.nordicsemi.android.kotlin.ble.server.main.BleGattServer +import no.nordicsemi.android.kotlin.ble.server.main.service.BleGattServerServiceType +import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattCharacteristic +import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattCharacteristicConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.BleServerGattServiceConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.BluetoothGattServerConnection +import no.nordicsemi.android.uart.repository.BATTERY_LEVEL_CHARACTERISTIC_UUID +import no.nordicsemi.android.uart.repository.BATTERY_SERVICE_UUID +import no.nordicsemi.android.uart.repository.UART_RX_CHARACTERISTIC_UUID +import no.nordicsemi.android.uart.repository.UART_SERVICE_UUID +import no.nordicsemi.android.uart.repository.UART_TX_CHARACTERISTIC_UUID +import javax.inject.Inject +import javax.inject.Singleton + +private const val STANDARD_DELAY = 1000L + +@SuppressLint("MissingPermission") +@Singleton +class UartServer @Inject constructor( + private val scope: CoroutineScope +) { + + lateinit var server: BleGattServer + + lateinit var glsCharacteristic: BleServerGattCharacteristic + lateinit var glsContextCharacteristic: BleServerGattCharacteristic + lateinit var racpCharacteristic: BleServerGattCharacteristic + lateinit var batteryLevelCharacteristic: BleServerGattCharacteristic + + private var lastRequest = byteArrayOf() + + val YOUNGEST_RECORD = byteArrayOf(0x07, 0x00, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x05, 0x00, 0x00, 0x26, 0xD2.toByte(), 0x11) + val OLDEST_RECORD = byteArrayOf(0x07, 0x04, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x11, 0x00, 0x00, 0x82.toByte(), 0xD2.toByte(), 0x11) + + val records = listOf( + YOUNGEST_RECORD, + byteArrayOf(0x07, 0x01, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x08, 0x00, 0x00, 0x3D, 0xD2.toByte(), 0x11), + byteArrayOf(0x07, 0x02, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x0B, 0x00, 0x00, 0x54, 0xD2.toByte(), 0x11), + byteArrayOf(0x07, 0x03, 0x00, 0xDC.toByte(), 0x07, 0x01, 0x01, 0x0C, 0x1E, 0x0E, 0x00, 0x00, 0x6B, 0xD2.toByte(), 0x11), + OLDEST_RECORD + ) + + val racp = byteArrayOf(0x06, 0x00, 0x01, 0x01) + + fun start( + context: Context, + device: MockServerDevice = MockServerDevice( + name = "GLS Server", + address = "55:44:33:22:11" + ), + ) = scope.launch { + val rxCharacteristic = BleServerGattCharacteristicConfig( + UART_RX_CHARACTERISTIC_UUID, + listOf(BleGattProperty.PROPERTY_NOTIFY), + listOf() + ) + + val txCharacteristic = BleServerGattCharacteristicConfig( + UART_TX_CHARACTERISTIC_UUID, + listOf(BleGattProperty.PROPERTY_INDICATE, BleGattProperty.PROPERTY_WRITE), + listOf(BleGattPermission.PERMISSION_WRITE) + ) + + val uartService = BleServerGattServiceConfig( + UART_SERVICE_UUID, + BleGattServerServiceType.SERVICE_TYPE_PRIMARY, + listOf(rxCharacteristic, txCharacteristic) + ) + + val batteryLevelCharacteristic = BleServerGattCharacteristicConfig( + BATTERY_LEVEL_CHARACTERISTIC_UUID, + listOf(BleGattProperty.PROPERTY_READ, BleGattProperty.PROPERTY_NOTIFY), + listOf(BleGattPermission.PERMISSION_READ) + ) + + val batteryService = BleServerGattServiceConfig( + BATTERY_SERVICE_UUID, + BleGattServerServiceType.SERVICE_TYPE_PRIMARY, + listOf(batteryLevelCharacteristic) + ) + + server = BleGattServer.create( + context = context, + config = arrayOf(uartService, batteryService), + mock = device + ) + + val advertiser = BleAdvertiser.create(context) + advertiser.advertise(config = BleAdvertiseConfig(), mock = device).launchIn(scope) + + launch { + server.connections + .mapNotNull { it.values.firstOrNull() } + .collect { setUpConnection(it) } + } + } + + internal fun stopServer() { + server.stopServer() + } + + 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) + } + + private fun startGlsService(connection: BluetoothGattServerConnection) { + racpCharacteristic.value + .filter { it.isNotEmpty() } + .onEach { lastRequest = it } + .launchIn(scope) + } + + internal fun continueWithResponse() { + sendResponse(lastRequest) + } + + private fun sendResponse(request: ByteArray) { + if (request.contentEquals(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords().value)) { + sendAll(glsCharacteristic) + racpCharacteristic.setValue(racp) + } else if (request.contentEquals(RecordAccessControlPointInputParser.reportLastStoredRecord().value)) { + sendLast(glsCharacteristic) + racpCharacteristic.setValue(racp) + } else if (request.contentEquals(RecordAccessControlPointInputParser.reportFirstStoredRecord().value)) { + sendFirst(glsCharacteristic) + racpCharacteristic.setValue(racp) + } + } + + private fun sendFirst(characteristics: BleServerGattCharacteristic) { + characteristics.setValue(records.first()) + } + + private fun sendLast(characteristics: BleServerGattCharacteristic) { + characteristics.setValue(records.last()) + } + + private fun sendAll(characteristics: BleServerGattCharacteristic) = scope.launch { + records.forEach { + characteristics.setValue(it) + delay(100) + } + } + + private fun startBatteryService(connection: BluetoothGattServerConnection) { + scope.launch { + repeat(100) { + batteryLevelCharacteristic.setValue(byteArrayOf(0x61)) + delay(STANDARD_DELAY) + batteryLevelCharacteristic.setValue(byteArrayOf(0x60)) + delay(STANDARD_DELAY) + batteryLevelCharacteristic.setValue(byteArrayOf(0x5F)) + delay(STANDARD_DELAY) + } + } + } +} \ No newline at end of file diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt new file mode 100644 index 00000000..68b70208 --- /dev/null +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.uart + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.uart.db.ConfigurationsDao +import no.nordicsemi.android.uart.db.ConfigurationsDatabase +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DaoHiltModule { + + @Provides + @Singleton + internal fun provideDao(db: ConfigurationsDatabase): ConfigurationsDao { + return db.dao() + } +} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt new file mode 100644 index 00000000..327198d4 --- /dev/null +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt @@ -0,0 +1,26 @@ +package no.nordicsemi.android.uart + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.uart.db.ConfigurationsDatabase +import no.nordicsemi.android.uart.db.MIGRATION_1_2 +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DbHiltModule { + + @Provides + @Singleton + internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase { + return Room.databaseBuilder( + context, + ConfigurationsDatabase::class.java, "toolbox_uart.db" + ).addMigrations(MIGRATION_1_2).build() + } +} 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 c4000fe0..961ef4b4 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 @@ -39,7 +39,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher -import no.nordicsemi.android.common.logger.NordicBlekLogger 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 @@ -52,6 +51,7 @@ import no.nordicsemi.android.uart.data.UARTRecord import no.nordicsemi.android.uart.data.UARTRecordType import no.nordicsemi.android.uart.data.UARTServiceData import no.nordicsemi.android.uart.data.parseWithNewLineChar +import no.nordicsemi.android.ui.view.NordicLoggerFactory import no.nordicsemi.android.ui.view.StringConst import javax.inject.Inject import javax.inject.Singleton @@ -62,7 +62,8 @@ class UARTRepository @Inject internal constructor( private val context: Context, private val serviceManager: ServiceManager, private val configurationDataSource: ConfigurationDataSource, - private val stringConst: StringConst + private val stringConst: StringConst, + private val loggerFactory: NordicLoggerFactory ) { private var logger: BlekLoggerAndLauncher? = null @@ -97,7 +98,7 @@ class UARTRepository @Inject internal constructor( private fun shouldClean() = !isOnScreen && !isServiceRunning fun launch(device: ServerDevice) { - logger = NordicBlekLogger.create(context, stringConst.APP_NAME, "UART", device.address) + logger = loggerFactory.createNordicLogger(context, stringConst.APP_NAME, "UART", device.address) _data.value = _data.value.copy(deviceName = device.name) serviceManager.startService(UARTService::class.java, device) } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt index ad632829..40812a05 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt @@ -56,11 +56,11 @@ import java.util.* import javax.inject.Inject val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") -private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") -private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") +internal val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") +internal val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") -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 val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") +internal val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") @SuppressLint("MissingPermission") @AndroidEntryPoint diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt index 17cd4cff..c528ef9d 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt @@ -77,6 +77,7 @@ import no.nordicsemi.android.uart.view.OnRunMacro import no.nordicsemi.android.uart.view.OpenLogger import no.nordicsemi.android.uart.view.UARTViewEvent import no.nordicsemi.android.uart.view.UARTViewState +import no.nordicsemi.android.ui.view.NordicLoggerFactory import javax.inject.Inject @HiltViewModel @@ -84,7 +85,8 @@ internal class UARTViewModel @Inject constructor( private val repository: UARTRepository, private val navigationManager: Navigator, private val dataSource: UARTPersistentDataSource, - private val analytics: AppAnalytics + private val analytics: AppAnalytics, + private val loggerFactory: NordicLoggerFactory ) : ViewModel() { private val _state = MutableStateFlow(UARTViewState()) @@ -126,7 +128,7 @@ internal class UARTViewModel @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) diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt new file mode 100644 index 00000000..18a53331 --- /dev/null +++ b/profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt @@ -0,0 +1,40 @@ +package no.nordicsemi.android.gls + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import no.nordicsemi.android.common.logger.BlekLoggerAndLauncher +import no.nordicsemi.android.ui.view.NordicLoggerFactory +import no.nordicsemi.android.ui.view.NordicLoggerFactoryHiltModule + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [NordicLoggerFactoryHiltModule::class] +) +class NordicLoggerFactoryTestModule { + + @Provides + fun createLogger(): NordicLoggerFactory { + return object : NordicLoggerFactory { + override fun createNordicLogger( + context: Context, + profile: String?, + key: String, + name: String?, + ): BlekLoggerAndLauncher { + return object : BlekLoggerAndLauncher { + override fun launch() { + + } + + override fun log(priority: Int, log: String) { + println(log) + } + } + } + } + } +} diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt new file mode 100644 index 00000000..70b59201 --- /dev/null +++ b/profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt @@ -0,0 +1,57 @@ +package no.nordicsemi.android.gls + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import no.nordicsemi.android.kotlin.ble.core.MockServerDevice +import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import no.nordicsemi.android.service.DEVICE_DATA +import no.nordicsemi.android.service.ServiceManager +import no.nordicsemi.android.service.ServiceManagerHiltModule +import no.nordicsemi.android.uart.repository.UARTService +import org.robolectric.Robolectric +import org.robolectric.android.controller.ServiceController +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [ServiceManagerHiltModule::class] +) +class ServiceManagerTestModule { + + private val componentName = ComponentName("org.robolectric", UARTService::class.java.name) + + @Provides + internal fun provideDevice(): MockServerDevice { + return MockServerDevice( + name = "GLS Server", + address = "55:44:33:22:11" + ) + } + + @Provides + internal fun provideServiceController( + @ApplicationContext context: Context, + device: MockServerDevice + ): ServiceController { + return Robolectric.buildService(UARTService::class.java, Intent(context, UARTService::class.java).apply { + putExtra(DEVICE_DATA, device) + }) + } + + @Provides + @Singleton + internal fun provideServiceManager(controller: ServiceController): ServiceManager { + return object : ServiceManager { + override fun startService(service: Class, device: ServerDevice) { + controller.create().startCommand(3, 4).get() + } + } + } +} diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt new file mode 100644 index 00000000..3dab6188 --- /dev/null +++ b/profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt @@ -0,0 +1,29 @@ +package no.nordicsemi.android.gls + +import android.content.Context +import androidx.room.Room +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import no.nordicsemi.android.uart.DbHiltModule +import no.nordicsemi.android.uart.db.ConfigurationsDatabase +import no.nordicsemi.android.uart.db.MIGRATION_1_2 +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [DbHiltModule::class] +) +class TestDbHiltModule { + @Provides + @Singleton + internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase { + return Room.inMemoryDatabaseBuilder( + context, + ConfigurationsDatabase::class.java + ).addMigrations(MIGRATION_1_2).build() + } +} \ No newline at end of file diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt new file mode 100644 index 00000000..36e316cb --- /dev/null +++ b/profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt @@ -0,0 +1,13 @@ +package no.nordicsemi.android.gls + +import dagger.Module +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn + +//@Module +//@TestInstallIn( +// components = [SingletonComponent::class], +// replaces = [AnalyticsModule::class] +//) +//class TestHiltModule { +//} diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt new file mode 100644 index 00000000..fcedf25a --- /dev/null +++ b/profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt @@ -0,0 +1,208 @@ +package no.nordicsemi.android.gls + +import android.content.Context +import androidx.test.rule.ServiceTestRule +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.HiltTestApplication +import dagger.hilt.android.testing.UninstallModules +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit4.MockKRule +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.common.navigation.di.NavigationModule +import no.nordicsemi.android.kotlin.ble.client.main.ClientScope +import no.nordicsemi.android.kotlin.ble.core.MockServerDevice +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.server.main.ServerScope +import no.nordicsemi.android.uart.data.UARTPersistentDataSource +import no.nordicsemi.android.uart.repository.UARTRepository +import no.nordicsemi.android.uart.view.DisconnectEvent +import no.nordicsemi.android.uart.viewmodel.UARTViewModel +import no.nordicsemi.android.ui.view.NordicLoggerFactory +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.annotation.Config +import javax.inject.Inject + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@HiltAndroidTest +@Config(application = HiltTestApplication::class) +@UninstallModules(NavigationModule::class) +@RunWith(RobolectricTestRunner::class) +internal class UARTViewModelTest { + + @get:Rule + val mockkRule = MockKRule(this) + + @get:Rule + val serviceRule = ServiceTestRule() + + @get:Rule + var hiltRule = HiltAndroidRule(this) + + @BindValue + @JvmField + val analyticsService: Navigator = mockk(relaxed = true) + + @RelaxedMockK + lateinit var analytics: AppAnalytics + + @MockK + lateinit var stringConst: StringConst + + @RelaxedMockK + lateinit var context: Context + + @RelaxedMockK + lateinit var logger: NordicBlekLogger + + @Inject + lateinit var repository: UARTRepository + + @Inject + lateinit var dataSource: UARTPersistentDataSource + + lateinit var viewModel: UARTViewModel + + lateinit var uartServer: UartServer + + @Inject + lateinit var device: MockServerDevice + + @Before + fun setUp() { + hiltRule.inject() + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun release() { + Dispatchers.resetMain() + } + + @Before + fun before() { + viewModel = UARTViewModel(repository, mockk(relaxed = true), dataSource, mockk(relaxed = true), object : + NordicLoggerFactory { + override fun createNordicLogger( + context: Context, + profile: String?, + key: String, + name: String?, + ): NordicBlekLogger { + return logger + } + + }) + runBlocking { + mockkStatic("no.nordicsemi.android.kotlin.ble.client.main.ClientScopeKt") + every { ClientScope } returns CoroutineScope(UnconfinedTestDispatcher()) + mockkStatic("no.nordicsemi.android.kotlin.ble.server.main.ServerScopeKt") + every { ServerScope } returns CoroutineScope(UnconfinedTestDispatcher()) + every { stringConst.APP_NAME } returns "Test" + + uartServer = UartServer(CoroutineScope(UnconfinedTestDispatcher())) + uartServer.start(spyk(), device) + } + } + + @Before + fun prepareLogger() { + mockkObject(NordicBlekLogger.Companion) + every { NordicBlekLogger.create(any(), any(), any(), any()) } returns mockk() + } + + @Test + fun `when connected should return state connected`() = runTest { + val connectedState = GattConnectionStateWithStatus( + GattConnectionState.STATE_CONNECTED, + BleGattConnectionStatus.SUCCESS + ) + viewModel.handleResult(NavigationResult.Success(device)) + + advanceUntilIdle() + + assertEquals(connectedState, viewModel.state.value.uartManagerState.connectionState) + } + + @Test + fun `when disconnected should return state connected`() = runTest { + val disconnectedState = GattConnectionStateWithStatus( + GattConnectionState.STATE_DISCONNECTED, + BleGattConnectionStatus.SUCCESS + ) + viewModel.handleResult(NavigationResult.Success(device)) + viewModel.onEvent(DisconnectEvent) + + advanceUntilIdle() + + assertEquals(disconnectedState, viewModel.state.value.uartManagerState.connectionState) + } +// +// @Test +// fun `when request last record then change status and get 1 record`() = runTest { +// viewModel.handleResult(NavigationResult.Success(device)) +// advanceUntilIdle() //Needed because of delay() in waitForBonding() +// assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) +// +// viewModel.onEvent(OnWorkingModeSelected(WorkingMode.LAST)) +// assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) +// +// glsServer.continueWithResponse() //continue server breakpoint +// +// assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) +// assertEquals(1, viewModel.state.value.glsServiceData.records.size) +// +// val parsedResponse = GlucoseMeasurementParser.parse(glsServer.OLDEST_RECORD) +// assertEquals(parsedResponse, viewModel.state.value.glsServiceData.records.keys.first()) +// } +// +// @Test +// fun `when request all record then change status and get 5 records`() = runTest { +// viewModel.handleResult(NavigationResult.Success(device)) +// advanceUntilIdle() //Needed because of delay() in waitForBonding() +// assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) +// +// viewModel.onEvent(OnWorkingModeSelected(WorkingMode.ALL)) +// assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) +// +// glsServer.continueWithResponse() //continue server breakpoint +// advanceUntilIdle() //We have to use because of delay() in sendAll() +// +// assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) +// assertEquals(5, viewModel.state.value.glsServiceData.records.size) +// +// val expectedRecords = glsServer.records.map { GlucoseMeasurementParser.parse(it) } +// assertContentEquals(expectedRecords, viewModel.state.value.glsServiceData.records.keys) +// } +}