diff --git a/plugins/sync/src/main/AndroidManifest.xml b/plugins/sync/src/main/AndroidManifest.xml index a4afffcc49..7a683c85be 100644 --- a/plugins/sync/src/main/AndroidManifest.xml +++ b/plugins/sync/src/main/AndroidManifest.xml @@ -8,6 +8,10 @@ android:name=".nsclient.services.NSClientService" android:enabled="true" android:exported="false" /> + dataPair.value.interfaceIDs.nightscoutId is DataSyncSelector.PairEffectiveProfileSwitch -> dataPair.value.interfaceIDs.nightscoutId is DataSyncSelector.PairOfflineEvent -> dataPair.value.interfaceIDs.nightscoutId - else -> throw IllegalStateException() + else -> error("Unsupported type") } when (dataPair) { is DataSyncSelector.PairBolus -> dataPair.value.toJson(false, dateUtil) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt index 7aa3e013c1..c05558ec8c 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/services/NSClientService.kt @@ -68,7 +68,8 @@ import java.net.URISyntaxException import java.util.* import javax.inject.Inject -@Suppress("SpellCheckingInspection") class NSClientService : DaggerService() { +@Suppress("SpellCheckingInspection") +class NSClientService : DaggerService() { @Inject lateinit var injector: HasAndroidInjector @Inject lateinit var aapsLogger: AAPSLogger diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 66432b7e78..a9543a679c 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -1,8 +1,12 @@ package app.aaps.plugins.sync.nsclientV3 +import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.os.Handler import android.os.HandlerThread +import android.os.IBinder import android.os.SystemClock import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen @@ -17,7 +21,6 @@ import app.aaps.core.interfaces.configuration.Constants import app.aaps.core.interfaces.db.PersistenceLayer import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag -import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.nsclient.NSAlarm import app.aaps.core.interfaces.nsclient.StoreDataForDb import app.aaps.core.interfaces.plugin.PluginBase @@ -29,7 +32,6 @@ import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAppExit import app.aaps.core.interfaces.rx.events.EventDeviceStatusChange -import app.aaps.core.interfaces.rx.events.EventDismissNotification import app.aaps.core.interfaces.rx.events.EventNSClientNewLog import app.aaps.core.interfaces.rx.events.EventNewHistoryData import app.aaps.core.interfaces.rx.events.EventOfflineChange @@ -44,29 +46,21 @@ import app.aaps.core.interfaces.source.NSClientSource import app.aaps.core.interfaces.sync.DataSyncSelector import app.aaps.core.interfaces.sync.NsClient import app.aaps.core.interfaces.sync.Sync -import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.DecimalFormatter import app.aaps.core.interfaces.utils.T import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.nssdk.NSAndroidClientImpl import app.aaps.core.nssdk.interfaces.NSAndroidClient -import app.aaps.core.nssdk.mapper.toNSDeviceStatus -import app.aaps.core.nssdk.mapper.toNSFood -import app.aaps.core.nssdk.mapper.toNSSgvV3 -import app.aaps.core.nssdk.mapper.toNSTreatment import app.aaps.core.nssdk.remotemodel.LastModified import app.aaps.database.ValueWrapper import app.aaps.database.entities.interfaces.TraceableDBEntry import app.aaps.plugins.sync.R -import app.aaps.plugins.sync.nsShared.NSAlarmObject import app.aaps.plugins.sync.nsShared.NSClientFragment -import app.aaps.plugins.sync.nsShared.NsIncomingDataProcessor import app.aaps.plugins.sync.nsShared.events.EventConnectivityOptionChanged import app.aaps.plugins.sync.nsShared.events.EventNSClientUpdateGuiData import app.aaps.plugins.sync.nsShared.events.EventNSClientUpdateGuiStatus import app.aaps.plugins.sync.nsclient.ReceiverDelegate -import app.aaps.plugins.sync.nsclient.data.NSDeviceStatusHandler import app.aaps.plugins.sync.nsclientV3.extensions.toNSBolus import app.aaps.plugins.sync.nsclientV3.extensions.toNSBolusWizard import app.aaps.plugins.sync.nsclientV3.extensions.toNSCarbs @@ -80,6 +74,7 @@ import app.aaps.plugins.sync.nsclientV3.extensions.toNSSvgV3 import app.aaps.plugins.sync.nsclientV3.extensions.toNSTemporaryBasal import app.aaps.plugins.sync.nsclientV3.extensions.toNSTemporaryTarget import app.aaps.plugins.sync.nsclientV3.extensions.toNSTherapyEvent +import app.aaps.plugins.sync.nsclientV3.services.NSClientV3Service import app.aaps.plugins.sync.nsclientV3.workers.DataSyncWorker import app.aaps.plugins.sync.nsclientV3.workers.LoadBgWorker import app.aaps.plugins.sync.nsclientV3.workers.LoadDeviceStatusWorker @@ -93,19 +88,11 @@ import com.google.gson.GsonBuilder import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import io.socket.client.Ack -import io.socket.client.IO -import io.socket.client.Socket -import io.socket.emitter.Emitter import kotlinx.serialization.json.Json -import org.json.JSONArray -import org.json.JSONObject -import java.net.URISyntaxException import java.security.InvalidParameterException import javax.inject.Inject import javax.inject.Singleton -@Suppress("SpellCheckingInspection") @OpenForTesting @Singleton class NSClientV3Plugin @Inject constructor( @@ -120,12 +107,9 @@ class NSClientV3Plugin @Inject constructor( private val receiverDelegate: ReceiverDelegate, private val config: Config, private val dateUtil: DateUtil, - private val uiInteraction: UiInteraction, private val dataSyncSelectorV3: DataSyncSelectorV3, private val persistenceLayer: PersistenceLayer, - private val nsDeviceStatusHandler: NSDeviceStatusHandler, private val nsClientSource: NSClientSource, - private val nsIncomingDataProcessor: NsIncomingDataProcessor, private val storeDataForDb: StoreDataForDb, private val decimalFormatter: DecimalFormatter ) : NsClient, Sync, PluginBase( @@ -156,28 +140,43 @@ class NSClientV3Plugin @Inject constructor( override val status get() = when { - sp.getBoolean(R.string.key_ns_paused, false) -> rh.gs(app.aaps.core.ui.R.string.paused) - isAllowed.not() -> blockingReason - sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true) && wsConnected -> "WS: " + rh.gs(app.aaps.core.interfaces.R.string.connected) - sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true) && !wsConnected -> "WS: " + rh.gs(R.string.not_connected) - lastOperationError != null -> rh.gs(app.aaps.core.ui.R.string.error) - nsAndroidClient?.lastStatus == null -> rh.gs(R.string.not_connected) - workIsRunning() -> rh.gs(R.string.working) - nsAndroidClient?.lastStatus?.apiPermissions?.isFull() == true -> rh.gs(app.aaps.core.interfaces.R.string.connected) - nsAndroidClient?.lastStatus?.apiPermissions?.isRead() == true -> rh.gs(R.string.read_only) - else -> rh.gs(app.aaps.core.ui.R.string.unknown) + sp.getBoolean(R.string.key_ns_paused, false) -> rh.gs(app.aaps.core.ui.R.string.paused) + isAllowed.not() -> blockingReason + sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true) && nsClientV3Service?.wsConnected == true -> "WS: " + rh.gs(app.aaps.core.interfaces.R.string.connected) + sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true) && nsClientV3Service?.wsConnected == false -> "WS: " + rh.gs(R.string.not_connected) + lastOperationError != null -> rh.gs(app.aaps.core.ui.R.string.error) + nsAndroidClient?.lastStatus == null -> rh.gs(R.string.not_connected) + workIsRunning() -> rh.gs(R.string.working) + nsAndroidClient?.lastStatus?.apiPermissions?.isFull() == true -> rh.gs(app.aaps.core.interfaces.R.string.connected) + nsAndroidClient?.lastStatus?.apiPermissions?.isRead() == true -> rh.gs(R.string.read_only) + else -> rh.gs(app.aaps.core.ui.R.string.unknown) } var lastOperationError: String? = null internal var nsAndroidClient: NSAndroidClient? = null + internal var nsClientV3Service: NSClientV3Service? = null - private val isAllowed get() = receiverDelegate.allowed - private val blockingReason get() = receiverDelegate.blockingReason + internal val isAllowed get() = receiverDelegate.allowed + internal val blockingReason get() = receiverDelegate.blockingReason val maxAge = T.days(100).msecs() internal var newestDataOnServer: LastModified? = null // timestamp of last modification for every collection provided by server internal var lastLoadedSrvModified = LastModified(LastModified.Collections()) // max srvLastModified timestamp of last fetched data for every collection internal var firstLoadContinueTimestamp = LastModified(LastModified.Collections()) // timestamp of last fetched data for every collection during initial load + internal var initialLoadFinished = false + + private val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName) { + aapsLogger.debug(LTag.NSCLIENT, "Service is disconnected") + nsClientV3Service = null + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + aapsLogger.debug(LTag.NSCLIENT, "Service is connected") + val localBinder = service as NSClientV3Service.LocalBinder + nsClientV3Service = localBinder.serviceInstance + } + } override fun onStart() { super.onStart() @@ -192,15 +191,20 @@ class NSClientV3Plugin @Inject constructor( setClient("START") receiverDelegate.grabReceiversState() + disposable += rxBus + .toObservable(EventAppExit::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ if (nsClientV3Service != null) context.unbindService(serviceConnection) }, fabricPrivacy::logException) disposable += rxBus .toObservable(EventConnectivityOptionChanged::class.java) .observeOn(aapsSchedulers.io) .subscribe({ ev -> rxBus.send(EventNSClientNewLog("● CONNECTIVITY", ev.blockingReason)) + assert(nsClientV3Service != null) if (ev.connected) { when { - isAllowed && storageSocket == null -> setClient("CONNECTIVITY") // socket must be created - !isAllowed && storageSocket != null -> shutdownWebsockets() + isAllowed && nsClientV3Service?.storageSocket == null -> setClient("CONNECTIVITY") // socket must be created + !isAllowed && nsClientV3Service?.storageSocket != null -> shutdownWebsockets() } if (isAllowed) executeLoop("CONNECTIVITY", forceNew = false) } @@ -348,267 +352,22 @@ class NSClientV3Plugin @Inject constructor( rxBus.send(EventSWSyncStatus(status)) } - /********************** - WS code - **********************/ - private var storageSocket: Socket? = null - private var alarmSocket: Socket? = null - internal var wsConnected = false - internal var initialLoadFinished = false + private fun initializeWebSockets(reason: String) { + if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true)) { + context.bindService(Intent(context, NSClientV3Service::class.java), serviceConnection, Context.BIND_AUTO_CREATE) + while (nsClientV3Service == null) { + aapsLogger.debug(LTag.NSCLIENT, "Waiting for service start") + SystemClock.sleep(100) + } + nsClientV3Service?.initializeWebSockets(reason) + } + } private fun shutdownWebsockets() { - storageSocket?.on(Socket.EVENT_CONNECT, onConnectStorage) - storageSocket?.on(Socket.EVENT_DISCONNECT, onDisconnectStorage) - storageSocket?.on("create", onDataCreateUpdate) - storageSocket?.on("update", onDataCreateUpdate) - storageSocket?.on("delete", onDataDelete) - storageSocket?.disconnect() - alarmSocket?.on(Socket.EVENT_CONNECT, onConnectAlarms) - alarmSocket?.on(Socket.EVENT_DISCONNECT, onDisconnectAlarm) - alarmSocket?.on("announcement", onAnnouncement) - alarmSocket?.on("alarm", onAlarm) - alarmSocket?.on("urgent_alarm", onUrgentAlarm) - alarmSocket?.on("clear_alarm", onClearAlarm) - alarmSocket?.disconnect() - wsConnected = false - storageSocket = null - alarmSocket = null + if (nsClientV3Service != null) context.unbindService(serviceConnection) + nsClientV3Service?.shutdownWebsockets() } - private fun initializeWebSockets(reason: String) { - if (!sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true)) return - if (sp.getString(app.aaps.core.utils.R.string.key_nsclientinternal_url, "").isEmpty()) return - val urlStorage = sp.getString(app.aaps.core.utils.R.string.key_nsclientinternal_url, "").lowercase().replace(Regex("/$"), "") + "/storage" - val urlAlarm = sp.getString(app.aaps.core.utils.R.string.key_nsclientinternal_url, "").lowercase().replace(Regex("/$"), "") + "/alarm" - if (!isAllowed) { - rxBus.send(EventNSClientNewLog("● WS", blockingReason)) - } else if (sp.getBoolean(R.string.key_ns_paused, false)) { - rxBus.send(EventNSClientNewLog("● WS", "paused")) - } else { - try { - // java io.client doesn't support multiplexing. create 2 sockets - storageSocket = IO.socket(urlStorage).also { socket -> - socket.on(Socket.EVENT_CONNECT, onConnectStorage) - socket.on(Socket.EVENT_DISCONNECT, onDisconnectStorage) - rxBus.send(EventNSClientNewLog("► WS", "do connect storage $reason")) - socket.connect() - socket.on("create", onDataCreateUpdate) - socket.on("update", onDataCreateUpdate) - socket.on("delete", onDataDelete) - } - if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_announcements, config.NSCLIENT) || - sp.getBoolean(app.aaps.core.utils.R.string.key_ns_alarms, config.NSCLIENT) - ) - alarmSocket = IO.socket(urlAlarm).also { socket -> - socket.on(Socket.EVENT_CONNECT, onConnectAlarms) - socket.on(Socket.EVENT_DISCONNECT, onDisconnectAlarm) - rxBus.send(EventNSClientNewLog("► WS", "do connect alarm $reason")) - socket.connect() - socket.on("announcement", onAnnouncement) - socket.on("alarm", onAlarm) - socket.on("urgent_alarm", onUrgentAlarm) - socket.on("clear_alarm", onClearAlarm) - } - } catch (e: URISyntaxException) { - rxBus.send(EventNSClientNewLog("● WS", "Wrong URL syntax")) - } catch (e: RuntimeException) { - rxBus.send(EventNSClientNewLog("● WS", "RuntimeException")) - } - } - } - - private val onConnectStorage = Emitter.Listener { - val socketId = storageSocket?.id() ?: "NULL" - rxBus.send(EventNSClientNewLog("◄ WS", "connected storage ID: $socketId")) - if (storageSocket != null) { - val authMessage = JSONObject().also { - it.put("accessToken", sp.getString(R.string.key_ns_client_token, "")) - it.put("collections", JSONArray(arrayOf("devicestatus", "entries", "profile", "treatments", "foods", "settings"))) - } - rxBus.send(EventNSClientNewLog("► WS", "requesting auth for storage")) - storageSocket?.emit("subscribe", authMessage, Ack { args -> - val response = args[0] as JSONObject - wsConnected = if (response.optBoolean("success")) { - rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}")) - // during disconnection updated data is not received - // thus run non WS load to get missing data - executeLoop("WS_CONNECT", forceNew = false) - true - } else { - rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed")) - false - } - rxBus.send(EventNSClientUpdateGuiStatus()) - }) - } - } - - private val onConnectAlarms = Emitter.Listener { - val socket = alarmSocket - val socketId = socket?.id() ?: "NULL" - rxBus.send(EventNSClientNewLog("◄ WS", "connected alarms ID: $socketId")) - if (socket != null) { - val authMessage = JSONObject().also { - it.put("accessToken", sp.getString(R.string.key_ns_client_token, "")) - } - rxBus.send(EventNSClientNewLog("► WS", "requesting auth for alarms")) - socket.emit("subscribe", authMessage, Ack { args -> - val response = args[0] as JSONObject - if (response.optBoolean("success")) rxBus.send(EventNSClientNewLog("◄ WS", response.optString("message"))) - else rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed")) - }) - } - } - - private val onDisconnectStorage = Emitter.Listener { args -> - aapsLogger.debug(LTag.NSCLIENT, "disconnect storage reason: ${args[0]}") - rxBus.send(EventNSClientNewLog("◄ WS", "disconnect storage event")) - wsConnected = false - initialLoadFinished = false - rxBus.send(EventNSClientUpdateGuiStatus()) - } - - private val onDisconnectAlarm = Emitter.Listener { args -> - aapsLogger.debug(LTag.NSCLIENT, "disconnect alarm reason: ${args[0]}") - rxBus.send(EventNSClientNewLog("◄ WS", "disconnect alarm event")) - } - - private val onDataCreateUpdate = Emitter.Listener { args -> - val response = args[0] as JSONObject - aapsLogger.debug(LTag.NSCLIENT, "onDataCreateUpdate: $response") - val collection = response.getString("colName") - val docJson = response.getJSONObject("doc") - val docString = response.getString("doc") - rxBus.send(EventNSClientNewLog("◄ WS CREATE/UPDATE", "$collection $docString")) - val srvModified = docJson.getLong("srvModified") - lastLoadedSrvModified.set(collection, srvModified) - storeLastLoadedSrvModified() - when (collection) { - "devicestatus" -> docString.toNSDeviceStatus().let { nsDeviceStatusHandler.handleNewData(arrayOf(it)) } - "entries" -> docString.toNSSgvV3()?.let { - nsIncomingDataProcessor.processSgvs(listOf(it)) - storeDataForDb.storeGlucoseValuesToDb() - } - - "profile" -> - nsIncomingDataProcessor.processProfile(docJson) - - "treatments" -> docString.toNSTreatment()?.let { - nsIncomingDataProcessor.processTreatments(listOf(it)) - storeDataForDb.storeTreatmentsToDb() - } - - "foods" -> docString.toNSFood()?.let { - nsIncomingDataProcessor.processFood(listOf(it)) - storeDataForDb.storeFoodsToDb() - } - - "settings" -> {} - } - } - - private val onDataDelete = Emitter.Listener { args -> - val response = args[0] as JSONObject - aapsLogger.debug(LTag.NSCLIENT, "onDataDelete: $response") - val collection = response.optString("colName") ?: return@Listener - val identifier = response.optString("identifier") ?: return@Listener - rxBus.send(EventNSClientNewLog("◄ WS DELETE", "$collection $identifier")) - if (collection == "treatments") { - storeDataForDb.deleteTreatment.add(identifier) - storeDataForDb.updateDeletedTreatmentsInDb() - } - if (collection == "entries") { - storeDataForDb.deleteGlucoseValue.add(identifier) - storeDataForDb.updateDeletedGlucoseValuesInDb() - } - } - - private val onAnnouncement = Emitter.Listener { args -> - - /* - { - "level":0, - "title":"Announcement", - "message":"test", - "plugin":{"name":"treatmentnotify","label":"Treatment Notifications","pluginType":"notification","enabled":true}, - "group":"Announcement", - "isAnnouncement":true, - "key":"9ac46ad9a1dcda79dd87dae418fce0e7955c68da" - } - */ - val data = args[0] as JSONObject - rxBus.send(EventNSClientNewLog("◄ ANNOUNCEMENT", data.optString("message"))) - aapsLogger.debug(LTag.NSCLIENT, data.toString()) - if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_announcements, config.NSCLIENT)) - uiInteraction.addNotificationWithAction(injector, NSAlarmObject(data)) - } - private val onAlarm = Emitter.Listener { args -> - - /* - { - "level":1, - "title":"Warning HIGH", - "message":"BG Now: 5 -0.2 → mmol\/L\nRaw BG: 4.8 mmol\/L Čistý\nBG 15m: 4.8 mmol\/L\nIOB: -0.02U\nCOB: 0g", - "eventName":"high", - "plugin":{"name":"simplealarms","label":"Simple Alarms","pluginType":"notification","enabled":true}, - "pushoverSound":"climb", - "debug":{"lastSGV":5,"thresholds":{"bgHigh":180,"bgTargetTop":75,"bgTargetBottom":72,"bgLow":70}}, - "group":"default", - "key":"simplealarms_1" - } - */ - val data = args[0] as JSONObject - rxBus.send(EventNSClientNewLog("◄ ALARM", data.optString("message"))) - aapsLogger.debug(LTag.NSCLIENT, data.toString()) - if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_alarms, config.NSCLIENT)) { - val snoozedTo = sp.getLong(rh.gs(app.aaps.core.utils.R.string.key_snoozed_to) + data.optString("level"), 0L) - if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) - uiInteraction.addNotificationWithAction(injector, NSAlarmObject(data)) - } - } - - private val onUrgentAlarm = Emitter.Listener { args: Array -> - val data = args[0] as JSONObject - rxBus.send(EventNSClientNewLog("◄ URGENT ALARM", data.optString("message"))) - aapsLogger.debug(LTag.NSCLIENT, data.toString()) - if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_alarms, config.NSCLIENT)) { - val snoozedTo = sp.getLong(rh.gs(app.aaps.core.utils.R.string.key_snoozed_to) + data.optString("level"), 0L) - if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) - uiInteraction.addNotificationWithAction(injector, NSAlarmObject(data)) - } - } - - private val onClearAlarm = Emitter.Listener { args -> - - /* - { - "clear":true, - "title":"All Clear", - "message":"default - Urgent was ack'd", - "group":"default" - } - */ - val data = args[0] as JSONObject - rxBus.send(EventNSClientNewLog("◄ CLEARALARM", data.optString("title"))) - aapsLogger.debug(LTag.NSCLIENT, data.toString()) - rxBus.send(EventDismissNotification(Notification.NS_ALARM)) - rxBus.send(EventDismissNotification(Notification.NS_URGENT_ALARM)) - } - - override fun handleClearAlarm(originalAlarm: NSAlarm, silenceTimeInMilliseconds: Long) { - if (!isEnabled()) return - if (!sp.getBoolean(R.string.key_ns_upload, true)) { - aapsLogger.debug(LTag.NSCLIENT, "Upload disabled. Message dropped") - return - } - alarmSocket?.emit("ack", originalAlarm.level(), originalAlarm.group(), silenceTimeInMilliseconds) - rxBus.send(EventNSClientNewLog("► ALARMACK ", "${originalAlarm.level()} ${originalAlarm.group()} $silenceTimeInMilliseconds")) - } - - /********************** - WS code end - **********************/ - override fun resend(reason: String) { // If WS is enabled, download is triggered by changes in NS. Thus uploadOnly // Exception is after reset to full sync (initialLoadFinished == false), where @@ -652,6 +411,15 @@ class NSClientV3Plugin @Inject constructor( dataSyncSelectorV3.resetToNextFullSync() } + override fun handleClearAlarm(originalAlarm: NSAlarm, silenceTimeInMilliseconds: Long) { + if (!isEnabled()) return + if (!sp.getBoolean(R.string.key_ns_upload, true)) { + aapsLogger.debug(LTag.NSCLIENT, "Upload disabled. Message dropped") + return + } + nsClientV3Service?.handleClearAlarm(originalAlarm, silenceTimeInMilliseconds) + } + override suspend fun nsAdd(collection: String, dataPair: DataSyncSelector.DataPair, progress: String, profile: Profile?): Boolean = dbOperation(collection, dataPair, progress, Operation.CREATE, profile) @@ -714,6 +482,7 @@ class NSClientV3Plugin @Inject constructor( } } catch (e: Exception) { aapsLogger.error(LTag.NSCLIENT, "Upload exception", e) + return false } return true } @@ -944,7 +713,7 @@ class NSClientV3Plugin @Inject constructor( sp.putString(R.string.key_ns_client_v3_last_modified, Json.encodeToString(LastModified.serializer(), lastLoadedSrvModified)) } - private fun executeLoop(origin: String, forceNew: Boolean) { + internal fun executeLoop(origin: String, forceNew: Boolean) { if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_use_ws, true) && initialLoadFinished) return if (sp.getBoolean(R.string.key_ns_paused, false)) { rxBus.send(EventNSClientNewLog("● RUN", "paused $origin")) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt new file mode 100644 index 0000000000..1cd66a9783 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -0,0 +1,337 @@ +package app.aaps.plugins.sync.nsclientV3.services + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.os.Binder +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import android.os.PowerManager +import app.aaps.core.interfaces.configuration.Config +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.notifications.Notification +import app.aaps.core.interfaces.nsclient.NSAlarm +import app.aaps.core.interfaces.nsclient.StoreDataForDb +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.* +import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.interfaces.ui.UiInteraction +import app.aaps.core.interfaces.utils.fabric.FabricPrivacy +import app.aaps.core.nssdk.mapper.toNSDeviceStatus +import app.aaps.core.nssdk.mapper.toNSFood +import app.aaps.core.nssdk.mapper.toNSSgvV3 +import app.aaps.core.nssdk.mapper.toNSTreatment +import app.aaps.plugins.sync.R +import app.aaps.plugins.sync.nsShared.NSAlarmObject +import app.aaps.plugins.sync.nsShared.NsIncomingDataProcessor +import app.aaps.plugins.sync.nsShared.events.EventNSClientUpdateGuiStatus +import app.aaps.plugins.sync.nsclient.data.NSDeviceStatusHandler +import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin +import dagger.android.DaggerService +import dagger.android.HasAndroidInjector +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.socket.client.Ack +import io.socket.client.IO +import io.socket.client.Socket +import io.socket.emitter.Emitter +import org.json.JSONArray +import org.json.JSONObject +import java.net.URISyntaxException +import java.util.* +import javax.inject.Inject + +@Suppress("SpellCheckingInspection") +class NSClientV3Service : DaggerService() { + + @Inject lateinit var injector: HasAndroidInjector + @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var rxBus: RxBus + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var sp: SP + @Inject lateinit var fabricPrivacy: FabricPrivacy + @Inject lateinit var nsClientV3Plugin: NSClientV3Plugin + @Inject lateinit var config: Config + @Inject lateinit var nsIncomingDataProcessor: NsIncomingDataProcessor + @Inject lateinit var storeDataForDb: StoreDataForDb + @Inject lateinit var uiInteraction: UiInteraction + @Inject lateinit var nsDeviceStatusHandler: NSDeviceStatusHandler + + private val disposable = CompositeDisposable() + + private var wakeLock: PowerManager.WakeLock? = null + private val binder: IBinder = LocalBinder() + private val handler = Handler(HandlerThread(this::class.simpleName + "Handler").also { it.start() }.looper) + + @SuppressLint("WakelockTimeout") + override fun onCreate() { + super.onCreate() + wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AndroidAPS:NSClientService") + wakeLock?.acquire() + } + + override fun onDestroy() { + super.onDestroy() + disposable.clear() + if (wakeLock?.isHeld == true) wakeLock?.release() + } + + inner class LocalBinder : Binder() { + + val serviceInstance: NSClientV3Service + get() = this@NSClientV3Service + } + + override fun onBind(intent: Intent): IBinder = binder + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int = START_STICKY + + internal var storageSocket: Socket? = null + private var alarmSocket: Socket? = null + internal var wsConnected = false + + internal fun shutdownWebsockets() { + storageSocket?.on(Socket.EVENT_CONNECT, onConnectStorage) + storageSocket?.on(Socket.EVENT_DISCONNECT, onDisconnectStorage) + storageSocket?.on("create", onDataCreateUpdate) + storageSocket?.on("update", onDataCreateUpdate) + storageSocket?.on("delete", onDataDelete) + storageSocket?.disconnect() + alarmSocket?.on(Socket.EVENT_CONNECT, onConnectAlarms) + alarmSocket?.on(Socket.EVENT_DISCONNECT, onDisconnectAlarm) + alarmSocket?.on("announcement", onAnnouncement) + alarmSocket?.on("alarm", onAlarm) + alarmSocket?.on("urgent_alarm", onUrgentAlarm) + alarmSocket?.on("clear_alarm", onClearAlarm) + alarmSocket?.disconnect() + wsConnected = false + storageSocket = null + alarmSocket = null + } + + internal fun initializeWebSockets(reason: String) { + if (sp.getString(app.aaps.core.utils.R.string.key_nsclientinternal_url, "").isEmpty()) return + val urlStorage = sp.getString(app.aaps.core.utils.R.string.key_nsclientinternal_url, "").lowercase().replace(Regex("/$"), "") + "/storage" + val urlAlarm = sp.getString(app.aaps.core.utils.R.string.key_nsclientinternal_url, "").lowercase().replace(Regex("/$"), "") + "/alarm" + if (!nsClientV3Plugin.isAllowed) { + rxBus.send(EventNSClientNewLog("● WS", nsClientV3Plugin.blockingReason)) + } else if (sp.getBoolean(R.string.key_ns_paused, false)) { + rxBus.send(EventNSClientNewLog("● WS", "paused")) + } else { + try { + // java io.client doesn't support multiplexing. create 2 sockets + storageSocket = IO.socket(urlStorage).also { socket -> + socket.on(Socket.EVENT_CONNECT, onConnectStorage) + socket.on(Socket.EVENT_DISCONNECT, onDisconnectStorage) + rxBus.send(EventNSClientNewLog("► WS", "do connect storage $reason")) + socket.connect() + socket.on("create", onDataCreateUpdate) + socket.on("update", onDataCreateUpdate) + socket.on("delete", onDataDelete) + } + if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_announcements, config.NSCLIENT) || + sp.getBoolean(app.aaps.core.utils.R.string.key_ns_alarms, config.NSCLIENT) + ) + alarmSocket = IO.socket(urlAlarm).also { socket -> + socket.on(Socket.EVENT_CONNECT, onConnectAlarms) + socket.on(Socket.EVENT_DISCONNECT, onDisconnectAlarm) + rxBus.send(EventNSClientNewLog("► WS", "do connect alarm $reason")) + socket.connect() + socket.on("announcement", onAnnouncement) + socket.on("alarm", onAlarm) + socket.on("urgent_alarm", onUrgentAlarm) + socket.on("clear_alarm", onClearAlarm) + } + } catch (e: URISyntaxException) { + rxBus.send(EventNSClientNewLog("● WS", "Wrong URL syntax")) + } catch (e: RuntimeException) { + rxBus.send(EventNSClientNewLog("● WS", "RuntimeException")) + } + } + } + + private val onConnectStorage = Emitter.Listener { + val socketId = storageSocket?.id() ?: "NULL" + rxBus.send(EventNSClientNewLog("◄ WS", "connected storage ID: $socketId")) + if (storageSocket != null) { + val authMessage = JSONObject().also { + it.put("accessToken", sp.getString(R.string.key_ns_client_token, "")) + it.put("collections", JSONArray(arrayOf("devicestatus", "entries", "profile", "treatments", "foods", "settings"))) + } + rxBus.send(EventNSClientNewLog("► WS", "requesting auth for storage")) + storageSocket?.emit("subscribe", authMessage, Ack { args -> + val response = args[0] as JSONObject + wsConnected = if (response.optBoolean("success")) { + rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}")) + // during disconnection updated data is not received + // thus run non WS load to get missing data + nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = false) + true + } else { + rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed")) + false + } + rxBus.send(EventNSClientUpdateGuiStatus()) + }) + } + } + + private val onConnectAlarms = Emitter.Listener { + val socket = alarmSocket + val socketId = socket?.id() ?: "NULL" + rxBus.send(EventNSClientNewLog("◄ WS", "connected alarms ID: $socketId")) + if (socket != null) { + val authMessage = JSONObject().also { + it.put("accessToken", sp.getString(R.string.key_ns_client_token, "")) + } + rxBus.send(EventNSClientNewLog("► WS", "requesting auth for alarms")) + socket.emit("subscribe", authMessage, Ack { args -> + val response = args[0] as JSONObject + if (response.optBoolean("success")) rxBus.send(EventNSClientNewLog("◄ WS", response.optString("message"))) + else rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed")) + }) + } + } + + private val onDisconnectStorage = Emitter.Listener { args -> + aapsLogger.debug(LTag.NSCLIENT, "disconnect storage reason: ${args[0]}") + rxBus.send(EventNSClientNewLog("◄ WS", "disconnect storage event")) + wsConnected = false + nsClientV3Plugin.initialLoadFinished = false + rxBus.send(EventNSClientUpdateGuiStatus()) + } + + private val onDisconnectAlarm = Emitter.Listener { args -> + aapsLogger.debug(LTag.NSCLIENT, "disconnect alarm reason: ${args[0]}") + rxBus.send(EventNSClientNewLog("◄ WS", "disconnect alarm event")) + } + + private val onDataCreateUpdate = Emitter.Listener { args -> + val response = args[0] as JSONObject + aapsLogger.debug(LTag.NSCLIENT, "onDataCreateUpdate: $response") + val collection = response.getString("colName") + val docJson = response.getJSONObject("doc") + val docString = response.getString("doc") + rxBus.send(EventNSClientNewLog("◄ WS CREATE/UPDATE", "$collection $docString")) + val srvModified = docJson.getLong("srvModified") + nsClientV3Plugin.lastLoadedSrvModified.set(collection, srvModified) + nsClientV3Plugin.storeLastLoadedSrvModified() + when (collection) { + "devicestatus" -> docString.toNSDeviceStatus().let { nsDeviceStatusHandler.handleNewData(arrayOf(it)) } + "entries" -> docString.toNSSgvV3()?.let { + nsIncomingDataProcessor.processSgvs(listOf(it)) + storeDataForDb.storeGlucoseValuesToDb() + } + + "profile" -> + nsIncomingDataProcessor.processProfile(docJson) + + "treatments" -> docString.toNSTreatment()?.let { + nsIncomingDataProcessor.processTreatments(listOf(it)) + storeDataForDb.storeTreatmentsToDb() + } + + "foods" -> docString.toNSFood()?.let { + nsIncomingDataProcessor.processFood(listOf(it)) + storeDataForDb.storeFoodsToDb() + } + + "settings" -> {} + } + } + + private val onDataDelete = Emitter.Listener { args -> + val response = args[0] as JSONObject + aapsLogger.debug(LTag.NSCLIENT, "onDataDelete: $response") + val collection = response.optString("colName") ?: return@Listener + val identifier = response.optString("identifier") ?: return@Listener + rxBus.send(EventNSClientNewLog("◄ WS DELETE", "$collection $identifier")) + if (collection == "treatments") { + storeDataForDb.deleteTreatment.add(identifier) + storeDataForDb.updateDeletedTreatmentsInDb() + } + if (collection == "entries") { + storeDataForDb.deleteGlucoseValue.add(identifier) + storeDataForDb.updateDeletedGlucoseValuesInDb() + } + } + + private val onAnnouncement = Emitter.Listener { args -> + + /* + { + "level":0, + "title":"Announcement", + "message":"test", + "plugin":{"name":"treatmentnotify","label":"Treatment Notifications","pluginType":"notification","enabled":true}, + "group":"Announcement", + "isAnnouncement":true, + "key":"9ac46ad9a1dcda79dd87dae418fce0e7955c68da" + } + */ + val data = args[0] as JSONObject + rxBus.send(EventNSClientNewLog("◄ ANNOUNCEMENT", data.optString("message"))) + aapsLogger.debug(LTag.NSCLIENT, data.toString()) + if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_announcements, config.NSCLIENT)) + uiInteraction.addNotificationWithAction(injector, NSAlarmObject(data)) + } + private val onAlarm = Emitter.Listener { args -> + + /* + { + "level":1, + "title":"Warning HIGH", + "message":"BG Now: 5 -0.2 → mmol\/L\nRaw BG: 4.8 mmol\/L Čistý\nBG 15m: 4.8 mmol\/L\nIOB: -0.02U\nCOB: 0g", + "eventName":"high", + "plugin":{"name":"simplealarms","label":"Simple Alarms","pluginType":"notification","enabled":true}, + "pushoverSound":"climb", + "debug":{"lastSGV":5,"thresholds":{"bgHigh":180,"bgTargetTop":75,"bgTargetBottom":72,"bgLow":70}}, + "group":"default", + "key":"simplealarms_1" + } + */ + val data = args[0] as JSONObject + rxBus.send(EventNSClientNewLog("◄ ALARM", data.optString("message"))) + aapsLogger.debug(LTag.NSCLIENT, data.toString()) + if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_alarms, config.NSCLIENT)) { + val snoozedTo = sp.getLong(rh.gs(app.aaps.core.utils.R.string.key_snoozed_to) + data.optString("level"), 0L) + if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) + uiInteraction.addNotificationWithAction(injector, NSAlarmObject(data)) + } + } + + private val onUrgentAlarm = Emitter.Listener { args: Array -> + val data = args[0] as JSONObject + rxBus.send(EventNSClientNewLog("◄ URGENT ALARM", data.optString("message"))) + aapsLogger.debug(LTag.NSCLIENT, data.toString()) + if (sp.getBoolean(app.aaps.core.utils.R.string.key_ns_alarms, config.NSCLIENT)) { + val snoozedTo = sp.getLong(rh.gs(app.aaps.core.utils.R.string.key_snoozed_to) + data.optString("level"), 0L) + if (snoozedTo == 0L || System.currentTimeMillis() > snoozedTo) + uiInteraction.addNotificationWithAction(injector, NSAlarmObject(data)) + } + } + + private val onClearAlarm = Emitter.Listener { args -> + + /* + { + "clear":true, + "title":"All Clear", + "message":"default - Urgent was ack'd", + "group":"default" + } + */ + val data = args[0] as JSONObject + rxBus.send(EventNSClientNewLog("◄ CLEARALARM", data.optString("title"))) + aapsLogger.debug(LTag.NSCLIENT, data.toString()) + rxBus.send(EventDismissNotification(Notification.NS_ALARM)) + rxBus.send(EventDismissNotification(Notification.NS_URGENT_ALARM)) + } + + fun handleClearAlarm(originalAlarm: NSAlarm, silenceTimeInMilliseconds: Long) { + alarmSocket?.emit("ack", originalAlarm.level(), originalAlarm.group(), silenceTimeInMilliseconds) + rxBus.send(EventNSClientNewLog("► ALARMACK ", "${originalAlarm.level()} ${originalAlarm.group()} $silenceTimeInMilliseconds")) + } +} diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/DataSyncWorker.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/DataSyncWorker.kt index b2d6059be5..4384fab58c 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/DataSyncWorker.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/workers/DataSyncWorker.kt @@ -23,14 +23,14 @@ class DataSyncWorker( @Inject lateinit var nsClientV3Plugin: NSClientV3Plugin override suspend fun doWorkAndLog(): Result { - if (activePlugin.activeNsClient?.hasWritePermission == true || nsClientV3Plugin.wsConnected) { + if (activePlugin.activeNsClient?.hasWritePermission == true || nsClientV3Plugin.nsClientV3Service?.wsConnected == true) { rxBus.send(EventNSClientNewLog("► UPL", "Start")) dataSyncSelectorV3.doUpload() rxBus.send(EventNSClientNewLog("► UPL", "End")) } else { if (activePlugin.activeNsClient?.hasWritePermission == true) rxBus.send(EventNSClientNewLog("► ERROR", "No write permission")) - else if (nsClientV3Plugin.wsConnected) + else if (nsClientV3Plugin.nsClientV3Service?.wsConnected == true) rxBus.send(EventNSClientNewLog("► ERROR", "Not connected")) // refresh token nsClientV3Plugin.scheduleIrregularExecution(refreshToken = true)