A work-in-progress implementation of the new modern PebbleKit API for communication between phone companion apps and the watchapps on the Pebble-OS running watches.
Based on this design document.
(See sample app for examples)
During the experimental phase, library is not yet pushed to the maven central. To use it, you will have to use git submodules to include it:
- Download the repository using
git submodule add https://github.com/pebble-dev/PebbleKitAndroid2.git
(If your project does not use git, you can just download this repo and put it intoPebbleKitAndroid2folder) - Include build in your app's
settings.gradle(.kts):
includeBuild("PebbleKitAndroid2") {
dependencySubstitution {
substitute(module("io.rebble.pebblekit2:client"))
.using(project(":client"))
}
}- Add dependency in your app's
build.gradle(.kts):implementation("io.rebble.pebblekit2:client:0.0.0")
In your watchapp's package.json, add a companionApp section:
{
"pebble": {
"companionApp": {
"android": {
"url": "{LINK TO THE GITHUB/PLAY STORE WHERE USERS CAN GET THE COMPANION APP}",
"apps": [
{
"package": "{COMPANION ANDROID APP PACKAGE}"
}
]
}
}
}
}You can specify as many packages as you want, for example if you have debug and release variants. Pebble app will connect to the first app on the list that is installed on the user's Android device
To send data to the watch, you need to first declare a data sending permission in your manifest:
<uses-permission android:name="io.rebble.pebblekit2.permission.SEND_DATA_TO_WATCH" />
this is a normal permission, so user does not need to confirm it explicitly.
Now that you have the permissions, create an instance of the PebbleSender:
val sender = DefaultPebbleSender()then craft your message ( see Sending and receiving data for more info on the message structure) by creating a map of tuples to send to the watch:
val dataToSend = mapOf(
1u to PebbleDictionaryItem.String("Hello at ${LocalTime.now().toString()}"),
2u to PebbleDictionaryItem.UInt8(10u),
)and send it by calling the method on the PebbleSender:
val result = sender.sendDataToPebble(APP_UUID, dataToSend)
where the APP_UUID is the UUID of the target watchapp. Optionally, if you want to send data to a specific watch
(and the target Pebble app supports multiple connected watches), you can specify watches parameter of the
sendDataToPebble method.
The result will contain the result of the sending
(see TransmissionResult).
sendDataToPebble() method is a suspending method and needs to be called from a coroutine. See
Kotlin Coroutines for more info.
when you are done with sending your messages, do not forget to call the close method:
sender.close()
First, create an implementation of the BasePebbleListenerService, where you override desired callbacks:
class PebbleListenerService : BasePebbleListenerService() {
override suspend fun onMessageReceived(
watchappUUID: UUID,
data: PebbleDictionary,
watch: WatchIdentifier
): ReceiveResult {
// ...
}
override fun onAppOpened(watchappUUID: UUID, watch: WatchIdentifier) {
// ...
}
override fun onAppClosed(watchappUUID: UUID, watch: WatchIdentifier) {
// ...
}
}Then, add the receiver as a service to your app's AndroidManifest.xml,
with the io.rebble.pebblekit2.RECEIVE_DATA_FROM_WATCH intent filter:
<service android:name="package.of.my.PebbleListenerService"
android:exported="true"
android:permission="io.rebble.pebblekit2.permission.RECEIVE_DATA_FROM_WATCH">
<intent-filter>
<action android:name="io.rebble.pebblekit2.RECEIVE_DATA_FROM_WATCH"/>
</intent-filter>
</service>Do not forget to declare the permissions, which ensures that only apps with the receiving permission can bind and send data to your app.
That's it. When your watchapp is opened on the watch, the listener service should be bounded and the start callback called.
You can call sender.startAppOnTheWatch() and sender.stopAppOnTheWatch() to start/stop your app on the watch
from the phone.
You can also use PebbleInfoRetriever to retrieve various infos about the status of the Pebble watch and its
connection.
To access that, you first have to declare a provider read permission in your manifest:
<uses-permission android:name="io.rebble.pebblekit2.permission.READ_PROVIDER"/>Then you can use the PebbleInfoRetriever to get the data`:
val infoRetriever = DefaultPebbleInfoRetriever(this)
infoRetriever.getConnectedWatches()
infoRetriever.getActiveApp(watchId)Alternatively, you can also use the content resolver directly:
val pebbleAppPackage = DefaultPebbleAndroidAppPicker.getCurrentlySelectedApp(context)
val cursor = contentResolver.query(
PebbleKitProviderContract.ConnectedWatch.getContentUri(pebbleAppPackage),
null,
null,
null,
null
)
cursor?.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getString(cursor.getColumnIndexOrThrow(PebbleKitProviderContract.ConnectedWatch.ID))
val name = cursor.getString(cursor.getColumnIndexOrThrow(PebbleKitProviderContract.ConnectedWatch.NAME))
// ...
}
}(Note that only the projection parameter is supported, other content resolver parameters are ignored)
PebbleInfoRetriever works only when the app is in the foreground. Unless your app has a service active all the time,
it will not be able to react to the changes in the background (for example,
to get a callback when a watch is (dis)connected while your app is in the background).
To do that, you can use Content URI monitoring in the JobScheduler or in the WorkManager.
For example:
val pebbleAppPackageName = DefaultPebbleAndroidAppPicker.getInstance(context)
.getCurrentlySelectedApp()
val triggerUri = PebbleKitProviderContract.ConnectedWatch.getContentUri(pebbleAppPackageName)
jobScheduler.schedule(
JobInfo.Builder(myId, ComponentName(context, MyJobService::class.java))
.addTriggerContentUri(JobInfo.TriggerContentUri(triggerUri, 0))
.build()
)above job will be triggered whenever a list of currently connected watches change, even if your app is sleeping in the background at that time.
By default, this library will auto-connect and auto-accept requests from any Pebble app. If your app is sending/receiving sensitive data, this might be a security risk as any app masquerading as a Pebble app could be used to steal that data.
To mitigate for this, you can disable automatic selection and manually grant access to the specific Pebble app,
using PebbleAndroidAppPicker object.
First, you have to disable automatic selection:
DefaultPebbleAndroidAppPicker.getInstance(context).enableAutoSelect = false
Then, you can get a list of all installed Pebble apps:
val apps = DefaultPebbleAndroidAppPicker.getInstance(context).getAllEligibleApps()
finally, select the app that you want to grant access to:
DefaultPebbleAndroidAppPicker.getInstance(context).selectApp(pebbleAppPackage)
Now, PebbleKit will only talk to the app with the pebbleAppPackage package.
All non-fatal errors and warnings from the PebbleKit library are logged using the Kermit library.
By default, they are only logged to the Logcat, but this can be customized by writing custom Kermit LogWriters.
See SERVER.MD.
- microPebble (from
1.0.0-alpha35)
Old PebbleKit relied on the broadcast intents to transfer data between the Pebble Android app and the companion app. This approach has two main weaknesses:
- Lax security: there is no way to know from which app the message originates. Malicious apps could pretend to be the either end of the conversation to extract data from either the Pebble app or the companion app
- Background work: in the years since the original Pebble app, Google has step up its war on background processing. It is no longer easy to stay active the background to talk to the Pebble app. And broadcasts do not help with this as they cannot wake the sleeping app or keep it alive to talk to the watch.
As an alternative we picked Bound Services. For this, a service is created on the either side and the other side binds to this service. Most importantly, while Pebble app is bound into the service of the companion app, this app is awakened and is kept awake while the watch app is running, which should help with the background issues.
To exchange messages with bound services, Google gives us two options: AIDL and Messenger. Former is a very nice exchange format, you declare interfaces and Google generates most of the code for you, allowing for very easy calling. Unfortunately, the original format is not backwards or forwards compatible, which would present challenges with evolving this library. Messenger does not have this issue (it uses Bundles underneath), but it's very cumbersome to set up, especially for bidirectional communication.
In the end we went for a sort of hybrid. In the AIDL, we created a very simple interface that sends a bundle to a service and receives a callback with another bundle. This interface is simple enough that we do not expect to change, so it can be used. On top of that, we use Android's Bundles, which use keys and values underneath and can be made backwards compatible.