Display Calendar Events on a Watch Face with Android Wear

The watch face displaying upcoming calendar events was developed in relation to the Grand Central Board, a board for the new Apple TV that is used by the Macoscope team on a daily basis. In my previous post, I described the process of drawing the watch face itself. In this episode, I’d like to elaborate on displaying upcoming calendar events on the watch. My primary assumption was that events will be pulled from only one calendar. There are a couple of different available ways to pull events from calendars, but not all of them fit the criteria described above. Below, you will find the approaches I’ve tested and my pick for the optimal one.
This entire project is based on Android Wear 1.0 gcbpanel

🔎 Using WearableCalendarContract for Querying Events

WearableCalendarContract is the equivalent of Android SDK’s CalendarContract for the Android Wear platform. Unfortunately, JavaDoc doesn’t say much about it and the knowledge base for the class is rather rudimentary and based mostly on developer experiments.

I tried using WearableCalendarContract to handle event projection in a manner identical to the one used for CalendarContract and ContentResolver but the attempt failed.

The structure of the WearableCalendarContract class itself allows us to infer that it is a very limited tool. Accessing our own calendars and then filtering events according to the id of our selected calendar would unfortunately be impossible with WearableCalendarContract.

Dan Wallach’s thread “WearableCalendarContract vs. CalendarContract” illustrates the differences between the two classes and demonstrates the additional limitations that the aforementioned class puts on developers.

What, then, should we use WearableContractCalendar for? Well, for example, we can utilize it when we want to get access to calendars that have been selected for synchronization with the watch via calendar settings in the Android Wear app and we don’t want to get events from more than one day away or we don’t want to filter events by any calendar id.

📱 Using a Handheld App for Event Synchronization

From my perspective, using a companion app on the phone for synchronizing events on the watch is one of the most sensible solutions in Android Wear 1.0.

The companion app on the phone allow us to tap into the broad range of possibilities offered by the Android SDK as well as synchronization with wearables. This sort of approach will also be used in our example.

Before sending any data to the watch, we should first solve the issue of synchronizing calendars with the phone. There are a couple of different solutions to that problem. Two of them are outlined below.

📅 Google Calendar API

Synchronizing calendars with a mobile device and sending calendar queries directly via the web is possible using the Google Calendar API.

One example of using the service can be seen in the CalendarApiService class. Such a solution, however, requires us to repeatedly query the service for new events, thus creating superfluous network activity. Additionally, the queried calendar has to be configured properly and has to allow access to its API via OAuth.

Below, I outlined another method of querying calendars for events that does not generate additional network activity and does not require any special configuration of the calendars.

📱 CalendarProvider API for Querying Events in the Handheld App

For my watch face I created an application that handles data synchronization between the calendar and the watch. The application has been included in the gcbpanel module. For better code transparency, I employed the Model-View-Presenter architecture in the project. Its main elements are SyncPreferencesPresenter, CalendarUseCase, SyncPreferencesView, and Event and CalendarModel models. WF2 To limit excessive synchronization in my application and utilize what the system itself offers us, I query the database for upcoming events using the CalendarProvider API. The CalendarRepository class contains a couple of useful functions that query databases for calendars and events.

Using CalendarContract.Instances#CONTENT_URI allows to to easily build a query and access the cursor containing instances of events from the calendar.

   Uri.Builder builder = Instances.CONTENT_URI.buildUpon();
   long now = System.currentTimeMillis();
   long timeTo = now + timeUnit.toMillis(timeInterval);
   ContentUris.appendId(builder, now);
   ContentUris.appendId(builder, Long.MAX_VALUE);

   Cursor cursor = contentResolver.query(builder.build(), INSTANCE_PROJECTION,
           INSTANCE_SELECTION, new String[]{Long.toString(calendarId), Long.toString(now), Long.toString(timeTo)},
           INSTANCE_ORDER);

To work properly, the URI has to contain timestamps of the oldest and the newest of the requested events, and that is dones by calling ContentUris.appendId(uriBuilder, time) on the time of the oldest event and then for the newest/upcoming event. The entire function fetching events in a given calendar can be found in the repository.

📱⌚ Synchronization Between Handheld App and Wearable

When the application has already fetched the events, we can start synchronizing them with the watch. Thanks to the Wearable Data Layer API communication between your handheld and your watch is fairly simple.

To make the communication in my project as simple as possible, I decided to use RxWear. As evidenced by our previous Android-oriented posts, we love and appreciate RxJava here👌

The method responsible for sending the list of events from the mobile device to the wearable can be found in the SyncJob class. Handling errors and establishing connections is very easy. Under RxWear you will find the Wearable Data Layer API wrapped in Observables or Singles. An example of sending a message from the phone is presented below:

private void sendEvents(List<Event> eventList) {
   RxWear.init(getContext());
   Gson gson = new Gson();
   final String eventsJson = gson.toJson(eventList);
   RxWear.Message.SendDataMap.toAllRemoteNodes(CommunicationConfig.EVENTS_LIST_PATH)
           .putString(CommunicationConfig.EVENTS_LIST_DATA_KEY, eventsJson)
           .toObservable().subscribe(new Action1<Integer>() {
       @Override
       public void call(Integer integer) {
           Log.d(getParams().getTag(), "JSON send to watch: " + eventsJson);
       }
   }, new Action1<Throwable>() {
       @Override
       public void call(Throwable throwable) {
           if (throwable instanceof GoogleAPIConnectionException) {
               Log.v(TAG, "Android Wear app is not installed");
           } else {
               Log.v(TAG, "Update events error", throwable);
           }
       }
   });
}

A well-trained eye will quickly notice that sending an object in JSON format is more or less redundant, but works splendidly for debugging purposes. When it comes to production code, you should ponder using RxWear.Data.PutSerializable and default methods of serializing objects in Android. The EVENTS_LIST_PATH and EVENTS_LIST_DATA_KEY constants are String values defined in the gcbmodel module. Sharing this module, and thus a common configuration, by the watch app and the phone app allows for better consistency between these keys, both of which are necessary for communication.

⌚ Receiving Events on the Watch

WF3

In my app, reception and management of events is handled by EventsManager, and thanks to using RxWear listening for incoming data packets is just as easy as sending them:

Subscription subscription = RxWear.Message.listen()
       .compose(MessageEventGetDataMap.filterByPath(CommunicationConfig.EVENTS_LIST_PATH))
       .map(dataMapToEventsListFunction)
       .observeOn(AndroidSchedulers.mainThread())
       .subscribe(new Action1<List<Event>>() {
           @Override
           public void call(List<Event> events) {
               Collections.sort(events, eventComparator);
               eventsLoaded(events);
               listener.onEventsListChanged();
           }
       }, new Action1<Throwable>() {
           @Override
           public void call(Throwable throwable) {
               listener.onEventsLoadFailure();
           }
       });

GCBWatchFace queries EventManager for the upcoming event and then places it in EventFormatter which draws a graphical representation of Event on the watch face in the GCBWatchFace.Engine#onDraw() method.

🔃 Repeated Synchronization of Events

Creating a watch face based on WatchFaceService gives us all the benefits of the service – including the ability to persistently listen for new events sent in by the phone. The app sending the events, however, required me to create additional logic for repeated updates. There are a couple of options when it comes to creating such a service, e.g. using the Sync Adapter. That solution, however, is mostly used for web synchronization and requires the creation of an additional account for synchronization purposes. From my perspective, for the purpose of communicating with Android Wear, configuring a service using Sync Adapter is simply too complicated.

There is another approach, however it depends on the Android version you’re using: you can either utilize AlarmManager or, for applications with a target above API 21, JobScheduler. As far as I consider JobScheduler a fine solution, I did not want to limit myself to devices with operating systems newer than KitKat, that is I didn’t want to create a complicated if else logic that would determine whether AlarmManager or JobScheduler would be used depending on the OS version the device would be running. That’s where the Android-Job library came in handy: depending on the SDK version, it employs the tools available for the platform to achieve the effect desired by the developer.

The configuration of JobRequest, as this is how you build jobs using Android-Job, is very intuitive and based on Builder Pattern. Proper configuration can be achieved with minimum effort and all the necessary details are outlined in the README of the Android-Job project. For my app, I created three SyncJob classes to handle sending events: one using RxWear to send events, the SyncJobCreator class that handles SyncJob creation management, and SyncJobScheduler that manages recurring SyncJob launching. Below is an example of planned job launched at defined intervals in minutes:

private void scheduleSendDataPeriodic(PersistableBundleCompat extras, long intervalInMinutes) {
   new JobRequest.Builder(SyncJob.TAG)
           .setPeriodic(TimeUnit.MINUTES.toMillis(intervalInMinutes))
           .setPersisted(true)
           .setExtras(extras)
           .setUpdateCurrent(true)
           .build()
           .schedule();
}

The setPeriodic(true) flag, in combination with android.permission.RECEIVE_BOOT_COMPLETED from AndroidManifest job is resistant to device reboots and should provide us with a consistent stream of fresh updates🚀.

Android Wear 2⃣.0⃣🎊

The release of a new Android Wear version was announced during this year’s Google I/O event. The list of the new API functions can be found in a Android Developers post. Undoubtedly, one of the more innovative features of the release will be the ability to build standalone apps for the watch with access to the Web and other components that were heretofore available only via the Data Layer API. Additionally, developers are hard at work preparing Watch Face Complications. The new functionalities of the library will revolutionize our perception of wearables and will definitely broaden their appeal and possible applications. In my opinion, the watch face example we provided above is an ideal candidate for testing Android Wear 2.0 but that’s a topic for a whole other blog post. 🙂

Over to You

I hope that our two posts on Android Wear accurately demonstrated the potential problems you may encounter when creating your own watch faces for Android Wear 1.0 and presented the possible solutions you may use to overcome them. As usual, I’m open to all observations and suggestions. The entire project can be found on GitHub.

Automated UI Testing with Jenkins CI