diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index a71dff193..849d15df1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiCalendarService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService; @@ -75,6 +76,7 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { private final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this); private final XiaomiWeatherService weatherService = new XiaomiWeatherService(this); private final XiaomiSystemService systemService = new XiaomiSystemService(this); + private final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); private final Map mServiceMap = new LinkedHashMap() {{ put(XiaomiAuthService.COMMAND_TYPE, authService); @@ -84,6 +86,7 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { put(XiaomiScheduleService.COMMAND_TYPE, scheduleService); put(XiaomiWeatherService.COMMAND_TYPE, weatherService); put(XiaomiSystemService.COMMAND_TYPE, systemService); + put(XiaomiCalendarService.COMMAND_TYPE, calendarService); }}; public XiaomiSupport() { @@ -286,6 +289,10 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { return; } systemService.setCurrentTime(builder); + + // TODO this should not be done here + calendarService.syncCalendar(builder); + builder.queue(getQueue()); } @@ -451,12 +458,12 @@ public class XiaomiSupport extends AbstractBTLEDeviceSupport { @Override public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) { - scheduleService.onAddCalendarEvent(calendarEventSpec); + calendarService.onAddCalendarEvent(calendarEventSpec); } @Override public void onDeleteCalendarEvent(final byte type, long id) { - scheduleService.onDeleteCalendarEvent(type, id); + calendarService.onDeleteCalendarEvent(type, id); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiCalendarService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiCalendarService.java new file mode 100644 index 000000000..aec0c79d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiCalendarService.java @@ -0,0 +1,149 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; +import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent; +import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; + +public class XiaomiCalendarService extends AbstractXiaomiService { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiCalendarService.class); + + public static final int COMMAND_TYPE = 12; + + private static final int CMD_CALENDAR_SET = 1; + + private static final int MAX_EVENTS = 50; // TODO confirm actual limit + + private final Set lastSync = new HashSet<>(); + + public XiaomiCalendarService(final XiaomiSupport support) { + super(support); + } + + @Override + public void handleCommand(final XiaomiProto.Command cmd) { + LOG.warn("Unknown calendar command {}", cmd.getSubtype()); + } + + @Override + public void initialize(final TransactionBuilder builder) { + syncCalendar(builder); + } + + @Override + public boolean onSendConfiguration(final String config, final Prefs prefs) { + switch (config) { + case DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR: + syncCalendar(); + return true; + } + + return false; + } + + public void onAddCalendarEvent(final CalendarEventSpec ignoredCalendarEventSpec) { + // we must sync everything + syncCalendar(); + } + + public void onDeleteCalendarEvent(final byte ignoredType, final long ignoredId) { + // we must sync everything + syncCalendar(); + } + + public void syncCalendar() { + final TransactionBuilder builder = getSupport().createTransactionBuilder("sync calendar"); + syncCalendar(builder); + builder.queue(getSupport().getQueue()); + } + + public void syncCalendar(final TransactionBuilder builder) { + final boolean syncEnabled = GBApplication.getDeviceSpecificSharedPrefs(getSupport().getDevice().getAddress()) + .getBoolean(PREF_SYNC_CALENDAR, false); + + final XiaomiProto.CalendarSync.Builder calendarSync = XiaomiProto.CalendarSync.newBuilder(); + + if (!syncEnabled) { + LOG.debug("Calendar sync is disabled"); + lastSync.clear(); + calendarSync.setDisabled(true); + } else { + final CalendarManager upcomingEvents = new CalendarManager(getSupport().getContext(), getSupport().getDevice().getAddress()); + final List calendarEvents = upcomingEvents.getCalendarEventList(); + + final Set thisSync = new HashSet<>(); + int nEvents = 0; + + for (final CalendarEvent calendarEvent : calendarEvents) { + if (nEvents++ > MAX_EVENTS) { + LOG.warn("Syncing only first {} events of {}", MAX_EVENTS, calendarEvents.size()); + break; + } + + thisSync.add(calendarEvent); + + final XiaomiProto.CalendarEvent xiaomiCalendarEvent = XiaomiProto.CalendarEvent.newBuilder() + .setTitle(calendarEvent.getTitle()) + .setDescription(StringUtils.ensureNotNull(calendarEvent.getDescription())) + .setLocation(StringUtils.ensureNotNull(calendarEvent.getLocation())) + .setStart(calendarEvent.getBeginSeconds()) + .setEnd((int) (calendarEvent.getEnd() / 1000)) + .setAllDay(calendarEvent.isAllDay()) + .setNotifyMinutesBefore(0) // TODO fetch from event + .build(); + + calendarSync.addEvent(xiaomiCalendarEvent); + } + + if (thisSync.equals(lastSync)) { + LOG.debug("Already synced this set of events, won't send to device"); + return; + } + + lastSync.clear(); + lastSync.addAll(thisSync); + } + + LOG.debug("Syncing {} calendar events", lastSync.size()); + + getSupport().sendCommand( + builder, + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_CALENDAR_SET) + .setCalendar(XiaomiProto.Calendar.newBuilder().setCalendarSync(calendarSync)) + .build() + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiScheduleService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiScheduleService.java index cf2b7d8af..98746f814 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiScheduleService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiScheduleService.java @@ -358,12 +358,4 @@ public class XiaomiScheduleService extends AbstractXiaomiService { .build() ); } - - public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) { - // TODO - } - - public void onDeleteCalendarEvent(final byte type, long id) { - // TODO - } } diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto index c7aadf728..16ae1d06d 100644 --- a/app/src/main/proto/xiaomi.proto +++ b/app/src/main/proto/xiaomi.proto @@ -12,6 +12,7 @@ message Command { optional Auth auth = 3; optional System system = 4; optional Health health = 10; + optional Calendar calendar = 14; optional Music music = 20; optional Notification notification = 9; optional Weather weather = 12; @@ -145,7 +146,7 @@ message Clock { required Date date = 1; required Time time = 2; required TimeZone timezone = 3; - required bool isNot24hour = 4; + optional bool isNot24hour = 4; } message Date { @@ -418,6 +419,29 @@ message RealTimeStats { optional uint32 standingHours = 6; } +// +// Calendar +// + +message Calendar { + optional CalendarSync calendarSync = 2; +} + +message CalendarSync { + repeated CalendarEvent event = 1; + optional bool disabled = 2; +} + +message CalendarEvent { + optional string title = 1; + optional string description = 2; + optional string location = 3; + optional uint32 start = 4; // unix epoch sec + optional uint32 end = 5; // unix epoch sec + optional bool allDay = 6; + optional uint32 notifyMinutesBefore = 7; +} + // // Music // @@ -544,7 +568,7 @@ message Schedule { // 17, 3 -> returns 17, 5 optional Alarm editAlarm = 3; - optional uint32 ackId = 4; // id of created or edited alarm and event + optional uint32 ackId = 4; // id of created or edited alarm and reminder // 17, 4 optional AlarmDelete deleteAlarm = 5; @@ -552,8 +576,8 @@ message Schedule { // 17, 8 get | 17, 9 set optional SleepMode sleepMode = 9; - // 17, 14 get: 10 -> 2: 50 // max events? - optional Events events = 10; + // 17, 14 get: 10 -> 2: 50 // max reminders? + optional Reminders reminders = 10; // 17,10 get/ret | 17,11 create | 17,13 delete optional WorldClocks worldClocks = 11; @@ -561,13 +585,13 @@ message Schedule { optional uint32 worldClockStatus = 13; // 0 on edit and create // 17, 15 - optional EventDetails createEvent = 14; + optional ReminderDetails createReminder = 14; // 17, 17 - optional Event editEvent = 15; + optional Reminder editReminder = 15; // 17, 18 - optional EventDelete deleteEvent = 17; + optional ReminderDelete deleteReminder = 17; } message Alarms { @@ -605,17 +629,17 @@ message SleepModeSchedule { optional uint32 unknown3 = 3; // 0 } -message Events { - repeated Event event = 1; - optional uint32 unknown2 = 2; // 50, max events? +message Reminders { + repeated Reminder reminder = 1; + optional uint32 unknown2 = 2; // 50, max reminder? } -message Event { +message Reminder { optional uint32 id = 1; - optional EventDetails eventDetails = 2; + optional ReminderDetails reminderDetails = 2; } -message EventDetails { +message ReminderDetails { optional Date date = 1; optional Time time = 2; optional uint32 repeatMode = 3; // 0 once, 1 daily, weekly (every monday), 7 monthly, 8 yearly @@ -623,7 +647,7 @@ message EventDetails { optional string title = 5; } -message EventDelete { +message ReminderDelete { repeated uint32 id = 1; }