Zepp OS: Fix reminder slot initialization (#3137, #4098)

This commit is contained in:
José Rebelo 2024-09-15 17:22:19 +01:00
parent c72966bc6c
commit 5ebb3b85b0
3 changed files with 193 additions and 54 deletions

3
.gitignore vendored
View File

@ -27,6 +27,9 @@ proguard/
.idea/* .idea/*
!.idea/icon.svg !.idea/icon.svg
!.idea/dictionaries
.idea/dictionaries/*
!.idea/dictionaries/t.xml
*.iml *.iml
MPChartLib MPChartLib

View File

@ -17,10 +17,12 @@
<w>autocrlf</w> <w>autocrlf</w>
<w>avamander</w> <w>avamander</w>
<w>baerts</w> <w>baerts</w>
<w>baos</w>
<w>barraca</w> <w>barraca</w>
<w>bedscastle</w> <w>bedscastle</w>
<w>bloß</w> <w>bloß</w>
<w>boun</w> <w>boun</w>
<w>breathwork</w>
<w>böhler</w> <w>böhler</w>
<w>carsten</w> <w>carsten</w>
<w>chamorro</w> <w>chamorro</w>
@ -31,6 +33,7 @@
<w>dantas</w> <w>dantas</w>
<w>dikay</w> <w>dikay</w>
<w>diorite</w> <w>diorite</w>
<w>dodgeball</w>
<w>dougal</w> <w>dougal</w>
<w>dreamwalker</w> <w>dreamwalker</w>
<w>drobnič</w> <w>drobnič</w>
@ -43,8 +46,10 @@
<w>françois</w> <w>françois</w>
<w>freeyourgadget</w> <w>freeyourgadget</w>
<w>gadgetbridge</w> <w>gadgetbridge</w>
<w>gateball</w>
<w>gbapplication</w> <w>gbapplication</w>
<w>getpebble</w> <w>getpebble</w>
<w>gfdi</w>
<w>gideão</w> <w>gideão</w>
<w>girolamo</w> <w>girolamo</w>
<w>gobbetti</w> <w>gobbetti</w>
@ -52,11 +57,14 @@
<w>greenrobot</w> <w>greenrobot</w>
<w>greffier</w> <w>greffier</w>
<w>guedes</w> <w>guedes</w>
<w>handcycling</w>
<w>hasants</w> <w>hasants</w>
<w>hauck</w> <w>hauck</w>
<w>hiit</w>
<w>hplus</w> <w>hplus</w>
<w>huami</w> <w>huami</w>
<w>ieee</w> <w>ieee</w>
<w>infini</w>
<w>inkscape</w> <w>inkscape</w>
<w>irul</w> <w>irul</w>
<w>itag</w> <w>itag</w>
@ -68,15 +76,19 @@
<w>josé</w> <w>josé</w>
<w>joão</w> <w>joão</w>
<w>julien</w> <w>julien</w>
<w>jumpmaster</w>
<w>junginger</w> <w>junginger</w>
<w>jyou</w> <w>jyou</w>
<w>kabaddi</w>
<w>kasha</w> <w>kasha</w>
<w>kaushan</w> <w>kaushan</w>
<w>keeshii</w> <w>keeshii</w>
<w>kitesurfing</w>
<w>kompact</w> <w>kompact</w>
<w>kranz</w> <w>kranz</w>
<w>kromke</w> <w>kromke</w>
<w>kronoz</w> <w>kronoz</w>
<w>lacross</w>
<w>ladbsoft</w> <w>ladbsoft</w>
<w>ladera</w> <w>ladera</w>
<w>lenovo</w> <w>lenovo</w>
@ -94,14 +106,18 @@
<w>mijia</w> <w>mijia</w>
<w>morpheuz</w> <w>morpheuz</w>
<w>mosenkovs</w> <w>mosenkovs</w>
<w>multisport</w>
<w>nephiel</w> <w>nephiel</w>
<w>nodomain</w> <w>nodomain</w>
<w>nordhøy</w> <w>nordhøy</w>
<w>normano</w> <w>normano</w>
<w>novotny</w> <w>novotny</w>
<w>oraclejdk</w> <w>oraclejdk</w>
<w>paddleboarding</w>
<w>padel</w>
<w>pebblekit</w> <w>pebblekit</w>
<w>pfeiffer</w> <w>pfeiffer</w>
<w>pickleball</w>
<w>pinetime</w> <w>pinetime</w>
<w>pivotto</w> <w>pivotto</w>
<w>postsorino</w> <w>postsorino</w>
@ -114,6 +130,7 @@
<w>rssi</w> <w>rssi</w>
<w>sami</w> <w>sami</w>
<w>schrecker</w> <w>schrecker</w>
<w>sepak</w>
<w>sergey</w> <w>sergey</w>
<w>sevostyanova</w> <w>sevostyanova</w>
<w>shahrabani</w> <w>shahrabani</w>
@ -125,9 +142,12 @@
<w>spotify</w> <w>spotify</w>
<w>stacktraces</w> <w>stacktraces</w>
<w>stefanek</w> <w>stefanek</w>
<w>stringset</w>
<w>subsport</w>
<w>szymon</w> <w>szymon</w>
<w>taavi</w> <w>taavi</w>
<w>tablename</w> <w>tablename</w>
<w>takraw</w>
<w>teclast</w> <w>teclast</w>
<w>tiparega</w> <w>tiparega</w>
<w>toleda</w> <w>toleda</w>
@ -146,6 +166,8 @@
<w>vebryn</w> <w>vebryn</w>
<w>veneziano</w> <w>veneziano</w>
<w>vibratissimo</w> <w>vibratissimo</w>
<w>wakeboarding</w>
<w>wakesurfing</w>
<w>walkjivefly</w> <w>walkjivefly</w>
<w>watchapp</w> <w>watchapp</w>
<w>watchapps</w> <w>watchapps</w>
@ -158,6 +180,7 @@
<w>xwatch</w> <w>xwatch</w>
<w>yaron</w> <w>yaron</w>
<w>zalewszczak</w> <w>zalewszczak</w>
<w>zepp</w>
<w>zetime</w> <w>zetime</w>
<w>zhong</w> <w>zhong</w>
</words> </words>

View File

@ -24,7 +24,13 @@ import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
@ -60,6 +66,8 @@ public class ZeppOsRemindersService extends AbstractZeppOsService {
private static final String PREF_CAPABILITY = "huami_2021_capability_reminders"; private static final String PREF_CAPABILITY = "huami_2021_capability_reminders";
private final Map<ZeppOsReminder, Integer> deviceReminders = new HashMap<>();
public ZeppOsRemindersService(final ZeppOsSupport support) { public ZeppOsRemindersService(final ZeppOsSupport support) {
super(support, false); super(support, false);
} }
@ -99,6 +107,9 @@ public class ZeppOsRemindersService extends AbstractZeppOsService {
case CMD_RESPONSE: case CMD_RESPONSE:
LOG.info("Got reminders from band"); LOG.info("Got reminders from band");
decodeAndUpdateReminders(payload); decodeAndUpdateReminders(payload);
final TransactionBuilder builder = getSupport().createTransactionBuilder("send reminders");
sendReminders(builder);
builder.queue(getSupport().getQueue());
return; return;
default: default:
LOG.warn("Unexpected reminders payload byte {}", String.format("0x%02x", payload[0])); LOG.warn("Unexpected reminders payload byte {}", String.format("0x%02x", payload[0]));
@ -108,8 +119,7 @@ public class ZeppOsRemindersService extends AbstractZeppOsService {
@Override @Override
public void initialize(final TransactionBuilder builder) { public void initialize(final TransactionBuilder builder) {
requestCapabilities(builder); requestCapabilities(builder);
//requestReminders(builder); requestReminders(builder);
sendReminders(builder);
} }
private void requestCapabilities(final TransactionBuilder builder) { private void requestCapabilities(final TransactionBuilder builder) {
@ -134,22 +144,72 @@ public class ZeppOsRemindersService extends AbstractZeppOsService {
return; return;
} }
// Send the reminders final Date currentDate = new Date();
for (int i = 0; i < reminders.size(); i++) { final Set<ZeppOsReminder> allReminders = new HashSet<>();
LOG.debug("Sending reminder at position {}", i); final LinkedList<ZeppOsReminder> toDelete = new LinkedList<>();
final LinkedList<ZeppOsReminder> toSend = new LinkedList<>();
sendReminderToDevice(builder, i, reminders.get(i)); for (final Reminder reminder : reminders) {
if (currentDate.after(reminder.getDate())) {
// Disregard reminders from the past, device ignores them anyway (does not even send
// them back when requesting).
continue;
} }
// Delete the remaining slots, skipping the sent reminders final ZeppOsReminder newReminder = new ZeppOsReminder(reminder);
for (int i = reminders.size(); i < reminderSlotCount; i++) { allReminders.add(newReminder);
LOG.debug("Deleting reminder at position {}", i);
sendReminderToDevice(builder, i, null); if (deviceReminders.containsKey(newReminder)) {
// Reminder exists and is up-to-date
continue;
}
toSend.push(newReminder);
}
for (final ZeppOsReminder reminder : deviceReminders.keySet()) {
if (!allReminders.contains(reminder)) {
toDelete.add(reminder);
} }
} }
protected void sendReminderToDevice(final TransactionBuilder builder, int position, final Reminder reminder) { for (final ZeppOsReminder reminder : toSend) {
if (!toDelete.isEmpty()) {
// If we have reminders to delete, replace them with the ones we want to send
final ZeppOsReminder reminderToReplace = toDelete.pop();
final Integer position = deviceReminders.get(reminderToReplace);
if (position == null) {
LOG.error("Failed to find position for {} - this should never happen", reminderToReplace);
// We somehow got out of sync - request all reminders again
requestReminders(builder);
return;
}
LOG.debug("Updating reminder at position {}", position);
sendReminderToDevice(builder, position, true, reminder);
deviceReminders.remove(reminderToReplace);
deviceReminders.put(reminder, position);
} else {
// Find the next available position
for (int position = 0; position < reminderSlotCount; position++) {
if (!deviceReminders.containsValue(position)) {
LOG.debug("Creating reminder at position {}", position);
sendReminderToDevice(builder, position, false, reminder);
deviceReminders.put(reminder, position);
break;
}
}
}
}
for (final ZeppOsReminder reminder : toDelete) {
final Integer position = deviceReminders.remove(reminder);
if (position != null) {
LOG.debug("Deleting reminder at position {}", position);
sendReminderToDevice(builder, position, true, null);
}
}
}
private void sendReminderToDevice(final TransactionBuilder builder, int position, final boolean update, final ZeppOsReminder reminder) {
final DeviceCoordinator coordinator = getCoordinator(); final DeviceCoordinator coordinator = getCoordinator();
final int reminderSlotCount = coordinator.getReminderSlotCount(getSupport().getDevice()); final int reminderSlotCount = coordinator.getReminderSlotCount(getSupport().getDevice());
if (position + 1 > reminderSlotCount) { if (position + 1 > reminderSlotCount) {
@ -164,24 +224,116 @@ public class ZeppOsRemindersService extends AbstractZeppOsService {
return; return;
} }
final String message; final ByteBuffer buf = ByteBuffer.allocate(1 + 10 + reminder.getText().getBytes(StandardCharsets.UTF_8).length + 1);
if (reminder.getMessage().length() > coordinator.getMaximumReminderMessageLength()) {
LOG.warn("The reminder message length {} is longer than {}, will be truncated",
reminder.getMessage().length(),
coordinator.getMaximumReminderMessageLength()
);
message = StringUtils.truncate(reminder.getMessage(), coordinator.getMaximumReminderMessageLength());
} else {
message = reminder.getMessage();
}
final ByteBuffer buf = ByteBuffer.allocate(1 + 10 + message.getBytes(StandardCharsets.UTF_8).length + 1);
buf.order(ByteOrder.LITTLE_ENDIAN); buf.order(ByteOrder.LITTLE_ENDIAN);
// Update does an upsert, so let's use it. If we call create twice on the same ID, it becomes weird // Update does an upsert, so let's use it. If we call create twice on the same ID, it becomes weird
buf.put(CMD_UPDATE); buf.put(update ? CMD_UPDATE : CMD_CREATE);
buf.put((byte) (position & 0xFF)); buf.put((byte) (position & 0xFF));
buf.putInt(reminder.getFlags());
buf.putInt(reminder.getTimestamp());
buf.put((byte) 0x00);
buf.put(reminder.getText().getBytes(StandardCharsets.UTF_8));
buf.put((byte) 0x00);
write(builder, buf.array());
}
private void decodeAndUpdateReminders(final byte[] payload) {
final int numReminders = payload[1] & 0xff;
if (payload.length < 3 + numReminders * 11) {
LOG.warn("Unexpected payload length of {} for {} reminders", payload.length, numReminders);
return;
}
LOG.debug("Got {} reminders from band", numReminders);
deviceReminders.clear();
int i = 3;
while (i < payload.length) {
if (payload.length - i < 11) {
LOG.error("Not enough bytes remaining to parse a reminder ({})", payload.length - i);
return;
}
final int reminderPosition = payload[i++] & 0xff;
final int reminderFlags = BLETypeConversions.toUint32(payload, i);
i += 4;
final int reminderTimestamp = BLETypeConversions.toUint32(payload, i);
i += 4;
i++; // 0 ?
final Date reminderDate = new Date(reminderTimestamp * 1000L);
final String reminderText = StringUtils.untilNullTerminator(payload, i);
if (reminderText == null) {
LOG.error("Failed to parse reminder text at pos {}", i);
return;
}
final ZeppOsReminder zeppOsReminder = new ZeppOsReminder(
reminderFlags,
reminderTimestamp,
reminderText
);
deviceReminders.put(zeppOsReminder, reminderPosition);
i += reminderText.length() + 1;
LOG.info("Reminder[{}]: {}, {}, {}", reminderPosition, String.format("0x%04x", reminderFlags), reminderDate, reminderText);
}
if (i != payload.length) {
LOG.error("Unexpected reminders payload trailer, {} bytes were not consumed", payload.length - i);
}
// TODO persist in database. Probably not trivial, because reminderPosition != reminderId
}
public static int getSlotCount(final Prefs devicePrefs) {
return devicePrefs.getInt(PREF_CAPABILITY, 0);
}
private class ZeppOsReminder {
final int flags;
final int timestamp;
final String text;
private ZeppOsReminder(final int flags, final int timestamp, final String text) {
this.flags = flags;
this.timestamp = timestamp;
this.text = text;
}
private ZeppOsReminder(final Reminder reminder) {
this.flags = getReminderFlags(reminder);
this.timestamp = (int) (reminder.getDate().getTime() / 1000L);
if (reminder.getMessage().length() > getCoordinator().getMaximumReminderMessageLength()) {
LOG.warn("The reminder message length {} is longer than {}, will be truncated",
reminder.getMessage().length(),
getCoordinator().getMaximumReminderMessageLength()
);
text = StringUtils.truncate(reminder.getMessage(), getCoordinator().getMaximumReminderMessageLength());
} else {
text = reminder.getMessage();
}
}
public int getFlags() {
return flags;
}
public int getTimestamp() {
return timestamp;
}
public String getText() {
return text;
}
private int getReminderFlags(final Reminder reminder) {
final Calendar cal = BLETypeConversions.createCalendar(); final Calendar cal = BLETypeConversions.createCalendar();
cal.setTime(reminder.getDate()); cal.setTime(reminder.getDate());
@ -205,62 +357,23 @@ public class ZeppOsRemindersService extends AbstractZeppOsService {
reminderFlags |= FLAG_REPEAT_YEAR; reminderFlags |= FLAG_REPEAT_YEAR;
break; break;
default: default:
LOG.warn("Unknown repetition for reminder in position {}, defaulting to once", position); LOG.warn("Unknown repetition for reminder {}, defaulting to once", reminder.getReminderId());
} }
buf.putInt(reminderFlags); return reminderFlags;
buf.putInt((int) (cal.getTimeInMillis() / 1000L));
buf.put((byte) 0x00);
buf.put(message.getBytes(StandardCharsets.UTF_8));
buf.put((byte) 0x00);
write(builder, buf.array());
} }
private void decodeAndUpdateReminders(final byte[] payload) { @Override
final int numReminders = payload[1] & 0xff; public boolean equals(final Object o) {
if (this == o) return true;
if (payload.length < 3 + numReminders * 11) { if (!(o instanceof ZeppOsReminder)) return false;
LOG.warn("Unexpected payload length of {} for {} reminders", payload.length, numReminders); final ZeppOsReminder that = (ZeppOsReminder) o;
return; return flags == that.flags && timestamp == that.timestamp && Objects.equals(text, that.text);
} }
LOG.debug("Got {} reminders from band", numReminders); @Override
public int hashCode() {
int i = 3; return Objects.hash(flags, timestamp, text);
while (i < payload.length) { }
if (payload.length - i < 11) {
LOG.error("Not enough bytes remaining to parse a reminder ({})", payload.length - i);
return;
}
final int reminderPosition = payload[i++] & 0xff;
final int reminderFlags = BLETypeConversions.toUint32(payload, i);
i += 4;
final int reminderTimestamp = BLETypeConversions.toUint32(payload, i);
i += 4;
i++; // 0 ?
final Date reminderDate = new Date(reminderTimestamp * 1000L);
final String reminderText = StringUtils.untilNullTerminator(payload, i);
if (reminderText == null) {
LOG.error("Failed to parse reminder text at pos {}", i);
return;
}
i += reminderText.length() + 1;
LOG.info("Reminder[{}]: {}, {}, {}", reminderPosition, String.format("0x%04x", reminderFlags), reminderDate, reminderText);
}
if (i != payload.length) {
LOG.error("Unexpected reminders payload trailer, {} bytes were not consumed", payload.length - i);
}
// TODO persist in database. Probably not trivial, because reminderPosition != reminderId
}
public static int getSlotCount(final Prefs devicePrefs) {
return devicePrefs.getInt(PREF_CAPABILITY, 0);
} }
} }