I'm trying to create a custom WearOS complication that shows the title of the next event. The watch has a built-in Agenda app that synchronizes with the phone's calendar. There is also a built-in complication that shows the time of the next event, from the same Agenda app. When I change the next event in my phone (the time and the title), the complication updates immediately (it takes 5-10 seconds) and the new information is also available in the watch's Agenda app. I want my complication to do the same, but it just doesn't update.
So far, I've created a SuspendingComplicationDataSourceService as follows:
class ComingEventComplication : SuspendingComplicationDataSourceService() {
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? {
val nextEvent = getNextCalendarEvents(this.contentResolver)[0]
return if (nextEvent != null) {
//Expose data to the complication
} else {
//Show a placeholder
}
}
//Preview data
override fun getPreviewData(type: ComplicationType): ComplicationData? {
[...]
}
}
I'm not showing the code to expose the data to make it shorter (but it works). The getNextCalendarEvents() function is the following:
fun getNextCalendarEvents(contentResolver: ContentResolver): List<CalendarEvent> {
val events = mutableListOf<CalendarEvent>()
// Define the projection (columns you want to retrieve)
val projection = arrayOf(
CalendarContract.Instances.EVENT_ID,
CalendarContract.Instances.TITLE,
CalendarContract.Instances.DESCRIPTION,
CalendarContract.Instances.EVENT_LOCATION,
CalendarContract.Instances.BEGIN,
CalendarContract.Instances.END,
CalendarContract.Instances.ALL_DAY
)
val startMillis = System.currentTimeMillis()
val endMillis = startMillis + TimeUnit.DAYS.toMillis(5)
val uriBuilder = WearableCalendarContract.Instances.CONTENT_URI.buildUpon()
ContentUris.appendId(uriBuilder, startMillis)
ContentUris.appendId(uriBuilder, endMillis)
val uri = uriBuilder.build()
// Query the Content Provider
val cursor = contentResolver.query(
uri,
projection,
null,
null,
null
)
cursor?.use {
while (it.moveToNext()) {
val eventId = it.getLong(it.getColumnIndexOrThrow(CalendarContract.Instances.EVENT_ID))
val title = it.getString(it.getColumnIndexOrThrow(CalendarContract.Instances.TITLE))
val description =
it.getString(it.getColumnIndexOrThrow(CalendarContract.Instances.DESCRIPTION))
val location =
it.getString(it.getColumnIndexOrThrow(CalendarContract.Instances.EVENT_LOCATION))
val beginTime = it.getLong(it.getColumnIndexOrThrow(CalendarContract.Instances.BEGIN))
val endTime = it.getLong(it.getColumnIndexOrThrow(CalendarContract.Instances.END))
val isAllDay =
it.getInt(it.getColumnIndexOrThrow(CalendarContract.Instances.ALL_DAY)) == 1
events.add(
CalendarEvent(
id = eventId,
title = title,
description = description,
location = location,
startTime = Instant.ofEpochMilli(beginTime),
endTime = Instant.ofEpochMilli(endTime),
isAllDay = isAllDay
)
)
}
}
return events.sortedBy { it.startTime }
}
with my custom data class:
data class CalendarEvent(
val title: String,
val startTime: Instant,
val endTime: Instant,
val isAllDay: Boolean,
val description: String?,
val location: String?,
val id: Long
)
Finally, my AndroidManifest setup is:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission-sdk-23 android:name="android.permission.READ_CALENDAR" />
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault" >
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
<service
android:name="developer.pion.complicationsprovider.services.ComingEventComplication"
android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER"
android:exported="true"
android:label="Test"
android:icon="@drawable/common_full_open_on_phone">
<intent-filter>
<action android:name="androidx.wear.watchface.complications.action.COMPLICATION_DATA_SOURCE" />
</intent-filter>
<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="SHORT_TEXT" />
</service>
</application>
This part works as expected: I add the complication and it shows the name of the next event in my calendar. The problem comes when I change the next event in my calendar on my phone. The built-in complication updates but not mine (I have to re-add it again). So far I've tried adding a WorkManager to update the complication (which is not very reliable if you set it to schedule every long time, and it may drain the battery otherwise), and using a ContentObserver :
class ContentObserver(
private val context: Context,
private val componentName: ComponentName
) : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
Log.d("DEBUG_", "onChange: Changed!")
requestComplicationUpdate()
}
private fun requestComplicationUpdate() {
val requester = ComplicationDataSourceUpdateRequester.create(
context = context,
complicationDataSourceComponent = componentName
)
requester.requestUpdateAll()
}
}
I do register my ContenObserver inside ComingEventComplication class:
override fun onCreate() {
super.onCreate()
val calendarUri: Uri = WearableCalendarContract.CONTENT_URI
// Create the ContentObserver instance.
calendarObserver = ContentObserver(
context = applicationContext,
componentName = ComponentName(applicationContext, ComingEventComplication::class.java)
)
// Register the observer.
contentResolver.registerContentObserver(
calendarUri,
true,
calendarObserver
)
}
override fun onDestroy() {
super.onDestroy()
// Unregister the observer to prevent memory leaks.
contentResolver.unregisterContentObserver(calendarObserver)
}
The DEBUG_ onChange: Changed! is never shown in my logcat. I also tried adding a receiver in my manifest:
<receiver android:name=".receivers.CalendarUpdateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PROVIDER_CHANGED" />
<data android:scheme="content" android:host="com.google.android.wearable.provider.calendar" />
</intent-filter>
</receiver>
setting the android:host as the Uri given by WearableCalendarContract.CONTENT_URI , WearableCalendarContract.Instances.CONTENT_URI and CalendarContract.CONTENT_URI . The receiver calls a dummy BroadcastReceiver that displays a log entry. I had no success with any of them.
Up to now I'm not sure if I'm setting something wrong or if I need a different approach. I've tested third-party complications that are able to update in less than a minute from the event change on phone, so I think there must be a reliable way of doing this. Any help will be appreciated!