/* Copyright (C) 2023-2024 Johannes Krude
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.casio;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.LocalDate;
import java.nio.charset.StandardCharsets;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.UUID;
import java.util.Arrays;
import java.util.List;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BcdUtil;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.casio.CasioConstants;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_AUTO;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT_AUTO;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT_DAY_MONTH;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT_MONTH_DAY;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_OPERATING_SOUNDS;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HOURLY_CHIME_ENABLE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AUTOLIGHT;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LIGHT_DURATION_LONGER;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_POWER_SAVING;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_CONNECTION_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIME_SYNC;
// this class is for those Casio watches which request reads on the 2C characteristic and write on the 2D characteristic
public abstract class Casio2C2DSupport extends CasioSupport {
public static final byte FEATURE_CURRENT_TIME = 0x09;
public static final byte FEATURE_ALERT_LEVEL = 0x0a;
public static final byte FEATURE_BLE_FEATURES = 0x10;
public static final byte FEATURE_SETTING_FOR_BLE = 0x11;
public static final byte FEATURE_SETTING_FOR_BASIC = 0x13;
public static final byte FEATURE_SETTING_FOR_ALM = 0x15;
public static final byte FEATURE_SETTING_FOR_ALM2 = 0x16;
public static final byte FEATURE_VERSION_INFORMATION = 0x20;
public static final byte FEATURE_APP_INFORMATION = 0x22;
public static final byte FEATURE_WATCH_NAME = 0x23;
public static final byte FEATURE_MODULE_ID = 0x26;
public static final byte FEATURE_WATCH_CONDITION = 0x28;
public static final byte FEATURE_DST_WATCH_STATE = (byte) 0x1d;
public static final byte FEATURE_DST_SETTING = (byte) 0x1e;
public static final byte FEATURE_WORLD_CITY = (byte) 0x1f;
public static final byte FEATURE_REMINDER_TITLE = 0x30;
public static final byte FEATURE_REMINDER_TIME = 0x31;
public static final byte FEATURE_CURRENT_TIME_MANAGER = 0x39;
public static final byte FEATURE_CONNECTION_PARAMETER_MANAGER = 0x3a;
public static final byte FEATURE_ADVERTISE_PARAMETER_MANAGER = 0x3b;
public static final byte FEATURE_SETTING_FOR_TARGET_VALUE = 0x43;
public static final byte FEATURE_SETTING_FOR_USER_PROFILE = 0x45;
public static final byte FEATURE_SERVICE_DISCOVERY_MANAGER = 0x47;
protected static Logger LOG;
LinkedList requests = new LinkedList<>();
public Casio2C2DSupport(Logger logger) {
super(logger);
LOG = logger;
}
@Override
public boolean connect() {
requests.clear();
return super.connect();
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
initializeDeviceSettings(builder);
return builder;
}
public void writeAllFeatures(TransactionBuilder builder, byte[] arr) {
if (!requests.isEmpty()) {
LOG.warn("writing while waiting for a response may lead to incorrect received responses");
}
builder.write(getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID), arr);
}
public void writeAllFeaturesRequest(TransactionBuilder builder, byte[] arr) {
builder.write(getCharacteristic(CasioConstants.CASIO_READ_REQUEST_FOR_ALL_FEATURES_CHARACTERISTIC_UUID), arr);
}
public interface ResponseHandler {
void handle(byte[] response);
}
public class FeatureResponses extends HashMap {
public byte[][] get(FeatureRequest[] requests) {
byte[][] result = new byte[requests.length][];
for (int i = 0; i < requests.length; i++) {
byte[] response = get(requests[i]);
if (response == null)
return null;
result[i] = response;
}
return result;
}
}
public interface ResponsesHandler {
void handle(FeatureResponses responses);
}
public static class FeatureRequest {
byte data[];
public FeatureRequest(byte arg0) {
data = new byte[] {arg0};
}
public FeatureRequest(byte arg0, byte arg1) {
data = new byte[] {arg0, arg1};
}
public static FeatureRequest parse(String str) {
byte[] data = RequestWithData.parseData(str);
if (data == null)
return null;
if (data.length == 1) {
return new FeatureRequest(data[0]);
} else if (data.length == 2) {
return new FeatureRequest(data[0], data[1]);
} else {
return null;
}
}
public byte[] getData() {
return data.clone();
}
@Override
public int hashCode() {
return Arrays.hashCode(data);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof FeatureRequest))
return false;
FeatureRequest fr = (FeatureRequest) o;
return Arrays.equals(data, fr.data);
}
public boolean matches(byte[] response) {
// 0xff is of type int which goes out of range etc. byte is signed.
if (response.length > 2 && response[0] == (byte) 0xFF && response[1] == (byte) 0x81) {
if (data.length < response.length - 2)
return false;
for (int i = 2; i < response.length; i++) {
if (response[i] != data[i-2])
return false;
}
return true;
} else {
if (response.length < data.length)
return false;
for (int i = 0; i < data.length; i++) {
if (response[i] != data[i])
return false;
}
return true;
}
}
}
private static class RequestWithData {
public FeatureRequest request;
public byte[] data;
public RequestWithData(FeatureRequest request, byte[] data) {
this.request = request;
this.data = data;
}
public static RequestWithData parse(String str) {
String[] kv = str.split(";");
if (kv.length != 2)
return null;
FeatureRequest request = FeatureRequest.parse(kv[0]);
byte[] data = parseData(kv[1]);
if (request == null || data == null)
return null;
return new RequestWithData(request, data);
}
public static byte[] parseData(String str) {
if (!str.matches("\\A\\[[0-9]+(, [0-9]+)*\\]\\z"))
return null;
String[] strings = str.replace("[", "").replace("]", "").split(", ");
byte[] result = new byte[strings.length];
for (int i = 0; i < result.length; i++)
result[i] = (byte) Integer.parseInt(strings[i]);
return result;
}
}
private static class RequestWithHandler {
public FeatureRequest request;
public ResponseHandler handler;
public RequestWithHandler(FeatureRequest request, ResponseHandler handler) {
this.request = request;
this.handler = handler;
}
}
public void requestFeature(TransactionBuilder builder, FeatureRequest request, ResponseHandler handler) {
builder.notify(getCharacteristic(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID), true);
writeAllFeaturesRequest(builder, request.getData());
builder.run((gatt) -> requests.add(new RequestWithHandler(request, handler)));
}
public void requestFeatures(TransactionBuilder builder, Set requests, ResponsesHandler handler) {
FeatureResponses responses = new FeatureResponses();
HashSet missing = new HashSet();
for (FeatureRequest request: requests) {
missing.add(request);
}
for (FeatureRequest request: requests) {
requestFeature(builder, request, data -> {
responses.put(request, data);
missing.remove(request);
if (missing.isEmpty()) {
handler.handle(responses);
}
});
}
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
UUID characteristicUUID = characteristic.getUuid();
if (characteristicUUID.equals(CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID)) {
byte[] response = characteristic.getValue();
Iterator it = requests.iterator();
while (it.hasNext()) {
RequestWithHandler rh = it.next();
if (rh.request.matches(response)) {
it.remove();
rh.handler.handle(response);
return true;
}
}
LOG.warn("unhandled response: " + Logging.formatBytes(response));
}
return super.onCharacteristicChanged(gatt, characteristic);
}
public void writeCurrentTime(TransactionBuilder builder, ZonedDateTime time) {
byte[] arr = new byte[11];
arr[0] = FEATURE_CURRENT_TIME;
byte[] tmp = prepareCurrentTime(time);
System.arraycopy(tmp, 0, arr, 1, 10);
writeAllFeatures(builder, arr);
}
FeatureCache featureCache = new FeatureCache();
public class FeatureCache {
Map values = null;
// can not be done on initialization, since the SharedPreferences are not yet available
void load() {
values = new HashMap();
Set serialized = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getStringSet("casio_features_current_values", new HashSet());
if (serialized == null)
return;
for (String str: serialized) {
if (str == null)
continue;
RequestWithData entry = RequestWithData.parse(str);
if (entry == null) {
LOG.warn("invalid casio_features_current_values entry: " + str);
continue;
}
values.put(entry.request, entry.data);
}
}
public void save(SharedPreferences.Editor editor) {
if (values == null)
return;
Set serialized = new HashSet();
for (Map.Entry entry: values.entrySet()) {
serialized.add(Arrays.toString(entry.getKey().getData()) + ";" + Arrays.toString(entry.getValue()));
}
editor.putStringSet("casio_features_current_values", serialized);
}
public byte[] get(FeatureRequest request) {
if (values == null)
load();
return values.get(request);
}
public byte[][] get(FeatureRequest[] requests) {
byte[][] result = new byte[requests.length][];
for (int i = 0; i < requests.length; i++) {
byte[] response = get(requests[i]);
if (response == null)
return null;
result[i] = response;
}
return result;
}
public void add(Map entries, SharedPreferences.Editor editor) {
if (values == null)
load();
for (Map.Entry entry: entries.entrySet()) {
values.put(entry.getKey(), entry.getValue());
}
save(editor);
}
}
public interface UpdateValuesHandler {
public void apply(byte[][] data);
}
public interface ReadValuesHandler {
public boolean apply(byte[][] data);
}
public abstract class DeviceSetting {
// which features to request
public abstract FeatureRequest[] getFeatureRequests();
// compares and updates watch data, cached previous data and GB state, returns true if data was changed
public abstract boolean mergeValues(byte[][] data, byte[][] previous, SharedPreferences.Editor editor);
public final boolean mergeValues(byte[][] data, byte[][] previous, UpdateValuesHandler updateValues, ReadValuesHandler readValues) {
boolean needsUpdating = false;
// check if GB state has changed
if (previous != null) {
byte[][] copies = new byte[previous.length][];
for (int i = 0; i < previous.length; i++) {
copies[i] = previous[i].clone();
}
updateValues.apply(copies);
for (int i = 0; i < previous.length; i++) {
if (!Arrays.equals(previous[i], copies[i])) {
needsUpdating = true;
break;
}
}
}
// update GB state and check if data needs change
if (!needsUpdating) {
needsUpdating = readValues.apply(data);
}
// maybe update data
if (needsUpdating) {
updateValues.apply(data);
return true;
}
return false;
}
};
ArrayList deviceSettings = new ArrayList();
DeviceAlarms deviceAlarms = new DeviceAlarms();
DeviceReminders deviceReminders = new DeviceReminders();
HashMap devicePreferenceByName = new HashMap();
{
deviceSettings.add(deviceAlarms);
deviceSettings.add(deviceReminders);
for (DevicePreference pref: supportedDevicePreferences()) {
deviceSettings.add(pref);
devicePreferenceByName.put(pref.getName(), pref);
}
}
void initializeDeviceSettings(TransactionBuilder builder) {
Set deviceSettingFeatures = new LinkedHashSet();
for (DeviceSetting ds: deviceSettings)
deviceSettingFeatures.addAll(Arrays.asList(ds.getFeatureRequests()));
requestFeatures(builder, deviceSettingFeatures, responses -> {
LinkedHashSet override = new LinkedHashSet();
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
for (DeviceSetting ds: deviceSettings) {
FeatureRequest[] requests = ds.getFeatureRequests();
byte[][] data = responses.get(requests);
if (data == null)
continue;
byte[][] previous = featureCache.get(requests);
if (ds.mergeValues(data, previous, editor)) {
override.addAll(Arrays.asList(requests));
}
}
featureCache.add(responses, editor);
editor.apply();
if (!override.isEmpty()) {
ArrayList updatedSettings = new ArrayList();
for (FeatureRequest fr: override) {
updatedSettings.add(responses.get(fr));
}
sendSettingsToDevice(updatedSettings.toArray(new byte[][] {}));
}
});
}
void sendSettingsToDevice(byte[][] settings) {
TransactionBuilder builder = createTransactionBuilder("DeviceSetting.write");
for (byte[] data: settings) {
writeAllFeatures(builder, data);
}
builder.run((gatt) -> GB.toast(getContext(), getContext().getString(R.string.user_feedback_set_settings_ok), Toast.LENGTH_SHORT, GB.INFO));
builder.queue(getQueue());
}
public abstract class DeviceItems- extends DeviceSetting {
public void onSet(ArrayList extends Item> items) {
if (!isInitialized()) {
return;
}
byte[][] currentValues = featureCache.get(getFeatureRequests());
if (currentValues == null) {
return;
}
updateValues(currentValues, items);
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
featureCache.save(editor);
editor.apply();
sendSettingsToDevice(currentValues);
}
public abstract void updateValues(byte[][] data, List extends Item> items);
}
public class DeviceAlarms extends DeviceItems {
@Override
public FeatureRequest[] getFeatureRequests() {
final int maxAlarms = gbDevice.getDeviceCoordinator().getAlarmSlotCount(gbDevice);
if (maxAlarms == 0) {
return new FeatureRequest[] {};
} else if (maxAlarms == 1) {
return new FeatureRequest[] {new FeatureRequest(FEATURE_SETTING_FOR_ALM)};
} else {
return new FeatureRequest[] {new FeatureRequest(FEATURE_SETTING_FOR_ALM), new FeatureRequest(FEATURE_SETTING_FOR_ALM2)};
}
};
@Override
public void updateValues(byte[][] data, List extends Alarm> alarms) {
for (Alarm alarm: alarms) {
updateValues(data, alarm);
}
}
public void updateValues(byte[][] data, Alarm alarm) {
int pos = alarm.getPosition();
int alm = pos == 0 ? 0 : 1;
int index = pos == 0 ? 1 : 1 + 4 * (pos-1);
if (data.length <= alm || index + 4 > data[alm].length) {
LOG.error("alarm data too small");
}
updateValue(data[alm], index, alarm);
}
public void updateValue(byte[] data, int index, Alarm alarm) {
if(alarm.getEnabled()) {
data[index] |= 0x40;
} else {
data[index] &= ~0x40;
}
//data[index+1] = 0x40;
data[index+2] = (byte) alarm.getHour();
data[index+3] = (byte) alarm.getMinute();
}
@Override
public final boolean mergeValues(byte[][] data, byte[][] previous, SharedPreferences.Editor editor) {
boolean changed = false;
LinkedHashSet foundAlarms = new LinkedHashSet();
for (nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm: DBHelper.getAlarms(gbDevice)) {
foundAlarms.add(alarm.getPosition());
if (mergeValues(data, previous, uData -> updateValues(uData, alarm), rData -> readValues(rData, alarm, false))) {
changed = true;
}
}
final int maxAlarms = gbDevice.getDeviceCoordinator().getAlarmSlotCount(gbDevice);
for (int i = 0; i < maxAlarms; i++) {
if (!foundAlarms.contains(i)) {
nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm = AlarmUtils.createDefaultAlarm(gbDevice, i);
if (alarm == null) {
continue;
}
readValues(data, alarm, true);
}
}
return changed;
}
public boolean readValues(byte[][] data, nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm, boolean forceStore) {
int pos = alarm.getPosition();
int alm = pos == 0 ? 0 : 1;
int index = pos == 0 ? 1 : 1+ 4 * (pos-1);
if (data.length <= alm || index + 4 > data[alm].length) {
LOG.error("alarm data too small");
}
return readValues(data[alm], index, alarm, forceStore);
}
public boolean readValues(byte[] data, int index, nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm, boolean forceStore) {
boolean enabled = (data[index] & 0x40) == 0x40;
byte hour = data[index+2];
byte minute = data[index+3];
if (forceStore || alarm.getEnabled() != enabled || alarm.getHour() != hour || alarm.getMinute() != minute) {
alarm.setEnabled(enabled);
alarm.setHour(hour);
alarm.setMinute(minute);
alarm.setRepetition(Alarm.ALARM_MON | Alarm.ALARM_TUE | Alarm.ALARM_WED | Alarm.ALARM_THU | Alarm.ALARM_FRI | Alarm.ALARM_SAT | Alarm.ALARM_SUN);
DBHelper.store(alarm);
Intent intent = new Intent(DeviceService.ACTION_SAVE_ALARMS);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
return false;
}
}
@Override
public void onSetAlarms(ArrayList extends Alarm> alarms) {
deviceAlarms.onSet(alarms);
}
public class DeviceReminders extends DeviceItems {
@Override
public FeatureRequest[] getFeatureRequests() {
final int maxReminders = gbDevice.getDeviceCoordinator().getReminderSlotCount(gbDevice);
FeatureRequest[] requests = new FeatureRequest[maxReminders*2];
for (int i = 0; i < maxReminders; i++) {
requests[2*i] = new FeatureRequest(FEATURE_REMINDER_TITLE, (byte) (i+1));
requests[2*i+1] = new FeatureRequest(FEATURE_REMINDER_TIME, (byte) (i+1));
}
return requests;
};
@Override
public void updateValues(byte[][] data, List extends Reminder> reminders) {
final int maxReminders = gbDevice.getDeviceCoordinator().getReminderSlotCount(gbDevice);
for (int i = 0; i < maxReminders; i++) {
if (i < reminders.size()) {
updateValues(data, i, reminders.get(i));
} else {
Arrays.fill(data[2*i], 2, 20, (byte) 0);
Arrays.fill(data[2*i+1], 2, 10, (byte) 0);
}
}
}
public void updateValues(byte[][] data, int pos, Reminder reminder) {
final byte[] message = reminder.getMessage().getBytes(StandardCharsets.US_ASCII);
System.arraycopy(message, 0, data[2*pos], 2, Math.min(message.length, 18));
Arrays.fill(data[2*pos], 2+message.length, 20, (byte) 0);
LocalDate start = LocalDate.ofInstant(reminder.getDate().toInstant(), ZoneId.of("UTC"));
if (start.getYear() < 2000) {
if (reminder.getRepetition() == Reminder.EVERY_WEEK) {
final LocalDate first = LocalDate.of(2000, 1, 1);
start = first.plusDays((start.getDayOfWeek().getValue()-first.getDayOfWeek().getValue())%7);
} else {
start = start.withYear(2000);
}
} else if (start.getYear() > 2099) {
if (reminder.getRepetition() == Reminder.EVERY_WEEK) {
final LocalDate last = LocalDate.of(2099, 31, 12);
start = last.minusDays((last.getDayOfWeek().getValue()-start.getDayOfWeek().getValue())%7);
} else {
final int dayOfMonth = start.getDayOfMonth();
start = start.withYear(2099);
if (start.getDayOfMonth() != dayOfMonth) {
start = start.withYear(2096); // leap-year
}
}
}
byte mode = 0;
LocalDate end = start;
byte weekdays = 0;
switch (reminder.getRepetition()) {
case Reminder.EVERY_DAY:
mode = 0x0B;
end = LocalDate.of(2099,12,31);
break;
case Reminder.EVERY_WEEK:
mode = 0x05;
weekdays = (byte) (1 << (start.getDayOfWeek().getValue()%7)); // Sunday is 1<<0
break;
case Reminder.EVERY_MONTH:
mode = 0x11;
break;
case Reminder.EVERY_YEAR:
mode = 0x09;
break;
default:
mode = 0x01;
}
data[2*pos+1][2] = mode;
data[2*pos+1][3] = BcdUtil.toBcd8(start.getYear()-2000);
data[2*pos+1][4] = BcdUtil.toBcd8(start.getMonthValue());
data[2*pos+1][5] = BcdUtil.toBcd8(start.getDayOfMonth());
data[2*pos+1][6] = BcdUtil.toBcd8(end.getYear()-2000);
data[2*pos+1][7] = BcdUtil.toBcd8(end.getMonthValue());
data[2*pos+1][8] = BcdUtil.toBcd8(end.getDayOfMonth());
data[2*pos+1][9] = weekdays;
}
@Override
public final boolean mergeValues(byte[][] data, byte[][] previous, SharedPreferences.Editor editor) {
List extends Reminder> reminders = DBHelper.getReminders(gbDevice);
// always overwrite watch values
return mergeValues(data, previous, uData -> updateValues(uData, reminders), rData -> false);
}
}
@Override
public void onSetReminders(ArrayList extends Reminder> reminders) {
deviceReminders.onSet(reminders);
}
public abstract class DevicePreference extends DeviceSetting {
byte feature;
public final FeatureRequest getFeatureRequest() {
return new FeatureRequest(feature);
};
@Override
public final FeatureRequest[] getFeatureRequests() {
return new FeatureRequest[] {getFeatureRequest()};
};
String name;
public final String getName() {
return name;
}
public abstract void updateValue(byte[] data);
@Override
public final boolean mergeValues(byte[][] data, byte[][] previous, SharedPreferences.Editor editor) {
return mergeValues(data, previous, uData -> updateValue(uData[0]), rData -> readValue(rData[0], editor));
}
public abstract boolean readValue(byte[] data, SharedPreferences.Editor editor);
};
public abstract Casio2C2DSupport.DevicePreference[] supportedDevicePreferences();
@Override
public void onSendConfiguration(String config) {
DevicePreference pref = devicePreferenceByName.get(config);
if (pref == null) {
LOG.warn("received configuration change for unsupported setting " + config);
return;
}
if (!isInitialized()) {
return;
}
byte[] currentValue = featureCache.get(pref.getFeatureRequest());
if (currentValue == null) {
LOG.error("unknown current watch value for config " + config);
return;
}
pref.updateValue(currentValue);
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
featureCache.save(editor);
editor.apply();
sendSettingsToDevice(new byte[][] {currentValue});
}
public class UnsignedByteDevicePreference extends DevicePreference {
int index;
@Override
public void updateValue(byte[] data) {
data[index] = (byte) getDevicePrefsValue();
}
@Override
public boolean readValue(byte[] data, SharedPreferences.Editor editor) {
return setDevicePrefsValue(editor, data[index] & 0xff);
}
public int getDevicePrefsValue() {
return getDevicePrefs().getInt(name, -1);
}
public boolean setDevicePrefsValue(SharedPreferences.Editor editor, int value) {
if (value != getDevicePrefsValue()) {
editor.putString(name, Integer.toString(value));
}
return false;
}
}
public class BoolDevicePreference extends DevicePreference {
int index;
byte mask;
@Override
public void updateValue(byte[] data) {
if (getDevicePrefsValue()) {
data[index] &= ~mask;
} else {
data[index] |= mask;
}
}
@Override
public boolean readValue(byte[] data, SharedPreferences.Editor editor) {
if ((data[index] & mask) == 0) {
return setDevicePrefsValue(editor, true);
} else {
return setDevicePrefsValue(editor, false);
}
}
public boolean getDevicePrefsValue() {
return getDevicePrefs().getBoolean(name, false);
}
public boolean setDevicePrefsValue(SharedPreferences.Editor editor, boolean value) {
if (value != getDevicePrefsValue()) {
editor.putBoolean(name, value);
}
return false;
}
}
public class InvertedBoolDevicePreference extends BoolDevicePreference {
@Override
public void updateValue(byte[] data) {
if (getDevicePrefsValue()) {
data[index] |= mask;
} else {
data[index] &= ~mask;
}
}
@Override
public boolean readValue(byte[] data, SharedPreferences.Editor editor) {
if ((data[index] & mask) == 0) {
return setDevicePrefsValue(editor, false);
} else {
return setDevicePrefsValue(editor, true);
}
}
}
interface AutoGetter {
public String get(DevicePrefs devicePrefs);
}
public class AutoBoolDevicePreference extends BoolDevicePreference {
AutoGetter getter;
String autoValue;
String trueValue;
String falseValue;
@Override
public boolean getDevicePrefsValue() {
return getter.get(getDevicePrefs()).equals(trueValue);
}
@Override
public boolean setDevicePrefsValue(SharedPreferences.Editor editor, boolean value) {
String strValue = value ? trueValue : falseValue;
if (!getter.get(getDevicePrefs()).equals(strValue)) {
if (getDevicePrefs().getString(name, autoValue).equals(autoValue)) {
return true;
} else {
editor.putString(name, strValue);
}
}
return false;
}
}
public class TimeSyncPreference extends BoolDevicePreference {
{ name = PREF_TIME_SYNC; }
{ feature = FEATURE_SETTING_FOR_BLE; }
{ index = 12; mask = (byte) (0x80 & 0xff); }
}
public class ConnectionDurationPreference extends UnsignedByteDevicePreference {
{ name = PREF_CONNECTION_DURATION; }
{ feature = FEATURE_SETTING_FOR_BLE; }
{ index = 14; }
@Override
public int getDevicePrefsValue() {
return getDevicePrefs().getInt(name, -1);
}
@Override
public boolean setDevicePrefsValue(SharedPreferences.Editor editor, int value) {
if (value != getDevicePrefsValue()) {
editor.putString(name, Integer.toString(value));
}
return false;
}
}
public class TimeFormatPreference extends AutoBoolDevicePreference {
{ name = PREF_TIMEFORMAT; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 1; mask = 0x01; }
{ getter = gbPrefs -> gbPrefs.getTimeFormat(); }
{ autoValue = PREF_TIMEFORMAT_AUTO; }
{ trueValue = PREF_TIMEFORMAT_12H; }
{ falseValue = PREF_TIMEFORMAT_24H; }
}
public class OperatingSoundPreference extends BoolDevicePreference {
{ name = PREF_OPERATING_SOUNDS; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 1; mask = 0x02; }
}
public class AutoLightPreference extends BoolDevicePreference {
{ name = PREF_AUTOLIGHT; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 1; mask = 0x04; }
}
public class PowerSavingPreference extends BoolDevicePreference {
{ name = PREF_POWER_SAVING; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 1; mask = 0x10; }
}
public class LongerLightDurationPreference extends InvertedBoolDevicePreference {
{ name = PREF_LIGHT_DURATION_LONGER; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 2; mask = 0x01; }
}
public class DayMonthOrderPreference extends AutoBoolDevicePreference {
{ name = PREF_DATEFORMAT; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 4; mask = 0x01; }
{ getter = gbPrefs -> gbPrefs.getDateFormatDayMonthOrder(); }
{ autoValue = PREF_DATEFORMAT_AUTO; }
{ trueValue = PREF_DATEFORMAT_MONTH_DAY; }
{ falseValue = PREF_DATEFORMAT_DAY_MONTH; }
}
public class LanguagePreference extends UnsignedByteDevicePreference {
{ name = PREF_LANGUAGE; }
{ feature = FEATURE_SETTING_FOR_BASIC; }
{ index = 5; }
String[] languages = { "en_US", "es_ES", "fr_FR"," de_DE", "it_IT", "ru_RU" };
@Override
public int getDevicePrefsValue() {
String value = getDevicePrefs().getString(name, PREF_LANGUAGE_AUTO);
int number = 0;
if (value.equals(PREF_LANGUAGE_AUTO)) {
String lang = Locale.getDefault().getLanguage() + "_";
for (int i=0; i < languages.length; i++) {
if (languages[i].startsWith(lang)) {
number = i;
break;
}
}
} else {
for (int i=0; i < languages.length; i++) {
if (value.equals(languages[i])) {
number = i;
break;
}
}
}
return number;
}
@Override
public boolean setDevicePrefsValue(SharedPreferences.Editor editor, int value) {
if (getDevicePrefsValue() != value) {
if (getDevicePrefs().getString(name, PREF_LANGUAGE_AUTO).equals(PREF_LANGUAGE_AUTO)) {
return true;
} else {
if (value < languages.length) {
editor.putString(name, languages[value]);
} else {
editor.putString(name, "unknown");
}
}
}
return false;
}
}
public class HourlyChimePreference extends InvertedBoolDevicePreference {
{ name = PREF_HOURLY_CHIME_ENABLE; }
{ feature = FEATURE_SETTING_FOR_ALM; }
{ index = 1; mask = (byte) (0x80 & 0xff); }
}
}