Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ data class NotificationConfig(
* Shown under "Canned messages" in the watch action menu.
*/
val cannedResponses: List<String> = listOf("Ok", "Yes", "No", "Call me", "Call you later"),
val overrideCalendarVibePattern: String? = null,
)

class NotificationConfigFlow(val flow: StateFlow<LibPebbleConfig>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private fun transformDescription(rawDescription: String): String {
return rawDescription.replace(regex, "").trimWithEllipsis(300)
}

fun CalendarEvent.toTimelineReminder(timestamp: Instant, pinUuid: Uuid): TimelineReminder =
fun CalendarEvent.toTimelineReminder(timestamp: Instant, pinUuid: Uuid, vibePattern: List<UInt>? = null): TimelineReminder =
buildTimelineReminder(
parentId = pinUuid,
timestamp = timestamp,
Expand All @@ -65,6 +65,7 @@ fun CalendarEvent.toTimelineReminder(timestamp: Instant, pinUuid: Uuid): Timelin
location { location }
}
tinyIcon { TimelineIcon.NotificationReminder }
vibePattern?.let { vibrationPattern { it } }
// TODO attendees
}
flags {
Expand Down Expand Up @@ -100,6 +101,7 @@ private object DefaultTitles {
fun CalendarEvent.toTimelinePin(
calendar: CalendarEntity,
supportsRsvpActions: Boolean,
vibePattern: List<UInt>? = null,
): TimelinePin = buildTimelinePin(
parentId = CALENDAR_APP_UUID,
timestamp = startTime,
Expand Down Expand Up @@ -161,6 +163,7 @@ fun CalendarEvent.toTimelinePin(
// }
stringList(TimelineAttribute.Headings) { headings }
stringList(TimelineAttribute.Paragraphs) { paragraphs }
vibePattern?.let { vibrationPattern { it } }
}
actions {
if (supportsRsvpActions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package io.rebble.libpebblecommon.calendar

import co.touchlab.kermit.Logger
import io.rebble.libpebblecommon.NotificationConfigFlow
import io.rebble.libpebblecommon.SystemAppIDs.CALENDAR_APP_UUID
import io.rebble.libpebblecommon.WatchConfigFlow
import io.rebble.libpebblecommon.connection.Calendar
import io.rebble.libpebblecommon.connection.endpointmanager.blobdb.TimeProvider
import io.rebble.libpebblecommon.database.dao.CalendarDao
import io.rebble.libpebblecommon.database.dao.TimelinePinRealDao
import io.rebble.libpebblecommon.database.dao.TimelineReminderRealDao
import io.rebble.libpebblecommon.database.dao.VibePatternDao
import io.rebble.libpebblecommon.database.entity.CalendarEntity
import io.rebble.libpebblecommon.database.entity.TimelinePin
import io.rebble.libpebblecommon.database.entity.TimelineReminder
Expand All @@ -16,6 +18,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
Expand All @@ -33,6 +38,8 @@ class PhoneCalendarSyncer(
private val libPebbleCoroutineScope: LibPebbleCoroutineScope,
private val timelineReminderDao: TimelineReminderRealDao,
private val watchConfig: WatchConfigFlow,
private val notificationConfigFlow: NotificationConfigFlow,
private val vibePatternDao: VibePatternDao,
) : Calendar {
private val logger = Logger.withTag("PhoneCalendarSyncer")
private val syncTrigger = MutableSharedFlow<Unit>()
Expand Down Expand Up @@ -67,6 +74,13 @@ class PhoneCalendarSyncer(
}
}
}
libPebbleCoroutineScope.launch {
notificationConfigFlow.flow
.map { it.notificationConfig.overrideCalendarVibePattern }
.distinctUntilChanged()
.drop(1)
.collect { requestSync() }
}
}
}

Expand Down Expand Up @@ -129,6 +143,8 @@ class PhoneCalendarSyncer(
val existingPins = timelinePinDao.getPinsForWatchapp(CALENDAR_APP_UUID)
val startDate = timeProvider.now() - 1.days
val endDate = (startDate + 7.days)
val calendarVibePattern = notificationConfigFlow.value.overrideCalendarVibePattern
?.let { vibePatternDao.getVibePattern(it)?.pattern }
val newPins = allCalendars.flatMap { calendar ->
if (!calendar.enabled || !pinsEnabled) {
return@flatMap emptyList()
Expand All @@ -141,7 +157,7 @@ class PhoneCalendarSyncer(
}
val supportsRsvp = systemCalendar.supportsPinActions()
events.map { event ->
EventAndPin(event, event.toTimelinePin(calendar, supportsRsvp))
EventAndPin(event, event.toTimelinePin(calendar, supportsRsvp, calendarVibePattern))
}
}
val remindersToInsert = mutableListOf<TimelineReminder>()
Expand All @@ -153,6 +169,7 @@ class PhoneCalendarSyncer(
remindersEnabled = remindersEnabled,
event = new.event,
pinId = existingPin?.itemId ?: newPin.itemId,
vibePattern = calendarVibePattern,
remindersToInsert = remindersToInsert,
remindersToDelete = remindersToDelete,
)
Expand Down Expand Up @@ -197,6 +214,7 @@ class PhoneCalendarSyncer(
remindersEnabled: Boolean,
event: CalendarEvent,
pinId: Uuid,
vibePattern: List<UInt>?,
remindersToInsert: MutableList<TimelineReminder>,
remindersToDelete: MutableList<Uuid>,
) {
Expand All @@ -211,12 +229,15 @@ class PhoneCalendarSyncer(
}
}.map { it.itemId }

remindersToInsert += eventReminderTimestamps.filter { t ->
if (!remindersEnabled) return@filter false
existingReminders.none { er ->
er.content.timestamp.instant == t
remindersToInsert += eventReminderTimestamps.mapNotNull { t ->
if (!remindersEnabled) return@mapNotNull null
val newReminder = event.toTimelineReminder(t, pinId, vibePattern)
val existing = existingReminders.find { er -> er.content.timestamp.instant == t }
if (existing != null && existing.recordHashCode() == newReminder.recordHashCode()) {
return@mapNotNull null
}
}.map { event.toTimelineReminder(it, pinId) }
newReminder.copy(itemId = existing?.itemId ?: newReminder.itemId)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this flow tested with a real watch (both when the vibe pattern changed, and when nothing changed)? The logic looks OK but it's the kind of thing which is complicated enough not to notice a bug..

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — this is the same implementation I've been running in microPebble (via matejdro/libpebble3) for ~2 months now, used daily on a real watch, so the calendar-reminder flow (including this dedup) has had a lot of real-world hours. Both cases behave as intended:

  • Override changed → reminders re-sync and fire with the new pattern.
  • Nothing changed → no reminder writes/churn — the recordHashCode() comparison skips unchanged ones.

The logic here is identical to the microPebble version, so it's well-exercised on hardware. Happy to re-verify any specific scenario you have in mind.

}
}

override fun calendars(): Flow<List<CalendarEntity>> = calendarDao.getFlow()
Expand Down