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 {
|
||||
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"
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,9 @@ dependencies {
|
|||
androidTestImplementation(Libs.AndroidX.Test.rules)
|
||||
androidTestImplementation(Libs.Google.truth)
|
||||
androidTestImplementation(Libs.AndroidX.Test.uiAutomator)
|
||||
androidTestImplementation(Libs.Mockito.core)
|
||||
androidTestImplementation(Libs.Mockito.android)
|
||||
androidTestImplementation(Libs.Mockito.kotlin)
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
|
|
|
@ -9,9 +9,6 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "app.aaps.plugins.sync"
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -33,6 +30,7 @@ dependencies {
|
|||
testImplementation(project(":shared:tests"))
|
||||
testImplementation(project(":implementation"))
|
||||
testImplementation(project(":plugins:aps"))
|
||||
androidTestImplementation(project(":shared:tests"))
|
||||
|
||||
// OpenHuman
|
||||
api(Libs.Squareup.Okhttp3.okhttp)
|
||||
|
@ -52,6 +50,10 @@ dependencies {
|
|||
// DataLayerListenerService
|
||||
api(Libs.Google.Android.PlayServices.wearable)
|
||||
|
||||
// Garmin
|
||||
api("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar")
|
||||
androidTestImplementation("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar")
|
||||
|
||||
kapt(Libs.Dagger.compiler)
|
||||
kapt(Libs.Dagger.androidProcessor)
|
||||
}
|
|
@ -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
|
||||
|
||||
data class GarminApplication(
|
||||
val client: GarminClient,
|
||||
val device: GarminDevice,
|
||||
val id: String,
|
||||
val name: String?) {
|
||||
|
||||
enum class Status {
|
||||
@Suppress("UNUSED")
|
||||
UNKNOWN,
|
||||
INSTALLED,
|
||||
@Suppress("UNUSED")
|
||||
NOT_INSTALLED,
|
||||
@Suppress("UNUSED")
|
||||
NOT_SUPPORTED;
|
||||
}
|
||||
val client get() = device.client
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
@ -35,5 +26,7 @@ data class GarminApplication(
|
|||
result = 31 * result + id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString() = "A[$device:$id:$name]"
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ interface GarminClient: Disposable {
|
|||
/** Name of the client. */
|
||||
val name: String
|
||||
|
||||
/** Asynchronously retrieves status information for the given application. */
|
||||
fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String)
|
||||
val connectedDevices: List<GarminDevice>
|
||||
|
||||
/** Register to receive messages from the given up. */
|
||||
fun registerForMessages(app: GarminApplication)
|
||||
|
||||
/** Asynchronously sends a message to an application. */
|
||||
fun sendMessage(app: GarminApplication, data: ByteArray)
|
||||
|
|
|
@ -5,34 +5,16 @@ import com.garmin.android.connectiq.IQDevice
|
|||
data class GarminDevice(
|
||||
val client: GarminClient,
|
||||
val id: Long,
|
||||
var name: String,
|
||||
var status: Status = Status.UNKNOWN) {
|
||||
var name: String) {
|
||||
|
||||
constructor(client: GarminClient, iqDevice: IQDevice): this(
|
||||
client,
|
||||
iqDevice.deviceIdentifier,
|
||||
iqDevice.friendlyName,
|
||||
Status.from(iqDevice.status)) {}
|
||||
|
||||
enum class Status {
|
||||
NOT_PAIRED,
|
||||
NOT_CONNECTED,
|
||||
CONNECTED,
|
||||
UNKNOWN;
|
||||
|
||||
companion object {
|
||||
fun from(ordinal: Int?): Status =
|
||||
values().firstOrNull { s -> s.ordinal == ordinal } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
iqDevice.friendlyName) {}
|
||||
|
||||
override fun toString(): String = "D[$name/$id]"
|
||||
|
||||
fun toIQDevice() = IQDevice().apply {
|
||||
deviceIdentifier = id
|
||||
friendlyName = name
|
||||
status = Status.UNKNOWN.ordinal }
|
||||
fun toIQDevice() = IQDevice(id, name)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
|
|
@ -6,22 +6,24 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
import app.aaps.core.interfaces.logging.LTag
|
||||
import app.aaps.core.utils.waitMillis
|
||||
import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService
|
||||
import com.garmin.android.connectiq.ConnectIQ.IQMessageStatus
|
||||
import com.garmin.android.connectiq.IQApp
|
||||
import com.garmin.android.connectiq.IQDevice
|
||||
import com.garmin.android.connectiq.IQMessage
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import java.time.Duration
|
||||
import java.lang.Thread.UncaughtExceptionHandler
|
||||
import java.time.Instant
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
|
@ -33,26 +35,31 @@ class GarminDeviceClient(
|
|||
private val retryWaitFactor: Long = 5L): Disposable, GarminClient {
|
||||
|
||||
override val name = "Device"
|
||||
private var executor = Executors.newSingleThreadExecutor { r ->
|
||||
Thread(r).apply {
|
||||
name = "Garmin callback"
|
||||
isDaemon = true
|
||||
uncaughtExceptionHandler = UncaughtExceptionHandler { _, e ->
|
||||
aapsLogger.error(LTag.GARMIN, "ConnectIQ callback failed", e) }
|
||||
}
|
||||
}
|
||||
private var bindLock = Object()
|
||||
private var ciqService: IConnectIQService? = null
|
||||
get() {
|
||||
val waitUntil = Instant.now().plusSeconds(2)
|
||||
synchronized (bindLock) {
|
||||
while(field?.asBinder()?.isBinderAlive != true) {
|
||||
if (field?.asBinder()?.isBinderAlive != true) {
|
||||
field = null
|
||||
if (state !in arrayOf(State.BINDING, State.RECONNECTING)) {
|
||||
aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service")
|
||||
state = State.RECONNECTING
|
||||
context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
bindService()
|
||||
}
|
||||
// Wait for the connection, that is the call to onServiceConnected.
|
||||
val wait = Duration.between(Instant.now(), waitUntil)
|
||||
if (wait > Duration.ZERO) bindLock.waitMillis(wait.toMillis())
|
||||
if (field == null) {
|
||||
bindLock.waitMillis(2_000L)
|
||||
if (field?.asBinder()?.isBinderAlive != true) {
|
||||
field = null
|
||||
// The [serviceConnection] didn't have a chance to reassign ciqService,
|
||||
// i.e. the wait timed out. Give up.
|
||||
aapsLogger.warn(LTag.GARMIN, "no ciqservice $this")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return field
|
||||
|
@ -80,7 +87,7 @@ class GarminDeviceClient(
|
|||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
var notifyReceiver: Boolean
|
||||
val ciq: IConnectIQService
|
||||
synchronized (bindLock) {
|
||||
synchronized(bindLock) {
|
||||
aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected")
|
||||
ciq = IConnectIQService.Stub.asInterface(service)
|
||||
notifyReceiver = state != State.RECONNECTING
|
||||
|
@ -89,14 +96,8 @@ class GarminDeviceClient(
|
|||
bindLock.notifyAll()
|
||||
}
|
||||
if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient)
|
||||
try {
|
||||
ciq.connectedDevices?.forEach { d ->
|
||||
receiver.onConnectDevice(this@GarminDeviceClient, d.deviceIdentifier, d.friendlyName)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
aapsLogger.error(LTag.GARMIN, "getting devices failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
synchronized(bindLock) {
|
||||
aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected")
|
||||
|
@ -114,9 +115,21 @@ class GarminDeviceClient(
|
|||
aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service")
|
||||
registerReceiver(sendMessageAction, ::onSendMessage)
|
||||
state = State.BINDING
|
||||
context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
bindService()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
context.bindService(serviceIntent, Context.BIND_AUTO_CREATE, executor, ciqServiceConnection)
|
||||
} else {
|
||||
context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
override val connectedDevices: List<GarminDevice>
|
||||
get() = ciqService?.connectedDevices?.map { iqDevice -> GarminDevice(this, iqDevice) }
|
||||
?: emptyList()
|
||||
|
||||
override fun isDisposed() = state == State.DISPOSED
|
||||
override fun dispose() {
|
||||
broadcastReceiver.forEach { context.unregisterReceiver(it) }
|
||||
|
@ -143,35 +156,14 @@ class GarminDeviceClient(
|
|||
context.registerReceiver(recv, IntentFilter(action))
|
||||
}
|
||||
|
||||
override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) {
|
||||
val action = createAction("APPLICATION_INFO_${device.id}_$appId")
|
||||
override fun registerForMessages(app: GarminApplication) {
|
||||
aapsLogger.info(LTag.GARMIN, "registerForMessage $name $app")
|
||||
val action = createAction("ON_MESSAGE_${app.device.id}_${app.id}")
|
||||
val iqApp = IQApp(app.id)
|
||||
synchronized (registeredActions) {
|
||||
if (!registeredActions.contains(action)) {
|
||||
registerReceiver(action) { intent -> onApplicationInfo(appId, device, intent) }
|
||||
}
|
||||
registeredActions.add(action)
|
||||
}
|
||||
ciqService?.getApplicationInfo(context.packageName, action, device.toIQDevice(), appId)
|
||||
}
|
||||
|
||||
/** Receives application info callbacks from ConnectIQ app.*/
|
||||
private fun onApplicationInfo(appId: String, device: GarminDevice, intent: Intent) {
|
||||
val receivedAppId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase(Locale.getDefault())
|
||||
val version = intent.getIntExtra(EXTRA_APPLICATION_VERSION, -1)
|
||||
val isInstalled = receivedAppId != null && version >= 0 && version != 65535
|
||||
|
||||
if (isInstalled) registerForMessages(device.id, appId)
|
||||
receiver.onApplicationInfo(device, appId, isInstalled)
|
||||
}
|
||||
|
||||
private fun registerForMessages(deviceId: Long, appId: String) {
|
||||
aapsLogger.info(LTag.GARMIN, "registerForMessage $name $appId")
|
||||
val action = createAction("ON_MESSAGE_${deviceId}_$appId")
|
||||
val app = IQApp().apply { applicationID = appId; displayName = "" }
|
||||
synchronized (registeredActions) {
|
||||
if (!registeredActions.contains(action)) {
|
||||
registerReceiver(action) { intent: Intent -> onReceiveMessage(app, intent) }
|
||||
ciqService?.registerApp(app, action, context.packageName)
|
||||
registerReceiver(action) { intent: Intent -> onReceiveMessage(iqApp, intent) }
|
||||
ciqService?.registerApp(iqApp, action, context.packageName)
|
||||
registeredActions.add(action)
|
||||
} else {
|
||||
aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered")
|
||||
|
@ -184,14 +176,15 @@ class GarminDeviceClient(
|
|||
val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice?
|
||||
val data = intent.getByteArrayExtra(EXTRA_PAYLOAD)
|
||||
if (iqDevice != null && data != null)
|
||||
receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationID, data)
|
||||
receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationId, data)
|
||||
}
|
||||
|
||||
/** Receives callback from ConnectIQ about message transfers. */
|
||||
private fun onSendMessage(intent: Intent) {
|
||||
val status = intent.getIntExtra(EXTRA_STATUS, 0)
|
||||
val statusOrd = intent.getIntExtra(EXTRA_STATUS, IQMessageStatus.FAILURE_UNKNOWN.ordinal)
|
||||
val status = IQMessageStatus.values().firstOrNull { s -> s.ordinal == statusOrd } ?: IQMessageStatus.FAILURE_UNKNOWN
|
||||
val deviceId = getDevice(intent)
|
||||
val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase()
|
||||
val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.uppercase()
|
||||
if (deviceId == null || appId == null) {
|
||||
aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'")
|
||||
} else {
|
||||
|
@ -203,21 +196,22 @@ class GarminDeviceClient(
|
|||
return
|
||||
}
|
||||
|
||||
var errorMessage: String? = null
|
||||
when (status) {
|
||||
IQMessage.FAILURE_DEVICE_NOT_CONNECTED,
|
||||
IQMessage.FAILURE_DURING_TRANSFER -> {
|
||||
IQMessageStatus.SUCCESS -> {}
|
||||
IQMessageStatus.FAILURE_DEVICE_NOT_CONNECTED,
|
||||
IQMessageStatus.FAILURE_DURING_TRANSFER -> {
|
||||
if (msg.attempt < MAX_RETRIES) {
|
||||
val delaySec = retryWaitFactor * msg.attempt
|
||||
Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
else -> {
|
||||
errorMessage = "error $status"
|
||||
}
|
||||
}
|
||||
queue.remove(msg)
|
||||
val errorMessage = status
|
||||
.takeUnless { it == IQMessage.SUCCESS }?.let { s -> "error $s" }
|
||||
queue.poll()
|
||||
receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage)
|
||||
if (queue.isNotEmpty()) {
|
||||
Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) }
|
||||
|
@ -237,8 +231,9 @@ class GarminDeviceClient(
|
|||
val app: GarminApplication,
|
||||
val data: ByteArray) {
|
||||
var attempt: Int = 0
|
||||
val creation = Instant.now()
|
||||
var lastAttempt: Instant? = null
|
||||
val iqApp get() = IQApp().apply { applicationID = app.id; displayName = app.name }
|
||||
val iqApp get() = IQApp(app.id, app.name, 0)
|
||||
val iqDevice get() = app.device.toIQDevice()
|
||||
}
|
||||
|
||||
|
@ -247,7 +242,17 @@ class GarminDeviceClient(
|
|||
override fun sendMessage(app: GarminApplication, data: ByteArray) {
|
||||
val msg = synchronized (messageQueues) {
|
||||
val msg = Message(app, data)
|
||||
val oldMessageCutOff = Instant.now().minusSeconds(30)
|
||||
val queue = messageQueues.getOrPut(app.device.id to app.id) { LinkedList() }
|
||||
while (true) {
|
||||
val oldMsg = queue.peek() ?: break
|
||||
if ((oldMsg.lastAttempt ?: oldMsg.creation).isBefore(oldMessageCutOff)) {
|
||||
aapsLogger.warn(LTag.GARMIN, "remove old msg ${msg.app}")
|
||||
queue.poll()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
queue.add(msg)
|
||||
// Make sure we have only one outstanding message per app, so we ensure
|
||||
// that always the first message in the queue is currently send.
|
||||
|
@ -266,10 +271,7 @@ class GarminDeviceClient(
|
|||
private fun sendMessage(msg: Message) {
|
||||
msg.attempt++
|
||||
msg.lastAttempt = Instant.now()
|
||||
val iqMsg = IQMessage().apply {
|
||||
messageData = msg.data
|
||||
notificationPackage = context.packageName
|
||||
notificationAction = sendMessageAction }
|
||||
val iqMsg = IQMessage(msg.data, context.packageName, sendMessageAction)
|
||||
ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp)
|
||||
}
|
||||
|
||||
|
@ -278,7 +280,6 @@ class GarminDeviceClient(
|
|||
companion object {
|
||||
const val CONNECTIQ_SERVICE_ACTION = "com.garmin.android.apps.connectmobile.CONNECTIQ_SERVICE_ACTION"
|
||||
const val EXTRA_APPLICATION_ID = "com.garmin.android.connectiq.EXTRA_APPLICATION_ID"
|
||||
const val EXTRA_APPLICATION_VERSION = "com.garmin.android.connectiq.EXTRA_APPLICATION_VERSION"
|
||||
const val EXTRA_REMOTE_DEVICE = "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE"
|
||||
const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD"
|
||||
const val EXTRA_STATUS = "com.garmin.android.connectiq.EXTRA_STATUS"
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
import app.aaps.core.interfaces.logging.LTag
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
|
||||
class GarminMessenger(
|
||||
private val aapsLogger: AAPSLogger,
|
||||
|
@ -17,8 +16,6 @@ class GarminMessenger(
|
|||
private var disposed: Boolean = false
|
||||
/** All devices that where connected since this instance was created. */
|
||||
private val devices = mutableMapOf<Long, GarminDevice>()
|
||||
@VisibleForTesting
|
||||
val liveApplications = mutableSetOf<GarminApplication>()
|
||||
private val clients = mutableListOf<GarminClient>()
|
||||
private val appIdNames = mutableMapOf<String, String>()
|
||||
init {
|
||||
|
@ -33,20 +30,14 @@ class GarminMessenger(
|
|||
|
||||
private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice {
|
||||
synchronized (devices) {
|
||||
return devices.getOrPut(deviceId) { GarminDevice(client, deviceId, "unknown") }
|
||||
return devices.getOrPut(deviceId) {
|
||||
client.connectedDevices.firstOrNull { d -> d.id == deviceId } ?:
|
||||
GarminDevice(client, deviceId, "unknown") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getApplication(client: GarminClient, deviceId: Long, appId: String): GarminApplication {
|
||||
synchronized (liveApplications) {
|
||||
var app = liveApplications.firstOrNull { app ->
|
||||
app.client == client && app.device.id == deviceId && app.id == appId }
|
||||
if (app == null) {
|
||||
app = GarminApplication(client, getDevice(client, deviceId), appId, appIdNames[appId])
|
||||
liveApplications.add(app)
|
||||
}
|
||||
return app
|
||||
}
|
||||
return GarminApplication(getDevice(client, deviceId), appId, appIdNames[appId])
|
||||
}
|
||||
|
||||
private fun startDeviceClient() {
|
||||
|
@ -61,45 +52,18 @@ class GarminMessenger(
|
|||
override fun onDisconnect(client: GarminClient) {
|
||||
aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}")
|
||||
clients.remove(client)
|
||||
synchronized (liveApplications) {
|
||||
liveApplications.removeIf { app -> app.client == client }
|
||||
synchronized (devices) {
|
||||
val deviceIds = devices.filter { (_, d) -> d.client == client }.map { (id, _) -> id }
|
||||
deviceIds.forEach { id -> devices.remove(id) }
|
||||
}
|
||||
client.dispose()
|
||||
when (client.name) {
|
||||
"Device" -> startDeviceClient()
|
||||
"Sim"-> GarminSimulatorClient(aapsLogger, this)
|
||||
when (client) {
|
||||
is GarminDeviceClient -> startDeviceClient()
|
||||
is GarminSimulatorClient -> GarminSimulatorClient(aapsLogger, this)
|
||||
else -> aapsLogger.warn(LTag.GARMIN, "onDisconnect unknown client $client")
|
||||
}
|
||||
}
|
||||
|
||||
/** Receives notifications that a device has connected.
|
||||
*
|
||||
* It will retrieve status information for all applications we care about (in [appIdNames]). */
|
||||
override fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) {
|
||||
val device = getDevice(client, deviceId).apply { name = deviceName }
|
||||
aapsLogger.info(LTag.GARMIN, "onConnectDevice $device")
|
||||
appIdNames.forEach { (id, name) -> client.retrieveApplicationInfo(device, id, name) }
|
||||
}
|
||||
|
||||
/** Receives notifications about disconnection of a device. */
|
||||
override fun onDisconnectDevice(client: GarminClient, deviceId: Long) {
|
||||
val device = getDevice(client, deviceId)
|
||||
aapsLogger.info(LTag.GARMIN,"onDisconnectDevice $device")
|
||||
synchronized (liveApplications) {
|
||||
liveApplications.removeIf { app -> app.device == device }
|
||||
}
|
||||
}
|
||||
|
||||
/** Receives notification about applications that are installed/uninstalled
|
||||
* on a device from the client. */
|
||||
override fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) {
|
||||
val app = getApplication(device.client, device.id, appId)
|
||||
aapsLogger.info(LTag.GARMIN, "onApplicationInfo add $app ${if (isInstalled) "" else "un"}installed")
|
||||
if (!isInstalled) {
|
||||
synchronized (liveApplications) { liveApplications.remove(app) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) {
|
||||
val app = getApplication(client, deviceId, appId)
|
||||
val msg = GarminSerializer.deserialize(data)
|
||||
|
@ -118,14 +82,14 @@ class GarminMessenger(
|
|||
}
|
||||
|
||||
fun sendMessage(device: GarminDevice, msg: Any) {
|
||||
liveApplications
|
||||
.filter { a -> a.device.id == device.id }
|
||||
.forEach { a -> sendMessage(a, msg) }
|
||||
appIdNames.forEach { (appId, _) ->
|
||||
sendMessage(getApplication(device.client, device.id, appId), msg)
|
||||
}
|
||||
}
|
||||
|
||||
/** Sends a message to all applications on all devices. */
|
||||
fun sendMessage(msg: Any) {
|
||||
liveApplications.forEach { app -> sendMessage(app, msg) }
|
||||
clients.forEach { cl -> cl.connectedDevices.forEach { d -> sendMessage(d, msg) }}
|
||||
}
|
||||
|
||||
private fun sendMessage(app: GarminApplication, msg: Any) {
|
||||
|
@ -139,7 +103,7 @@ class GarminMessenger(
|
|||
msg.toString()
|
||||
}
|
||||
val data = GarminSerializer.serialize(msg)
|
||||
aapsLogger.info(LTag.GARMIN, "sendMessage $app $app ${data.size} bytes $s")
|
||||
aapsLogger.info(LTag.GARMIN, "sendMessage $app ${data.size} bytes $s")
|
||||
try {
|
||||
app.client.sendMessage(app, data)
|
||||
} catch (e: IllegalStateException) {
|
||||
|
|
|
@ -64,12 +64,12 @@ class GarminPlugin @Inject constructor(
|
|||
|
||||
/** Garmin ConnectIQ application id for native communication. Phone pushes values. */
|
||||
private val glucoseAppIds = mapOf(
|
||||
"c9e90ee7e6924829a8b45e7dafff5cb4" to "GlucoseWatch_Dev",
|
||||
"1107ca6c2d5644b998d4bcb3793f2b7c" to "GlucoseDataField_Dev",
|
||||
"928fe19a4d3a4259b50cb6f9ddaf0f4a" to "GlucoseWidget_Dev",
|
||||
"662dfcf7f5a147de8bd37f09574adb11" to "GlucoseWatch",
|
||||
"815c7328c21248c493ad9ac4682fe6b3" to "GlucoseDataField",
|
||||
"4bddcc1740084a1fab83a3b2e2fcf55b" to "GlucoseWidget",
|
||||
"C9E90EE7E6924829A8B45E7DAFFF5CB4" to "GlucoseWatch_Dev",
|
||||
"1107CA6C2D5644B998D4BCB3793F2B7C" to "GlucoseDataField_Dev",
|
||||
"928FE19A4D3A4259B50CB6F9DDAF0F4A" to "GlucoseWidget_Dev",
|
||||
"662DFCF7F5A147DE8BD37F09574ADB11" to "GlucoseWatch",
|
||||
"815C7328C21248C493AD9AC4682FE6B3" to "GlucoseDataField",
|
||||
"4BDDCC1740084A1FAB83A3B2E2FCF55B" to "GlucoseWidget",
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -90,9 +90,7 @@ class GarminPlugin @Inject constructor(
|
|||
"communication_debug_mode" -> setupGarminMessenger()
|
||||
"communication_http", "communication_http_port" -> setupHttpServer()
|
||||
"garmin_aaps_key" -> sendPhoneAppMessage()
|
||||
else -> return
|
||||
}
|
||||
aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}")
|
||||
}
|
||||
|
||||
private fun setupGarminMessenger() {
|
||||
|
@ -121,7 +119,8 @@ class GarminPlugin @Inject constructor(
|
|||
.subscribe(::onNewBloodGlucose)
|
||||
)
|
||||
setupHttpServer()
|
||||
// setupGarminMessenger()
|
||||
if (garminAapsKey.isNotEmpty())
|
||||
setupGarminMessenger()
|
||||
}
|
||||
|
||||
fun setupHttpServer() {
|
||||
|
@ -332,7 +331,7 @@ class GarminPlugin @Inject constructor(
|
|||
if (test) return
|
||||
if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) {
|
||||
loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device)
|
||||
} else {
|
||||
} else if (avg > 0) {
|
||||
aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,19 +11,6 @@ interface GarminReceiver {
|
|||
fun onConnect(client: GarminClient)
|
||||
fun onDisconnect(client: GarminClient)
|
||||
|
||||
/**
|
||||
* Notifies that a device is connected. This will be called for all connected devices
|
||||
* initially.
|
||||
*/
|
||||
fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String)
|
||||
fun onDisconnectDevice(client: GarminClient, deviceId: Long)
|
||||
|
||||
/**
|
||||
* Provides application info after a call to
|
||||
* {@link ConnectIqClient#retrieveApplicationInfo retrieveApplicationInfo}.
|
||||
*/
|
||||
fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean)
|
||||
|
||||
/**
|
||||
* Delivers received device app messages.
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@ package app.aaps.plugins.sync.garmin
|
|||
import app.aaps.core.interfaces.logging.AAPSLogger
|
||||
import app.aaps.core.interfaces.logging.LTag
|
||||
import com.garmin.android.connectiq.IQApp
|
||||
import com.garmin.android.connectiq.IQApp.IQAppStatus
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import java.io.InputStream
|
||||
|
@ -35,25 +36,23 @@ class GarminSimulatorClient(
|
|||
private val connections: MutableList<Connection> = Collections.synchronizedList(mutableListOf())
|
||||
private var nextDeviceId = AtomicLong(1)
|
||||
@VisibleForTesting
|
||||
val iqApp = IQApp().apply {
|
||||
applicationID = "SimApp"
|
||||
status = GarminApplication.Status.INSTALLED.ordinal
|
||||
displayName = "Simulator"
|
||||
version = 1 }
|
||||
val iqApp = IQApp("SimApp", IQAppStatus.INSTALLED, "Simulator", 1)
|
||||
private val readyLock = ReentrantLock()
|
||||
private val readyCond = readyLock.newCondition()
|
||||
override val connectedDevices: List<GarminDevice> get() = connections.map { c -> c.device }
|
||||
|
||||
override fun registerForMessages(app: GarminApplication) {
|
||||
}
|
||||
|
||||
private inner class Connection(private val socket: Socket): Disposable {
|
||||
val device = GarminDevice(
|
||||
this@GarminSimulatorClient,
|
||||
nextDeviceId.getAndAdd(1L),
|
||||
"Sim@${socket.remoteSocketAddress}",
|
||||
GarminDevice.Status.CONNECTED)
|
||||
"Sim@${socket.remoteSocketAddress}")
|
||||
|
||||
fun start() {
|
||||
executor.execute {
|
||||
try {
|
||||
receiver.onConnectDevice(this@GarminSimulatorClient, device.id, device.name)
|
||||
run()
|
||||
} catch (e: Throwable) {
|
||||
aapsLogger.error(LTag.GARMIN, "$device failed", e)
|
||||
|
@ -79,7 +78,7 @@ class GarminSimulatorClient(
|
|||
val data = readAvailable(socket.inputStream) ?: break
|
||||
if (data.isNotEmpty()) {
|
||||
kotlin.runCatching {
|
||||
receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationID, data)
|
||||
receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationId, data)
|
||||
}
|
||||
}
|
||||
} catch (e: SocketException) {
|
||||
|
@ -89,7 +88,6 @@ class GarminSimulatorClient(
|
|||
}
|
||||
aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" )
|
||||
connections.remove(this)
|
||||
receiver.onDisconnectDevice(this@GarminSimulatorClient, device.id)
|
||||
}
|
||||
|
||||
private fun readAvailable(input: InputStream): ByteArray? {
|
||||
|
@ -162,10 +160,6 @@ class GarminSimulatorClient(
|
|||
|
||||
override fun isDisposed() = serverSocket.isClosed
|
||||
|
||||
override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) {
|
||||
receiver.onApplicationInfo(device, appId, appId == iqApp.applicationID)
|
||||
}
|
||||
|
||||
private fun getConnection(device: GarminDevice): Connection? {
|
||||
return connections.firstOrNull { c -> c.device.id == device.id }
|
||||
}
|
||||
|
|
|
@ -8,135 +8,103 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
|||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stub
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
|
||||
class GarminMessengerTest: TestBase() {
|
||||
private val context = mock<Context>()
|
||||
private val client1 = mock<GarminClient>() {
|
||||
on { name } doReturn "Mock1"
|
||||
}
|
||||
private val client2 = mock<GarminClient>() {
|
||||
on { name } doReturn "Mock2"
|
||||
}
|
||||
|
||||
private var appId1 = "appId1"
|
||||
private val appId2 = "appId2"
|
||||
|
||||
private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name")
|
||||
private val msgs: Queue<Pair<GarminApplication, Any>> = LinkedList()
|
||||
private val outMessages = mutableListOf<Pair<GarminApplication, ByteArray>>()
|
||||
private val inMessages = mutableListOf<Pair<GarminApplication, Any>>()
|
||||
private var messenger = GarminMessenger(
|
||||
aapsLogger, context, apps, { app, msg -> msgs.add(app to msg) }, false, false)
|
||||
private val deviceId = 11L
|
||||
private val deviceName = "$deviceId-name"
|
||||
private val device = GarminDevice(client1, deviceId, deviceName)
|
||||
aapsLogger, context, apps, { app, msg -> inMessages.add(app to msg) },
|
||||
enableConnectIq = false, enableSimulator = false)
|
||||
private val client1 = mock<GarminClient>() {
|
||||
on { name } doReturn "Mock1"
|
||||
on { sendMessage(any(), any()) } doAnswer { a ->
|
||||
outMessages.add(a.getArgument<GarminApplication>(0) to a.getArgument(1))
|
||||
Unit
|
||||
}
|
||||
}
|
||||
private val client2 = mock<GarminClient>() {
|
||||
on { name } doReturn "Mock2"
|
||||
on { sendMessage(any(), any()) } doAnswer { a ->
|
||||
outMessages.add(a.getArgument<GarminApplication>(0) to a.getArgument(1))
|
||||
Unit
|
||||
}
|
||||
}
|
||||
private val device1 = GarminDevice(client1, 11L, "dev1-name")
|
||||
private val device2 = GarminDevice(client2, 12L, "dev2-name")
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
messenger.onConnect(client1)
|
||||
messenger.onConnect(client2)
|
||||
client1.stub {
|
||||
on { connectedDevices } doReturn listOf(device1)
|
||||
}
|
||||
client2.stub {
|
||||
on { connectedDevices } doReturn listOf(device2)
|
||||
}
|
||||
}
|
||||
@AfterEach
|
||||
fun cleanup() {
|
||||
messenger.dispose()
|
||||
verify(client1).dispose()
|
||||
verify(client2).dispose()
|
||||
assertTrue(messenger.isDisposed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onConnectDevice() {
|
||||
messenger.onConnectDevice(client1, deviceId, deviceName)
|
||||
verify(client1).retrieveApplicationInfo(device, appId1, apps[appId1]!!)
|
||||
verify(client1).retrieveApplicationInfo(device, appId2, apps[appId2]!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onApplicationInfo() {
|
||||
messenger.onApplicationInfo(device, appId1, true)
|
||||
val app = messenger.liveApplications.first()
|
||||
assertEquals(device, app.device)
|
||||
assertEquals(appId1, app.id)
|
||||
assertEquals(apps[appId1], app.name)
|
||||
|
||||
messenger.onApplicationInfo(device, appId1, false)
|
||||
assertEquals(0, messenger.liveApplications.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDisconnectDevice() {
|
||||
messenger.onConnectDevice(client1, deviceId, deviceName)
|
||||
messenger.onApplicationInfo(device, appId1, true)
|
||||
messenger.onApplicationInfo(device2, appId1, true)
|
||||
assertEquals(2, messenger.liveApplications.size)
|
||||
messenger.onDisconnectDevice(client1, device2.id)
|
||||
assertEquals(1, messenger.liveApplications.size)
|
||||
assertEquals(appId1, messenger.liveApplications.first().id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDisconnect() {
|
||||
messenger.onApplicationInfo(device, appId1, true)
|
||||
messenger.onApplicationInfo(device2, appId2, true)
|
||||
assertEquals(2, messenger.liveApplications.size)
|
||||
messenger.onDisconnect(client1)
|
||||
assertEquals(1, messenger.liveApplications.size)
|
||||
val app = messenger.liveApplications.first()
|
||||
assertEquals(device2, app.device)
|
||||
assertEquals(appId2, app.id)
|
||||
assertEquals(apps[appId2], app.name)
|
||||
val msg = "foo"
|
||||
messenger.sendMessage(msg)
|
||||
outMessages.forEach { (app, payload) ->
|
||||
assertEquals(client2, app.device.client)
|
||||
assertEquals(msg, GarminSerializer.deserialize(payload))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onReceiveMessage() {
|
||||
val data = GarminSerializer.serialize("foo")
|
||||
messenger.onReceiveMessage(client1, device.id, appId1, data)
|
||||
val (app, payload) = msgs.remove()
|
||||
messenger.onReceiveMessage(client1, device1.id, appId1, data)
|
||||
val (app, payload) = inMessages.removeAt(0)
|
||||
assertEquals(appId1, app.id)
|
||||
assertEquals("foo", payload)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendMessageDevice() {
|
||||
messenger.onApplicationInfo(device, appId1, true)
|
||||
messenger.onApplicationInfo(device, appId2, true)
|
||||
|
||||
val msgs = mutableListOf<Pair<GarminApplication, ByteArray>>()
|
||||
whenever(client1.sendMessage(any(), any())).thenAnswer { i ->
|
||||
msgs.add(i.getArgument<GarminApplication>(0) to i.getArgument(1))
|
||||
}
|
||||
|
||||
messenger.sendMessage(device, "foo")
|
||||
assertEquals(2, msgs.size)
|
||||
val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second
|
||||
val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second
|
||||
messenger.sendMessage(device1, "foo")
|
||||
assertEquals(2, outMessages.size)
|
||||
val msg1 = outMessages.first { (app, _) -> app.id == appId1 }.second
|
||||
val msg2 = outMessages.first { (app, _) -> app.id == appId2 }.second
|
||||
assertEquals("foo", GarminSerializer.deserialize(msg1))
|
||||
assertEquals("foo", GarminSerializer.deserialize(msg2))
|
||||
messenger.onSendMessage(client1, device.id, appId1, null)
|
||||
messenger.onSendMessage(client1, device1.id, appId1, null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onSendMessageAll() {
|
||||
messenger.onApplicationInfo(device, appId1, true)
|
||||
messenger.onApplicationInfo(device2, appId2, true)
|
||||
assertEquals(2, messenger.liveApplications.size)
|
||||
|
||||
val msgs = mutableListOf<Pair<GarminApplication, ByteArray>>()
|
||||
whenever(client1.sendMessage(any(), any())).thenAnswer { i ->
|
||||
msgs.add(i.getArgument<GarminApplication>(0) to i.getArgument(1))
|
||||
}
|
||||
whenever(client2.sendMessage(any(), any())).thenAnswer { i ->
|
||||
msgs.add(i.getArgument<GarminApplication>(0) to i.getArgument(1))
|
||||
}
|
||||
|
||||
messenger.sendMessage(listOf("foo"))
|
||||
assertEquals(2, msgs.size)
|
||||
val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second
|
||||
val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second
|
||||
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg1))
|
||||
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg2))
|
||||
messenger.onSendMessage(client1, device.id, appId1, null)
|
||||
assertEquals(4, outMessages.size)
|
||||
val msg11 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId1 }.second
|
||||
val msg12 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId2 }.second
|
||||
val msg21 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId1 }.second
|
||||
val msg22 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId2 }.second
|
||||
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg11))
|
||||
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg12))
|
||||
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg21))
|
||||
assertEquals(listOf("foo"), GarminSerializer.deserialize(msg22))
|
||||
messenger.onSendMessage(client1, device1.id, appId1, null)
|
||||
}
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
package app.aaps.plugins.sync.garmin
|
||||
|
||||
import app.aaps.shared.tests.TestBase
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.argThat
|
||||
import org.mockito.kotlin.doAnswer
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.isNull
|
||||
import org.mockito.kotlin.mock
|
||||
|
@ -19,36 +16,14 @@ import java.time.Duration
|
|||
|
||||
class GarminSimulatorClientTest: TestBase() {
|
||||
|
||||
private var device: GarminDevice? = null
|
||||
private var app: GarminApplication? = null
|
||||
private lateinit var client: GarminSimulatorClient
|
||||
private val receiver: GarminReceiver = mock() {
|
||||
on { onConnectDevice(any(), any(), any()) }.doAnswer { i ->
|
||||
device = GarminDevice(client, i.getArgument(1), i.getArgument(2))
|
||||
app = GarminApplication(
|
||||
client, device!!, client.iqApp.applicationID, client.iqApp.displayName)
|
||||
}
|
||||
}
|
||||
private val receiver: GarminReceiver = mock()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
client = GarminSimulatorClient(aapsLogger, receiver, 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retrieveApplicationInfo() {
|
||||
assertTrue(client.awaitReady(Duration.ofSeconds(10)))
|
||||
val port = client.port
|
||||
val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1))
|
||||
Socket(ip, port).use { socket ->
|
||||
assertTrue(socket.isConnected)
|
||||
verify(receiver).onConnect(client)
|
||||
verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any())
|
||||
client.retrieveApplicationInfo(app!!.device, app!!.id, app!!.name!!)
|
||||
}
|
||||
verify(receiver).onApplicationInfo(app!!.device, app!!.id, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun receiveMessage() {
|
||||
val payload = "foo".toByteArray()
|
||||
|
@ -60,11 +35,11 @@ class GarminSimulatorClientTest: TestBase() {
|
|||
socket.getOutputStream().write(payload)
|
||||
socket.getOutputStream().flush()
|
||||
verify(receiver).onConnect(client)
|
||||
verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any())
|
||||
}
|
||||
verify(receiver, timeout(1_000)).onReceiveMessage(
|
||||
eq(client), eq(device!!.id), eq("SimApp"),
|
||||
argThat { p -> payload.contentEquals(p) })
|
||||
assertEquals(1, client.connectedDevices.size)
|
||||
val device: GarminDevice = client.connectedDevices.first()
|
||||
verify(receiver, timeout(1_000))
|
||||
.onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -73,14 +48,16 @@ class GarminSimulatorClientTest: TestBase() {
|
|||
assertTrue(client.awaitReady(Duration.ofSeconds(10)))
|
||||
val port = client.port
|
||||
val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1))
|
||||
val device: GarminDevice
|
||||
val app: GarminApplication
|
||||
Socket(ip, port).use { socket ->
|
||||
assertTrue(socket.isConnected)
|
||||
verify(receiver).onConnect(client)
|
||||
verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any())
|
||||
assertNotNull(device)
|
||||
assertNotNull(app)
|
||||
client.sendMessage(app!!, payload)
|
||||
assertEquals(1, client.connectedDevices.size)
|
||||
device = client.connectedDevices.first()
|
||||
app = GarminApplication(device, "SIMAPP", "T")
|
||||
client.sendMessage(app, payload)
|
||||
}
|
||||
verify(receiver).onSendMessage(eq(client), any(), eq(app!!.id), isNull())
|
||||
verify(receiver, timeout(1_000)).onSendMessage(eq(client), eq(device.id), eq(app.id), isNull())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue