mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-24 16:21:16 +02:00
Mi Band 8: Implement reminders
This commit is contained in:
parent
cca34af13b
commit
7124d337e1
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user