Skip to content

Instantly share code, notes, and snippets.

@matejdro
Last active May 11, 2025 17:48
Show Gist options
  • Save matejdro/daa54e308b47aaec29a8c2856fd739a8 to your computer and use it in GitHub Desktop.
Save matejdro/daa54e308b47aaec29a8c2856fd739a8 to your computer and use it in GitHub Desktop.

PebbleKit Android V2

Old PebbleKit Android depends on app and Pebble app sending broadcasts on every update. Not only is this extremely insecure (every other app on the phone can listen in on those), it is also not working anymore when targeting new SDK versions, so it cannot be used for the new apps (Cobble / Core Devices).

Here is my proposal on how can we create a modern, more convenient and more secure version. It is loosly based on the way WearOS communications work.

Note that "Pebble App" below refers to any Android app that communicates with the watch directly (such as Cobble).

I presume that most apps (both companion apps and the Pebble App) will be written in Kotlin, so following examples are in Kotlin. To support apps written in Java, we can either write the SDK for Java and provide Kotlin wrappers or write it for Kotlin and provide Java wrappers (since Java apps are in minority nowadays, I would go for the latter).

Linking

Every Pebble OS app that wants to communicate with a companion app, should have a companionApp section in its package.json:

"pebble": {
 "companionApp": {
	"android": {
	  "url": "https://play.google.com/store/apps/details?id=...",
	  "apps": [
		 {
			"package": "com.app1",
			"sha256CertFingerprint": "..."
		 },
		 {
			"package": "com.app1",
			"sha256CertFingerprint": "..."
		 },
		 {
			"package": "com.app2",
			"sha256CertFingerprint": "..."
		 }
	  ]
	}
 }
}

Section contains an url that points to where user can download the companion app from (so it can be shown/linked to from the Pebble app UI). Then it has a list of apps. Every entry in that list has a combination of app's package and the SHA256 fingerprint of the companion app’s signing certificate. This ensures that watch app can only communicate with the genuine companion app of that watch app and prevents impersonation attacks.

PebbleSender

To initiate data sending, companion app has to create a PebbleSender instance. That class can then send data to the Pebble app.

val pebbleSender = PebbleSender(context);

val messageResult: PebbleSendResult = pebbleSender.sendDataToPebble(watchAppUuid, pebbleDictionaryData)
val startResult: PebbleSendResult = pebbleSender.startAppOnPebble(watchAppUuid)
val stopResult: PebbleSendResult = pebbleSender.stopAppOnPebble()
// Returns an object with watch's platform, OS version etc. or null if not connected
val watchInfo: PebbleWatchInfo? = pebbleSender.getConnectedWatchInfo()
// Returns null if there is no Pebble app installed
val pebbleAppPackage: String? = pebbleSender.getPebbleAppPackage()

// After we are done using it
pebbleSender.close()

All functions on the PebbleSender would be suspend, meaning that they would be async and wait for the message to be delivered, making dealing with waiting for acks/nacks much esasier than it was with the old SDK. PebbleSendResult could be an enum, something like that:

enum class PebbleSendResult {
	SUCCESS,
	FAILED_NO_PEBBLE_APP_INSTALLED,
	FAILED_WATCH_DISCONNECTED,
	FAILED_WATCH_NACKED,
	FAILED_TIMEOUT
}

Behind the scenes, this class would create a ServiceConnection that would bind to a service inside Pebble app. Unlike broadcasts, service binding gives the bound service information about originating process, so Pebble app can prevent data from an unauthorized app being sent to the watch. However, I would personally still keep start/stop app calls free for all apps to allow for automation use cases (automation apps like Tasker that would open some apps at specific times, for example).

Receiving watchapp events

To receive app lifecyle related events from the watch, companion app would have to create a service that extends PebbleWatchappEventsListenerService and add it to the manifest, something like that:

 <service android:name=".MyPebbleListenerService" >
   <intent-filter>
     <action android:name="io.rebble.BIND_TO_WATCHAPP_EVENTS" />
   </intent-filter>
 </service>

Then, after a watch app with defined companionApp section in its package.json would open on the watch, Pebble app would search through all apps listed and attempt to find a match of a package name and app signing certificate.

When a match is find, it would bind to a listener serivce in the target app. This service would stay bound and active until several seconds after user has closed the watch app. This will allow the target app to stay alive and not be killed by the Android's aggressive war on background work, as long as the watch app is open.

Service would have several useful callbacks that apps can override:

public class MyPebbleListenerService extends PebbleWatchappEventsListenerService {
  override fun onAppStarted(watchAppUUID: UUID) {} 
	override fun onAppStopped(watchAppUUID: UUID) {}  
	override fun onMessageReceived(watchAppUUID: UUID, data: PebbleDictionary, reply: MessageReply) {} 
}

Note that the onMessageReceived callback receives a Reply object. This allows app to send back ACK/NACK of the message to the watch (as with the sender, both functions are suspend):

	override fun onMessageReceived(watchAppUUID: UUID, data: PebbleDictionary, reply: MessageReply) {
		coroutineScope.launch {
			val isMessageValid = processMessage(data)
			if (isMessageValid) {
				reply.acknowledge()	
			} else {
				reply.notAcknowledge()	
			}
		}
	} 

Receiving general events

Not all events are related to the watchapp lifecycle. For those events, PebbleGeneralEventsListenerService can be used in a similar manner to the watchapp event receiver.

public class MyPebbleGeneralEventListenerService extends PebbleGeneralEventsListenerService {
  override fun onWatchConnected(watchInfo: PebbleWatchInfo) {} 
	override fun onWatchDisconnected() {} 
	override fun onDataLogReceived(logUUID: UUID, timestamp: Long, tag: Long, data: ByteArray) {}
}

Then, the listener would be declared in the manifest. However, this time, app has to define every event type separately. This allows the companion app to filter on which event would it be woken up, saving on battery life

 <service android:name=".MyPebbleGeneralEventListenerService" >
   <intent-filter>
     <action android:name="io.rebble.RECEIVE_DATALOG" />
     <action android:name="io.rebble.RECEIVE_WATCH_CONNECTED" />
     <action android:name="io.rebble.RECEIVE_WATCH_DISCONNECTED" />
   </intent-filter>
 </service>

Because these events are not tied to the specific app's lifecycle, Pebble app would bind to the app's service, trigger the event and unbind after several seconds, so the app is not kept alive for longer than it needs to.

Protecting against fake Pebble apps

With the app's package names and certificate hashes in the pbw file, we have a protection against fake companion apps trying to talk to the watchapps. However, the reverse can still happen:

For example, someone could create a malicious flashlight app that would, after installed, bind to all companion apps and exfiltrate their data without the user knowing anything.

One way of solving this would be hardcoding specific Pebble app's package name and certificate to the SDK, but I feel this would go against Pebble's hackability ethos as it would completely block all 3rd party Pebble apps from talking to the PebbleKit V2 apps.

I don't have a perfect solution to this, but the best I could come with, was a prompt "Do you want this app to connect to the Pebble app X"? whenever user opens a companion app for the first time. We could include UI and most of the code in the SDK, so it would be easy to integrate into the apps.

Would appreciate some more brainstorming and ideas on this.

Wishful thinking

Above more or less replicate existing PebbleKit Android features, but there are several avenues of making user experience with those apps better. This section is basically my wishful thinking on what we could do now that we have control over both companion app and watch's firmware.

Closing to last app

Currently, Pebble OS has no concept of app backstack. Any app is closed back to the watchface. This is okay in most cases, but it's annoying for apps that open on their own. For example, if user is on the Timer app and a notificiations watchapp opens up, closing that app would go back to the watchface instead of to the Timer that was open before, frustrating the user.

Ideally, this should be fixed on the Pebble OS side, but if this is unfeasible or we are not willing to this, there is a workaround that we can do this to emulate this functionality from phone side.

Only thing we have to do, is add a version of the app open callback that supplies the UUID of the last open app:

override fun onAppStarted(watchAppUUID: UUID, lastOpenApp: UUID?) {} 

Then, in above example, after the notification app is opened over Timer, it would receive Timer's UUID in lastOpenApp and the companion app can save this. When user closes, companion app can call startAppOnPebble with this UUID, re-opening the Timer that was open before

Background syncing to the watch

Currently, watchapps can only receive data when they are open. This is not ideal:

  • It slows down user's experience, because watchapps must sync on every startup
  • It makes usage without phone much worse. If I last opened app Y last week and I open it today without bluetooth connection, app could, at best, show last week's data.

As a solution for this that neatly slots into existing SDK, I propose adding an ability for an app to push data into Pebble's storage. For example:

pebbleSender.setWatchStorage(appUUID: UUID, key: Int, value: ..., urgent: Boolean = false)

This can then be read on the watch's side using persist_read C functions.

Of course, after setWatchStorage is called, nothing would happen at first. Then Pebble app would periodically (maybe once an hour or so) batch sync all pending updates of all companion apps to the watch. With two exceptions:

  • If the target watchapp is currently open, it would sync immediately. Similarly, after watchapp opens, it would sync all pending data for that app immediately upon open, so user sees the freshest data. For this to work, C api would need some way to listen for storage updates
  • If the urgent parameter is set to true, data would be synced as soon as possible. This is for more time sensitive pieces of data. I assume this would especially come in handy with the upcoming complications system

Add missing APIs

For example:

  • Microphone API (both access to raw audio data and to direct access to transcribed text without pingponging to the watch and back)
  • Timeline API (for faster and offline syncing, without involvement of the Pebble servers)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment