Fix Garmin communication. Use Garmin lib rather than aidl, get rid of not needed functionality.

This commit is contained in:
Robert Buessow 2023-11-19 23:10:20 +01:00
parent f3d1acffcd
commit a36081d3df
18 changed files with 484 additions and 381 deletions

View file

@ -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"
}

View file

@ -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<Test> {

View file

@ -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)
}

View file

@ -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<String, BroadcastReceiver>()
// Maps app ids to intent actions.
private val receivers = mutableMapOf<String, String>()
private val receiver = mock<GarminReceiver>()
private val binder = mock<IBinder>() {
on { isBinderAlive } doReturn true
}
private val ciqService = mock<IConnectIQService>() {
on { asBinder() } doReturn binder
on { connectedDevices } doReturn listOf(IQDevice(1L, "TDevice"))
on { registerApp(any(), any(), any()) }.doAnswer { i ->
receivers[i.getArgument<IQApp>(0).applicationId] = i.getArgument(1)
}
}
private val context = mock<Context>() {
on { packageName } doReturn this@GarminDeviceClientTest.packageName
on { registerReceiver(any<BroadcastReceiver>(), any()) } doAnswer { i ->
actions[i.getArgument<IntentFilter>(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<Executor>(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) })
}
}

View file

@ -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<IQDevice> getConnectedDevices();
List<IQDevice> 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);
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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]"
}

View file

@ -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<GarminDevice>
/** 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)

View file

@ -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

View file

@ -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<GarminDevice>
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"

View file

@ -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<Long, GarminDevice>()
@VisibleForTesting
val liveApplications = mutableSetOf<GarminApplication>()
private val clients = mutableListOf<GarminClient>()
private val appIdNames = mutableMapOf<String, String>()
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) {

View file

@ -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")
}
}

View file

@ -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.
*/

View file

@ -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<Connection> = 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<GarminDevice> 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 }
}

View file

@ -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<Context>()
private val client1 = mock<GarminClient>() {
on { name } doReturn "Mock1"
}
private val client2 = mock<GarminClient>() {
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<Pair<GarminApplication, Any>> = LinkedList()
private val outMessages = mutableListOf<Pair<GarminApplication, ByteArray>>()
private val inMessages = mutableListOf<Pair<GarminApplication, Any>>()
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<GarminClient>() {
on { name } doReturn "Mock1"
on { sendMessage(any(), any()) } doAnswer { a ->
outMessages.add(a.getArgument<GarminApplication>(0) to a.getArgument(1))
Unit
}
}
private val client2 = mock<GarminClient>() {
on { name } doReturn "Mock2"
on { sendMessage(any(), any()) } doAnswer { a ->
outMessages.add(a.getArgument<GarminApplication>(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<Pair<GarminApplication, ByteArray>>()
whenever(client1.sendMessage(any(), any())).thenAnswer { i ->
msgs.add(i.getArgument<GarminApplication>(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<Pair<GarminApplication, ByteArray>>()
whenever(client1.sendMessage(any(), any())).thenAnswer { i ->
msgs.add(i.getArgument<GarminApplication>(0) to i.getArgument(1))
}
whenever(client2.sendMessage(any(), any())).thenAnswer { i ->
msgs.add(i.getArgument<GarminApplication>(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)
}
}

View file

@ -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())
}
}