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 extends Reminder> 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 extends WorldClock> 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");