diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureReminders.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureReminders.java index 56da54632..3630aeed6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureReminders.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureReminders.java @@ -16,13 +16,17 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.content.BroadcastReceiver; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -49,7 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Reminder; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -61,12 +65,25 @@ public class ConfigureReminders extends AbstractGBActivity { private GBReminderListAdapter mGBReminderListAdapter; private GBDevice gbDevice; + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + if (DeviceService.ACTION_SAVE_REMINDERS.equals(intent.getAction())) { + updateRemindersFromDB(); + } + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_configure_reminders); + IntentFilter filterLocal = new IntentFilter(); + filterLocal.addAction(DeviceService.ACTION_SAVE_REMINDERS); + LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); + gbDevice = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE); mGBReminderListAdapter = new GBReminderListAdapter(this); @@ -118,6 +135,12 @@ public class ConfigureReminders extends AbstractGBActivity { }); } + @Override + protected void onDestroy() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); + super.onDestroy(); + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java index 03907ab6e..7849820df 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiCoordinator.java @@ -213,13 +213,13 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator { @Override public int getMaximumReminderMessageLength() { // TODO does it? - return 0; + return 20; } @Override public int getReminderSlotCount(final GBDevice device) { - // TODO Does it? - return 0; + // TODO fetch from watch + return 50; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 6198425e5..1ac8c8323 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -59,6 +59,7 @@ public interface DeviceService extends EventHandler { String ACTION_SET_CONSTANT_VIBRATION = PREFIX + ".action.set_constant_vibration"; String ACTION_SET_ALARMS = PREFIX + ".action.set_alarms"; String ACTION_SAVE_ALARMS = PREFIX + ".action.save_alarms"; + String ACTION_SAVE_REMINDERS = PREFIX + ".action.save_reminders"; String ACTION_SET_REMINDERS = PREFIX + ".action.set_reminders"; String ACTION_SET_LOYALTY_CARDS = PREFIX + ".action.set_loyalty_cards"; String ACTION_SET_WORLD_CLOCKS = PREFIX + ".action.set_world_clocks"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java index 2f7a3122a..e29017385 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiPreferences.java @@ -20,6 +20,7 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; +import java.util.TimeZone; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; @@ -42,6 +43,18 @@ public final class XiaomiPreferences { .build(); } + public static Date toDate(final XiaomiProto.Date date, final XiaomiProto.Time time) { + // For some reason, the watch expects those in UTC... + // TODO double-check with official app, this does not make sense + final Calendar calendar = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")); + calendar.set( + date.getYear(), date.getMonth() - 1, date.getDay(), + time.getHour(), time.getMinute(), time.getSecond() + ); + + return calendar.getTime(); + } + /** * Returns the preference key where to save the list of possible value for a preference, comma-separated. */ 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 086a70e25..95402dd14 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 @@ -24,14 +24,25 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; +import java.util.GregorianCalendar; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TimeZone; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.Reminder; @@ -55,6 +66,10 @@ public class XiaomiScheduleService extends AbstractXiaomiService { private static final int CMD_SLEEP_MODE_SET = 9; private static final int CMD_WORLD_CLOCKS_GET = 10; private static final int CMD_WORLD_CLOCKS_SET = 11; + private static final int CMD_REMINDERS_GET = 14; + private static final int CMD_REMINDERS_CREATE = 15; + private static final int CMD_REMINDERS_EDIT = 17; + private static final int CMD_REMINDERS_DELETE = 18; private static final int REPETITION_ONCE = 0; private static final int REPETITION_DAILY = 1; @@ -65,17 +80,22 @@ public class XiaomiScheduleService extends AbstractXiaomiService { private static final int ALARM_SMART = 1; private static final int ALARM_NORMAL = 2; + // Reminders created by this service will have this prefix + private static final String REMINDER_DB_PREFIX = "xiaomi_"; + private static final Map WORLD_CLOCK_CODES = new HashMap() {{ put("Europe/Lisbon", "C173"); put("Australia/Sydney", "C151"); // TODO map everything }}; - // Map of alarm position to Alarm, as returned by the band, indexed by GB watch position (0-indexed), - // does NOT match watch ID + // Map of alarm position to Alarm/Reminder, as returned by the watch, indexed by GB position (0-indexed), + // does NOT match the ID returned by the watch, but should be offset by 1 private final Map watchAlarms = new HashMap<>(); + private final Map watchReminders = new HashMap<>(); private int pendingAlarmAcks = 0; + private int pendingReminderAcks = 0; public XiaomiScheduleService(final XiaomiSupport support) { super(support); @@ -101,12 +121,26 @@ public class XiaomiScheduleService extends AbstractXiaomiService { case CMD_SLEEP_MODE_GET: handleSleepModeConfig(cmd.getSchedule().getSleepMode()); break; + case CMD_REMINDERS_GET: + handleReminders(cmd.getSchedule().getReminders()); + break; + case CMD_REMINDERS_CREATE: + pendingReminderAcks--; + if (pendingReminderAcks <= 0) { + final TransactionBuilder builder = getSupport().createTransactionBuilder("request reminders after all acks"); + requestReminders(builder); + builder.queue(getSupport().getQueue()); + } + break; } + + LOG.warn("Unknown schedule command {}", cmd.getSubtype()); } @Override public void initialize(final TransactionBuilder builder) { requestAlarms(builder); + requestReminders(builder); requestWorldClocks(builder); getSupport().sendCommand(builder, COMMAND_TYPE, CMD_SLEEP_MODE_GET); } @@ -126,8 +160,204 @@ public class XiaomiScheduleService extends AbstractXiaomiService { return false; } + public void requestReminders(final TransactionBuilder builder) { + getSupport().sendCommand(builder, COMMAND_TYPE, CMD_REMINDERS_GET); + } + + public void handleReminders(final XiaomiProto.Reminders reminders) { + LOG.debug("Got {} reminders from the watch", reminders.getReminderCount()); + + watchReminders.clear(); + for (final XiaomiProto.Reminder reminder : reminders.getReminderList()) { + final nodomain.freeyourgadget.gadgetbridge.entities.Reminder gbReminder = new nodomain.freeyourgadget.gadgetbridge.entities.Reminder(); + gbReminder.setReminderId(REMINDER_DB_PREFIX + reminder.getId()); + gbReminder.setMessage(reminder.getReminderDetails().getTitle()); + gbReminder.setDate(XiaomiPreferences.toDate(reminder.getReminderDetails().getDate(), reminder.getReminderDetails().getTime())); + + switch (reminder.getReminderDetails().getRepeatMode()) { + case REPETITION_ONCE: + gbReminder.setRepetition(Alarm.ALARM_ONCE); + break; + case REPETITION_DAILY: + gbReminder.setRepetition(Alarm.ALARM_DAILY); + break; + case REPETITION_WEEKLY: + gbReminder.setRepetition(reminder.getReminderDetails().getRepeatFlags()); + break; + } + + watchReminders.put(gbReminder.getReminderId(), gbReminder); + } + + final List dbReminders = DBHelper.getReminders(getSupport().getDevice()); + + final Set dbReminderIds = new HashSet<>(); + + int numUpdatedReminders = 0; + + // Delete reminders that do not exist on the watch anymore + for (nodomain.freeyourgadget.gadgetbridge.entities.Reminder reminder : dbReminders) { + if (!reminder.getReminderId().startsWith(REMINDER_DB_PREFIX)) { + LOG.debug("Deleting reminder {}", reminder.getReminderId()); + DBHelper.delete(reminder); + numUpdatedReminders++; + continue; + } + + dbReminderIds.add(reminder.getReminderId()); + } + + // Persist unknown reminders + // We assume that reminders are not modifiable from the watch, unlike alarms + try (DBHandler db = GBApplication.acquireDB()) { + final DaoSession daoSession = db.getDaoSession(); + final Device device = DBHelper.getDevice(getSupport().getDevice(), daoSession); + final User user = DBHelper.getUser(daoSession); + + for (final Reminder watchReminder : watchReminders.values()) { + final String reminderId = watchReminder.getReminderId(); + if (dbReminderIds.contains(reminderId)) { + continue; + } + + // Reminder not known - persist it to database + LOG.info("Persisting reminder {}", reminderId); + + final nodomain.freeyourgadget.gadgetbridge.entities.Reminder reminder = new nodomain.freeyourgadget.gadgetbridge.entities.Reminder(); + reminder.setReminderId(watchReminder.getReminderId()); + reminder.setDate(watchReminder.getDate()); + reminder.setMessage(watchReminder.getMessage()); + reminder.setRepetition(watchReminder.getRepetition()); + reminder.setDeviceId(device.getId()); + reminder.setUserId(user.getId()); + + DBHelper.store(reminder); + + numUpdatedReminders++; + } + } catch (final Exception e) { + LOG.error("Error accessing database", e); + } + + if (numUpdatedReminders > 0) { + final Intent intent = new Intent(DeviceService.ACTION_SAVE_REMINDERS); + LocalBroadcastManager.getInstance(getSupport().getContext()).sendBroadcast(intent); + } + } + public void onSetReminders(final ArrayList reminders) { - // TODO + final List remindersToDelete = new ArrayList<>(); + + pendingReminderAcks = 0; + + final Set newReminderIds = new HashSet<>(); + for (final Reminder reminder : reminders) { + newReminderIds.add(reminder.getReminderId()); + } + + for (final Reminder watchReminder : watchReminders.values()) { + if (!newReminderIds.contains(watchReminder.getReminderId())) { + final Integer watchId = Integer.parseInt(watchReminder.getReminderId().replace(REMINDER_DB_PREFIX, "")); + remindersToDelete.add(watchId); + } + } + + for (final Integer id : remindersToDelete) { + watchReminders.remove(REMINDER_DB_PREFIX + id); + } + + for (final Reminder reminder : reminders) { + final boolean isCreateReminder; + if (reminder.getReminderId().startsWith(REMINDER_DB_PREFIX) && watchReminders.containsKey(reminder.getReminderId())) { + // Update reminder on the watch if needed + final Reminder watchReminder = watchReminders.get(reminder.getReminderId()); + if (watchReminder != null && remindersEqual(reminder, watchReminder)) { + LOG.debug("Reminder {} is already up-to-date on watch", watchReminder.getReminderId()); + continue; + } + + isCreateReminder = (watchReminder == null); + } else { + isCreateReminder = true; + } + + final Calendar reminderTime = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")); + reminderTime.setTimeInMillis(reminder.getDate().getTime()); + + final XiaomiProto.ReminderDetails.Builder reminderDetails = XiaomiProto.ReminderDetails.newBuilder() + .setTime(XiaomiProto.Time.newBuilder() + .setHour(reminderTime.get(Calendar.HOUR_OF_DAY)) + .setMinute(reminderTime.get(Calendar.MINUTE)) + .setSecond(reminderTime.get(Calendar.SECOND)) + .setMillisecond(reminderTime.get(Calendar.MILLISECOND)) + .build()) + .setDate(XiaomiProto.Date.newBuilder() + .setYear(reminderTime.get(Calendar.YEAR)) + .setMonth(reminderTime.get(Calendar.MONTH) + 1) + .setDay(reminderTime.get(Calendar.DATE)) + .build()) + .setTitle(reminder.getMessage()); + + switch (reminder.getRepetition()) { + case Alarm.ALARM_ONCE: + reminderDetails.setRepeatMode(REPETITION_ONCE); + break; + case Alarm.ALARM_DAILY: + reminderDetails.setRepeatMode(REPETITION_DAILY); + break; + default: + reminderDetails.setRepeatMode(REPETITION_WEEKLY); + reminderDetails.setRepeatFlags(reminder.getRepetition()); + break; + } + + final XiaomiProto.Schedule.Builder schedule = XiaomiProto.Schedule.newBuilder(); + + if (!isCreateReminder) { + // update existing alarm + LOG.debug("Update reminder {}", reminder.getReminderId()); + watchReminders.put(reminder.getReminderId(), reminder); + schedule.setEditReminder( + XiaomiProto.Reminder.newBuilder() + .setId(Integer.parseInt(reminder.getReminderId().replace(REMINDER_DB_PREFIX, ""))) + .setReminderDetails(reminderDetails) + .build() + ); + } else { + LOG.debug("Create reminder {}", reminder.getReminderId()); + // watchReminders will be updated later, since we don't know the correct ID here + pendingReminderAcks++; + schedule.setCreateReminder(reminderDetails); + } + + getSupport().sendCommand( + (isCreateReminder ? "create" : "update") + " reminder " + reminder.getReminderId(), + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(isCreateReminder ? CMD_REMINDERS_CREATE : CMD_REMINDERS_EDIT) + .setSchedule(schedule) + .build() + ); + } + + if (!remindersToDelete.isEmpty()) { + final XiaomiProto.ReminderDelete reminderDelete = XiaomiProto.ReminderDelete.newBuilder() + .addAllId(remindersToDelete) + .build(); + + final XiaomiProto.Schedule schedule = XiaomiProto.Schedule.newBuilder() + .setDeleteReminder(reminderDelete) + .build(); + + getSupport().sendCommand( + "delete " + remindersToDelete.size() + " reminders", + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_REMINDERS_DELETE) + .setSchedule(schedule) + .build() + ); + } } public void onSetWorldClocks(final ArrayList clocks) { @@ -338,6 +568,12 @@ public class XiaomiScheduleService extends AbstractXiaomiService { alarm1.getRepetition() == alarm2.getRepetition(); } + private boolean remindersEqual(final Reminder reminder1, final Reminder reminder2) { + return Objects.equals(reminder1.getMessage(), reminder2.getMessage()) && + Objects.equals(reminder1.getDate(), reminder2.getDate()) && + reminder1.getRepetition() == reminder2.getRepetition(); + } + private void handleSleepModeConfig(final XiaomiProto.SleepMode sleepMode) { LOG.debug("Got sleep mode config");