Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<ReceiveResult>()

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<ReceiveResult>()

onDataLogSessionFinished(
watchappUUID,
session,
watch.value,
{ completableDeferred.complete(it) },
)

return completableDeferred.await()
}

final override fun onAppOpened(watchappUUID: UUID, watch: WatchIdentifier) {
onAppOpened(watchappUUID, watch.value)
}
Expand Down Expand Up @@ -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<ReceiveResult>,
) {
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<ReceiveResult>,
) {
responder.accept(ReceiveResult.Nack)
}

/**
* One of registered apps for this companion app has been opened on a watch
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"

Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading