diff --git a/README.MD b/README.MD index 4de1552..9c4ee19 100644 --- a/README.MD +++ b/README.MD @@ -117,6 +117,49 @@ with the `io.rebble.pebblekit2.RECEIVE_DATA_FROM_WATCH` intent filter: That's it. When your watchapp is opened on the watch, the listener service should be bounded and the start callback called. +### Receive data logs + +Data logging is the store-and-forward alternative to messages. The watchapp writes fixed-size items to +the watch storage with +[data_logging_log()](https://developer.rebble.io/guides/communication/datalogging/), also when the +phone is out of range. The watch sends the items when it is connected. + +To receive the logged data, override the data log callbacks of the `BasePebbleListenerService`: + +```kotlin +override suspend fun onDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: WatchIdentifier, +): ReceiveResult { + // data contains data.size / session.itemSize items, in the sequence the watchapp logged them + return ReceiveResult.Ack +} + +override suspend fun onDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: WatchIdentifier, +): ReceiveResult { + // the watchapp called data_logging_finish() and the watch sent all the data of the session + return ReceiveResult.Ack +} +``` + +The watchapp UUID, the `tag` from `data_logging_create()` and the `timestamp` of the session start +identify a session (see `DataLogSession`). Two recordings come as two different sessions. + +Return `ReceiveResult.Ack` only after you stored the data. The Pebble app can then discard it. Return +`ReceiveResult.Nack` if you could not store the data. The Pebble app can then try the delivery again +later. The Pebble app can send the same batch more than one time; store the items so that a repeated +batch does not add duplicate data. + +Data logs are different from messages: they are not connected to an open watchapp. The watch can send +stored data at all times, for example when it connects again after it was out of range. Android can +thus bind your service at all times. + ### Starting/stopping the app You can call `sender.startAppOnTheWatch()` and `sender.stopAppOnTheWatch()` to start/stop your app on the watch diff --git a/client-java/src/main/kotlin/io/rebble/pebblekit2/client/java/BaseJavaPebbleListenerService.kt b/client-java/src/main/kotlin/io/rebble/pebblekit2/client/java/BaseJavaPebbleListenerService.kt index 21e02dc..6352fd9 100644 --- a/client-java/src/main/kotlin/io/rebble/pebblekit2/client/java/BaseJavaPebbleListenerService.kt +++ b/client-java/src/main/kotlin/io/rebble/pebblekit2/client/java/BaseJavaPebbleListenerService.kt @@ -1,6 +1,7 @@ package io.rebble.pebblekit2.client.java import io.rebble.pebblekit2.client.BasePebbleListenerService +import io.rebble.pebblekit2.common.model.DataLogSession import io.rebble.pebblekit2.common.model.PebbleDictionary import io.rebble.pebblekit2.common.model.PebbleDictionaryItem import io.rebble.pebblekit2.common.model.ReceiveResult @@ -33,6 +34,44 @@ public abstract class BaseJavaPebbleListenerService : BasePebbleListenerService( return completableDeferred.await() } + final override suspend fun onDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: WatchIdentifier, + ): ReceiveResult { + val completableDeferred = CompletableDeferred() + + onDataLogReceived( + watchappUUID, + session, + data, + itemsLeft, + watch.value, + { completableDeferred.complete(it) }, + ) + + return completableDeferred.await() + } + + final override suspend fun onDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: WatchIdentifier, + ): ReceiveResult { + val completableDeferred = CompletableDeferred() + + onDataLogSessionFinished( + watchappUUID, + session, + watch.value, + { completableDeferred.complete(it) }, + ) + + return completableDeferred.await() + } + final override fun onAppOpened(watchappUUID: UUID, watch: WatchIdentifier) { onAppOpened(watchappUUID, watch.value) } @@ -63,6 +102,48 @@ public abstract class BaseJavaPebbleListenerService : BasePebbleListenerService( responder.accept(ReceiveResult.Nack) } + /** + * The watch sent a batch of items from a data logging [session] of one of the registered apps. + * + * [data] contains `data.size / session.itemSize` full items, in the sequence the watchapp + * logged them. [itemsLeft] is the number of items that stay on the watch after this batch. + * + * Passed [watch] parameter corresponds to the [WatchIdentifier.value]. + * + * You MUST call [responder] after you are done processing this callback. Use + * [ReceiveResult.Ack] only after you stored the data; the Pebble app can then discard it. Use + * [ReceiveResult.Nack] if you could not store the data; the Pebble app can then try the + * delivery again later. + */ + protected open fun onDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: String, + responder: Consumer, + ) { + responder.accept(ReceiveResult.Nack) + } + + /** + * A data logging [session] of one of the registered apps is complete. The watchapp called + * `data_logging_finish()`, and the Pebble app sends this event after you acknowledged all the + * batches of the session. + * + * Passed [watch] parameter corresponds to the [WatchIdentifier.value]. + * + * You MUST call [responder] after you are done processing this callback. + */ + protected open fun onDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: String, + responder: Consumer, + ) { + responder.accept(ReceiveResult.Nack) + } + /** * One of registered apps for this companion app has been opened on a watch * diff --git a/client/src/main/kotlin/io/rebble/pebblekit2/client/BasePebbleListenerService.kt b/client/src/main/kotlin/io/rebble/pebblekit2/client/BasePebbleListenerService.kt index 928e205..b514d3b 100644 --- a/client/src/main/kotlin/io/rebble/pebblekit2/client/BasePebbleListenerService.kt +++ b/client/src/main/kotlin/io/rebble/pebblekit2/client/BasePebbleListenerService.kt @@ -7,10 +7,12 @@ import android.os.IBinder import androidx.core.os.bundleOf import co.touchlab.kermit.Logger import io.rebble.pebblekit2.PebbleKitBundleKeys +import io.rebble.pebblekit2.common.model.DataLogSession import io.rebble.pebblekit2.common.model.PebbleDictionary import io.rebble.pebblekit2.common.model.PebbleDictionaryItem import io.rebble.pebblekit2.common.model.ReceiveResult import io.rebble.pebblekit2.common.model.WatchIdentifier +import io.rebble.pebblekit2.common.model.fromBundle import io.rebble.pebblekit2.common.model.mapFromBundle import io.rebble.pebblekit2.common.model.toBundle import io.rebble.pebblekit2.common.util.UniversalRequestResponseSuspending @@ -40,6 +42,47 @@ public abstract class BasePebbleListenerService : Service() { return ReceiveResult.Nack } + /** + * The watch sent a batch of items from a data logging [session] of one of the registered apps. + * + * Data logging is the store-and-forward alternative to messages. The watchapp writes items to + * the watch storage, also when the phone is out of range. The watch sends the items when it is + * connected. + * + * [data] contains `data.size / session.itemSize` full items, in the sequence the watchapp + * logged them. [itemsLeft] is the number of items that stay on the watch after this batch. + * + * Return [ReceiveResult.Ack] only after you stored the data. The Pebble app can then discard + * it. Return [ReceiveResult.Nack] if you could not store the data. The Pebble app can then try + * the delivery again later. The Pebble app can send the same batch more than one time; store + * the items so that a repeated batch does not add duplicate data. + */ + public open suspend fun onDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: WatchIdentifier, + ): ReceiveResult { + return ReceiveResult.Nack + } + + /** + * A data logging [session] of one of the registered apps is complete. The watchapp called + * `data_logging_finish()`, and the Pebble app sends this event after you acknowledged all the + * batches of the session. + * + * Return [ReceiveResult.Ack] after you processed the event. Return [ReceiveResult.Nack] if you + * could not process it. The Pebble app can then send the event again later. + */ + public open suspend fun onDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: WatchIdentifier, + ): ReceiveResult { + return ReceiveResult.Nack + } + /** * One of registered apps for this companion app has been opened on a watch */ @@ -83,6 +126,14 @@ public abstract class BasePebbleListenerService : Service() { handleReceiveData(data, callingPackage) } + PebbleKitBundleKeys.ACTION_DATA_LOG_RECEIVED -> { + handleDataLogReceived(data, callingPackage) + } + + PebbleKitBundleKeys.ACTION_DATA_LOG_SESSION_FINISHED -> { + handleDataLogSessionFinished(data, callingPackage) + } + PebbleKitBundleKeys.ACTION_APP_OPENED -> { handleAppOpened(data, callingPackage) } @@ -125,6 +176,82 @@ public abstract class BasePebbleListenerService : Service() { return bundleOf(PebbleKitBundleKeys.KEY_TRANSMISSION_RESULTS to result.toBundle()) } + private suspend fun handleDataLogReceived(input: Bundle, callingPackage: String?): Bundle { + val watchappUuid = input.getString(PebbleKitBundleKeys.KEY_WATCHAPP_UUID) + ?.let { UUID.fromString(it) } + if (watchappUuid == null) { + LOGGER.w { "Got a missing watchapp UUID from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + + val watchId = input.getString(PebbleKitBundleKeys.KEY_WATCH_ID) + ?.let { WatchIdentifier(it) } + if (watchId == null) { + LOGGER.w { "Got a missing watch ID from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + + val data = input.getByteArray(PebbleKitBundleKeys.KEY_DATA_LOG_DATA) + if (data == null) { + LOGGER.w { "Got missing data log data from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + + val session = validDataLogSession(input, data, callingPackage) ?: return Bundle() + + if (!input.containsKey(PebbleKitBundleKeys.KEY_DATA_LOG_ITEMS_LEFT)) { + LOGGER.w { "Got a missing data log items-left from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + val itemsLeft = input.getLong(PebbleKitBundleKeys.KEY_DATA_LOG_ITEMS_LEFT) + + val result = onDataLogReceived(watchappUuid, session, data, itemsLeft, watchId) + + return bundleOf(PebbleKitBundleKeys.KEY_RECEIVE_RESULT to result.toBundle()) + } + + private fun validDataLogSession(input: Bundle, data: ByteArray, callingPackage: String?): DataLogSession? { + val sessionBundle = input.getBundle(PebbleKitBundleKeys.KEY_DATA_LOG_SESSION) + if (sessionBundle == null) { + LOGGER.w { "Got a missing data log session from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return null + } + + val session = DataLogSession.fromBundle(sessionBundle) + if (session.itemSize <= 0 || data.size % session.itemSize != 0) { + LOGGER.w { "Got an invalid data log batch from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return null + } + + return session + } + + private suspend fun handleDataLogSessionFinished(input: Bundle, callingPackage: String?): Bundle { + val watchappUuid = input.getString(PebbleKitBundleKeys.KEY_WATCHAPP_UUID) + ?.let { UUID.fromString(it) } + if (watchappUuid == null) { + LOGGER.w { "Got a missing watchapp UUID from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + + val watchId = input.getString(PebbleKitBundleKeys.KEY_WATCH_ID) + ?.let { WatchIdentifier(it) } + if (watchId == null) { + LOGGER.w { "Got a missing watch ID from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + + val sessionBundle = input.getBundle(PebbleKitBundleKeys.KEY_DATA_LOG_SESSION) + if (sessionBundle == null) { + LOGGER.w { "Got a missing data log session from ${callingPackage ?: "UNKNOWN"}. Ignoring event..." } + return Bundle() + } + + val result = onDataLogSessionFinished(watchappUuid, DataLogSession.fromBundle(sessionBundle), watchId) + + return bundleOf(PebbleKitBundleKeys.KEY_RECEIVE_RESULT to result.toBundle()) + } + private fun handleAppOpened(input: Bundle, callingPackage: String?): Bundle { val watchappUuid = input.getString(PebbleKitBundleKeys.KEY_WATCHAPP_UUID) ?.let { UUID.fromString(it) } diff --git a/common-api/src/main/kotlin/io/rebble/pebblekit2/common/model/DataLogSession.kt b/common-api/src/main/kotlin/io/rebble/pebblekit2/common/model/DataLogSession.kt new file mode 100644 index 0000000..cd813da --- /dev/null +++ b/common-api/src/main/kotlin/io/rebble/pebblekit2/common/model/DataLogSession.kt @@ -0,0 +1,30 @@ +package io.rebble.pebblekit2.common.model + +import kotlin.time.Instant + +/** + * A data logging session. The watch makes a session when a watchapp calls `data_logging_create()`. + * + * The watchapp UUID, the [tag] and the [timestamp] identify a session. All items in a session + * have the same [itemSize]. + */ +public data class DataLogSession( + /** + * The tag that the watchapp gave to `data_logging_create()`. The watchapp uses different tags + * for different types of data. + */ + val tag: Long, + + /** + * The time when the watch made the session, with one-second resolution. It separates two + * sessions with the same [tag], unless the watchapp made both in the same second. + */ + val timestamp: Instant, + + /** + * The size, in bytes, of one data item. + */ + val itemSize: Int, +) { + public companion object +} diff --git a/common/src/main/kotlin/io/rebble/pebblekit2/PebbleKitBundleKeys.kt b/common/src/main/kotlin/io/rebble/pebblekit2/PebbleKitBundleKeys.kt index 7fb2c4a..a5597a2 100644 --- a/common/src/main/kotlin/io/rebble/pebblekit2/PebbleKitBundleKeys.kt +++ b/common/src/main/kotlin/io/rebble/pebblekit2/PebbleKitBundleKeys.kt @@ -9,6 +9,10 @@ public object PebbleKitBundleKeys { public const val KEY_TRANSMISSION_RESULTS: String = "TRANSMISSION_RESULTS" public const val KEY_RECEIVE_RESULT: String = "TRANSMISSION_RESULTS" + public const val KEY_DATA_LOG_SESSION: String = "DATA_LOG_SESSION" + public const val KEY_DATA_LOG_DATA: String = "DATA_LOG_DATA" + public const val KEY_DATA_LOG_ITEMS_LEFT: String = "DATA_LOG_ITEMS_LEFT" + public const val KEY_TIMELINE_PIN: String = "TIMELINE_PIN" public const val KEY_TIMELINE_PIN_ID: String = "TIMELINE_PIN_ID" @@ -19,6 +23,9 @@ public object PebbleKitBundleKeys { public const val ACTION_APP_OPENED: String = "APP_OPENED" public const val ACTION_APP_CLOSED: String = "APP_CLOSED" + public const val ACTION_DATA_LOG_RECEIVED: String = "DATA_LOG_RECEIVED" + public const val ACTION_DATA_LOG_SESSION_FINISHED: String = "DATA_LOG_SESSION_FINISHED" + public const val ACTION_START_APP: String = "START_APP" public const val ACTION_STOP_APP: String = "STOP_APP" diff --git a/common/src/main/kotlin/io/rebble/pebblekit2/common/model/DataLogSessionSerialization.kt b/common/src/main/kotlin/io/rebble/pebblekit2/common/model/DataLogSessionSerialization.kt new file mode 100644 index 0000000..b16075c --- /dev/null +++ b/common/src/main/kotlin/io/rebble/pebblekit2/common/model/DataLogSessionSerialization.kt @@ -0,0 +1,26 @@ +package io.rebble.pebblekit2.common.model + +import android.os.Bundle +import kotlin.time.Instant + +public fun DataLogSession.Companion.fromBundle(bundle: Bundle): DataLogSession { + return DataLogSession( + tag = bundle.getLong(BUNDLE_KEY_TAG), + timestamp = Instant.fromEpochSeconds(bundle.getLong(BUNDLE_KEY_TIMESTAMP)), + itemSize = bundle.getInt(BUNDLE_KEY_ITEM_SIZE), + ) +} + +public fun DataLogSession.toBundle(): Bundle { + val bundle = Bundle() + + bundle.putLong(BUNDLE_KEY_TAG, tag) + bundle.putLong(BUNDLE_KEY_TIMESTAMP, timestamp.epochSeconds) + bundle.putInt(BUNDLE_KEY_ITEM_SIZE, itemSize) + + return bundle +} + +private const val BUNDLE_KEY_TAG = "TAG" +private const val BUNDLE_KEY_TIMESTAMP = "TIMESTAMP" +private const val BUNDLE_KEY_ITEM_SIZE = "ITEM_SIZE" diff --git a/docs/SERVER.MD b/docs/SERVER.MD index ccb08f8..b2fc081 100644 --- a/docs/SERVER.MD +++ b/docs/SERVER.MD @@ -62,6 +62,43 @@ delay(5.seconds) connector.close() ``` +# Forward data logging to apps + +The watch sends data logging items of third-party watchapps (see +[Datalogging](https://developer.rebble.io/guides/communication/datalogging/)). Forward each batch to +the companion app of the watchapp through the same `PebbleListenerConnector`: + +```kotlin +val result = connector.sendOnDataLogReceived(watchappUUID, session, data, itemsLeft, watch) +``` + +A batch must contain only whole items: `data.size` must be a multiple of `session.itemSize`. Send +the batches of one session in sequence: await the result of a call before you send the next batch +of that session. Keep one call well below the 1 MB Android binder transaction limit; do not put +more than 100 KB of data into one call. + +The watch closes a session after it sent all of its data. When the companion app has acknowledged +all the batches of the session, call: + +```kotlin +val result = connector.sendOnDataLogSessionFinished(watchappUUID, session, watch) +``` + +Data logging is different from messages: it is not connected to an open watchapp. The watch sends +stored data each time it is connected. The bind operation wakes the companion app. Thus make these +calls also outside of the app opened/closed window. + +The result tells you what to do with the data. `ReceiveResult.Ack` means that the companion app +stored the data. You can then discard it. `ReceiveResult.Nack`, `ReceiveResult.Unknown` (the +companion app has an old PebbleKit library) and `null` (no connection to the app) mean that the +companion app did not store the data. Keep the data and send it again later, a limited number of +times. Discard the data when the attempts are used up or when the companion app is no longer +installed; do not retry without a limit. The companion app must tolerate a batch that it gets more +than one time, so a retry after an unclear result is safe. + +The watch deletes its copy when the Pebble app confirms the receipt on the Pebble protocol level. +Data that the companion app did not acknowledge lives only in the Pebble app; treat it with care. + # Implementing status provider To allow apps to access various infos about the status of the watch and the connection, you have to implement the diff --git a/sample/android-java/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.java b/sample/android-java/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.java index 8f2bf01..832778e 100644 --- a/sample/android-java/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.java +++ b/sample/android-java/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.java @@ -2,6 +2,7 @@ import android.util.Log; import io.rebble.pebblekit2.client.java.BaseJavaPebbleListenerService; +import io.rebble.pebblekit2.common.model.DataLogSession; import io.rebble.pebblekit2.common.model.PebbleDictionaryItem; import io.rebble.pebblekit2.common.model.ReceiveResult; import org.jetbrains.annotations.NotNull; @@ -21,6 +22,29 @@ public void onMessageReceived(@NotNull UUID watchappUUID, responder.accept(ReceiveResult.Ack.INSTANCE); } + @Override + protected void onDataLogReceived(@NotNull UUID watchappUUID, + @NotNull DataLogSession session, + @NotNull byte[] data, + long itemsLeft, + @NotNull String watch, + @NotNull Consumer<@NotNull ReceiveResult> responder) { + Log.d("PebbleListenerService", "Received " + (data.length / session.getItemSize()) + + " data log items of session " + session + " from app " + watchappUUID); + + responder.accept(ReceiveResult.Ack.INSTANCE); + } + + @Override + protected void onDataLogSessionFinished(@NotNull UUID watchappUUID, + @NotNull DataLogSession session, + @NotNull String watch, + @NotNull Consumer<@NotNull ReceiveResult> responder) { + Log.d("PebbleListenerService", "Data log session " + session + " from app " + watchappUUID + " finished"); + + responder.accept(ReceiveResult.Ack.INSTANCE); + } + @Override protected void onAppOpened(@NotNull UUID watchappUUID, @NotNull String watch) { Log.d("PebbleListenerService", "App " + watchappUUID + " on the watch " + watch + " opened"); diff --git a/sample/android/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.kt b/sample/android/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.kt index fd7bd44..16e1245 100644 --- a/sample/android/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.kt +++ b/sample/android/app/src/main/java/io/rebble/pebblekit2/sample/PebbleListenerService.kt @@ -2,6 +2,7 @@ package io.rebble.pebblekit2.sample import android.util.Log import io.rebble.pebblekit2.client.BasePebbleListenerService +import io.rebble.pebblekit2.common.model.DataLogSession import io.rebble.pebblekit2.common.model.PebbleDictionary import io.rebble.pebblekit2.common.model.ReceiveResult import io.rebble.pebblekit2.common.model.WatchIdentifier @@ -17,6 +18,29 @@ class PebbleListenerService : BasePebbleListenerService() { return ReceiveResult.Ack } + override suspend fun onDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: WatchIdentifier, + ): ReceiveResult { + Log.d( + "PebbleListenerService", + "Received ${data.size / session.itemSize} data log items of session $session from app $watchappUUID" + ) + return ReceiveResult.Ack + } + + override suspend fun onDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: WatchIdentifier, + ): ReceiveResult { + Log.d("PebbleListenerService", "Data log session $session from app $watchappUUID finished") + return ReceiveResult.Ack + } + override fun onAppOpened(watchappUUID: UUID, watch: WatchIdentifier) { Log.d("PebbleListenerService", "App $watchappUUID on the watch $watch opened") } diff --git a/server-api/src/main/kotlin/io/rebble/pebblekit2/server/PebbleListenerConnector.kt b/server-api/src/main/kotlin/io/rebble/pebblekit2/server/PebbleListenerConnector.kt index 8fc738d..00f03fa 100644 --- a/server-api/src/main/kotlin/io/rebble/pebblekit2/server/PebbleListenerConnector.kt +++ b/server-api/src/main/kotlin/io/rebble/pebblekit2/server/PebbleListenerConnector.kt @@ -1,5 +1,6 @@ package io.rebble.pebblekit2.server +import io.rebble.pebblekit2.common.model.DataLogSession import io.rebble.pebblekit2.common.model.PebbleDictionary import io.rebble.pebblekit2.common.model.ReceiveResult import io.rebble.pebblekit2.common.model.WatchIdentifier @@ -18,6 +19,52 @@ public interface PebbleListenerConnector : AutoCloseable { watch: WatchIdentifier, ): ReceiveResult? + /** + * Send a batch of items from a data logging [session] of one of the registered apps to the + * target app. + * + * [data] must contain only whole items: `data.size` must be a multiple of `session.itemSize`. + * [itemsLeft] is the number of items that stay on the watch after this batch. Send the batches + * of one session in sequence: await the result of a call before you send the next batch of + * that session. Keep one call well below the 1 MB Android binder transaction limit; do not put + * more than 100 KB of data into one call. + * + * [ReceiveResult.Ack] means that the target app stored the data. You can then discard it. + * [ReceiveResult.Nack], [ReceiveResult.Unknown] (the target app has an old PebbleKit library) + * and `null` mean that the target app did not store the data. Keep the data and send it again + * later, a limited number of times. Discard the data when the attempts are used up or when the + * target app is no longer installed. The target app must tolerate a batch that it gets more + * than one time, so a retry after an unclear result is safe. + * + * @return null if the target app could not be reached + */ + public suspend fun sendOnDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: WatchIdentifier, + ): ReceiveResult? { + return null + } + + /** + * Tell the target app that a data logging [session] of one of the registered apps is complete. + * Send this only after the target app acknowledged all the batches of the session. + * + * The result has the same meaning as in [sendOnDataLogReceived]: on [ReceiveResult.Nack], + * [ReceiveResult.Unknown] or `null`, send the event again later, a limited number of times. + * + * @return null if the target app could not be reached + */ + public suspend fun sendOnDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: WatchIdentifier, + ): ReceiveResult? { + return null + } + /** * One of registered apps for this companion app has been opened on a watch * diff --git a/server/src/main/kotlin/io/rebble/pebblekit2/server/DefaultPebbleListenerConnector.kt b/server/src/main/kotlin/io/rebble/pebblekit2/server/DefaultPebbleListenerConnector.kt index b2d234e..42c381e 100644 --- a/server/src/main/kotlin/io/rebble/pebblekit2/server/DefaultPebbleListenerConnector.kt +++ b/server/src/main/kotlin/io/rebble/pebblekit2/server/DefaultPebbleListenerConnector.kt @@ -7,6 +7,7 @@ import androidx.core.os.bundleOf import io.rebble.pebblekit2.PebbleKitBundleKeys import io.rebble.pebblekit2.common.PebbleKitIntents import io.rebble.pebblekit2.common.UniversalRequestResponse +import io.rebble.pebblekit2.common.model.DataLogSession import io.rebble.pebblekit2.common.model.PebbleDictionary import io.rebble.pebblekit2.common.model.ReceiveResult import io.rebble.pebblekit2.common.model.WatchIdentifier @@ -59,6 +60,60 @@ public class DefaultPebbleListenerConnector( PebbleKitBundleKeys.KEY_WATCH_ID to watch.value ) + return requestReceiveResult(connection, bundle) + } + + /** + * Send a batch of items from a data logging [session] of one of the registered apps to the + * target app. + * + * @return null if the target app could not be reached + */ + override suspend fun sendOnDataLogReceived( + watchappUUID: UUID, + session: DataLogSession, + data: ByteArray, + itemsLeft: Long, + watch: WatchIdentifier, + ): ReceiveResult? { + val connection = connector.getOrConnect() ?: return null + + val bundle = bundleOf( + PebbleKitBundleKeys.KEY_ACTION to PebbleKitBundleKeys.ACTION_DATA_LOG_RECEIVED, + PebbleKitBundleKeys.KEY_WATCHAPP_UUID to watchappUUID.toString(), + PebbleKitBundleKeys.KEY_DATA_LOG_SESSION to session.toBundle(), + PebbleKitBundleKeys.KEY_DATA_LOG_DATA to data, + PebbleKitBundleKeys.KEY_DATA_LOG_ITEMS_LEFT to itemsLeft, + PebbleKitBundleKeys.KEY_WATCH_ID to watch.value + ) + + return requestReceiveResult(connection, bundle) + } + + /** + * Tell the target app that a data logging [session] of one of the registered apps is complete. + * Send this only after the target app acknowledged all the batches of the session. + * + * @return null if the target app could not be reached + */ + override suspend fun sendOnDataLogSessionFinished( + watchappUUID: UUID, + session: DataLogSession, + watch: WatchIdentifier, + ): ReceiveResult? { + val connection = connector.getOrConnect() ?: return null + + val bundle = bundleOf( + PebbleKitBundleKeys.KEY_ACTION to PebbleKitBundleKeys.ACTION_DATA_LOG_SESSION_FINISHED, + PebbleKitBundleKeys.KEY_WATCHAPP_UUID to watchappUUID.toString(), + PebbleKitBundleKeys.KEY_DATA_LOG_SESSION to session.toBundle(), + PebbleKitBundleKeys.KEY_WATCH_ID to watch.value + ) + + return requestReceiveResult(connection, bundle) + } + + private suspend fun requestReceiveResult(connection: UniversalRequestResponse, bundle: Bundle): ReceiveResult? { val returnBundle = try { connection.request(bundle) ?: return null } catch (e: IllegalArgumentException) {