diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 719175d1a0..db6e48f53a 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -152,8 +152,11 @@ object Libs { } object Mockito { + private const val mockitoVersion = "5.6.0" - const val jupiter = "org.mockito:mockito-junit-jupiter:5.6.0" + const val android = "org.mockito:mockito-android:$mockitoVersion" + const val core = "org.mockito:mockito-core:$mockitoVersion" + const val jupiter = "org.mockito:mockito-junit-jupiter:$mockitoVersion" const val kotlin = "org.mockito.kotlin:mockito-kotlin:5.1.0" } diff --git a/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts b/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts index 22fc2175b7..bd520b0863 100644 --- a/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts +++ b/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts @@ -22,6 +22,9 @@ dependencies { androidTestImplementation(Libs.AndroidX.Test.rules) androidTestImplementation(Libs.Google.truth) androidTestImplementation(Libs.AndroidX.Test.uiAutomator) + androidTestImplementation(Libs.Mockito.core) + androidTestImplementation(Libs.Mockito.android) + androidTestImplementation(Libs.Mockito.kotlin) } tasks.withType { diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts index 1bea79a6c3..81f4671523 100644 --- a/plugins/sync/build.gradle.kts +++ b/plugins/sync/build.gradle.kts @@ -9,9 +9,6 @@ plugins { android { namespace = "app.aaps.plugins.sync" - buildFeatures { - aidl = true - } } dependencies { @@ -33,6 +30,7 @@ dependencies { testImplementation(project(":shared:tests")) testImplementation(project(":implementation")) testImplementation(project(":plugins:aps")) + androidTestImplementation(project(":shared:tests")) // OpenHuman api(Libs.Squareup.Okhttp3.okhttp) @@ -52,6 +50,10 @@ dependencies { // DataLayerListenerService api(Libs.Google.Android.PlayServices.wearable) + // Garmin + api("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar") + androidTestImplementation("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar") + kapt(Libs.Dagger.compiler) kapt(Libs.Dagger.androidProcessor) } \ No newline at end of file diff --git a/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt b/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt new file mode 100644 index 0000000000..4f63ea3671 --- /dev/null +++ b/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt @@ -0,0 +1,301 @@ +package app.aaps.plugins.sync.garmin + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.os.Binder +import android.os.IBinder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.aaps.shared.tests.TestBase +import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService +import com.garmin.android.connectiq.ConnectIQ +import com.garmin.android.connectiq.IQApp +import com.garmin.android.connectiq.IQDevice +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.timeout +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.kotlin.argThat +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import java.util.concurrent.Executor + +@RunWith(AndroidJUnit4::class) +class GarminDeviceClientTest: TestBase() { + private val serviceDescriptor = "com.garmin.android.apps.connectmobile.connectiq.IConnectIQService" + private lateinit var client: GarminDeviceClient + private lateinit var serviceConnection: ServiceConnection + private lateinit var device: GarminDevice + private val packageName = "TestPackage" + private val actions = mutableMapOf() + // Maps app ids to intent actions. + private val receivers = mutableMapOf() + + private val receiver = mock() + private val binder = mock() { + on { isBinderAlive } doReturn true + } + private val ciqService = mock() { + on { asBinder() } doReturn binder + on { connectedDevices } doReturn listOf(IQDevice(1L, "TDevice")) + on { registerApp(any(), any(), any()) }.doAnswer { i -> + receivers[i.getArgument(0).applicationId] = i.getArgument(1) + } + } + private val context = mock() { + on { packageName } doReturn this@GarminDeviceClientTest.packageName + on { registerReceiver(any(), any()) } doAnswer { i -> + actions[i.getArgument(1).getAction(0)] = i.getArgument(0) + Intent() + } + on { unregisterReceiver(any()) } doAnswer { i -> + val keys = actions.entries.filter {(_, br) -> br == i.getArgument(0) }.map { (k, _) -> k } + keys.forEach { k -> actions.remove(k) } + } + on { bindService(any(), eq(Context.BIND_AUTO_CREATE), any(), any()) }. doAnswer { i -> + serviceConnection = i.getArgument(3) + i.getArgument(2).execute { + serviceConnection.onServiceConnected( + GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT, + Binder().apply { attachInterface(ciqService, serviceDescriptor) }) + } + true + } + on { bindService(any(), any(), eq(Context.BIND_AUTO_CREATE)) }. doAnswer { i -> + serviceConnection = i.getArgument(1) + serviceConnection.onServiceConnected( + GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT, + Binder().apply { attachInterface(ciqService, serviceDescriptor) }) + true + } + } + + @Before + fun setup() { + client = GarminDeviceClient(aapsLogger, context, receiver, retryWaitFactor = 0L) + device = GarminDevice(client, 1L, "TDevice") + verify(receiver, timeout(2_000L)).onConnect(client) + } + + @After + fun shutdown() { + if (::client.isInitialized) client.dispose() + assertEquals(0, actions.size) // make sure all broadcastReceivers were unregistered + verify(context).unbindService(serviceConnection) + } + + @Test + fun connect() { + } + + @Test + fun disconnect() { + serviceConnection.onServiceDisconnected(GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT) + verify(receiver).onDisconnect(client) + assertEquals(0, actions.size) + } + + @Test + fun connectedDevices() { + assertEquals(listOf(device), client.connectedDevices) + verify(ciqService).connectedDevices + } + + @Test + fun reconnectDeadBinder() { + whenever(binder.isBinderAlive).thenReturn(false, true) + assertEquals(listOf(device), client.connectedDevices) + + verify(ciqService).connectedDevices + verify(ciqService, times(2)).asBinder() + verify(context, times(2)) + .bindService(any(), eq(Context.BIND_AUTO_CREATE), any(), any()) + + verifyNoMoreInteractions(ciqService) + verifyNoMoreInteractions(receiver) + } + + @Test + fun sendMessage() { + val appId = "APPID1" + val data = "Hello, World!".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data) + verify(ciqService).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat { iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + actions[client.sendMessageAction]!!.onReceive(context, intent) // extra on receive will be ignored + verify(receiver).onSendMessage(client, device.id, appId, null) + } + + @Test + fun sendMessage_failNoRetry() { + val appId = "APPID1" + val data = "Hello, World!".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data) + verify(ciqService).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.FAILURE_MESSAGE_TOO_LARGE.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver).onSendMessage(client, device.id, appId, "error FAILURE_MESSAGE_TOO_LARGE") + } + + @Test + fun sendMessage_failRetry() { + val appId = "APPID1" + val data = "Hello, World!".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data) + verify(ciqService).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.FAILURE_DURING_TRANSFER.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + verifyNoMoreInteractions(receiver) + + // Verify retry ... + verify(ciqService, timeout(10_000L).times( 2)).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + intent.putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver).onSendMessage(client, device.id, appId, null) + } + + @Test + fun sendMessage_2toSameApp() { + val appId = "APPID1" + val data1 = "m1".toByteArray() + val data2 = "m2".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data1) + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data2) + verify(ciqService).sendMessage( + argThat { iqMsg -> data1.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + verify(ciqService, atLeastOnce()).asBinder() + verifyNoMoreInteractions(ciqService) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver).onSendMessage(client, device.id, appId, null) + + verify(ciqService, timeout(5000L)).sendMessage( + argThat { iqMsg -> data2.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver, times(2)).onSendMessage(client, device.id, appId, null) + } + + @Test + fun sendMessage_2to2Apps() { + val appId1 = "APPID1" + val appId2 = "APPID2" + val data1 = "m1".toByteArray() + val data2 = "m2".toByteArray() + + client.sendMessage(GarminApplication(device, appId1, "$appId1-name"), data1) + client.sendMessage(GarminApplication(device, appId2, "$appId2-name"), data2) + verify(ciqService).sendMessage( + argThat { iqMsg -> data1.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId1 }) + verify(ciqService, timeout(5000L)).sendMessage( + argThat { iqMsg -> data2.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId2 }) + + val intent1 = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId1) + } + actions[client.sendMessageAction]!!.onReceive(context, intent1) + verify(receiver).onSendMessage(client, device.id, appId1, null) + + val intent2 = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId2) + } + actions[client.sendMessageAction]!!.onReceive(context, intent2) + verify(receiver).onSendMessage(client, device.id, appId2, null) + } + + @Test + fun receiveMessage() { + val app = GarminApplication(GarminDevice(client, 1L, "D1"), "APPID1", "N1") + client.registerForMessages(app) + assertTrue(receivers.contains(app.id)) + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, app.device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_PAYLOAD, "foo".toByteArray()) + } + actions[receivers[app.id]]!!.onReceive(context, intent) + verify(receiver).onReceiveMessage( + eq(client), + eq(app.device.id), + eq(app.id), + argThat { payload -> "foo" == String(payload) }) + } +} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl b/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl deleted file mode 100644 index 8aaf0eb761..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -//IConnectIQService -package com.garmin.android.apps.connectmobile.connectiq; - -import com.garmin.android.connectiq.IQDevice; -import com.garmin.android.connectiq.IQApp; -import com.garmin.android.connectiq.IQMessage; - -interface IConnectIQService { - boolean openStore(String applicationID); - List getConnectedDevices(); - List getKnownDevices(); - - // Remote device methods - int getStatus(in IQDevice device); - - // Messages and Commands - oneway void getApplicationInfo(String notificationPackage, String notificationAction, in IQDevice device, String applicationID); - oneway void openApplication(String notificationPackage, String notificationAction, in IQDevice device, in IQApp app); - - // Pending intent will be fired to let the sdk know a message has been transferred. - oneway void sendMessage(in IQMessage message, in IQDevice device, in IQApp app); - oneway void sendImage(in IQMessage image, in IQDevice device, in IQApp app); - - // registers a companion app with the remote service so that it can receive messages from remote device. - oneway void registerApp(in IQApp app, String notificationAction, String notificationPackage); -} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl deleted file mode 100644 index 2115dafd35..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -package com.garmin.android.connectiq; - -parcelable IQApp { - String applicationID; - int status; - String displayName; - int version; -} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl deleted file mode 100644 index ec1d4f3217..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -package com.garmin.android.connectiq; - -parcelable IQDevice { - long deviceIdentifier; - String friendlyName; - int status; -} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl deleted file mode 100644 index cbf6335fb0..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -package com.garmin.android.connectiq; - -parcelable IQMessage { - const int SUCCESS = 0; - const int FAILURE_UNKNOWN = 1; - const int FAILURE_INVALID_FORMAT = 2; - const int FAILURE_MESSAGE_TOO_LARGE = 3; - const int FAILURE_UNSUPPORTED_TYPE = 4; - const int FAILURE_DURING_TRANSFER = 5; - const int FAILURE_INVALID_DEVICE = 6; - const int FAILURE_DEVICE_NOT_CONNECTED = 7; - - byte[] messageData; - String notificationPackage; - String notificationAction; -} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt index 3aec9257a9..6ff37f0a62 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt @@ -1,20 +1,11 @@ package app.aaps.plugins.sync.garmin data class GarminApplication( - val client: GarminClient, val device: GarminDevice, val id: String, val name: String?) { - enum class Status { - @Suppress("UNUSED") - UNKNOWN, - INSTALLED, - @Suppress("UNUSED") - NOT_INSTALLED, - @Suppress("UNUSED") - NOT_SUPPORTED; - } + val client get() = device.client override fun equals(other: Any?): Boolean { if (this === other) return true @@ -35,5 +26,7 @@ data class GarminApplication( result = 31 * result + id.hashCode() return result } + + override fun toString() = "A[$device:$id:$name]" } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt index 9c1631497d..6333f2ae82 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt @@ -6,8 +6,10 @@ interface GarminClient: Disposable { /** Name of the client. */ val name: String - /** Asynchronously retrieves status information for the given application. */ - fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) + val connectedDevices: List + + /** Register to receive messages from the given up. */ + fun registerForMessages(app: GarminApplication) /** Asynchronously sends a message to an application. */ fun sendMessage(app: GarminApplication, data: ByteArray) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt index b0fc0ead8f..1df255d38a 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt @@ -5,34 +5,16 @@ import com.garmin.android.connectiq.IQDevice data class GarminDevice( val client: GarminClient, val id: Long, - var name: String, - var status: Status = Status.UNKNOWN) { + var name: String) { constructor(client: GarminClient, iqDevice: IQDevice): this( client, iqDevice.deviceIdentifier, - iqDevice.friendlyName, - Status.from(iqDevice.status)) {} - - enum class Status { - NOT_PAIRED, - NOT_CONNECTED, - CONNECTED, - UNKNOWN; - - companion object { - fun from(ordinal: Int?): Status = - values().firstOrNull { s -> s.ordinal == ordinal } ?: UNKNOWN - } - } - + iqDevice.friendlyName) {} override fun toString(): String = "D[$name/$id]" - fun toIQDevice() = IQDevice().apply { - deviceIdentifier = id - friendlyName = name - status = Status.UNKNOWN.ordinal } + fun toIQDevice() = IQDevice(id, name) override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt index 8ef347d8a6..ec81ddb512 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt @@ -6,22 +6,24 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection +import android.os.Build import android.os.IBinder import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.utils.waitMillis import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService +import com.garmin.android.connectiq.ConnectIQ.IQMessageStatus import com.garmin.android.connectiq.IQApp import com.garmin.android.connectiq.IQDevice import com.garmin.android.connectiq.IQMessage import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers import org.jetbrains.annotations.VisibleForTesting -import java.time.Duration +import java.lang.Thread.UncaughtExceptionHandler import java.time.Instant import java.util.LinkedList -import java.util.Locale import java.util.Queue +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -33,26 +35,31 @@ class GarminDeviceClient( private val retryWaitFactor: Long = 5L): Disposable, GarminClient { override val name = "Device" + private var executor = Executors.newSingleThreadExecutor { r -> + Thread(r).apply { + name = "Garmin callback" + isDaemon = true + uncaughtExceptionHandler = UncaughtExceptionHandler { _, e -> + aapsLogger.error(LTag.GARMIN, "ConnectIQ callback failed", e) } + } + } private var bindLock = Object() private var ciqService: IConnectIQService? = null get() { - val waitUntil = Instant.now().plusSeconds(2) synchronized (bindLock) { - while(field?.asBinder()?.isBinderAlive != true) { + if (field?.asBinder()?.isBinderAlive != true) { field = null if (state !in arrayOf(State.BINDING, State.RECONNECTING)) { aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service") state = State.RECONNECTING - context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + bindService() } - // Wait for the connection, that is the call to onServiceConnected. - val wait = Duration.between(Instant.now(), waitUntil) - if (wait > Duration.ZERO) bindLock.waitMillis(wait.toMillis()) - if (field == null) { + bindLock.waitMillis(2_000L) + if (field?.asBinder()?.isBinderAlive != true) { + field = null // The [serviceConnection] didn't have a chance to reassign ciqService, // i.e. the wait timed out. Give up. aapsLogger.warn(LTag.GARMIN, "no ciqservice $this") - return null } } return field @@ -80,7 +87,7 @@ class GarminDeviceClient( override fun onServiceConnected(name: ComponentName?, service: IBinder?) { var notifyReceiver: Boolean val ciq: IConnectIQService - synchronized (bindLock) { + synchronized(bindLock) { aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected") ciq = IConnectIQService.Stub.asInterface(service) notifyReceiver = state != State.RECONNECTING @@ -89,14 +96,8 @@ class GarminDeviceClient( bindLock.notifyAll() } if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient) - try { - ciq.connectedDevices?.forEach { d -> - receiver.onConnectDevice(this@GarminDeviceClient, d.deviceIdentifier, d.friendlyName) - } - } catch (e: Exception) { - aapsLogger.error(LTag.GARMIN, "getting devices failed", e) - } } + override fun onServiceDisconnected(name: ComponentName?) { synchronized(bindLock) { aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected") @@ -114,9 +115,21 @@ class GarminDeviceClient( aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service") registerReceiver(sendMessageAction, ::onSendMessage) state = State.BINDING - context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + bindService() } + private fun bindService() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + context.bindService(serviceIntent, Context.BIND_AUTO_CREATE, executor, ciqServiceConnection) + } else { + context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + } + } + + override val connectedDevices: List + get() = ciqService?.connectedDevices?.map { iqDevice -> GarminDevice(this, iqDevice) } + ?: emptyList() + override fun isDisposed() = state == State.DISPOSED override fun dispose() { broadcastReceiver.forEach { context.unregisterReceiver(it) } @@ -143,35 +156,14 @@ class GarminDeviceClient( context.registerReceiver(recv, IntentFilter(action)) } - override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { - val action = createAction("APPLICATION_INFO_${device.id}_$appId") + override fun registerForMessages(app: GarminApplication) { + aapsLogger.info(LTag.GARMIN, "registerForMessage $name $app") + val action = createAction("ON_MESSAGE_${app.device.id}_${app.id}") + val iqApp = IQApp(app.id) synchronized (registeredActions) { if (!registeredActions.contains(action)) { - registerReceiver(action) { intent -> onApplicationInfo(appId, device, intent) } - } - registeredActions.add(action) - } - ciqService?.getApplicationInfo(context.packageName, action, device.toIQDevice(), appId) - } - - /** Receives application info callbacks from ConnectIQ app.*/ - private fun onApplicationInfo(appId: String, device: GarminDevice, intent: Intent) { - val receivedAppId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase(Locale.getDefault()) - val version = intent.getIntExtra(EXTRA_APPLICATION_VERSION, -1) - val isInstalled = receivedAppId != null && version >= 0 && version != 65535 - - if (isInstalled) registerForMessages(device.id, appId) - receiver.onApplicationInfo(device, appId, isInstalled) - } - - private fun registerForMessages(deviceId: Long, appId: String) { - aapsLogger.info(LTag.GARMIN, "registerForMessage $name $appId") - val action = createAction("ON_MESSAGE_${deviceId}_$appId") - val app = IQApp().apply { applicationID = appId; displayName = "" } - synchronized (registeredActions) { - if (!registeredActions.contains(action)) { - registerReceiver(action) { intent: Intent -> onReceiveMessage(app, intent) } - ciqService?.registerApp(app, action, context.packageName) + registerReceiver(action) { intent: Intent -> onReceiveMessage(iqApp, intent) } + ciqService?.registerApp(iqApp, action, context.packageName) registeredActions.add(action) } else { aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered") @@ -184,14 +176,15 @@ class GarminDeviceClient( val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice? val data = intent.getByteArrayExtra(EXTRA_PAYLOAD) if (iqDevice != null && data != null) - receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationID, data) + receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationId, data) } /** Receives callback from ConnectIQ about message transfers. */ private fun onSendMessage(intent: Intent) { - val status = intent.getIntExtra(EXTRA_STATUS, 0) + val statusOrd = intent.getIntExtra(EXTRA_STATUS, IQMessageStatus.FAILURE_UNKNOWN.ordinal) + val status = IQMessageStatus.values().firstOrNull { s -> s.ordinal == statusOrd } ?: IQMessageStatus.FAILURE_UNKNOWN val deviceId = getDevice(intent) - val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase() + val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.uppercase() if (deviceId == null || appId == null) { aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'") } else { @@ -203,21 +196,22 @@ class GarminDeviceClient( return } + var errorMessage: String? = null when (status) { - IQMessage.FAILURE_DEVICE_NOT_CONNECTED, - IQMessage.FAILURE_DURING_TRANSFER -> { + IQMessageStatus.SUCCESS -> {} + IQMessageStatus.FAILURE_DEVICE_NOT_CONNECTED, + IQMessageStatus.FAILURE_DURING_TRANSFER -> { if (msg.attempt < MAX_RETRIES) { val delaySec = retryWaitFactor * msg.attempt Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS) return } } - - else -> {} + else -> { + errorMessage = "error $status" + } } - queue.remove(msg) - val errorMessage = status - .takeUnless { it == IQMessage.SUCCESS }?.let { s -> "error $s" } + queue.poll() receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage) if (queue.isNotEmpty()) { Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) } @@ -237,8 +231,9 @@ class GarminDeviceClient( val app: GarminApplication, val data: ByteArray) { var attempt: Int = 0 + val creation = Instant.now() var lastAttempt: Instant? = null - val iqApp get() = IQApp().apply { applicationID = app.id; displayName = app.name } + val iqApp get() = IQApp(app.id, app.name, 0) val iqDevice get() = app.device.toIQDevice() } @@ -247,7 +242,17 @@ class GarminDeviceClient( override fun sendMessage(app: GarminApplication, data: ByteArray) { val msg = synchronized (messageQueues) { val msg = Message(app, data) + val oldMessageCutOff = Instant.now().minusSeconds(30) val queue = messageQueues.getOrPut(app.device.id to app.id) { LinkedList() } + while (true) { + val oldMsg = queue.peek() ?: break + if ((oldMsg.lastAttempt ?: oldMsg.creation).isBefore(oldMessageCutOff)) { + aapsLogger.warn(LTag.GARMIN, "remove old msg ${msg.app}") + queue.poll() + } else { + break + } + } queue.add(msg) // Make sure we have only one outstanding message per app, so we ensure // that always the first message in the queue is currently send. @@ -266,10 +271,7 @@ class GarminDeviceClient( private fun sendMessage(msg: Message) { msg.attempt++ msg.lastAttempt = Instant.now() - val iqMsg = IQMessage().apply { - messageData = msg.data - notificationPackage = context.packageName - notificationAction = sendMessageAction } + val iqMsg = IQMessage(msg.data, context.packageName, sendMessageAction) ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp) } @@ -278,7 +280,6 @@ class GarminDeviceClient( companion object { const val CONNECTIQ_SERVICE_ACTION = "com.garmin.android.apps.connectmobile.CONNECTIQ_SERVICE_ACTION" const val EXTRA_APPLICATION_ID = "com.garmin.android.connectiq.EXTRA_APPLICATION_ID" - const val EXTRA_APPLICATION_VERSION = "com.garmin.android.connectiq.EXTRA_APPLICATION_VERSION" const val EXTRA_REMOTE_DEVICE = "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE" const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD" const val EXTRA_STATUS = "com.garmin.android.connectiq.EXTRA_STATUS" diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt index 8f8671a6ec..feaf1743fc 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt @@ -4,7 +4,6 @@ import android.content.Context import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import io.reactivex.rxjava3.disposables.Disposable -import org.jetbrains.annotations.VisibleForTesting class GarminMessenger( private val aapsLogger: AAPSLogger, @@ -17,8 +16,6 @@ class GarminMessenger( private var disposed: Boolean = false /** All devices that where connected since this instance was created. */ private val devices = mutableMapOf() - @VisibleForTesting - val liveApplications = mutableSetOf() private val clients = mutableListOf() private val appIdNames = mutableMapOf() init { @@ -33,20 +30,14 @@ class GarminMessenger( private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice { synchronized (devices) { - return devices.getOrPut(deviceId) { GarminDevice(client, deviceId, "unknown") } + return devices.getOrPut(deviceId) { + client.connectedDevices.firstOrNull { d -> d.id == deviceId } ?: + GarminDevice(client, deviceId, "unknown") } } } private fun getApplication(client: GarminClient, deviceId: Long, appId: String): GarminApplication { - synchronized (liveApplications) { - var app = liveApplications.firstOrNull { app -> - app.client == client && app.device.id == deviceId && app.id == appId } - if (app == null) { - app = GarminApplication(client, getDevice(client, deviceId), appId, appIdNames[appId]) - liveApplications.add(app) - } - return app - } + return GarminApplication(getDevice(client, deviceId), appId, appIdNames[appId]) } private fun startDeviceClient() { @@ -61,45 +52,18 @@ class GarminMessenger( override fun onDisconnect(client: GarminClient) { aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}") clients.remove(client) - synchronized (liveApplications) { - liveApplications.removeIf { app -> app.client == client } + synchronized (devices) { + val deviceIds = devices.filter { (_, d) -> d.client == client }.map { (id, _) -> id } + deviceIds.forEach { id -> devices.remove(id) } } client.dispose() - when (client.name) { - "Device" -> startDeviceClient() - "Sim"-> GarminSimulatorClient(aapsLogger, this) + when (client) { + is GarminDeviceClient -> startDeviceClient() + is GarminSimulatorClient -> GarminSimulatorClient(aapsLogger, this) else -> aapsLogger.warn(LTag.GARMIN, "onDisconnect unknown client $client") } } - /** Receives notifications that a device has connected. - * - * It will retrieve status information for all applications we care about (in [appIdNames]). */ - override fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) { - val device = getDevice(client, deviceId).apply { name = deviceName } - aapsLogger.info(LTag.GARMIN, "onConnectDevice $device") - appIdNames.forEach { (id, name) -> client.retrieveApplicationInfo(device, id, name) } - } - - /** Receives notifications about disconnection of a device. */ - override fun onDisconnectDevice(client: GarminClient, deviceId: Long) { - val device = getDevice(client, deviceId) - aapsLogger.info(LTag.GARMIN,"onDisconnectDevice $device") - synchronized (liveApplications) { - liveApplications.removeIf { app -> app.device == device } - } - } - - /** Receives notification about applications that are installed/uninstalled - * on a device from the client. */ - override fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) { - val app = getApplication(device.client, device.id, appId) - aapsLogger.info(LTag.GARMIN, "onApplicationInfo add $app ${if (isInstalled) "" else "un"}installed") - if (!isInstalled) { - synchronized (liveApplications) { liveApplications.remove(app) } - } - } - override fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) { val app = getApplication(client, deviceId, appId) val msg = GarminSerializer.deserialize(data) @@ -118,14 +82,14 @@ class GarminMessenger( } fun sendMessage(device: GarminDevice, msg: Any) { - liveApplications - .filter { a -> a.device.id == device.id } - .forEach { a -> sendMessage(a, msg) } + appIdNames.forEach { (appId, _) -> + sendMessage(getApplication(device.client, device.id, appId), msg) + } } /** Sends a message to all applications on all devices. */ fun sendMessage(msg: Any) { - liveApplications.forEach { app -> sendMessage(app, msg) } + clients.forEach { cl -> cl.connectedDevices.forEach { d -> sendMessage(d, msg) }} } private fun sendMessage(app: GarminApplication, msg: Any) { @@ -139,7 +103,7 @@ class GarminMessenger( msg.toString() } val data = GarminSerializer.serialize(msg) - aapsLogger.info(LTag.GARMIN, "sendMessage $app $app ${data.size} bytes $s") + aapsLogger.info(LTag.GARMIN, "sendMessage $app ${data.size} bytes $s") try { app.client.sendMessage(app, data) } catch (e: IllegalStateException) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index ce67a6e8bb..6981d3d58e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -64,12 +64,12 @@ class GarminPlugin @Inject constructor( /** Garmin ConnectIQ application id for native communication. Phone pushes values. */ private val glucoseAppIds = mapOf( - "c9e90ee7e6924829a8b45e7dafff5cb4" to "GlucoseWatch_Dev", - "1107ca6c2d5644b998d4bcb3793f2b7c" to "GlucoseDataField_Dev", - "928fe19a4d3a4259b50cb6f9ddaf0f4a" to "GlucoseWidget_Dev", - "662dfcf7f5a147de8bd37f09574adb11" to "GlucoseWatch", - "815c7328c21248c493ad9ac4682fe6b3" to "GlucoseDataField", - "4bddcc1740084a1fab83a3b2e2fcf55b" to "GlucoseWidget", + "C9E90EE7E6924829A8B45E7DAFFF5CB4" to "GlucoseWatch_Dev", + "1107CA6C2D5644B998D4BCB3793F2B7C" to "GlucoseDataField_Dev", + "928FE19A4D3A4259B50CB6F9DDAF0F4A" to "GlucoseWidget_Dev", + "662DFCF7F5A147DE8BD37F09574ADB11" to "GlucoseWatch", + "815C7328C21248C493AD9AC4682FE6B3" to "GlucoseDataField", + "4BDDCC1740084A1FAB83A3B2E2FCF55B" to "GlucoseWidget", ) @VisibleForTesting @@ -90,9 +90,7 @@ class GarminPlugin @Inject constructor( "communication_debug_mode" -> setupGarminMessenger() "communication_http", "communication_http_port" -> setupHttpServer() "garmin_aaps_key" -> sendPhoneAppMessage() - else -> return } - aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") } private fun setupGarminMessenger() { @@ -121,7 +119,8 @@ class GarminPlugin @Inject constructor( .subscribe(::onNewBloodGlucose) ) setupHttpServer() - // setupGarminMessenger() + if (garminAapsKey.isNotEmpty()) + setupGarminMessenger() } fun setupHttpServer() { @@ -332,7 +331,7 @@ class GarminPlugin @Inject constructor( if (test) return if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) { loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device) - } else { + } else if (avg > 0) { aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd") } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt index c308e4bead..e8e93bac51 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt @@ -11,19 +11,6 @@ interface GarminReceiver { fun onConnect(client: GarminClient) fun onDisconnect(client: GarminClient) - /** - * Notifies that a device is connected. This will be called for all connected devices - * initially. - */ - fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) - fun onDisconnectDevice(client: GarminClient, deviceId: Long) - - /** - * Provides application info after a call to - * {@link ConnectIqClient#retrieveApplicationInfo retrieveApplicationInfo}. - */ - fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) - /** * Delivers received device app messages. */ diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt index 56b1ce1449..75f8d8e436 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt @@ -3,6 +3,7 @@ package app.aaps.plugins.sync.garmin import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import com.garmin.android.connectiq.IQApp +import com.garmin.android.connectiq.IQApp.IQAppStatus import io.reactivex.rxjava3.disposables.Disposable import org.jetbrains.annotations.VisibleForTesting import java.io.InputStream @@ -35,25 +36,23 @@ class GarminSimulatorClient( private val connections: MutableList = Collections.synchronizedList(mutableListOf()) private var nextDeviceId = AtomicLong(1) @VisibleForTesting - val iqApp = IQApp().apply { - applicationID = "SimApp" - status = GarminApplication.Status.INSTALLED.ordinal - displayName = "Simulator" - version = 1 } + val iqApp = IQApp("SimApp", IQAppStatus.INSTALLED, "Simulator", 1) private val readyLock = ReentrantLock() private val readyCond = readyLock.newCondition() + override val connectedDevices: List get() = connections.map { c -> c.device } + + override fun registerForMessages(app: GarminApplication) { + } private inner class Connection(private val socket: Socket): Disposable { val device = GarminDevice( this@GarminSimulatorClient, nextDeviceId.getAndAdd(1L), - "Sim@${socket.remoteSocketAddress}", - GarminDevice.Status.CONNECTED) + "Sim@${socket.remoteSocketAddress}") fun start() { executor.execute { try { - receiver.onConnectDevice(this@GarminSimulatorClient, device.id, device.name) run() } catch (e: Throwable) { aapsLogger.error(LTag.GARMIN, "$device failed", e) @@ -79,7 +78,7 @@ class GarminSimulatorClient( val data = readAvailable(socket.inputStream) ?: break if (data.isNotEmpty()) { kotlin.runCatching { - receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationID, data) + receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationId, data) } } } catch (e: SocketException) { @@ -89,7 +88,6 @@ class GarminSimulatorClient( } aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" ) connections.remove(this) - receiver.onDisconnectDevice(this@GarminSimulatorClient, device.id) } private fun readAvailable(input: InputStream): ByteArray? { @@ -162,10 +160,6 @@ class GarminSimulatorClient( override fun isDisposed() = serverSocket.isClosed - override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { - receiver.onApplicationInfo(device, appId, appId == iqApp.applicationID) - } - private fun getConnection(device: GarminDevice): Connection? { return connections.firstOrNull { c -> c.device.id == device.id } } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt index 52962aebcb..3497e4c900 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt @@ -8,135 +8,103 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.stub import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import java.util.LinkedList -import java.util.Queue class GarminMessengerTest: TestBase() { private val context = mock() - private val client1 = mock() { - on { name } doReturn "Mock1" - } - private val client2 = mock() { - on { name } doReturn "Mock2" - } + private var appId1 = "appId1" private val appId2 = "appId2" private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name") - private val msgs: Queue> = LinkedList() + private val outMessages = mutableListOf>() + private val inMessages = mutableListOf>() private var messenger = GarminMessenger( - aapsLogger, context, apps, { app, msg -> msgs.add(app to msg) }, false, false) - private val deviceId = 11L - private val deviceName = "$deviceId-name" - private val device = GarminDevice(client1, deviceId, deviceName) + aapsLogger, context, apps, { app, msg -> inMessages.add(app to msg) }, + enableConnectIq = false, enableSimulator = false) + private val client1 = mock() { + on { name } doReturn "Mock1" + on { sendMessage(any(), any()) } doAnswer { a -> + outMessages.add(a.getArgument(0) to a.getArgument(1)) + Unit + } + } + private val client2 = mock() { + on { name } doReturn "Mock2" + on { sendMessage(any(), any()) } doAnswer { a -> + outMessages.add(a.getArgument(0) to a.getArgument(1)) + Unit + } + } + private val device1 = GarminDevice(client1, 11L, "dev1-name") private val device2 = GarminDevice(client2, 12L, "dev2-name") @BeforeEach fun setup() { messenger.onConnect(client1) messenger.onConnect(client2) + client1.stub { + on { connectedDevices } doReturn listOf(device1) + } + client2.stub { + on { connectedDevices } doReturn listOf(device2) + } } @AfterEach fun cleanup() { messenger.dispose() + verify(client1).dispose() + verify(client2).dispose() assertTrue(messenger.isDisposed) } - @Test - fun onConnectDevice() { - messenger.onConnectDevice(client1, deviceId, deviceName) - verify(client1).retrieveApplicationInfo(device, appId1, apps[appId1]!!) - verify(client1).retrieveApplicationInfo(device, appId2, apps[appId2]!!) - } - - @Test - fun onApplicationInfo() { - messenger.onApplicationInfo(device, appId1, true) - val app = messenger.liveApplications.first() - assertEquals(device, app.device) - assertEquals(appId1, app.id) - assertEquals(apps[appId1], app.name) - - messenger.onApplicationInfo(device, appId1, false) - assertEquals(0, messenger.liveApplications.size) - } - - @Test - fun onDisconnectDevice() { - messenger.onConnectDevice(client1, deviceId, deviceName) - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device2, appId1, true) - assertEquals(2, messenger.liveApplications.size) - messenger.onDisconnectDevice(client1, device2.id) - assertEquals(1, messenger.liveApplications.size) - assertEquals(appId1, messenger.liveApplications.first().id) - } - @Test fun onDisconnect() { - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device2, appId2, true) - assertEquals(2, messenger.liveApplications.size) messenger.onDisconnect(client1) - assertEquals(1, messenger.liveApplications.size) - val app = messenger.liveApplications.first() - assertEquals(device2, app.device) - assertEquals(appId2, app.id) - assertEquals(apps[appId2], app.name) + val msg = "foo" + messenger.sendMessage(msg) + outMessages.forEach { (app, payload) -> + assertEquals(client2, app.device.client) + assertEquals(msg, GarminSerializer.deserialize(payload)) + } } @Test fun onReceiveMessage() { val data = GarminSerializer.serialize("foo") - messenger.onReceiveMessage(client1, device.id, appId1, data) - val (app, payload) = msgs.remove() + messenger.onReceiveMessage(client1, device1.id, appId1, data) + val (app, payload) = inMessages.removeAt(0) assertEquals(appId1, app.id) assertEquals("foo", payload) } @Test fun sendMessageDevice() { - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device, appId2, true) - - val msgs = mutableListOf>() - whenever(client1.sendMessage(any(), any())).thenAnswer { i -> - msgs.add(i.getArgument(0) to i.getArgument(1)) - } - - messenger.sendMessage(device, "foo") - assertEquals(2, msgs.size) - val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second - val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second + messenger.sendMessage(device1, "foo") + assertEquals(2, outMessages.size) + val msg1 = outMessages.first { (app, _) -> app.id == appId1 }.second + val msg2 = outMessages.first { (app, _) -> app.id == appId2 }.second assertEquals("foo", GarminSerializer.deserialize(msg1)) assertEquals("foo", GarminSerializer.deserialize(msg2)) - messenger.onSendMessage(client1, device.id, appId1, null) + messenger.onSendMessage(client1, device1.id, appId1, null) } @Test fun onSendMessageAll() { - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device2, appId2, true) - assertEquals(2, messenger.liveApplications.size) - - val msgs = mutableListOf>() - whenever(client1.sendMessage(any(), any())).thenAnswer { i -> - msgs.add(i.getArgument(0) to i.getArgument(1)) - } - whenever(client2.sendMessage(any(), any())).thenAnswer { i -> - msgs.add(i.getArgument(0) to i.getArgument(1)) - } - messenger.sendMessage(listOf("foo")) - assertEquals(2, msgs.size) - val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second - val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second - assertEquals(listOf("foo"), GarminSerializer.deserialize(msg1)) - assertEquals(listOf("foo"), GarminSerializer.deserialize(msg2)) - messenger.onSendMessage(client1, device.id, appId1, null) + assertEquals(4, outMessages.size) + val msg11 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId1 }.second + val msg12 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId2 }.second + val msg21 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId1 }.second + val msg22 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId2 }.second + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg11)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg12)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg21)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg22)) + messenger.onSendMessage(client1, device1.id, appId1, null) } } \ No newline at end of file diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt index b713af0f05..997213304a 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt @@ -1,13 +1,10 @@ package app.aaps.plugins.sync.garmin import app.aaps.shared.tests.TestBase -import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.argThat -import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock @@ -19,36 +16,14 @@ import java.time.Duration class GarminSimulatorClientTest: TestBase() { - private var device: GarminDevice? = null - private var app: GarminApplication? = null private lateinit var client: GarminSimulatorClient - private val receiver: GarminReceiver = mock() { - on { onConnectDevice(any(), any(), any()) }.doAnswer { i -> - device = GarminDevice(client, i.getArgument(1), i.getArgument(2)) - app = GarminApplication( - client, device!!, client.iqApp.applicationID, client.iqApp.displayName) - } - } + private val receiver: GarminReceiver = mock() @BeforeEach fun setup() { client = GarminSimulatorClient(aapsLogger, receiver, 0) } - @Test - fun retrieveApplicationInfo() { - assertTrue(client.awaitReady(Duration.ofSeconds(10))) - val port = client.port - val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) - Socket(ip, port).use { socket -> - assertTrue(socket.isConnected) - verify(receiver).onConnect(client) - verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) - client.retrieveApplicationInfo(app!!.device, app!!.id, app!!.name!!) - } - verify(receiver).onApplicationInfo(app!!.device, app!!.id, true) - } - @Test fun receiveMessage() { val payload = "foo".toByteArray() @@ -60,11 +35,11 @@ class GarminSimulatorClientTest: TestBase() { socket.getOutputStream().write(payload) socket.getOutputStream().flush() verify(receiver).onConnect(client) - verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) } - verify(receiver, timeout(1_000)).onReceiveMessage( - eq(client), eq(device!!.id), eq("SimApp"), - argThat { p -> payload.contentEquals(p) }) + assertEquals(1, client.connectedDevices.size) + val device: GarminDevice = client.connectedDevices.first() + verify(receiver, timeout(1_000)) + .onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload)) } @Test @@ -73,14 +48,16 @@ class GarminSimulatorClientTest: TestBase() { assertTrue(client.awaitReady(Duration.ofSeconds(10))) val port = client.port val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) + val device: GarminDevice + val app: GarminApplication Socket(ip, port).use { socket -> assertTrue(socket.isConnected) verify(receiver).onConnect(client) - verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) - assertNotNull(device) - assertNotNull(app) - client.sendMessage(app!!, payload) + assertEquals(1, client.connectedDevices.size) + device = client.connectedDevices.first() + app = GarminApplication(device, "SIMAPP", "T") + client.sendMessage(app, payload) } - verify(receiver).onSendMessage(eq(client), any(), eq(app!!.id), isNull()) + verify(receiver, timeout(1_000)).onSendMessage(eq(client), eq(device.id), eq(app.id), isNull()) } } \ No newline at end of file