Fix Garmin communication. Use Garmin lib rather than aidl, get rid of not needed functionality.
This commit is contained in:
parent
f3d1acffcd
commit
a36081d3df
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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) })
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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]"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue