1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-29 12:05:53 +01:00

Mi Band 8: Implement reminders

This commit is contained in:
José Rebelo 2023-10-08 19:17:58 +01:00
parent cca34af13b
commit 7124d337e1
5 changed files with 280 additions and 7 deletions

View File

@ -16,13 +16,17 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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);

View File

@ -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

View File

@ -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";

View File

@ -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.
*/

View File

@ -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<String, String> WORLD_CLOCK_CODES = new HashMap<String, String>() {{
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<Integer, Alarm> watchAlarms = new HashMap<>();
private final Map<String, Reminder> 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<nodomain.freeyourgadget.gadgetbridge.entities.Reminder> dbReminders = DBHelper.getReminders(getSupport().getDevice());
final Set<String> 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<Integer> remindersToDelete = new ArrayList<>();
pendingReminderAcks = 0;
final Set<String> 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");