diff --git a/README.md b/README.md
index 4984b7de..1f42bc0e 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,20 @@ See https://github.com/coredevices/libpebble3/wiki/Roadmap
See https://github.com/coredevices/libpebble3/wiki/Roadmap
+## Enabling PebbleKit Android Classic
+
+To fully enable PebbleKit Android Classic, you have to add this provider to your `AndroidManifest.xml`:
+
+```xml
+
+```
+
+Note that by doing so, your app will not be able to be installed alongside other apps that also do this.
+
# Contributing
We aren't actively encouraging contributions yet, while we are aggressively building out feature parity with the original Pebble apps. CI testing is not comprehensive, so changes need to be manually tested with real hardware using CoreApp, and our roadmap/bug tracker is not currently on github. We will update when this changes.
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.android.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.android.kt
new file mode 100644
index 00000000..fe806ceb
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.android.kt
@@ -0,0 +1,13 @@
+package io.rebble.libpebblecommon.connection.endpointmanager
+
+import io.rebble.libpebblecommon.connection.CompanionApp
+import io.rebble.libpebblecommon.js.CompanionAppDevice
+import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo
+import io.rebble.libpebblecommon.pebblekit.PebbleKitClassic
+
+actual fun createPlatformSpecificCompanionAppControl(
+ device: CompanionAppDevice,
+ appInfo: PbwAppInfo
+): CompanionApp? {
+ return PebbleKitClassic(device, appInfo)
+}
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewJsRunner.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewJsRunner.kt
index 8d73a43d..4fb18003 100644
--- a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewJsRunner.kt
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewJsRunner.kt
@@ -49,7 +49,7 @@ class WebViewJsRunner(
libPebble: LibPebble,
jsTokenUtil: JsTokenUtil,
- device: PebbleJSDevice,
+ device: CompanionAppDevice,
private val scope: CoroutineScope,
appInfo: PbwAppInfo,
lockerEntry: LockerEntry,
@@ -397,4 +397,4 @@ class WebViewJsRunner(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPKJSInterface.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPKJSInterface.kt
index a01de182..ac7e73e5 100644
--- a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPKJSInterface.kt
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPKJSInterface.kt
@@ -8,7 +8,7 @@ import io.rebble.libpebblecommon.connection.LibPebble
class WebViewPKJSInterface(
jsRunner: JsRunner,
- device: PebbleJSDevice,
+ device: CompanionAppDevice,
private val context: Context,
libPebble: LibPebble,
jsTokenUtil: JsTokenUtil,
@@ -40,4 +40,4 @@ class WebViewPKJSInterface(
override fun openURL(url: String): String {
return super.openURL(url)
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPrivatePKJSInterface.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPrivatePKJSInterface.kt
index ae2be559..1b2ff0ee 100644
--- a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPrivatePKJSInterface.kt
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/js/WebViewPrivatePKJSInterface.kt
@@ -11,7 +11,7 @@ import androidx.core.net.toUri
class WebViewPrivatePKJSInterface(
jsRunner: WebViewJsRunner,
- device: PebbleJSDevice,
+ device: CompanionAppDevice,
scope: CoroutineScope,
outgoingAppMessages: MutableSharedFlow,
logMessages: MutableSharedFlow
@@ -95,4 +95,4 @@ class WebViewPrivatePKJSInterface(
override fun getActivePebbleWatchInfo(): String {
return super.getActivePebbleWatchInfo()
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleDictionary.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleDictionary.kt
new file mode 100644
index 00000000..7c37e92b
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleDictionary.kt
@@ -0,0 +1,330 @@
+package io.rebble.libpebblecommon.pebblekit
+
+import android.util.Base64
+import io.rebble.libpebblecommon.pebblekit.PebbleTuple.TupleType
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * A collection of key-value pairs of heterogeneous types. PebbleDictionaries are the primary structure used to exchange
+ * data between the phone and watch.
+ *
+ *
+ * To accommodate the mixed-types contained within a PebbleDictionary, an internal JSON representation is used when
+ * exchanging the dictionary between Android processes.
+ *
+ * This is a copy of the file from the original Pebble SDK, converted to Kotlin
+ *
+ * @author zulak@getpebble.com
+ */
+class PebbleDictionary : Iterable {
+ val tuples: MutableMap = HashMap()
+
+ /**
+ * {@inheritDoc}
+ */
+ override fun iterator(): MutableIterator {
+ return tuples.values.iterator()
+ }
+
+ /**
+ * Returns the number of key-value pairs in this dictionary.
+ *
+ * @return the number of key-value pairs in this dictionary
+ */
+ fun size(): Int {
+ return tuples.size
+ }
+
+ /**
+ * Returns true if this dictionary contains a mapping for the specified key.
+ *
+ * @param key key whose presence in this dictionary is to be tested
+ * @return true if this dictionary contains a mapping for the specified key
+ */
+ fun contains(key: Int): Boolean {
+ return tuples.containsKey(key)
+ }
+
+ /**
+ * Removes the mapping for a key from this map if it is present.
+ *
+ * @param key key to be removed from the dictionary
+ */
+ fun remove(key: Int) {
+ tuples.remove(key)
+ }
+
+ /**
+ * Associate the specified byte array with the provided key in the dictionary. If another key-value pair with the
+ * same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param bytes value to be associated with the specified key
+ */
+ fun addBytes(key: Int, bytes: ByteArray) {
+ val t = PebbleTuple.create(key, TupleType.BYTES, PebbleTuple.Width.NONE, bytes)
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified String with the provided key in the dictionary. If another key-value pair with the same
+ * key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param value value to be associated with the specified key
+ */
+ fun addString(key: Int, value: String) {
+ val t =
+ PebbleTuple.create(key, TupleType.STRING, PebbleTuple.Width.NONE, value)
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified signed byte with the provided key in the dictionary. If another key-value pair with the
+ * same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param b value to be associated with the specified key
+ */
+ fun addInt8(key: Int, b: Byte) {
+ val t = PebbleTuple.create(key, TupleType.INT, PebbleTuple.Width.BYTE, b.toInt())
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified unsigned byte with the provided key in the dictionary. If another key-value pair with the
+ * same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param b value to be associated with the specified key
+ */
+ fun addUint8(key: Int, b: Byte) {
+ val t = PebbleTuple.create(key, TupleType.UINT, PebbleTuple.Width.BYTE, b.toInt())
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified signed short with the provided key in the dictionary. If another key-value pair with the
+ * same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param s value to be associated with the specified key
+ */
+ fun addInt16(key: Int, s: Short) {
+ val t = PebbleTuple.create(key, TupleType.INT, PebbleTuple.Width.SHORT, s.toInt())
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified unsigned short with the provided key in the dictionary. If another key-value pair with
+ * the same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param s value to be associated with the specified key
+ */
+ fun addUint16(key: Int, s: Short) {
+ val t = PebbleTuple.create(key, TupleType.UINT, PebbleTuple.Width.SHORT, s.toInt())
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified signed int with the provided key in the dictionary. If another key-value pair with the
+ * same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param i value to be associated with the specified key
+ */
+ fun addInt32(key: Int, i: Int) {
+ val t = PebbleTuple.create(key, TupleType.INT, PebbleTuple.Width.WORD, i)
+ addTuple(t)
+ }
+
+ /**
+ * Associate the specified unsigned int with the provided key in the dictionary. If another key-value pair with the
+ * same key is already present in the dictionary, it will be replaced.
+ *
+ * @param key key with which the specified value is associated
+ * @param i value to be associated with the specified key
+ */
+ fun addUint32(key: Int, i: Int) {
+ val t = PebbleTuple.create(key, TupleType.UINT, PebbleTuple.Width.WORD, i)
+ addTuple(t)
+ }
+
+ private fun getTuple(key: Int, type: TupleType): PebbleTuple? {
+ if (!tuples.containsKey(key) || tuples.get(key) == null) {
+ return null
+ }
+
+ val t = tuples.getValue(key)
+ if (t.type != type) {
+ throw PebbleDictTypeException(key.toLong(), type, t.type)
+ }
+ return t
+ }
+
+ /**
+ * Returns the signed integer to which the specified key is mapped, or null if the key does not exist in this
+ * dictionary.
+ *
+ * @param key key whose associated value is to be returned
+ * @return value to which the specified key is mapped
+ */
+ fun getInteger(key: Int): Long? {
+ val tuple = getTuple(key, TupleType.INT)
+ if (tuple == null) {
+ return null
+ }
+ return tuple.value as Long?
+ }
+
+ /**
+ * Returns the unsigned integer as a long to which the specified key is mapped, or null if the key does not exist in this
+ * dictionary. We are using the Long type here so that we can remove the guava dependency. This is done so that we dont
+ * have incompatibility issues with the UnsignedInteger class from the Holo application, which uses a newer version of Guava.
+ *
+ * @param key key whose associated value is to be returned
+ * @return value to which the specified key is mapped
+ */
+ fun getUnsignedIntegerAsLong(key: Int): Long? {
+ val tuple = getTuple(key, TupleType.UINT)
+ if (tuple == null) {
+ return null
+ }
+ return tuple.value as Long?
+ }
+
+ /**
+ * Returns the byte array to which the specified key is mapped, or null if the key does not exist in this
+ * dictionary.
+ *
+ * @param key key whose associated value is to be returned
+ * @return value to which the specified key is mapped
+ */
+ fun getBytes(key: Int): ByteArray? {
+ val tuple = getTuple(key, TupleType.BYTES)
+ if (tuple == null) {
+ return null
+ }
+ return tuple.value as ByteArray?
+ }
+
+ /**
+ * Returns the string to which the specified key is mapped, or null if the key does not exist in this dictionary.
+ *
+ * @param key key whose associated value is to be returned
+ * @return value to which the specified key is mapped
+ */
+ fun getString(key: Int): String? {
+ val tuple = getTuple(key, TupleType.STRING)
+ if (tuple == null) {
+ return null
+ }
+ return tuple.value as String?
+ }
+
+ fun addTuple(tuple: PebbleTuple) {
+ if (tuples.size > 0xff) {
+ throw TupleOverflowException()
+ }
+
+ tuples.put(tuple.key, tuple)
+ }
+
+ class PebbleDictTypeException(key: Long, expected: TupleType, actual: TupleType) : RuntimeException(
+ String.format(
+ "Expected type '%s', but got '%s' for key 0x%08x", expected.name, actual.name, key
+ )
+ )
+
+ class TupleOverflowException : RuntimeException("Too many tuples in dict")
+
+ /**
+ * Returns a JSON representation of this dictionary.
+ *
+ * @return a JSON representation of this dictionary
+ */
+ fun toJsonString(): String? {
+ try {
+ val array = JSONArray()
+ for (t in tuples.values) {
+ array.put(serializeTuple(t))
+ }
+ return array.toString()
+ } catch (je: JSONException) {
+ je.printStackTrace()
+ }
+ return null
+ }
+
+ companion object {
+ private const val KEY = "key"
+ private const val TYPE = "type"
+ private const val LENGTH = "length"
+ private const val VALUE = "value"
+
+ /**
+ * Deserializes a JSON representation of a PebbleDictionary.
+ *
+ * @param jsonString the JSON representation to be deserialized
+ * @throws JSONException thrown if the specified JSON representation cannot be parsed
+ */
+ @Throws(JSONException::class)
+ fun fromJson(jsonString: String?): PebbleDictionary {
+ val d = PebbleDictionary()
+
+ val elements = JSONArray(jsonString)
+ for (idx in 0.. {
+ val bytes = Base64.decode(o.getString(VALUE), Base64.NO_WRAP)
+ d.addBytes(key, bytes)
+ }
+
+ TupleType.STRING -> d.addString(key, o.getString(VALUE))
+ TupleType.INT -> if (width == PebbleTuple.Width.BYTE) {
+ d.addInt8(key, o.getInt(VALUE).toByte())
+ } else if (width == PebbleTuple.Width.SHORT) {
+ d.addInt16(key, o.getInt(VALUE).toShort())
+ } else if (width == PebbleTuple.Width.WORD) {
+ d.addInt32(key, o.getInt(VALUE))
+ }
+
+ TupleType.UINT -> if (width == PebbleTuple.Width.BYTE) {
+ d.addUint8(key, o.getInt(VALUE).toByte())
+ } else if (width == PebbleTuple.Width.SHORT) {
+ d.addUint16(key, o.getInt(VALUE).toShort())
+ } else if (width == PebbleTuple.Width.WORD) {
+ d.addUint32(key, o.getInt(VALUE))
+ }
+
+ null -> {}
+ }
+ }
+
+ return d
+ }
+
+ @Throws(JSONException::class)
+ private fun serializeTuple(t: PebbleTuple): JSONObject {
+ val j = JSONObject()
+ j.put(KEY, t.key)
+ j.put(TYPE, t.type.tupleName)
+ j.put(LENGTH, t.width.value)
+
+ when (t.type) {
+ TupleType.BYTES -> j.put(VALUE, Base64.encodeToString(t.value as ByteArray?, Base64.NO_WRAP))
+ TupleType.STRING, TupleType.INT, TupleType.UINT -> j.put(VALUE, t.value)
+ }
+
+ return j
+ }
+ }
+}
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitClassic.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitClassic.kt
new file mode 100644
index 00000000..6b55e34e
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitClassic.kt
@@ -0,0 +1,228 @@
+package io.rebble.libpebblecommon.pebblekit
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import co.touchlab.kermit.Logger
+import io.rebble.libpebblecommon.connection.CompanionApp
+import io.rebble.libpebblecommon.connection.ConnectedPebble
+import io.rebble.libpebblecommon.di.LibPebbleKoinComponent
+import io.rebble.libpebblecommon.js.CompanionAppDevice
+import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo
+import io.rebble.libpebblecommon.services.appmessage.AppMessageData
+import io.rebble.libpebblecommon.services.appmessage.AppMessageDictionary
+import io.rebble.libpebblecommon.services.appmessage.AppMessageResult
+import io.rebble.libpebblecommon.util.asFlow
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.withTimeoutOrNull
+import java.util.UUID
+import kotlin.uuid.Uuid
+import kotlin.uuid.toJavaUuid
+import kotlin.uuid.toKotlinUuid
+
+class PebbleKitClassic(
+ private val device: CompanionAppDevice,
+ val appInfo: PbwAppInfo,
+) :
+ LibPebbleKoinComponent, CompanionApp {
+ companion object {
+ private val logger = Logger.withTag(PebbleKitClassic::class.simpleName!!)
+ }
+
+ val uuid: Uuid by lazy { Uuid.parse(appInfo.uuid) }
+ private var runningScope: CoroutineScope? = null
+
+ private val context: Context = getKoin().get()
+
+ private suspend fun replyNACK(id: UByte) {
+ withTimeoutOrNull(1000) {
+ device.sendAppMessageResult(AppMessageResult.ACK(id))
+ }
+ }
+
+ private suspend fun replyACK(id: UByte) {
+ withTimeoutOrNull(1000) {
+ device.sendAppMessageResult(AppMessageResult.ACK(id))
+ }
+ }
+
+ private fun launchIncomingAppMessageHandler(device: ConnectedPebble.AppMessages, scope: CoroutineScope) {
+ device.inboundAppMessages(uuid).onEach { appMessageData ->
+ logger.d { "Got inbound message" }
+
+ val pebbleDictionary = appMessageData.data.toPebbleDictionary()
+ val intent = Intent(INTENT_APP_RECEIVE).apply {
+ putExtra(APP_UUID, appMessageData.uuid.toJavaUuid())
+ putExtra(TRANSACTION_ID, appMessageData.transactionId.toInt())
+
+ putExtra(MSG_DATA, pebbleDictionary.toJsonString())
+ }
+
+ // Regular broadcasts are sometimes delayed on Android 14+. Use ordered ones instead.
+ // https://stackoverflow.com/questions/77842817/slow-intent-broadcast-delivery-on-android-14
+ context.sendOrderedBroadcast(intent, null)
+ }.catch {
+ logger.e(it) { "Error receiving app message: ${it.message}" }
+ }.launchIn(scope)
+ }
+
+ // TODO app start and stop intents
+
+ private fun launchOutgoingAppMessageHandlers(device: ConnectedPebble.AppMessages, scope: CoroutineScope) {
+ scope.launch {
+ IntentFilter(INTENT_APP_ACK).asFlow(context).collect { intent ->
+ logger.d { "Got outbound ack" }
+ val transactionId: Int = intent.getIntExtra(TRANSACTION_ID, 0)
+ replyACK(transactionId.toUByte())
+ }
+ }
+
+ scope.launch {
+ IntentFilter(INTENT_APP_NACK).asFlow(context).collect { intent ->
+ logger.d { "Got outbound nack" }
+ val transactionId: Int = intent.getIntExtra(TRANSACTION_ID, 0)
+ replyNACK(transactionId.toUByte())
+ }
+ }
+
+ scope.launch {
+ IntentFilter(INTENT_APP_SEND).asFlow(context).collect { intent ->
+ logger.d { "Got outbound message" }
+ val uuid = intent.getSerializableExtra(APP_UUID, UUID::class.java) ?: return@collect
+ val dictionary: PebbleDictionary = PebbleDictionary.fromJson(
+ intent.getStringExtra(MSG_DATA)
+ )
+ val transactionId: Int = intent.getIntExtra(TRANSACTION_ID, 0)
+
+ val msg = AppMessageData(transactionId.toUByte(), uuid.toKotlinUuid(), dictionary.toAppMessageDict())
+ val result = device.sendAppMessage(msg)
+ logger.d { "Result from the app: $result" }
+ val intentAction = when (result) {
+ is AppMessageResult.ACK -> INTENT_APP_RECEIVE_ACK
+ is AppMessageResult.NACK -> INTENT_APP_RECEIVE_NACK
+ }
+
+ val intent = Intent(intentAction).apply {
+ putExtra(TRANSACTION_ID, result.transactionId.toInt())
+ }
+
+ // Regular broadcasts are sometimes delayed on Android 14+. Use ordered ones instead.
+ // https://stackoverflow.com/questions/77842817/slow-intent-broadcast-delivery-on-android-14
+ context.sendOrderedBroadcast(intent, null)
+ }
+ }
+ }
+
+ override suspend fun start(connectionScope: CoroutineScope) {
+ val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
+ logger.e(throwable) { "Unhandled exception in PebbleKitClassic: ${throwable.message}" }
+ }
+ val scope = connectionScope + Job() + CoroutineName("PebbleKitClassic-$uuid") + exceptionHandler
+ runningScope = scope
+ launchIncomingAppMessageHandler(device, scope)
+ launchOutgoingAppMessageHandlers(device, scope)
+ }
+
+ override suspend fun stop() {
+ runningScope?.cancel()
+ }
+}
+
+private fun PebbleDictionary.toAppMessageDict(): AppMessageDictionary {
+ return tuples.mapValues {
+ when (it.value.type) {
+ PebbleTuple.TupleType.BYTES -> it.value.value as ByteArray
+ PebbleTuple.TupleType.STRING -> it.value.value as String
+ PebbleTuple.TupleType.UINT -> when (it.value.length) {
+ 1 -> (it.value.value as Number).toLong().toUByte()
+ 2 -> (it.value.value as Number).toLong().toUShort()
+ 4 -> (it.value.value as Number).toLong().toUInt()
+ else -> error("Unknown UINT size ${it.value.length}")
+ }
+
+ PebbleTuple.TupleType.INT -> when (it.value.length) {
+ 1 -> (it.value.value as Number).toByte()
+ 2 -> (it.value.value as Number).toShort()
+ 4 -> (it.value.value as Number).toInt()
+ else -> error("Unknown INT size ${it.value.length}")
+ }
+ }
+ }
+}
+
+private fun AppMessageDictionary.toPebbleDictionary(): PebbleDictionary {
+ val dict = PebbleDictionary()
+ for ((k, value) in this) {
+ when (value) {
+ is String -> dict.addString(k, value)
+ is UByteArray -> dict.addBytes(k, value.toByteArray())
+ is ByteArray -> dict.addBytes(k, value)
+ is Int -> dict.addInt32(k, value)
+ is Long -> dict.addInt32(k, value.toInt())
+ is ULong -> dict.addUint32(k, value.toInt())
+ is UInt -> dict.addUint32(k, value.toInt())
+ is Short -> dict.addInt16(k, value)
+ is UShort -> dict.addUint16(k, value.toShort())
+ is Byte -> dict.addInt8(k, value)
+ is UByte -> dict.addUint8(k, value.toByte())
+ else -> throw IllegalArgumentException("Unsupported type: ${value::class.simpleName}")
+ }
+ }
+
+ return dict
+}
+
+/**
+ * Intent broadcast to pebble.apk to indicate that a message was received from the watch. To avoid protocol timeouts
+ * on the watch, applications *must* ACK or NACK all received messages.
+ */
+private const val INTENT_APP_ACK = "com.getpebble.action.app.ACK"
+
+/**
+ * Intent broadcast to pebble.apk to indicate that a message was unsuccessfully received from the watch.
+ */
+private const val INTENT_APP_NACK = "com.getpebble.action.app.NACK"
+
+/**
+ * Intent broadcast from pebble.apk containing one-or-more key-value pairs sent from the watch to the phone.
+ */
+private const val INTENT_APP_RECEIVE = "com.getpebble.action.app.RECEIVE"
+
+/**
+ * Intent broadcast from pebble.apk indicating that a sent message was successfully received by a watch app.
+ */
+private const val INTENT_APP_RECEIVE_ACK = "com.getpebble.action.app.RECEIVE_ACK"
+
+/**
+ * Intent broadcast from pebble.apk indicating that a sent message was not received by a watch app.
+ */
+private const val INTENT_APP_RECEIVE_NACK = "com.getpebble.action.app.RECEIVE_NACK"
+
+/**
+ * Intent broadcast to pebble.apk containing one-or-more key-value pairs to be sent to the watch from the phone.
+ */
+private const val INTENT_APP_SEND = "com.getpebble.action.app.SEND"
+
+/**
+ * The bundle-key used to store a message's transaction id.
+ */
+private const val TRANSACTION_ID = "transaction_id"
+
+/**
+ * The bundle-key used to store a message's UUID.
+ */
+private const val APP_UUID = "uuid"
+
+/**
+ * The bundle-key used to store a message's JSON payload send-to or received-from the watch.
+ */
+private const val MSG_DATA = "msg_data"
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitClassicStartListeners.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitClassicStartListeners.kt
new file mode 100644
index 00000000..018f10aa
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitClassicStartListeners.kt
@@ -0,0 +1,61 @@
+package io.rebble.libpebblecommon.pebblekit
+
+import android.content.Context
+import android.content.IntentFilter
+import co.touchlab.kermit.Logger
+import io.rebble.libpebblecommon.connection.LibPebble
+import io.rebble.libpebblecommon.di.LibPebbleCoroutineScope
+import io.rebble.libpebblecommon.di.LibPebbleKoinComponent
+import io.rebble.libpebblecommon.util.asFlow
+import kotlinx.coroutines.launch
+import java.util.UUID
+import kotlin.uuid.toKotlinUuid
+
+class PebbleKitClassicStartListeners() :
+ LibPebbleKoinComponent {
+ companion object {
+ private val logger = Logger.withTag(PebbleKitClassicStartListeners::class.simpleName!!)
+ }
+
+ private val context: Context = getKoin().get()
+ private val libPebble: LibPebble = getKoin().get()
+ private val connectionScope: LibPebbleCoroutineScope = getKoin().get()
+
+
+ fun init() {
+ connectionScope.launch {
+ IntentFilter(INTENT_APP_START).asFlow(context).collect { intent ->
+ logger.v { "Got intent: $intent" }
+ val uuid = intent.getSerializableExtra(APP_UUID, UUID::class.java) ?: return@collect
+ logger.d { "Got app start: $uuid" }
+ libPebble.launchApp(uuid.toKotlinUuid())
+ }
+ }
+
+ connectionScope.launch {
+ IntentFilter(INTENT_APP_STOP).asFlow(context).collect { intent ->
+ val uuid = intent.getSerializableExtra(APP_UUID, UUID::class.java) ?: return@collect
+ logger.d { "Got app stop: $uuid" }
+ libPebble.stopApp(uuid.toKotlinUuid())
+ }
+ }
+ }
+}
+
+/**
+ * Intent broadcast to pebble.apk responsible for launching a watch-app on the connected watch. This intent is
+ * idempotent.
+ */
+private const val INTENT_APP_START = "com.getpebble.action.app.START"
+
+/**
+ * Intent broadcast to pebble.apk responsible for closing a running watch-app on the connected watch. This intent is
+ * idempotent.
+ */
+private const val INTENT_APP_STOP = "com.getpebble.action.app.STOP"
+
+
+/**
+ * The bundle-key used to store a message's UUID.
+ */
+private const val APP_UUID = "uuid"
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitProvider.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitProvider.kt
new file mode 100644
index 00000000..3fc8fb1f
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitProvider.kt
@@ -0,0 +1,153 @@
+package io.rebble.libpebblecommon.pebblekit
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import androidx.core.net.toUri
+import io.rebble.libpebblecommon.connection.ConnectedPebbleDevice
+import io.rebble.libpebblecommon.connection.LibPebble
+import io.rebble.libpebblecommon.connection.Watches
+import io.rebble.libpebblecommon.di.LibPebbleCoroutineScope
+import io.rebble.libpebblecommon.di.LibPebbleKoinComponent
+
+class PebbleKitProvider : ContentProvider(), LibPebbleKoinComponent {
+ private var initialized = false
+ private lateinit var watches: Watches
+ private lateinit var libPebbleCoroutineScope: LibPebbleCoroutineScope
+ override fun onCreate(): Boolean {
+ // Do not initialize anything here as this gets called before Application.onCreate, so LibPebble has not yet been
+ // initialized at this point
+
+ return true
+ }
+
+ private fun initializeIfNeeded(): Boolean {
+ if (initialized) {
+ return true
+ }
+
+ watches = getKoin().getOrNull() ?: return false
+ libPebbleCoroutineScope = getKoin().get()
+
+ initialized = true
+
+ return true
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor? {
+ if (uri != URI_CONTENT_BASALT) {
+ return null
+ }
+
+ val cursor = MatrixCursor(CURSOR_COLUMN_NAMES)
+
+ cursor.use { _ ->
+ val appInitialized = initializeIfNeeded()
+ if (!appInitialized) {
+ // App is not initialized yet. Return disconnected for now, we will update when the app will init.
+
+ cursor.addRow(
+ listOf(
+ 0, // Connected
+ 0, // App Message support
+ 0, // Data Logging support
+ 0, // Major version support
+ 0, // Minor version support
+ 0, // Point version support
+ "", // Version Tag
+ )
+ )
+
+ return@use
+ }
+
+ val connectedWatch = watches.watches.value.filterIsInstance().firstOrNull()
+
+ if (connectedWatch != null) {
+ val fwVersion = connectedWatch.watchInfo.runningFwVersion
+ cursor.addRow(
+ listOf(
+ 1, // Connected
+ 1, // App Message support
+ 0, // Data Logging support
+ fwVersion.major, // Major version support
+ fwVersion.minor, // Minor version support
+ fwVersion.patch, // Point version support
+ fwVersion.suffix, // Version Tag
+ )
+ )
+ } else {
+ cursor.addRow(
+ listOf(
+ 0, // Connected
+ 0, // App Message support
+ 0, // Data Logging support
+ 0, // Major version support
+ 0, // Minor version support
+ 0, // Point version support
+ "", // Version Tag
+ )
+ )
+ }
+ }
+
+ return cursor
+ }
+
+ override fun getType(uri: Uri): String? {
+ return null
+ }
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ // This provider is read-only
+ return null
+ }
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
+ // This provider is read-only
+ return 0
+ }
+
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
+ // This provider is read-only
+ return 0
+ }
+
+ companion object {
+ internal val URI_CONTENT_BASALT = "content://com.getpebble.android.provider.basalt/state".toUri()
+ }
+}
+
+private val CURSOR_COLUMN_NAMES = arrayOf(
+ KIT_STATE_COLUMN_CONNECTED.toString(),
+ KIT_STATE_COLUMN_APPMSG_SUPPORT.toString(),
+ KIT_STATE_COLUMN_DATALOGGING_SUPPORT.toString(),
+ KIT_STATE_COLUMN_VERSION_MAJOR.toString(),
+ KIT_STATE_COLUMN_VERSION_MINOR.toString(),
+ KIT_STATE_COLUMN_VERSION_POINT.toString(),
+ KIT_STATE_COLUMN_VERSION_TAG.toString()
+)
+
+
+private const val KIT_STATE_COLUMN_CONNECTED = 0
+
+private const val KIT_STATE_COLUMN_APPMSG_SUPPORT = 1
+
+private const val KIT_STATE_COLUMN_DATALOGGING_SUPPORT = 2
+
+private const val KIT_STATE_COLUMN_VERSION_MAJOR = 3
+
+private const val KIT_STATE_COLUMN_VERSION_MINOR = 4
+
+private const val KIT_STATE_COLUMN_VERSION_POINT = 5
+
+private const val KIT_STATE_COLUMN_VERSION_TAG = 6
+
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitProviderNotifier.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitProviderNotifier.kt
new file mode 100644
index 00000000..457d21ec
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleKitProviderNotifier.kt
@@ -0,0 +1,29 @@
+package io.rebble.libpebblecommon.pebblekit
+
+import android.content.Context
+import io.rebble.libpebblecommon.connection.ConnectedPebbleDevice
+import io.rebble.libpebblecommon.connection.LibPebble
+import io.rebble.libpebblecommon.connection.Watches
+import io.rebble.libpebblecommon.di.LibPebbleCoroutineScope
+import io.rebble.libpebblecommon.di.LibPebbleKoinComponent
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+class PebbleKitProviderNotifier : LibPebbleKoinComponent {
+ fun init() {
+ val watches: Watches = getKoin().get()
+ val libPebbleCoroutineScope: LibPebbleCoroutineScope = getKoin().get()
+ val context: Context = getKoin().get()
+
+ libPebbleCoroutineScope.launch {
+ watches.watches.map {
+ it.any { it is ConnectedPebbleDevice }
+ }
+ .distinctUntilChanged()
+ .collect {
+ context.contentResolver.notifyChange(PebbleKitProvider.URI_CONTENT_BASALT, null)
+ }
+ }
+ }
+}
diff --git a/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleTuple.kt b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleTuple.kt
new file mode 100644
index 00000000..08dca88a
--- /dev/null
+++ b/libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/pebblekit/PebbleTuple.kt
@@ -0,0 +1,117 @@
+package io.rebble.libpebblecommon.pebblekit
+
+import java.nio.charset.Charset
+import java.util.Locale
+
+/**
+ * A key-value pair stored in a [PebbleDictionary].
+ *
+ * This is a copy of the file from the original Pebble SDK, converted to Kotlin
+ *
+ * @author zulak@getpebble.com
+ */
+class PebbleTuple private constructor(
+ /**
+ * The integer key identifying the tuple.
+ */
+ val key: Int,
+ /**
+ * The type of value contained in the tuple.
+ */
+ val type: TupleType,
+ /**
+ * The 'width' of the tuple's value; This value will always be 'NONE' for non-integer types.
+ */
+ val width: Width,
+ /**
+ * The length of the tuple's value in bytes.
+ */
+ val length: Int,
+ /**
+ * The value being associated with the tuple's key.
+ */
+ val value: Any?
+) {
+ class ValueOverflowException : RuntimeException("Value exceeds tuple capacity")
+
+ enum class Width(val value: Int) {
+ NONE(0),
+ BYTE(1),
+ SHORT(2),
+ WORD(4);
+
+ companion object {
+ fun fromValue(widthValue: Int): Width {
+ for (width in entries) {
+ if (widthValue == width.value) {
+ return width
+ }
+ }
+
+ throw IllegalArgumentException("Unknown width value: " + widthValue)
+ }
+ }
+ }
+
+ enum class TupleType(ord: Int) {
+ BYTES(0),
+ STRING(1),
+ UINT(2),
+ INT(3);
+
+ val ord: Byte
+
+ init {
+ this.ord = ord.toByte()
+ }
+
+ val tupleName: String
+ get() = name.lowercase(Locale.US)
+ }
+
+ companion object {
+ private val UTF8: Charset = Charset.forName("UTF-8")
+
+ val TYPE_NAMES: MutableMap = HashMap()
+
+ init {
+ for (t in TupleType.entries) {
+ TYPE_NAMES.put(t.tupleName, t)
+ }
+ }
+
+ val WIDTH_MAP: MutableMap = HashMap()
+
+ init {
+ for (w in Width.entries) {
+ WIDTH_MAP.put(w.value, w)
+ }
+ }
+
+ fun create(
+ key: Int, type: TupleType, width: Width, value: Int
+ ): PebbleTuple {
+ return PebbleTuple(key, type, width, width.value, value)
+ }
+
+ fun create(
+ key: Int, type: TupleType, width: Width, value: Any
+ ): PebbleTuple {
+ var length = Int.Companion.MAX_VALUE
+ if (width != Width.NONE) {
+ length = width.value
+ } else if (type == TupleType.BYTES) {
+ length = (value as ByteArray).size
+ } else if (type == TupleType.STRING) {
+ length = (value as String).toByteArray(UTF8).size
+ }
+
+ if (length > 0xffff) {
+ throw ValueOverflowException()
+ }
+
+ return PebbleTuple(key, type, width, length, value)
+ }
+ }
+}
+
diff --git a/libpebble3/src/androidMain/kotlin/main.kt b/libpebble3/src/androidMain/kotlin/main.kt
index 34be2189..3fc515b0 100644
--- a/libpebble3/src/androidMain/kotlin/main.kt
+++ b/libpebble3/src/androidMain/kotlin/main.kt
@@ -1,5 +1,12 @@
package io.rebble.libpebblecommon
import io.rebble.libpebblecommon.packets.PhoneAppVersion
+import io.rebble.libpebblecommon.pebblekit.PebbleKitClassicStartListeners
+import io.rebble.libpebblecommon.pebblekit.PebbleKitProviderNotifier
-actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.Android
\ No newline at end of file
+actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.Android
+
+actual fun performPlatformSpecificInit() {
+ PebbleKitClassicStartListeners().init()
+ PebbleKitProviderNotifier().init()
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/CompanionApp.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/CompanionApp.kt
new file mode 100644
index 00000000..4d426abf
--- /dev/null
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/CompanionApp.kt
@@ -0,0 +1,8 @@
+package io.rebble.libpebblecommon.connection
+
+import kotlinx.coroutines.CoroutineScope
+
+interface CompanionApp {
+ suspend fun start(connectionScope: CoroutineScope)
+ suspend fun stop()
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/FakeLibPebble.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/FakeLibPebble.kt
index 67a8741b..d13e3de1 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/FakeLibPebble.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/FakeLibPebble.kt
@@ -91,6 +91,10 @@ class FakeLibPebble : LibPebble {
// No-op
}
+ override suspend fun stopApp(uuid: Uuid) {
+ // No-op
+ }
+
override fun doStuffAfterPermissionsGranted() {
// No-op
}
@@ -384,6 +388,8 @@ class FakeConnectedDevice(
override suspend fun launchApp(uuid: Uuid) {}
+ override suspend fun stopApp(uuid: Uuid) {}
+
override val runningApp: StateFlow = MutableStateFlow(null)
override val watchInfo: WatchInfo = WatchInfo(
runningFwVersion = FirmwareVersion.from(
@@ -420,7 +426,10 @@ class FakeConnectedDevice(
override suspend fun updateTime() {}
- override val inboundAppMessages: Flow = MutableSharedFlow()
+ override fun inboundAppMessages(appUuid: Uuid): Flow {
+ return MutableSharedFlow()
+ }
+
override val transactionSequence: Iterator = iterator { }
override suspend fun sendAppMessage(appMessageData: AppMessageData): AppMessageResult =
@@ -449,7 +458,9 @@ class FakeConnectedDevice(
override val musicActions: Flow = MutableSharedFlow()
override val updateRequestTrigger: Flow = MutableSharedFlow()
+ @Deprecated("Use more generic currentCompanionAppSession instead and cast if necessary")
override val currentPKJSSession: StateFlow = MutableStateFlow(null)
+ override val currentCompanionAppSession: StateFlow = MutableStateFlow(null)
override suspend fun startDevConnection() {}
override suspend fun stopDevConnection() {}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/LibPebble.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/LibPebble.kt
index 83de4f61..788936ae 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/LibPebble.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/LibPebble.kt
@@ -35,6 +35,7 @@ import io.rebble.libpebblecommon.locker.LockerWrapper
import io.rebble.libpebblecommon.notification.NotificationApi
import io.rebble.libpebblecommon.notification.NotificationListenerConnection
import io.rebble.libpebblecommon.packets.ProtocolCapsFlag
+import io.rebble.libpebblecommon.performPlatformSpecificInit
import io.rebble.libpebblecommon.services.FirmwareVersion
import io.rebble.libpebblecommon.services.WatchInfo
import io.rebble.libpebblecommon.time.TimeChanged
@@ -76,6 +77,7 @@ interface LibPebble : Scanning, RequestSync, LockerApi, NotificationApps, CallMa
suspend fun markNotificationRead(itemId: Uuid)
suspend fun sendPing(cookie: UInt)
suspend fun launchApp(uuid: Uuid)
+ suspend fun stopApp(uuid: Uuid)
// ....
fun doStuffAfterPermissionsGranted()
@@ -256,6 +258,8 @@ class LibPebble3(
libPebbleCoroutineScope.launch { forEachConnectedWatch { updateTime() } }
}
housekeeping.init()
+
+ performPlatformSpecificInit()
}
override val config: StateFlow = libPebbleConfigFlow.config
@@ -285,6 +289,10 @@ class LibPebble3(
forEachConnectedWatch { launchApp(uuid) }
}
+ override suspend fun stopApp(uuid: Uuid) {
+ forEachConnectedWatch { stopApp(uuid) }
+ }
+
override fun doStuffAfterPermissionsGranted() {
phoneCalendarSyncer.init()
missedCallSyncer.init()
@@ -328,4 +336,4 @@ class LibPebble3(
return libPebble
}
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/PebbleDevice.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/PebbleDevice.kt
index 0037a2e5..583a6125 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/PebbleDevice.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/PebbleDevice.kt
@@ -107,6 +107,7 @@ sealed interface ConnectedPebbleDevice :
ConnectedPebble.AppMessages,
ConnectedPebble.Music,
ConnectedPebble.PKJS,
+ ConnectedPebble.CompanionAppControl,
ConnectedPebble.Screenshot,
ConnectedPebble.Language
@@ -118,10 +119,10 @@ sealed interface ConnectedPebbleDevice :
*/
object ConnectedPebble {
interface AppMessages {
- val inboundAppMessages: Flow
val transactionSequence: Iterator
suspend fun sendAppMessage(appMessageData: AppMessageData): AppMessageResult
suspend fun sendAppMessageResult(appMessageResult: AppMessageResult)
+ fun inboundAppMessages(appUuid: Uuid): Flow
}
interface Debug {
@@ -170,13 +171,19 @@ object ConnectedPebble {
interface AppRunState {
suspend fun launchApp(uuid: Uuid)
+ suspend fun stopApp(uuid: Uuid)
val runningApp: StateFlow
}
interface PKJS {
+ @Deprecated("Use more generic currentCompanionAppSession instead and cast if necessary")
val currentPKJSSession: StateFlow
}
+ interface CompanionAppControl {
+ val currentCompanionAppSession: StateFlow
+ }
+
interface Time {
suspend fun updateTime()
}
@@ -215,6 +222,7 @@ object ConnectedPebble {
val coreDump: CoreDump,
val music: Music,
val pkjs: PKJS,
+ val companionAppControl: CompanionAppControl,
val devConnection: DevConnection,
val screenshot: Screenshot,
val language: Language,
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/RealPebbleDevice.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/RealPebbleDevice.kt
index 7a548716..b18ade6f 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/RealPebbleDevice.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/RealPebbleDevice.kt
@@ -247,6 +247,7 @@ internal class RealConnectedPebbleDevice(
ConnectedPebble.CoreDump by services.coreDump,
ConnectedPebble.Music by services.music,
ConnectedPebble.PKJS by services.pkjs,
+ ConnectedPebble.CompanionAppControl by services.companionAppControl,
ConnectedPebble.DevConnection by services.devConnection,
ConnectedPebble.Screenshot by services.screenshot,
ConnectedPebble.Language by services.language {
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/TransportConnector.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/TransportConnector.kt
index 3ad5e2fd..4298e202 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/TransportConnector.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/TransportConnector.kt
@@ -13,7 +13,7 @@ import io.rebble.libpebblecommon.connection.endpointmanager.AppOrderManager
import io.rebble.libpebblecommon.connection.endpointmanager.DebugPebbleProtocolSender
import io.rebble.libpebblecommon.connection.endpointmanager.FirmwareUpdater
import io.rebble.libpebblecommon.connection.endpointmanager.LanguagePackInstaller
-import io.rebble.libpebblecommon.connection.endpointmanager.PKJSLifecycleManager
+import io.rebble.libpebblecommon.connection.endpointmanager.CompanionAppLifecycleManager
import io.rebble.libpebblecommon.connection.endpointmanager.audio.VoiceSessionManager
import io.rebble.libpebblecommon.connection.endpointmanager.blobdb.BlobDB
import io.rebble.libpebblecommon.connection.endpointmanager.musiccontrol.MusicControlManager
@@ -129,7 +129,7 @@ class RealPebbleConnector(
private val appMessageService: AppMessageService,
private val timelineActionManager: TimelineActionManager,
private val blobDB: BlobDB,
- private val pkjsLifecycleManager: PKJSLifecycleManager,
+ private val companionAppLifecycleManager: CompanionAppLifecycleManager,
private val appFetchProvider: AppFetchProvider,
private val debugPebbleProtocolSender: DebugPebbleProtocolSender,
private val logDumpService: LogDumpService,
@@ -249,7 +249,7 @@ class RealPebbleConnector(
timelineActionManager.init()
appFetchProvider.init(watchInfo.platform.watchType)
appMessageService.init()
- pkjsLifecycleManager.init(identifier, watchInfo)
+ companionAppLifecycleManager.init(identifier, watchInfo)
phoneControlManager.init()
musicControlManager.init()
voiceSessionManager.init()
@@ -269,7 +269,8 @@ class RealPebbleConnector(
logs = logDumpService,
coreDump = getBytesService,
music = musicService,
- pkjs = pkjsLifecycleManager,
+ pkjs = companionAppLifecycleManager,
+ companionAppControl = companionAppLifecycleManager,
devConnection = devConnectionManager,
screenshot = screenshotService,
language = languagePackInstaller,
@@ -289,4 +290,4 @@ private val FW_3_0_0 = FirmwareVersion(
isRecovery = false,
isSlot0 = false,
isDualSlot = false,
-)
\ No newline at end of file
+)
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/devconnection/DevConnectionManager.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/devconnection/DevConnectionManager.kt
index 383103f1..3bf2d85e 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/devconnection/DevConnectionManager.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/devconnection/DevConnectionManager.kt
@@ -1,12 +1,10 @@
package io.rebble.libpebblecommon.connection.devconnection
-import co.touchlab.kermit.Logger
import io.rebble.libpebblecommon.connection.ConnectedPebble
import io.rebble.libpebblecommon.connection.PebbleIdentifier
import io.rebble.libpebblecommon.connection.PebbleProtocolHandler
-import io.rebble.libpebblecommon.connection.endpointmanager.PKJSLifecycleManager
+import io.rebble.libpebblecommon.connection.endpointmanager.CompanionAppLifecycleManager
import io.rebble.libpebblecommon.di.ConnectionCoroutineScope
-import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -24,7 +22,7 @@ class DevConnectionManager(
private val transport: Flow,
private val identifier: PebbleIdentifier,
private val protocolHandler: PebbleProtocolHandler,
- private val pkjsLifecycleManager: PKJSLifecycleManager,
+ private val companionAppLifecycleManager: CompanionAppLifecycleManager,
private val scope: ConnectionCoroutineScope
): ConnectedPebble.DevConnection {
private val job: MutableStateFlow = MutableStateFlow(null)
@@ -35,7 +33,7 @@ class DevConnectionManager(
false
)
override suspend fun startDevConnection() {
- val inboundPKJSLogs = pkjsLifecycleManager.currentPKJSSession.flatMapLatest { it?.logMessages ?: emptyFlow() }
+ val inboundPKJSLogs = companionAppLifecycleManager.currentPKJSSession.flatMapLatest { it?.logMessages ?: emptyFlow() }
job.value = scope.launch {
var last: DevConnectionTransport? = null
transport.onCompletion {
@@ -59,4 +57,4 @@ class DevConnectionManager(
override suspend fun stopDevConnection() {
job.value?.cancel()
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.kt
new file mode 100644
index 00000000..0cf33933
--- /dev/null
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.kt
@@ -0,0 +1,132 @@
+package io.rebble.libpebblecommon.connection.endpointmanager
+
+import co.touchlab.kermit.Logger
+import io.rebble.libpebblecommon.connection.CompanionApp
+import io.rebble.libpebblecommon.connection.ConnectedPebble
+import io.rebble.libpebblecommon.connection.PebbleIdentifier
+import io.rebble.libpebblecommon.database.dao.LockerEntryRealDao
+import io.rebble.libpebblecommon.database.entity.LockerEntry
+import io.rebble.libpebblecommon.di.ConnectionCoroutineScope
+import io.rebble.libpebblecommon.disk.pbw.PbwApp
+import io.rebble.libpebblecommon.js.CompanionAppDevice
+import io.rebble.libpebblecommon.js.PKJSApp
+import io.rebble.libpebblecommon.locker.Locker
+import io.rebble.libpebblecommon.locker.LockerPBWCache
+import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo
+import io.rebble.libpebblecommon.services.WatchInfo
+import io.rebble.libpebblecommon.services.app.AppRunStateService
+import io.rebble.libpebblecommon.services.appmessage.AppMessageService
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onEach
+
+class CompanionAppLifecycleManager(
+ private val lockerPBWCache: LockerPBWCache,
+ private val lockerEntryDao: LockerEntryRealDao,
+ private val appRunStateService: AppRunStateService,
+ private val appMessagesService: AppMessageService,
+ private val locker: Locker,
+ private val scope: ConnectionCoroutineScope
+): ConnectedPebble.PKJS, ConnectedPebble.CompanionAppControl {
+ companion object {
+ private val logger = Logger.withTag(CompanionAppLifecycleManager::class.simpleName!!)
+ }
+
+ private lateinit var device: CompanionAppDevice
+
+
+ private val runningApp: MutableStateFlow = MutableStateFlow(null)
+ @Deprecated("Use more generic currentCompanionAppSession instead and cast if necessary")
+ override val currentPKJSSession: StateFlow = PKJSStateFlow(runningApp)
+
+ override val currentCompanionAppSession: StateFlow
+ get() = runningApp.asStateFlow()
+
+ private suspend fun handleAppStop() {
+ runningApp.value?.stop()
+ runningApp.value = null
+ }
+
+ private suspend fun handleNewRunningApp(lockerEntry: LockerEntry, scope: CoroutineScope) {
+ try {
+ val pbw = PbwApp(lockerPBWCache.getPBWFileForApp(lockerEntry.id, lockerEntry.version, locker))
+ if (runningApp.value != null) {
+ logger.w { "App ${lockerEntry.id} is already running, stopping it before starting a new one" }
+ runningApp.value?.stop()
+ }
+
+ runningApp.value = createCompanionApp(pbw, lockerEntry).also {
+ it?.start(scope)
+ }
+ } catch (e: Exception) {
+ logger.e(e) { "Failed to init Companion app for app ${lockerEntry.id}: ${e.message}" }
+ runningApp.value = null
+ return
+ }
+ }
+
+ private fun createCompanionApp(
+ pbw: PbwApp,
+ lockerEntry: LockerEntry
+ ): CompanionApp? {
+ return when {
+ pbw.hasPKJS -> {
+ val jsPath = lockerPBWCache.getPKJSFileForApp(lockerEntry.id, lockerEntry.version)
+ PKJSApp(
+ device,
+ jsPath,
+ pbw.info,
+ lockerEntry,
+ )
+ }
+ else -> {
+ logger.v { "App ${lockerEntry.id} does not have a PKJS, falling back to platform based PebbleKit" }
+ createPlatformSpecificCompanionAppControl(device, pbw.info)
+ }
+ }
+ }
+
+ fun init(identifier: PebbleIdentifier, watchInfo: WatchInfo) {
+ this.device = CompanionAppDevice(
+ identifier,
+ watchInfo,
+ appMessagesService
+ )
+ appRunStateService.runningApp.onEach {
+ handleAppStop()
+ if (it != null) {
+ val lockerEntry = lockerEntryDao.getEntry(it)
+ lockerEntry?.let { handleNewRunningApp(lockerEntry, scope) }
+ }
+ }.onCompletion {
+ // Unsure if this is needed
+ handleAppStop()
+ }.launchIn(scope)
+ }
+}
+
+/**
+ * Hack to keep backwards compatibilty with the old ConnectedPebble.PKJS interface. It creates a state flow that only
+ * exposes PKJSApp instances
+ */
+@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
+class PKJSStateFlow(private val runningAppStateFlow: StateFlow): StateFlow {
+ override val value: PKJSApp?
+ get() = runningAppStateFlow.value as? PKJSApp
+ override val replayCache: List
+ get() = runningAppStateFlow.replayCache.map { it as? PKJSApp }
+
+ override suspend fun collect(collector: FlowCollector): Nothing {
+ runningAppStateFlow.map { it as? PKJSApp }.collect(collector)
+ throw IllegalStateException("This collect should never stop because parent is a state flow")
+ }
+}
+
+expect fun createPlatformSpecificCompanionAppControl(device: CompanionAppDevice, appInfo: PbwAppInfo): CompanionApp?
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/PKJSLifecycleManager.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/PKJSLifecycleManager.kt
deleted file mode 100644
index 69d1c6f6..00000000
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/PKJSLifecycleManager.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package io.rebble.libpebblecommon.connection.endpointmanager
-
-import co.touchlab.kermit.Logger
-import io.rebble.libpebblecommon.connection.ConnectedPebble
-import io.rebble.libpebblecommon.connection.PebbleIdentifier
-import io.rebble.libpebblecommon.database.dao.LockerEntryRealDao
-import io.rebble.libpebblecommon.database.entity.LockerEntry
-import io.rebble.libpebblecommon.di.ConnectionCoroutineScope
-import io.rebble.libpebblecommon.disk.pbw.PbwApp
-import io.rebble.libpebblecommon.js.PKJSApp
-import io.rebble.libpebblecommon.js.PebbleJSDevice
-import io.rebble.libpebblecommon.locker.Locker
-import io.rebble.libpebblecommon.locker.LockerPBWCache
-import io.rebble.libpebblecommon.services.WatchInfo
-import io.rebble.libpebblecommon.services.app.AppRunStateService
-import io.rebble.libpebblecommon.services.appmessage.AppMessageService
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
-
-class PKJSLifecycleManager(
- private val lockerPBWCache: LockerPBWCache,
- private val lockerEntryDao: LockerEntryRealDao,
- private val appRunStateService: AppRunStateService,
- private val appMessagesService: AppMessageService,
- private val locker: Locker,
- private val scope: ConnectionCoroutineScope
-): ConnectedPebble.PKJS {
- companion object {
- private val logger = Logger.withTag(PKJSLifecycleManager::class.simpleName!!)
- }
- private lateinit var device: PebbleJSDevice
- private val runningApp: MutableStateFlow = MutableStateFlow(null)
- override val currentPKJSSession: StateFlow = runningApp.asStateFlow()
-
- private suspend fun handleAppStop() {
- runningApp.value?.stop()
- runningApp.value = null
- }
-
- private suspend fun handleNewRunningApp(lockerEntry: LockerEntry, scope: CoroutineScope) {
- try {
- val pbw = PbwApp(lockerPBWCache.getPBWFileForApp(lockerEntry.id, lockerEntry.version, locker))
- if (runningApp.value != null) {
- logger.w { "App ${lockerEntry.id} is already running, stopping it before starting a new one" }
- runningApp.value?.stop()
- }
- if (!pbw.hasPKJS) {
- logger.v { "App ${lockerEntry.id} does not have PKJS" }
- runningApp.value = null
- return
- }
-
- val jsPath = lockerPBWCache.getPKJSFileForApp(lockerEntry.id, lockerEntry.version)
- runningApp.value = PKJSApp(
- device,
- jsPath,
- pbw.info,
- lockerEntry,
- ).apply {
- start(scope)
- }
- } catch (e: Exception) {
- logger.e(e) { "Failed to init PKJS for app ${lockerEntry.id}: ${e.message}" }
- runningApp.value = null
- return
- }
- }
-
- fun init(identifier: PebbleIdentifier, watchInfo: WatchInfo) {
- this.device = PebbleJSDevice(
- identifier,
- watchInfo,
- appMessagesService
- )
- appRunStateService.runningApp.onEach {
- handleAppStop()
- if (it != null) {
- val lockerEntry = lockerEntryDao.getEntry(it)
- lockerEntry?.let { handleNewRunningApp(lockerEntry, scope) }
- }
- }.onCompletion {
- // Unsure if this is needed
- handleAppStop()
- }.launchIn(scope)
- }
-}
\ No newline at end of file
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/di/LibPebbleModule.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/di/LibPebbleModule.kt
index 60b990bf..4be7b0c3 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/di/LibPebbleModule.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/di/LibPebbleModule.kt
@@ -71,7 +71,7 @@ import io.rebble.libpebblecommon.connection.endpointmanager.AppOrderManager
import io.rebble.libpebblecommon.connection.endpointmanager.DebugPebbleProtocolSender
import io.rebble.libpebblecommon.connection.endpointmanager.FirmwareUpdater
import io.rebble.libpebblecommon.connection.endpointmanager.LanguagePackInstaller
-import io.rebble.libpebblecommon.connection.endpointmanager.PKJSLifecycleManager
+import io.rebble.libpebblecommon.connection.endpointmanager.CompanionAppLifecycleManager
import io.rebble.libpebblecommon.connection.endpointmanager.RealFirmwareUpdater
import io.rebble.libpebblecommon.connection.endpointmanager.audio.VoiceSessionManager
import io.rebble.libpebblecommon.connection.endpointmanager.blobdb.BlobDB
@@ -476,7 +476,7 @@ fun initKoin(
scopedOf(::TimelineActionManager)
scopedOf(::AppFetchProvider)
scopedOf(::DebugPebbleProtocolSender)
- scopedOf(::PKJSLifecycleManager)
+ scopedOf(::CompanionAppLifecycleManager)
scopedOf(::BlobDB)
scopedOf(::PhoneControlManager)
scopedOf(::MusicControlManager)
@@ -494,7 +494,7 @@ fun initKoin(
},
identifier = get(),
protocolHandler = get(),
- pkjsLifecycleManager = get(),
+ companionAppLifecycleManager = get(),
scope = get(),
)
}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PebbleJSDevice.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/CompanionAppDevice.kt
similarity index 81%
rename from libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PebbleJSDevice.kt
rename to libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/CompanionAppDevice.kt
index 29d7f525..8605ddd9 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PebbleJSDevice.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/CompanionAppDevice.kt
@@ -4,8 +4,8 @@ import io.rebble.libpebblecommon.connection.ConnectedPebble
import io.rebble.libpebblecommon.connection.PebbleIdentifier
import io.rebble.libpebblecommon.services.WatchInfo
-class PebbleJSDevice(
+class CompanionAppDevice(
val identifier: PebbleIdentifier,
val watchInfo: WatchInfo,
appMessages: ConnectedPebble.AppMessages,
-): ConnectedPebble.AppMessages by appMessages
\ No newline at end of file
+): ConnectedPebble.AppMessages by appMessages
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/JsRunner.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/JsRunner.kt
index 09e510fa..58a34571 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/JsRunner.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/JsRunner.kt
@@ -14,7 +14,7 @@ abstract class JsRunner(
val appInfo: PbwAppInfo,
val lockerEntry: LockerEntry,
val jsPath: Path,
- val device: PebbleJSDevice,
+ val device: CompanionAppDevice,
private val urlOpenRequests: Channel,
) {
abstract suspend fun start()
@@ -55,4 +55,4 @@ class AppMessageRequest(
data class Sent(val result: AppMessageResult) : State()
}
val state = MutableStateFlow(State.Pending)
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSApp.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSApp.kt
index b52e72e8..f6328ac3 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSApp.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSApp.kt
@@ -1,6 +1,7 @@
package io.rebble.libpebblecommon.js
import co.touchlab.kermit.Logger
+import io.rebble.libpebblecommon.connection.CompanionApp
import io.rebble.libpebblecommon.connection.ConnectedPebble
import io.rebble.libpebblecommon.database.entity.LockerEntry
import io.rebble.libpebblecommon.di.LibPebbleKoinComponent
@@ -51,11 +52,11 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.uuid.Uuid
class PKJSApp(
- val device: PebbleJSDevice,
+ val device: CompanionAppDevice,
private val jsPath: Path,
val appInfo: PbwAppInfo,
val lockerEntry: LockerEntry,
-): LibPebbleKoinComponent {
+): LibPebbleKoinComponent, CompanionApp {
companion object {
private val logger = Logger.withTag(PKJSApp::class.simpleName!!)
}
@@ -81,12 +82,7 @@ class PKJSApp(
}
private fun launchIncomingAppMessageHandler(device: ConnectedPebble.AppMessages, scope: CoroutineScope) {
- device.inboundAppMessages.onEach { appMessageData ->
- if (appMessageData.uuid != uuid) {
- logger.v { "App message for different app: ${appMessageData.uuid} != $uuid, sending NACK" }
- replyNACK(appMessageData.transactionId)
- return@onEach
- }
+ device.inboundAppMessages(uuid).onEach { appMessageData ->
jsRunner?.let { runner ->
if (!runner.readyState.value) {
logger.w { "JsRunner not ready, waiting" }
@@ -155,7 +151,7 @@ class PKJSApp(
)
}
- suspend fun start(connectionScope: CoroutineScope) {
+ override suspend fun start(connectionScope: CoroutineScope) {
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
logger.e(throwable) { "Unhandled exception in PKJSApp: ${throwable.message}" }
}
@@ -167,7 +163,7 @@ class PKJSApp(
jsRunner?.start() ?: error("JsRunner not initialized")
}
- suspend fun stop() {
+ override suspend fun stop() {
jsRunner?.stop()
runningScope?.cancel()
jsRunner = null
@@ -260,4 +256,4 @@ private fun String.toAppMessageData(appInfo: PbwAppInfo, transactionId: UByte):
uuid = Uuid.parse(appInfo.uuid),
data = tuples
)
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSInterface.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSInterface.kt
index 09d5d26a..0b70cd77 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSInterface.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PKJSInterface.kt
@@ -4,14 +4,13 @@ import io.rebble.libpebblecommon.connection.LibPebble
import io.rebble.libpebblecommon.database.entity.buildTimelineNotification
import io.rebble.libpebblecommon.packets.blobdb.TimelineItem
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.IO
import kotlinx.coroutines.runBlocking
import kotlin.time.Clock
import kotlin.uuid.Uuid
abstract class PKJSInterface(
protected val jsRunner: JsRunner,
- protected val device: PebbleJSDevice,
+ protected val device: CompanionAppDevice,
private val libPebble: LibPebble,
private val jsTokenUtil: JsTokenUtil,
) {
@@ -71,4 +70,4 @@ abstract class PKJSInterface(
}
return url
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PrivatePKJSInterface.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PrivatePKJSInterface.kt
index 8757785b..2c4c3052 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PrivatePKJSInterface.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/js/PrivatePKJSInterface.kt
@@ -6,7 +6,6 @@ import io.rebble.cobble.shared.data.js.fromWatchInfo
import io.rebble.libpebblecommon.services.appmessage.AppMessageResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.IO
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter
@@ -23,7 +22,7 @@ import kotlin.uuid.Uuid
abstract class PrivatePKJSInterface(
protected val jsRunner: JsRunner,
- private val device: PebbleJSDevice,
+ private val device: CompanionAppDevice,
protected val scope: CoroutineScope,
private val outgoingAppMessages: MutableSharedFlow,
private val logMessages: MutableSharedFlow,
@@ -180,4 +179,4 @@ abstract class PrivatePKJSInterface(
logger.v { "privateFnConfirmReadySignal($success)" }
jsRunner.onReadyConfirmed(success)
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/main.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/main.kt
index 8e29234b..678b1ded 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/main.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/main.kt
@@ -2,4 +2,6 @@ package io.rebble.libpebblecommon
import io.rebble.libpebblecommon.packets.PhoneAppVersion
-expect fun getPlatform(): PhoneAppVersion.OSType
\ No newline at end of file
+expect fun getPlatform(): PhoneAppVersion.OSType
+
+expect fun performPlatformSpecificInit()
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppMessage.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppMessage.kt
index fd5f7530..daa78041 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppMessage.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppMessage.kt
@@ -75,7 +75,7 @@ class AppMessageTuple() : StructMappable() {
}.valueNumber
}
- val dataAsUnsignedNumber: Long
+ val dataAsUnsignedNumber: ULong
get() {
val obj = when (val size = dataLength.get().toInt()) {
1 -> SUByte(StructMapper())
@@ -85,7 +85,7 @@ class AppMessageTuple() : StructMappable() {
}
return obj.apply {
fromBytes(DataBuffer(data.get()))
- }.valueNumber
+ }.valueNumber.toULong()
}
override fun toString(): String {
@@ -311,4 +311,4 @@ fun appmessagePacketsRegister() {
AppMessage.endpoint,
AppMessage.Message.AppMessageNACK.value
) { AppMessage.AppMessageNACK() }
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/app/AppRunStateService.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/app/AppRunStateService.kt
index d05280e1..eacfd93d 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/app/AppRunStateService.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/app/AppRunStateService.kt
@@ -27,7 +27,7 @@ class AppRunStateService(
runningApp.first { it == uuid }
}
- suspend fun stopApp(uuid: Uuid) {
+ override suspend fun stopApp(uuid: Uuid) {
protocolHandler.send(AppRunStateMessage.AppRunStateStop(uuid))
}
@@ -49,4 +49,4 @@ class AppRunStateService(
}
}.launchIn(scope)
}
-}
\ No newline at end of file
+}
diff --git a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/appmessage/AppMessageService.kt b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/appmessage/AppMessageService.kt
index fb70090d..d99e321f 100644
--- a/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/appmessage/AppMessageService.kt
+++ b/libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/services/appmessage/AppMessageService.kt
@@ -11,20 +11,24 @@ import io.rebble.libpebblecommon.services.ProtocolService
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlin.uuid.Uuid
+private const val APPMESSAGE_BUFFER_SIZE = 16
+
class AppMessageService(
private val protocolHandler: PebbleProtocolHandler,
private val scope: ConnectionCoroutineScope
) : ProtocolService, ConnectedPebble.AppMessages {
- private val receivedMessages = Channel(Channel.BUFFERED)
- override val inboundAppMessages: Flow = receivedMessages.consumeAsFlow().shareIn(scope, started = SharingStarted.Lazily)
+ private val receivedMessages = HashMap>()
override val transactionSequence: Iterator = AppMessageTransactionSequence().iterator()
+ private val mapAccessMutex = Mutex()
private var appMessageCallback: CompletableDeferred? = null
fun init() {
@@ -34,12 +38,14 @@ class AppMessageService(
appMessageCallback?.complete(it)
appMessageCallback = null
}
+
is AppMessage.AppMessageNACK -> {
appMessageCallback?.complete(it)
appMessageCallback = null
}
+
is AppMessage.AppMessagePush -> {
- receivedMessages.trySend(it.appMessageData())
+ getReceivedMessagesChannel(it.uuid.get()).trySend(it.appMessageData())
}
}
}.launchIn(scope)
@@ -79,6 +85,18 @@ class AppMessageService(
suspend fun send(packet: AppCustomizationSetStockAppTitleMessage) {
protocolHandler.send(packet)
}
+
+ override fun inboundAppMessages(appUuid: Uuid): Flow {
+ return suspend { getReceivedMessagesChannel(appUuid) }.asFlow().flatMapConcat { it.receiveAsFlow() }
+ }
+
+ private suspend fun getReceivedMessagesChannel(appUuid: Uuid): Channel {
+ receivedMessages[appUuid]?.let { return it }
+
+ return mapAccessMutex.withLock {
+ receivedMessages.getOrPut(appUuid) { Channel(APPMESSAGE_BUFFER_SIZE) }
+ }
+ }
}
private fun Map.toAppMessageTuples(): List {
@@ -121,4 +139,4 @@ private fun AppMessage.AppMessagePush.appMessageData(): AppMessageData {
}
private fun AppMessage.AppMessageACK.appMessageResult() = AppMessageResult.ACK(transactionId.get())
-private fun AppMessage.AppMessageNACK.appMessageResult() = AppMessageResult.NACK(transactionId.get())
\ No newline at end of file
+private fun AppMessage.AppMessageNACK.appMessageResult() = AppMessageResult.NACK(transactionId.get())
diff --git a/libpebble3/src/iosMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.ios.kt b/libpebble3/src/iosMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.ios.kt
new file mode 100644
index 00000000..5c329b1f
--- /dev/null
+++ b/libpebble3/src/iosMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.ios.kt
@@ -0,0 +1,13 @@
+package io.rebble.libpebblecommon.connection.endpointmanager
+
+import io.rebble.libpebblecommon.connection.CompanionApp
+import io.rebble.libpebblecommon.js.CompanionAppDevice
+import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo
+
+actual fun createPlatformSpecificCompanionAppControl(
+ device: CompanionAppDevice,
+ appInfo: PbwAppInfo
+): CompanionApp? {
+ // PebbleKit iOS is not supported (yet?)
+ return null
+}
diff --git a/libpebble3/src/iosMain/kotlin/main.kt b/libpebble3/src/iosMain/kotlin/main.kt
index f2efe2d1..0b5bd8eb 100644
--- a/libpebble3/src/iosMain/kotlin/main.kt
+++ b/libpebble3/src/iosMain/kotlin/main.kt
@@ -2,4 +2,6 @@ package io.rebble.libpebblecommon
import io.rebble.libpebblecommon.packets.PhoneAppVersion
-actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.IOS
\ No newline at end of file
+actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.IOS
+
+actual fun performPlatformSpecificInit() {}
diff --git a/libpebble3/src/jvmMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.jvm.kt b/libpebble3/src/jvmMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.jvm.kt
new file mode 100644
index 00000000..246f8cbc
--- /dev/null
+++ b/libpebble3/src/jvmMain/kotlin/io/rebble/libpebblecommon/connection/endpointmanager/CompanionAppLifecycleManager.jvm.kt
@@ -0,0 +1,13 @@
+package io.rebble.libpebblecommon.connection.endpointmanager
+
+import io.rebble.libpebblecommon.connection.CompanionApp
+import io.rebble.libpebblecommon.js.CompanionAppDevice
+import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo
+
+actual fun createPlatformSpecificCompanionAppControl(
+ device: CompanionAppDevice,
+ appInfo: PbwAppInfo
+): CompanionApp? {
+ // No JVM specific PebbleKit exists (yet?)
+ return null
+}
diff --git a/libpebble3/src/jvmMain/kotlin/main.kt b/libpebble3/src/jvmMain/kotlin/main.kt
index 702366b3..3210e453 100644
--- a/libpebble3/src/jvmMain/kotlin/main.kt
+++ b/libpebble3/src/jvmMain/kotlin/main.kt
@@ -2,4 +2,6 @@ package io.rebble.libpebblecommon
import io.rebble.libpebblecommon.packets.PhoneAppVersion
-actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.Unknown
\ No newline at end of file
+actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.Unknown
+
+actual fun performPlatformSpecificInit() {}