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 { 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" 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.AndroidX.Test.rules)
androidTestImplementation(Libs.Google.truth) androidTestImplementation(Libs.Google.truth)
androidTestImplementation(Libs.AndroidX.Test.uiAutomator) androidTestImplementation(Libs.AndroidX.Test.uiAutomator)
androidTestImplementation(Libs.Mockito.core)
androidTestImplementation(Libs.Mockito.android)
androidTestImplementation(Libs.Mockito.kotlin)
} }
tasks.withType<Test> { tasks.withType<Test> {

View file

@ -9,9 +9,6 @@ plugins {
android { android {
namespace = "app.aaps.plugins.sync" namespace = "app.aaps.plugins.sync"
buildFeatures {
aidl = true
}
} }
dependencies { dependencies {
@ -33,6 +30,7 @@ dependencies {
testImplementation(project(":shared:tests")) testImplementation(project(":shared:tests"))
testImplementation(project(":implementation")) testImplementation(project(":implementation"))
testImplementation(project(":plugins:aps")) testImplementation(project(":plugins:aps"))
androidTestImplementation(project(":shared:tests"))
// OpenHuman // OpenHuman
api(Libs.Squareup.Okhttp3.okhttp) api(Libs.Squareup.Okhttp3.okhttp)
@ -52,6 +50,10 @@ dependencies {
// DataLayerListenerService // DataLayerListenerService
api(Libs.Google.Android.PlayServices.wearable) 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.compiler)
kapt(Libs.Dagger.androidProcessor) 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 package app.aaps.plugins.sync.garmin
data class GarminApplication( data class GarminApplication(
val client: GarminClient,
val device: GarminDevice, val device: GarminDevice,
val id: String, val id: String,
val name: String?) { val name: String?) {
enum class Status { val client get() = device.client
@Suppress("UNUSED")
UNKNOWN,
INSTALLED,
@Suppress("UNUSED")
NOT_INSTALLED,
@Suppress("UNUSED")
NOT_SUPPORTED;
}
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -35,5 +26,7 @@ data class GarminApplication(
result = 31 * result + id.hashCode() result = 31 * result + id.hashCode()
return result return result
} }
override fun toString() = "A[$device:$id:$name]"
} }

View file

@ -6,8 +6,10 @@ interface GarminClient: Disposable {
/** Name of the client. */ /** Name of the client. */
val name: String val name: String
/** Asynchronously retrieves status information for the given application. */ val connectedDevices: List<GarminDevice>
fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String)
/** Register to receive messages from the given up. */
fun registerForMessages(app: GarminApplication)
/** Asynchronously sends a message to an application. */ /** Asynchronously sends a message to an application. */
fun sendMessage(app: GarminApplication, data: ByteArray) fun sendMessage(app: GarminApplication, data: ByteArray)

View file

@ -5,34 +5,16 @@ import com.garmin.android.connectiq.IQDevice
data class GarminDevice( data class GarminDevice(
val client: GarminClient, val client: GarminClient,
val id: Long, val id: Long,
var name: String, var name: String) {
var status: Status = Status.UNKNOWN) {
constructor(client: GarminClient, iqDevice: IQDevice): this( constructor(client: GarminClient, iqDevice: IQDevice): this(
client, client,
iqDevice.deviceIdentifier, iqDevice.deviceIdentifier,
iqDevice.friendlyName, 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
}
}
override fun toString(): String = "D[$name/$id]" override fun toString(): String = "D[$name/$id]"
fun toIQDevice() = IQDevice().apply { fun toIQDevice() = IQDevice(id, name)
deviceIdentifier = id
friendlyName = name
status = Status.UNKNOWN.ordinal }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true

View file

@ -6,22 +6,24 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.Build
import android.os.IBinder import android.os.IBinder
import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.utils.waitMillis import app.aaps.core.utils.waitMillis
import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService 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.IQApp
import com.garmin.android.connectiq.IQDevice import com.garmin.android.connectiq.IQDevice
import com.garmin.android.connectiq.IQMessage import com.garmin.android.connectiq.IQMessage
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
import java.time.Duration import java.lang.Thread.UncaughtExceptionHandler
import java.time.Instant import java.time.Instant
import java.util.LinkedList import java.util.LinkedList
import java.util.Locale
import java.util.Queue import java.util.Queue
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -33,26 +35,31 @@ class GarminDeviceClient(
private val retryWaitFactor: Long = 5L): Disposable, GarminClient { private val retryWaitFactor: Long = 5L): Disposable, GarminClient {
override val name = "Device" 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 bindLock = Object()
private var ciqService: IConnectIQService? = null private var ciqService: IConnectIQService? = null
get() { get() {
val waitUntil = Instant.now().plusSeconds(2)
synchronized (bindLock) { synchronized (bindLock) {
while(field?.asBinder()?.isBinderAlive != true) { if (field?.asBinder()?.isBinderAlive != true) {
field = null field = null
if (state !in arrayOf(State.BINDING, State.RECONNECTING)) { if (state !in arrayOf(State.BINDING, State.RECONNECTING)) {
aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service") aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service")
state = State.RECONNECTING state = State.RECONNECTING
context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) bindService()
} }
// Wait for the connection, that is the call to onServiceConnected. bindLock.waitMillis(2_000L)
val wait = Duration.between(Instant.now(), waitUntil) if (field?.asBinder()?.isBinderAlive != true) {
if (wait > Duration.ZERO) bindLock.waitMillis(wait.toMillis()) field = null
if (field == null) {
// The [serviceConnection] didn't have a chance to reassign ciqService, // The [serviceConnection] didn't have a chance to reassign ciqService,
// i.e. the wait timed out. Give up. // i.e. the wait timed out. Give up.
aapsLogger.warn(LTag.GARMIN, "no ciqservice $this") aapsLogger.warn(LTag.GARMIN, "no ciqservice $this")
return null
} }
} }
return field return field
@ -80,7 +87,7 @@ class GarminDeviceClient(
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
var notifyReceiver: Boolean var notifyReceiver: Boolean
val ciq: IConnectIQService val ciq: IConnectIQService
synchronized (bindLock) { synchronized(bindLock) {
aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected") aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected")
ciq = IConnectIQService.Stub.asInterface(service) ciq = IConnectIQService.Stub.asInterface(service)
notifyReceiver = state != State.RECONNECTING notifyReceiver = state != State.RECONNECTING
@ -89,14 +96,8 @@ class GarminDeviceClient(
bindLock.notifyAll() bindLock.notifyAll()
} }
if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient) 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?) { override fun onServiceDisconnected(name: ComponentName?) {
synchronized(bindLock) { synchronized(bindLock) {
aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected") aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected")
@ -114,9 +115,21 @@ class GarminDeviceClient(
aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service") aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service")
registerReceiver(sendMessageAction, ::onSendMessage) registerReceiver(sendMessageAction, ::onSendMessage)
state = State.BINDING 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 isDisposed() = state == State.DISPOSED
override fun dispose() { override fun dispose() {
broadcastReceiver.forEach { context.unregisterReceiver(it) } broadcastReceiver.forEach { context.unregisterReceiver(it) }
@ -143,35 +156,14 @@ class GarminDeviceClient(
context.registerReceiver(recv, IntentFilter(action)) context.registerReceiver(recv, IntentFilter(action))
} }
override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { override fun registerForMessages(app: GarminApplication) {
val action = createAction("APPLICATION_INFO_${device.id}_$appId") aapsLogger.info(LTag.GARMIN, "registerForMessage $name $app")
val action = createAction("ON_MESSAGE_${app.device.id}_${app.id}")
val iqApp = IQApp(app.id)
synchronized (registeredActions) { synchronized (registeredActions) {
if (!registeredActions.contains(action)) { if (!registeredActions.contains(action)) {
registerReceiver(action) { intent -> onApplicationInfo(appId, device, intent) } registerReceiver(action) { intent: Intent -> onReceiveMessage(iqApp, intent) }
} ciqService?.registerApp(iqApp, action, context.packageName)
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)
registeredActions.add(action) registeredActions.add(action)
} else { } else {
aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered") aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered")
@ -184,14 +176,15 @@ class GarminDeviceClient(
val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice? val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice?
val data = intent.getByteArrayExtra(EXTRA_PAYLOAD) val data = intent.getByteArrayExtra(EXTRA_PAYLOAD)
if (iqDevice != null && data != null) 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. */ /** Receives callback from ConnectIQ about message transfers. */
private fun onSendMessage(intent: Intent) { 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 deviceId = getDevice(intent)
val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase() val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.uppercase()
if (deviceId == null || appId == null) { if (deviceId == null || appId == null) {
aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'") aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'")
} else { } else {
@ -203,21 +196,22 @@ class GarminDeviceClient(
return return
} }
var errorMessage: String? = null
when (status) { when (status) {
IQMessage.FAILURE_DEVICE_NOT_CONNECTED, IQMessageStatus.SUCCESS -> {}
IQMessage.FAILURE_DURING_TRANSFER -> { IQMessageStatus.FAILURE_DEVICE_NOT_CONNECTED,
IQMessageStatus.FAILURE_DURING_TRANSFER -> {
if (msg.attempt < MAX_RETRIES) { if (msg.attempt < MAX_RETRIES) {
val delaySec = retryWaitFactor * msg.attempt val delaySec = retryWaitFactor * msg.attempt
Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS) Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS)
return return
} }
} }
else -> {
else -> {} errorMessage = "error $status"
}
} }
queue.remove(msg) queue.poll()
val errorMessage = status
.takeUnless { it == IQMessage.SUCCESS }?.let { s -> "error $s" }
receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage) receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage)
if (queue.isNotEmpty()) { if (queue.isNotEmpty()) {
Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) } Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) }
@ -237,8 +231,9 @@ class GarminDeviceClient(
val app: GarminApplication, val app: GarminApplication,
val data: ByteArray) { val data: ByteArray) {
var attempt: Int = 0 var attempt: Int = 0
val creation = Instant.now()
var lastAttempt: Instant? = null 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() val iqDevice get() = app.device.toIQDevice()
} }
@ -247,7 +242,17 @@ class GarminDeviceClient(
override fun sendMessage(app: GarminApplication, data: ByteArray) { override fun sendMessage(app: GarminApplication, data: ByteArray) {
val msg = synchronized (messageQueues) { val msg = synchronized (messageQueues) {
val msg = Message(app, data) val msg = Message(app, data)
val oldMessageCutOff = Instant.now().minusSeconds(30)
val queue = messageQueues.getOrPut(app.device.id to app.id) { LinkedList() } 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) queue.add(msg)
// Make sure we have only one outstanding message per app, so we ensure // Make sure we have only one outstanding message per app, so we ensure
// that always the first message in the queue is currently send. // that always the first message in the queue is currently send.
@ -266,10 +271,7 @@ class GarminDeviceClient(
private fun sendMessage(msg: Message) { private fun sendMessage(msg: Message) {
msg.attempt++ msg.attempt++
msg.lastAttempt = Instant.now() msg.lastAttempt = Instant.now()
val iqMsg = IQMessage().apply { val iqMsg = IQMessage(msg.data, context.packageName, sendMessageAction)
messageData = msg.data
notificationPackage = context.packageName
notificationAction = sendMessageAction }
ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp) ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp)
} }
@ -278,7 +280,6 @@ class GarminDeviceClient(
companion object { companion object {
const val CONNECTIQ_SERVICE_ACTION = "com.garmin.android.apps.connectmobile.CONNECTIQ_SERVICE_ACTION" 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_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_REMOTE_DEVICE = "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE"
const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD" const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD"
const val EXTRA_STATUS = "com.garmin.android.connectiq.EXTRA_STATUS" 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.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import org.jetbrains.annotations.VisibleForTesting
class GarminMessenger( class GarminMessenger(
private val aapsLogger: AAPSLogger, private val aapsLogger: AAPSLogger,
@ -17,8 +16,6 @@ class GarminMessenger(
private var disposed: Boolean = false private var disposed: Boolean = false
/** All devices that where connected since this instance was created. */ /** All devices that where connected since this instance was created. */
private val devices = mutableMapOf<Long, GarminDevice>() private val devices = mutableMapOf<Long, GarminDevice>()
@VisibleForTesting
val liveApplications = mutableSetOf<GarminApplication>()
private val clients = mutableListOf<GarminClient>() private val clients = mutableListOf<GarminClient>()
private val appIdNames = mutableMapOf<String, String>() private val appIdNames = mutableMapOf<String, String>()
init { init {
@ -33,20 +30,14 @@ class GarminMessenger(
private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice { private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice {
synchronized (devices) { 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 { private fun getApplication(client: GarminClient, deviceId: Long, appId: String): GarminApplication {
synchronized (liveApplications) { return GarminApplication(getDevice(client, deviceId), appId, appIdNames[appId])
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
}
} }
private fun startDeviceClient() { private fun startDeviceClient() {
@ -61,45 +52,18 @@ class GarminMessenger(
override fun onDisconnect(client: GarminClient) { override fun onDisconnect(client: GarminClient) {
aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}") aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}")
clients.remove(client) clients.remove(client)
synchronized (liveApplications) { synchronized (devices) {
liveApplications.removeIf { app -> app.client == client } val deviceIds = devices.filter { (_, d) -> d.client == client }.map { (id, _) -> id }
deviceIds.forEach { id -> devices.remove(id) }
} }
client.dispose() client.dispose()
when (client.name) { when (client) {
"Device" -> startDeviceClient() is GarminDeviceClient -> startDeviceClient()
"Sim"-> GarminSimulatorClient(aapsLogger, this) is GarminSimulatorClient -> GarminSimulatorClient(aapsLogger, this)
else -> aapsLogger.warn(LTag.GARMIN, "onDisconnect unknown client $client") 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) { override fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) {
val app = getApplication(client, deviceId, appId) val app = getApplication(client, deviceId, appId)
val msg = GarminSerializer.deserialize(data) val msg = GarminSerializer.deserialize(data)
@ -118,14 +82,14 @@ class GarminMessenger(
} }
fun sendMessage(device: GarminDevice, msg: Any) { fun sendMessage(device: GarminDevice, msg: Any) {
liveApplications appIdNames.forEach { (appId, _) ->
.filter { a -> a.device.id == device.id } sendMessage(getApplication(device.client, device.id, appId), msg)
.forEach { a -> sendMessage(a, msg) } }
} }
/** Sends a message to all applications on all devices. */ /** Sends a message to all applications on all devices. */
fun sendMessage(msg: Any) { 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) { private fun sendMessage(app: GarminApplication, msg: Any) {
@ -139,7 +103,7 @@ class GarminMessenger(
msg.toString() msg.toString()
} }
val data = GarminSerializer.serialize(msg) 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 { try {
app.client.sendMessage(app, data) app.client.sendMessage(app, data)
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {

View file

@ -64,12 +64,12 @@ class GarminPlugin @Inject constructor(
/** Garmin ConnectIQ application id for native communication. Phone pushes values. */ /** Garmin ConnectIQ application id for native communication. Phone pushes values. */
private val glucoseAppIds = mapOf( private val glucoseAppIds = mapOf(
"c9e90ee7e6924829a8b45e7dafff5cb4" to "GlucoseWatch_Dev", "C9E90EE7E6924829A8B45E7DAFFF5CB4" to "GlucoseWatch_Dev",
"1107ca6c2d5644b998d4bcb3793f2b7c" to "GlucoseDataField_Dev", "1107CA6C2D5644B998D4BCB3793F2B7C" to "GlucoseDataField_Dev",
"928fe19a4d3a4259b50cb6f9ddaf0f4a" to "GlucoseWidget_Dev", "928FE19A4D3A4259B50CB6F9DDAF0F4A" to "GlucoseWidget_Dev",
"662dfcf7f5a147de8bd37f09574adb11" to "GlucoseWatch", "662DFCF7F5A147DE8BD37F09574ADB11" to "GlucoseWatch",
"815c7328c21248c493ad9ac4682fe6b3" to "GlucoseDataField", "815C7328C21248C493AD9AC4682FE6B3" to "GlucoseDataField",
"4bddcc1740084a1fab83a3b2e2fcf55b" to "GlucoseWidget", "4BDDCC1740084A1FAB83A3B2E2FCF55B" to "GlucoseWidget",
) )
@VisibleForTesting @VisibleForTesting
@ -90,9 +90,7 @@ class GarminPlugin @Inject constructor(
"communication_debug_mode" -> setupGarminMessenger() "communication_debug_mode" -> setupGarminMessenger()
"communication_http", "communication_http_port" -> setupHttpServer() "communication_http", "communication_http_port" -> setupHttpServer()
"garmin_aaps_key" -> sendPhoneAppMessage() "garmin_aaps_key" -> sendPhoneAppMessage()
else -> return
} }
aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}")
} }
private fun setupGarminMessenger() { private fun setupGarminMessenger() {
@ -121,7 +119,8 @@ class GarminPlugin @Inject constructor(
.subscribe(::onNewBloodGlucose) .subscribe(::onNewBloodGlucose)
) )
setupHttpServer() setupHttpServer()
// setupGarminMessenger() if (garminAapsKey.isNotEmpty())
setupGarminMessenger()
} }
fun setupHttpServer() { fun setupHttpServer() {
@ -332,7 +331,7 @@ class GarminPlugin @Inject constructor(
if (test) return if (test) return
if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) { if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) {
loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device) loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device)
} else { } else if (avg > 0) {
aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd") aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd")
} }
} }

View file

@ -11,19 +11,6 @@ interface GarminReceiver {
fun onConnect(client: GarminClient) fun onConnect(client: GarminClient)
fun onDisconnect(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. * 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.AAPSLogger
import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.logging.LTag
import com.garmin.android.connectiq.IQApp import com.garmin.android.connectiq.IQApp
import com.garmin.android.connectiq.IQApp.IQAppStatus
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
import java.io.InputStream import java.io.InputStream
@ -35,25 +36,23 @@ class GarminSimulatorClient(
private val connections: MutableList<Connection> = Collections.synchronizedList(mutableListOf()) private val connections: MutableList<Connection> = Collections.synchronizedList(mutableListOf())
private var nextDeviceId = AtomicLong(1) private var nextDeviceId = AtomicLong(1)
@VisibleForTesting @VisibleForTesting
val iqApp = IQApp().apply { val iqApp = IQApp("SimApp", IQAppStatus.INSTALLED, "Simulator", 1)
applicationID = "SimApp"
status = GarminApplication.Status.INSTALLED.ordinal
displayName = "Simulator"
version = 1 }
private val readyLock = ReentrantLock() private val readyLock = ReentrantLock()
private val readyCond = readyLock.newCondition() 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 { private inner class Connection(private val socket: Socket): Disposable {
val device = GarminDevice( val device = GarminDevice(
this@GarminSimulatorClient, this@GarminSimulatorClient,
nextDeviceId.getAndAdd(1L), nextDeviceId.getAndAdd(1L),
"Sim@${socket.remoteSocketAddress}", "Sim@${socket.remoteSocketAddress}")
GarminDevice.Status.CONNECTED)
fun start() { fun start() {
executor.execute { executor.execute {
try { try {
receiver.onConnectDevice(this@GarminSimulatorClient, device.id, device.name)
run() run()
} catch (e: Throwable) { } catch (e: Throwable) {
aapsLogger.error(LTag.GARMIN, "$device failed", e) aapsLogger.error(LTag.GARMIN, "$device failed", e)
@ -79,7 +78,7 @@ class GarminSimulatorClient(
val data = readAvailable(socket.inputStream) ?: break val data = readAvailable(socket.inputStream) ?: break
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
kotlin.runCatching { kotlin.runCatching {
receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationID, data) receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationId, data)
} }
} }
} catch (e: SocketException) { } catch (e: SocketException) {
@ -89,7 +88,6 @@ class GarminSimulatorClient(
} }
aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" ) aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" )
connections.remove(this) connections.remove(this)
receiver.onDisconnectDevice(this@GarminSimulatorClient, device.id)
} }
private fun readAvailable(input: InputStream): ByteArray? { private fun readAvailable(input: InputStream): ByteArray? {
@ -162,10 +160,6 @@ class GarminSimulatorClient(
override fun isDisposed() = serverSocket.isClosed 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? { private fun getConnection(device: GarminDevice): Connection? {
return connections.firstOrNull { c -> c.device.id == device.id } 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.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.LinkedList
import java.util.Queue
class GarminMessengerTest: TestBase() { class GarminMessengerTest: TestBase() {
private val context = mock<Context>() 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 var appId1 = "appId1"
private val appId2 = "appId2" private val appId2 = "appId2"
private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name") 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( private var messenger = GarminMessenger(
aapsLogger, context, apps, { app, msg -> msgs.add(app to msg) }, false, false) aapsLogger, context, apps, { app, msg -> inMessages.add(app to msg) },
private val deviceId = 11L enableConnectIq = false, enableSimulator = false)
private val deviceName = "$deviceId-name" private val client1 = mock<GarminClient>() {
private val device = GarminDevice(client1, deviceId, deviceName) 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") private val device2 = GarminDevice(client2, 12L, "dev2-name")
@BeforeEach @BeforeEach
fun setup() { fun setup() {
messenger.onConnect(client1) messenger.onConnect(client1)
messenger.onConnect(client2) messenger.onConnect(client2)
client1.stub {
on { connectedDevices } doReturn listOf(device1)
}
client2.stub {
on { connectedDevices } doReturn listOf(device2)
}
} }
@AfterEach @AfterEach
fun cleanup() { fun cleanup() {
messenger.dispose() messenger.dispose()
verify(client1).dispose()
verify(client2).dispose()
assertTrue(messenger.isDisposed) 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 @Test
fun onDisconnect() { fun onDisconnect() {
messenger.onApplicationInfo(device, appId1, true)
messenger.onApplicationInfo(device2, appId2, true)
assertEquals(2, messenger.liveApplications.size)
messenger.onDisconnect(client1) messenger.onDisconnect(client1)
assertEquals(1, messenger.liveApplications.size) val msg = "foo"
val app = messenger.liveApplications.first() messenger.sendMessage(msg)
assertEquals(device2, app.device) outMessages.forEach { (app, payload) ->
assertEquals(appId2, app.id) assertEquals(client2, app.device.client)
assertEquals(apps[appId2], app.name) assertEquals(msg, GarminSerializer.deserialize(payload))
}
} }
@Test @Test
fun onReceiveMessage() { fun onReceiveMessage() {
val data = GarminSerializer.serialize("foo") val data = GarminSerializer.serialize("foo")
messenger.onReceiveMessage(client1, device.id, appId1, data) messenger.onReceiveMessage(client1, device1.id, appId1, data)
val (app, payload) = msgs.remove() val (app, payload) = inMessages.removeAt(0)
assertEquals(appId1, app.id) assertEquals(appId1, app.id)
assertEquals("foo", payload) assertEquals("foo", payload)
} }
@Test @Test
fun sendMessageDevice() { fun sendMessageDevice() {
messenger.onApplicationInfo(device, appId1, true) messenger.sendMessage(device1, "foo")
messenger.onApplicationInfo(device, appId2, true) assertEquals(2, outMessages.size)
val msg1 = outMessages.first { (app, _) -> app.id == appId1 }.second
val msgs = mutableListOf<Pair<GarminApplication, ByteArray>>() val msg2 = outMessages.first { (app, _) -> app.id == appId2 }.second
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
assertEquals("foo", GarminSerializer.deserialize(msg1)) assertEquals("foo", GarminSerializer.deserialize(msg1))
assertEquals("foo", GarminSerializer.deserialize(msg2)) assertEquals("foo", GarminSerializer.deserialize(msg2))
messenger.onSendMessage(client1, device.id, appId1, null) messenger.onSendMessage(client1, device1.id, appId1, null)
} }
@Test @Test
fun onSendMessageAll() { 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")) messenger.sendMessage(listOf("foo"))
assertEquals(2, msgs.size) assertEquals(4, outMessages.size)
val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second val msg11 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId1 }.second
val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second val msg12 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId2 }.second
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg1)) val msg21 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId1 }.second
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg2)) val msg22 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId2 }.second
messenger.onSendMessage(client1, device.id, appId1, null) 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 package app.aaps.plugins.sync.garmin
import app.aaps.shared.tests.TestBase 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.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test 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.eq
import org.mockito.kotlin.isNull import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
@ -19,36 +16,14 @@ import java.time.Duration
class GarminSimulatorClientTest: TestBase() { class GarminSimulatorClientTest: TestBase() {
private var device: GarminDevice? = null
private var app: GarminApplication? = null
private lateinit var client: GarminSimulatorClient private lateinit var client: GarminSimulatorClient
private val receiver: GarminReceiver = mock() { 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)
}
}
@BeforeEach @BeforeEach
fun setup() { fun setup() {
client = GarminSimulatorClient(aapsLogger, receiver, 0) 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 @Test
fun receiveMessage() { fun receiveMessage() {
val payload = "foo".toByteArray() val payload = "foo".toByteArray()
@ -60,11 +35,11 @@ class GarminSimulatorClientTest: TestBase() {
socket.getOutputStream().write(payload) socket.getOutputStream().write(payload)
socket.getOutputStream().flush() socket.getOutputStream().flush()
verify(receiver).onConnect(client) verify(receiver).onConnect(client)
verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any())
} }
verify(receiver, timeout(1_000)).onReceiveMessage( assertEquals(1, client.connectedDevices.size)
eq(client), eq(device!!.id), eq("SimApp"), val device: GarminDevice = client.connectedDevices.first()
argThat { p -> payload.contentEquals(p) }) verify(receiver, timeout(1_000))
.onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload))
} }
@Test @Test
@ -73,14 +48,16 @@ class GarminSimulatorClientTest: TestBase() {
assertTrue(client.awaitReady(Duration.ofSeconds(10))) assertTrue(client.awaitReady(Duration.ofSeconds(10)))
val port = client.port val port = client.port
val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1))
val device: GarminDevice
val app: GarminApplication
Socket(ip, port).use { socket -> Socket(ip, port).use { socket ->
assertTrue(socket.isConnected) assertTrue(socket.isConnected)
verify(receiver).onConnect(client) verify(receiver).onConnect(client)
verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) assertEquals(1, client.connectedDevices.size)
assertNotNull(device) device = client.connectedDevices.first()
assertNotNull(app) app = GarminApplication(device, "SIMAPP", "T")
client.sendMessage(app!!, payload) 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())
} }
} }