From eef224ca4e61146603a40cdc74b785a01d48f250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 10 Aug 2025 23:16:09 +0100 Subject: [PATCH] Garmin: Set alarms from phone --- .../gadgetbridge/daogen/GBDaoGenerator.java | 4 +- .../gadgetbridge/activities/AlarmDetails.java | 308 +++++++++--------- .../activities/ConfigureAlarms.java | 6 +- .../devicesettings/DeviceSettingsUtils.java | 18 +- .../DeviceSpecificSettingsFragment.java | 8 +- .../schema/GadgetbridgeUpdate_112.java | 28 ++ .../devices/AbstractDeviceCoordinator.java | 20 ++ .../devices/DeviceCoordinator.java | 6 + .../watches/GarminWatchCoordinator.java | 40 +++ .../zeppos/ZeppOsSettingsCustomizer.java | 2 +- .../gadgetbridge/model/Alarm.java | 55 +++- .../devices/garmin/FileTransferHandler.java | 18 +- .../service/devices/garmin/GarminSupport.java | 101 ++++++ .../garmin/fit/FieldDefinitionFactory.java | 66 ++-- .../garmin/fit/FitRecordDataBuilder.java | 2 +- .../devices/garmin/fit/GlobalFITMessage.java | 14 +- .../devices/garmin/fit/RecordData.java | 44 +-- .../garmin/fit/codegen/FitCodeGen.java | 2 + .../FieldDefinitionAlarmLabel.java | 67 ++++ .../garmin/fit/messages/FitAlarmSettings.java | 11 +- .../fit/messages/FitDeviceSettings.java | 68 ++++ .../devices/huawei/HuaweiSupportProvider.java | 2 +- .../huawei/requests/GetEventAlarmList.java | 8 +- .../huawei/requests/GetSmartAlarmList.java | 8 +- .../gadgetbridge/util/AlarmUtils.java | 6 +- .../res/layout/activity_alarm_details.xml | 84 ++++- app/src/main/res/values/strings.xml | 7 + 27 files changed, 725 insertions(+), 278 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_112.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarmLabel.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 29f0b7be81..422dc6afde 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -58,7 +58,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(111, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(112, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -1242,6 +1242,8 @@ public class GBDaoGenerator { alarm.addBooleanProperty("unused").notNull(); alarm.addStringProperty("title"); alarm.addStringProperty("description"); + alarm.addIntProperty("soundCode").notNull(); + alarm.addBooleanProperty("backlight").notNull(); alarm.addToOne(user, userId); alarm.addToOne(device, deviceId); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java index 73b11b0352..f1d395d386 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AlarmDetails.java @@ -23,137 +23,89 @@ import android.text.Spanned; import android.text.format.DateFormat; import android.view.MenuItem; import android.view.View; +import android.widget.ArrayAdapter; import android.widget.CheckedTextView; -import android.widget.EditText; -import android.widget.TimePicker; import java.text.NumberFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.databinding.ActivityAlarmDetailsBinding; import nodomain.freeyourgadget.gadgetbridge.entities.Alarm; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm.ALARM_LABEL; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm.ALARM_SOUND; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class AlarmDetails extends AbstractGBActivity { + private ActivityAlarmDetailsBinding binding; private Alarm alarm; - private TimePicker timePicker; - private CheckedTextView cbSmartWakeup; - private CheckedTextView cbSnooze; - private CheckedTextView cbMonday; - private CheckedTextView cbTuesday; - private CheckedTextView cbWednesday; - private CheckedTextView cbThursday; - private CheckedTextView cbFriday; - private CheckedTextView cbSaturday; - private CheckedTextView cbSunday; - private EditText title; - private EditText description; - private EditText smartWakeupInterval; private GBDevice device; + private final Map textToAlarmSound = new HashMap<>(ALARM_SOUND.values().length); + private final Map textToAlarmLabel = new HashMap<>(ALARM_LABEL.values().length); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_alarm_details); + binding = ActivityAlarmDetailsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); alarm = (Alarm) getIntent().getSerializableExtra(nodomain.freeyourgadget.gadgetbridge.model.Alarm.EXTRA_ALARM); device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE); + if (device == null) { + throw new IllegalArgumentException("No device provided to AlarmDetails"); + } - title = findViewById(R.id.alarm_title); - description = findViewById(R.id.alarm_description); + binding.cbSmartWakeup.setOnClickListener(v -> { + ((CheckedTextView) v).toggle(); + binding.cbSmartWakeupInterval.setEnabled(((CheckedTextView) v).isChecked()); + }); + binding.cbSnooze.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbMonday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbTuesday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbWednesday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbThursday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbFriday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbSaturday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + binding.cbSunday.setOnClickListener(v -> ((CheckedTextView) v).toggle()); - timePicker = findViewById(R.id.alarm_time_picker); - cbSmartWakeup = findViewById(R.id.alarm_cb_smart_wakeup); - smartWakeupInterval = findViewById(R.id.alarm_cb_smart_wakeup_interval); - cbSnooze = findViewById(R.id.alarm_cb_snooze); - cbMonday = findViewById(R.id.alarm_cb_monday); - cbTuesday = findViewById(R.id.alarm_cb_tuesday); - cbWednesday = findViewById(R.id.alarm_cb_wednesday); - cbThursday = findViewById(R.id.alarm_cb_thursday); - cbFriday = findViewById(R.id.alarm_cb_friday); - cbSaturday = findViewById(R.id.alarm_cb_saturday); - cbSunday = findViewById(R.id.alarm_cb_sunday); - - - cbSmartWakeup.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - smartWakeupInterval.setEnabled(((CheckedTextView) v).isChecked()); - } - }); - cbSnooze.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbMonday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbTuesday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbWednesday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbThursday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbFriday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbSaturday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - cbSunday.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - ((CheckedTextView) v).toggle(); - } - }); - - timePicker.setIs24HourView(DateFormat.is24HourFormat(GBApplication.getContext())); - timePicker.setCurrentHour(alarm.getHour()); - timePicker.setCurrentMinute(alarm.getMinute()); + binding.timePicker.setIs24HourView(DateFormat.is24HourFormat(GBApplication.getContext())); + binding.timePicker.setCurrentHour(alarm.getHour()); + binding.timePicker.setCurrentMinute(alarm.getMinute()); boolean smartAlarmSupported = supportsSmartWakeup(alarm.getPosition()); boolean smartAlarmForced = forcedSmartWakeup(alarm.getPosition()); boolean smartAlarmIntervalSupported = supportsSmartWakeupInterval(alarm.getPosition()); - cbSmartWakeup.setChecked(alarm.getSmartWakeup() || smartAlarmForced); - cbSmartWakeup.setVisibility(smartAlarmSupported ? View.VISIBLE : View.GONE); + binding.cbSmartWakeup.setChecked(alarm.getSmartWakeup() || smartAlarmForced); + binding.cbSmartWakeup.setVisibility(smartAlarmSupported ? View.VISIBLE : View.GONE); if (smartAlarmForced) { - cbSmartWakeup.setEnabled(false); + binding.cbSmartWakeup.setEnabled(false); // Force the text to be visible for the "interval" part // Enabled or not can still be seen in the checkmark // TODO: I'd like feedback on this if (GBApplication.isDarkThemeEnabled()) - cbSmartWakeup.setTextColor(getResources().getColor(android.R.color.secondary_text_dark)); + binding.cbSmartWakeup.setTextColor(getResources().getColor(android.R.color.secondary_text_dark)); else - cbSmartWakeup.setTextColor(getResources().getColor(android.R.color.secondary_text_light)); + binding.cbSmartWakeup.setTextColor(getResources().getColor(android.R.color.secondary_text_light)); } if (smartAlarmIntervalSupported) - cbSmartWakeup.setText(R.string.alarm_smart_wakeup_interval); + binding.cbSmartWakeup.setText(R.string.alarm_smart_wakeup_interval); - smartWakeupInterval.setVisibility(smartAlarmSupported && smartAlarmIntervalSupported ? View.VISIBLE : View.GONE); - smartWakeupInterval.setEnabled(alarm.getSmartWakeup() || smartAlarmForced); - if (alarm.getSmartWakeupInterval() != null) - smartWakeupInterval.setText(NumberFormat.getInstance().format(alarm.getSmartWakeupInterval())); - smartWakeupInterval.setFilters(new InputFilter[] { + binding.cbSmartWakeupInterval.setVisibility(smartAlarmSupported && smartAlarmIntervalSupported ? View.VISIBLE : View.GONE); + binding.cbSmartWakeupInterval.setEnabled(alarm.getSmartWakeup() || smartAlarmForced); + if (alarm.getSmartWakeupInterval() != null) { + binding.cbSmartWakeupInterval.setText(NumberFormat.getInstance().format(alarm.getSmartWakeupInterval())); + } + binding.cbSmartWakeupInterval.setFilters(new InputFilter[]{ new InputFilter() { @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { @@ -164,8 +116,8 @@ public class AlarmDetails extends AbstractGBActivity { try { int value = Integer.parseInt(strValue); if (value > 255) { - smartWakeupInterval.setText("255"); - smartWakeupInterval.setSelection(3); // Move cursor to end + binding.cbSmartWakeupInterval.setText("255"); + binding.cbSmartWakeupInterval.setSelection(3); // Move cursor to end } } catch (NumberFormatException e) { return ""; @@ -175,87 +127,111 @@ public class AlarmDetails extends AbstractGBActivity { } }); - cbSnooze.setChecked(alarm.getSnooze()); - int snoozeVisibility = supportsSnoozing(device) ? View.VISIBLE : View.GONE; - cbSnooze.setVisibility(snoozeVisibility); + binding.cbSnooze.setChecked(alarm.getSnooze()); + binding.cbSnooze.setVisibility(supportsSnoozing() ? View.VISIBLE : View.GONE); - title.setVisibility(supportsTitle() ? View.VISIBLE : View.GONE); - title.setText(alarm.getTitle()); + binding.cbBacklight.setChecked(alarm.getBacklight()); + binding.cbBacklight.setVisibility(supportsAlarmBacklight() ? View.VISIBLE : View.GONE); + binding.cbBacklight.setOnClickListener(v -> ((CheckedTextView) v).toggle()); + + binding.presetLabelLayout.setVisibility(supportsAlarmTitlePresets() ? View.VISIBLE : View.GONE); + if (supportsAlarmTitlePresets()) { + final List alarmTitlePresets = getAlarmTitlePresets(); + String[] items = new String[alarmTitlePresets.size()]; + for (int i = 0; i < alarmTitlePresets.size(); i++) { + items[i] = getString(alarmTitlePresets.get(i).getLabel()); + if (alarmTitlePresets.get(i).name().equals(alarm.getTitle())) { + binding.presetLabelSpinner.setText(items[i], false); + } + textToAlarmLabel.put(items[i], alarmTitlePresets.get(i)); + } + final ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items); + binding.presetLabelSpinner.setAdapter(adapter); + if (StringUtils.isEmpty(binding.presetLabelSpinner.getText().toString())) { + binding.presetLabelSpinner.setText(items[0], false); + } + } + + binding.soundModeLayout.setVisibility(supportsAlarmSounds() ? View.VISIBLE : View.GONE); + if (supportsAlarmSounds()) { + String[] items = new String[ALARM_SOUND.values().length - 1]; + for (int i = 0; i < items.length; i++) { + final ALARM_SOUND alarmSound = ALARM_SOUND.values()[i + 1]; // skip unused + items[i] = getString(alarmSound.getLabel()); + if (alarm.getSoundCode() == i) { + binding.soundModeSpinner.setText(items[i], false); + } + textToAlarmSound.put(items[i], alarmSound); + } + final ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items); + binding.soundModeSpinner.setAdapter(adapter); + } + + binding.title.setVisibility(supportsTitle() ? View.VISIBLE : View.GONE); + binding.title.setText(alarm.getTitle()); final int titleLimit = getAlarmTitleLimit(); if (titleLimit > 0) { - title.setFilters(new InputFilter[]{new InputFilter.LengthFilter(titleLimit)}); + binding.title.setFilters(new InputFilter[]{new InputFilter.LengthFilter(titleLimit)}); } - description.setVisibility(supportsDescription() ? View.VISIBLE : View.GONE); - description.setText(alarm.getDescription()); + binding.description.setVisibility(supportsDescription() ? View.VISIBLE : View.GONE); + binding.description.setText(alarm.getDescription()); - cbMonday.setChecked(alarm.getRepetition(Alarm.ALARM_MON)); - cbTuesday.setChecked(alarm.getRepetition(Alarm.ALARM_TUE)); - cbWednesday.setChecked(alarm.getRepetition(Alarm.ALARM_WED)); - cbThursday.setChecked(alarm.getRepetition(Alarm.ALARM_THU)); - cbFriday.setChecked(alarm.getRepetition(Alarm.ALARM_FRI)); - cbSaturday.setChecked(alarm.getRepetition(Alarm.ALARM_SAT)); - cbSunday.setChecked(alarm.getRepetition(Alarm.ALARM_SUN)); + binding.cbMonday.setChecked(alarm.getRepetition(Alarm.ALARM_MON)); + binding.cbTuesday.setChecked(alarm.getRepetition(Alarm.ALARM_TUE)); + binding.cbWednesday.setChecked(alarm.getRepetition(Alarm.ALARM_WED)); + binding.cbThursday.setChecked(alarm.getRepetition(Alarm.ALARM_THU)); + binding.cbFriday.setChecked(alarm.getRepetition(Alarm.ALARM_FRI)); + binding.cbSaturday.setChecked(alarm.getRepetition(Alarm.ALARM_SAT)); + binding.cbSunday.setChecked(alarm.getRepetition(Alarm.ALARM_SUN)); } private boolean supportsSmartWakeup(int position) { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.supportsSmartWakeup(device, position); - } - return false; + return device.getDeviceCoordinator().supportsSmartWakeup(device, position); } private boolean supportsSmartWakeupInterval(int position) { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.supportsSmartWakeupInterval(device, position); - } - return false; + return device.getDeviceCoordinator().supportsSmartWakeupInterval(device, position); } /** * The alarm at this position *must* be a smart alarm */ private boolean forcedSmartWakeup(int position) { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.forcedSmartWakeup(device, position); - } - return false; + return device.getDeviceCoordinator().forcedSmartWakeup(device, position); } private boolean supportsTitle() { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.supportsAlarmTitle(device); - } - return false; + return device.getDeviceCoordinator().supportsAlarmTitle(device); } private int getAlarmTitleLimit() { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.getAlarmTitleLimit(device); - } - return -1; + return device.getDeviceCoordinator().getAlarmTitleLimit(device); } private boolean supportsDescription() { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.supportsAlarmDescription(device); - } - return false; + return device.getDeviceCoordinator().supportsAlarmDescription(device); } - private boolean supportsSnoozing(GBDevice device) { - if (device != null) { - DeviceCoordinator coordinator = device.getDeviceCoordinator(); - return coordinator.supportsAlarmSnoozing(device); - } - return false; + private boolean supportsSnoozing() { + return device.getDeviceCoordinator().supportsAlarmSnoozing(device); + } + + private boolean supportsAlarmSounds() { + return device.getDeviceCoordinator().supportsAlarmSounds(device); + } + + private boolean supportsAlarmBacklight() { + return device.getDeviceCoordinator().supportsAlarmBacklight(device); + } + + private boolean supportsAlarmTitlePresets() { + return device.getDeviceCoordinator().supportsAlarmTitlePresets(device); + } + + private List getAlarmTitlePresets() { + return device.getDeviceCoordinator().getAlarmTitlePresets(device); } @Override @@ -271,20 +247,40 @@ public class AlarmDetails extends AbstractGBActivity { private void updateAlarm() { // Set alarm as used and enabled if time has changed - if (alarm.getUnused() && alarm.getHour() != timePicker.getCurrentHour() || alarm.getMinute() != timePicker.getCurrentMinute()) { + if (alarm.getUnused() && alarm.getHour() != binding.timePicker.getCurrentHour() || alarm.getMinute() != binding.timePicker.getCurrentMinute()) { alarm.setUnused(false); alarm.setEnabled(true); } - alarm.setSmartWakeup(supportsSmartWakeup(alarm.getPosition()) && cbSmartWakeup.isChecked()); - String interval = smartWakeupInterval.getText().toString(); - alarm.setSmartWakeupInterval(interval.equals("") ? null : Integer.parseInt(interval)); - alarm.setSnooze(supportsSnoozing(device) && cbSnooze.isChecked()); - int repetitionMask = AlarmUtils.createRepetitionMask(cbMonday.isChecked(), cbTuesday.isChecked(), cbWednesday.isChecked(), cbThursday.isChecked(), cbFriday.isChecked(), cbSaturday.isChecked(), cbSunday.isChecked()); + alarm.setSmartWakeup(supportsSmartWakeup(alarm.getPosition()) && binding.cbSmartWakeup.isChecked()); + String interval = binding.cbSmartWakeupInterval.getText().toString(); + alarm.setSmartWakeupInterval(interval.isEmpty() ? null : Integer.parseInt(interval)); + alarm.setSnooze(supportsSnoozing() && binding.cbSnooze.isChecked()); + int repetitionMask = AlarmUtils.createRepetitionMask( + binding.cbMonday.isChecked(), + binding.cbTuesday.isChecked(), + binding.cbWednesday.isChecked(), + binding.cbThursday.isChecked(), + binding.cbFriday.isChecked(), + binding.cbSaturday.isChecked(), + binding.cbSunday.isChecked() + ); alarm.setRepetition(repetitionMask); - alarm.setHour(timePicker.getCurrentHour()); - alarm.setMinute(timePicker.getCurrentMinute()); - alarm.setTitle(title.getText().toString()); - alarm.setDescription(description.getText().toString()); + alarm.setHour(binding.timePicker.getCurrentHour()); + alarm.setMinute(binding.timePicker.getCurrentMinute()); + alarm.setBacklight(binding.cbBacklight.isChecked()); + if (supportsAlarmTitlePresets()) { + final ALARM_LABEL alarmLabel = textToAlarmLabel.get(binding.presetLabelSpinner.getText().toString()); + if (alarmLabel != null) { + alarm.setTitle(alarmLabel.name()); + } else { + alarm.setTitle(""); + } + } else { + alarm.setTitle(binding.title.getText().toString()); + } + final ALARM_SOUND alarmSound = textToAlarmSound.get(binding.soundModeSpinner.getText().toString()); + alarm.setSoundCode(Objects.requireNonNullElse(alarmSound, ALARM_SOUND.UNSET).ordinal()); + alarm.setDescription(binding.description.getText().toString()); DBHelper.store(alarm); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java index 7b3f545450..051bb86b45 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java @@ -25,7 +25,6 @@ import android.content.IntentFilter; import android.os.Bundle; import android.view.MenuItem; -import androidx.annotation.NonNull; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -43,12 +42,9 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.entities.Alarm; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.Device; -import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; public class ConfigureAlarms extends AbstractGBActivity { @@ -134,7 +130,7 @@ public class ConfigureAlarms extends AbstractGBActivity { } } if (!found) { - LOG.info("adding missing alarm at position " + position); + LOG.info("adding missing alarm at position {}", position); alarms.add(position, AlarmUtils.createDefaultAlarm(daoSession, getGbDevice(), position)); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsUtils.java index 9226932f34..90d7d94068 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsUtils.java @@ -38,6 +38,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -72,6 +73,7 @@ public final class DeviceSettingsUtils { /** * Populates a list preference, or hides it if no known supported values are known. + * @noinspection ConstantValue */ public static void populateOrHideListPreference(final CharSequence prefKey, final DeviceSpecificSettingsHandler handler, @@ -137,11 +139,10 @@ public final class DeviceSettingsUtils { final String possibleValue = possibleValues.get(i); final CharSequence knownLabel = entryNames.get(possibleValue); - if (knownLabel != null) { - entries[i] = knownLabel; - } else { - entries[i] = handler.getContext().getString(R.string.menuitem_unknown_app, possibleValue); - } + entries[i] = Objects.requireNonNullElseGet( + knownLabel, + () -> handler.getContext().getString(R.string.menuitem_unknown_app, possibleValue) + ); values[i] = possibleValue; } @@ -194,7 +195,7 @@ public final class DeviceSettingsUtils { }); } - public static void sortListPreference(final ListPreference listPreference) { + public static void sortListPreference(final ListPreference listPreference, final boolean keepFirst) { final CharSequence[] entries = listPreference.getEntries(); final CharSequence[] entryValues = listPreference.getEntryValues(); @@ -211,9 +212,8 @@ public final class DeviceSettingsUtils { combined[i][1] = entryValues[i].toString(); } - // Sort, keeping "auto" at the top - final boolean hasAuto = "auto".contentEquals(entryValues[0]); - Arrays.sort(combined, hasAuto ? 1 : 0, length, Comparator.comparing(o -> o[0])); + // Sort, keeping "the first" at the top + Arrays.sort(combined, keepFirst ? 1 : 0, length, Comparator.comparing(o -> o[0])); // Reassign sorted values for (int i = 0; i < length; i++) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index f009f21274..e83cc27b68 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -416,13 +416,15 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i languageListPreference.setEntries(entries); languageListPreference.setEntryValues(values); } - - DeviceSettingsUtils.sortListPreference(languageListPreference); + DeviceSettingsUtils.sortListPreference( + languageListPreference, + supportedLanguages.length > 0 && "auto".equals(supportedLanguages[0]) + ); } final ListPreference transliterationPreference = findPreference(DeviceSettingsPreferenceConst.PREF_TRANSLITERATION_LANGUAGES); if (transliterationPreference != null) { - DeviceSettingsUtils.sortListPreference(transliterationPreference); + DeviceSettingsUtils.sortListPreference(transliterationPreference, false); } String disconnectNotificationState = prefs.getString(PREF_DISCONNECT_NOTIFICATION, PREF_DO_NOT_DISTURB_OFF); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_112.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_112.java new file mode 100644 index 0000000000..f270a4882a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_112.java @@ -0,0 +1,28 @@ +package nodomain.freeyourgadget.gadgetbridge.database.schema; + +import android.database.sqlite.SQLiteDatabase; + +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript; +import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao; + +public class GadgetbridgeUpdate_112 implements DBUpdateScript { + @Override + public void upgradeSchema(SQLiteDatabase db) { + if (!DBHelper.existsColumn(AlarmDao.TABLENAME, AlarmDao.Properties.SoundCode.columnName, db)) { + String ADD_COLUMN_SOUND_CODE = "ALTER TABLE " + AlarmDao.TABLENAME + " ADD COLUMN " + + AlarmDao.Properties.SoundCode.columnName + " INTEGER NOT NULL DEFAULT 0;"; + db.execSQL(ADD_COLUMN_SOUND_CODE); + } + + if (!DBHelper.existsColumn(AlarmDao.TABLENAME, AlarmDao.Properties.Backlight.columnName, db)) { + String ADD_COLUMN_SOUND_CODE = "ALTER TABLE " + AlarmDao.TABLENAME + " ADD COLUMN " + + AlarmDao.Properties.Backlight.columnName + " BOOLEAN NOT NULL DEFAULT TRUE;"; + db.execSQL(ADD_COLUMN_SOUND_CODE); + } + } + + @Override + public void downgradeSchema(SQLiteDatabase db) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 729f4fdcb6..d2eb399d36 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -75,6 +75,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; @@ -725,6 +726,25 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + public boolean supportsAlarmSounds(@NonNull GBDevice device) { + return false; + } + + @Override + public boolean supportsAlarmBacklight(@NonNull GBDevice device) { + return false; + } + + @Override + public boolean supportsAlarmTitlePresets(@NonNull GBDevice device) { + return false; + } + + @Override + public List getAlarmTitlePresets(@NonNull GBDevice device) { + return Collections.emptyList(); + } + @Override public boolean supportsMusicInfo(@NonNull GBDevice device) { return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index a9bec555f1..6f40be851b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.BodyEnergySample; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; @@ -544,6 +545,11 @@ public interface DeviceCoordinator { */ boolean supportsAlarmDescription(@NonNull GBDevice device); + boolean supportsAlarmSounds(@NonNull GBDevice device); + boolean supportsAlarmBacklight(@NonNull GBDevice device); + boolean supportsAlarmTitlePresets(@NonNull GBDevice device); + List getAlarmTitlePresets(@NonNull GBDevice device); + /** * Returns true if the given device supports heart rate measurements. * @return diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/watches/GarminWatchCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/watches/GarminWatchCoordinator.java index be264767ce..a1248d5833 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/watches/GarminWatchCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/watches/GarminWatchCoordinator.java @@ -2,9 +2,14 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches; import androidx.annotation.NonNull; +import java.util.Arrays; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; public abstract class GarminWatchCoordinator extends GarminCoordinator { @Override @@ -12,6 +17,41 @@ public abstract class GarminWatchCoordinator extends GarminCoordinator { return R.drawable.ic_device_zetime; } + @Override + public int getAlarmSlotCount(final GBDevice device) { + return supports(device, GarminCapability.REALTIME_SETTINGS) ? 0 : 10; + } + + @Override + public boolean supportsAlarmSounds(@NonNull final GBDevice device) { + return true; + } + + @Override + public boolean supportsAlarmBacklight(@NonNull final GBDevice device) { + return true; + } + + @Override + public boolean supportsAlarmTitlePresets(@NonNull final GBDevice device) { + return true; + } + + @Override + public List getAlarmTitlePresets(@NonNull final GBDevice device) { + return Arrays.asList( + Alarm.ALARM_LABEL.NONE, + Alarm.ALARM_LABEL.WAKE_UP, + Alarm.ALARM_LABEL.WORKOUT, + Alarm.ALARM_LABEL.REMINDER, + Alarm.ALARM_LABEL.APPOINTMENT, + Alarm.ALARM_LABEL.TRAINING, + Alarm.ALARM_LABEL.CLASS, + Alarm.ALARM_LABEL.MEDITATE, + Alarm.ALARM_LABEL.BEDTIME + ); + } + @Override public boolean supportsCalendarEvents(final GBDevice device) { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsSettingsCustomizer.java index 9a6c422d93..5c386d8f98 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsSettingsCustomizer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsSettingsCustomizer.java @@ -255,7 +255,7 @@ public class ZeppOsSettingsCustomizer extends HuamiSettingsCustomizer { // Since we populate the values dynamically, we need to sort it here final Preference languagePref = handler.findPreference("language"); if (languagePref != null) { - DeviceSettingsUtils.sortListPreference((ListPreference) languagePref); + DeviceSettingsUtils.sortListPreference((ListPreference) languagePref, true); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java index 59c3a599dd..4ce2b353f2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Alarm.java @@ -17,8 +17,12 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; +import androidx.annotation.StringRes; + import java.io.Serializable; +import nodomain.freeyourgadget.gadgetbridge.R; + public interface Alarm extends Serializable { /** * The {@link android.os.Bundle} name for transferring parceled alarms. @@ -36,6 +40,51 @@ public interface Alarm extends Serializable { byte ALARM_DAILY = Alarm.ALARM_MON | Alarm.ALARM_TUE | Alarm.ALARM_WED | Alarm.ALARM_THU | Alarm.ALARM_FRI | Alarm.ALARM_SAT | Alarm.ALARM_SUN; + enum ALARM_SOUND { + UNSET(R.string.unset), + OFF(R.string.off), + TONE(R.string.pref_header_sound), + VIBRATION(R.string.title_activity_vibration), + TONE_AND_VIBRATION(R.string.sound_and_vibration), + ; + + @StringRes + private final int label; + + ALARM_SOUND(final int label) { + this.label = label; + } + + public int getLabel() { + return label; + } + } + + /// Enum name will be persisted as the title + enum ALARM_LABEL { + NONE(R.string.none), + WAKE_UP(R.string.wake_up_time), + WORKOUT(R.string.pref_header_workout), + REMINDER(R.string.reminder), + APPOINTMENT(R.string.alarm_label_appointment), + TRAINING(R.string.alarm_label_training), + CLASS(R.string.alarm_label_class), + MEDITATE(R.string.alarm_label_meditate), + BEDTIME(R.string.bedtime), + ; + + @StringRes + private final int label; + + ALARM_LABEL(final int label) { + this.label = label; + } + + public int getLabel() { + return label; + } + } + int getPosition(); boolean getEnabled(); @@ -61,4 +110,8 @@ public interface Alarm extends Serializable { String getTitle(); String getDescription(); -} \ No newline at end of file + + int getSoundCode(); + + boolean getBacklight(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java index de7000b16a..e67c0f84f3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java @@ -237,7 +237,9 @@ public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYP private UploadRequestMessage setCreateFileStatusMessage(CreateFileStatusMessage createFileStatusMessage) { if (createFileStatusMessage.canProceed()) { LOG.info("SENDING UPLOAD FILE"); - updateUploadProgress(0); + if (currentlyUploading.directoryEntry.filetype != FileType.FILETYPE.SETTINGS) { + updateUploadProgress(0); + } return new UploadRequestMessage(createFileStatusMessage.getFileIndex(), currentlyUploading.getDataSize()); } else { LOG.warn("Cannot proceed with upload"); @@ -255,23 +257,31 @@ public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYP return currentlyUploading.take(); } else { LOG.warn("Cannot proceed with upload"); - updateUploadProgress(-1); + if (currentlyUploading.directoryEntry.filetype != FileType.FILETYPE.SETTINGS) { + updateUploadProgress(-1); + } this.currentlyUploading = null; } return null; } private GFDIMessage processUploadProgress(FileTransferDataStatusMessage fileTransferDataStatusMessage) { + final boolean showNotification = currentlyUploading.directoryEntry.filetype != FileType.FILETYPE.SETTINGS; + if (currentlyUploading.getDataSize() <= fileTransferDataStatusMessage.getDataOffset()) { this.currentlyUploading = null; LOG.info("SENDING SYNC COMPLETE!!!"); - updateUploadProgress(100); + if (showNotification) { + updateUploadProgress(100); + } return new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0); } else { if (fileTransferDataStatusMessage.canProceed()) { LOG.info("SENDING NEXT CHUNK!!!"); - updateUploadProgress((100 * currentlyUploading.dataHolder.position()) / currentlyUploading.dataHolder.limit()); + if (showNotification) { + updateUploadProgress((100 * currentlyUploading.dataHolder.position()) / currentlyUploading.dataHolder.limit()); + } if (fileTransferDataStatusMessage.getDataOffset() != currentlyUploading.dataHolder.position()) throw new IllegalStateException("Received file transfer status with unaligned offset"); return currentlyUploading.take(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index 31905cd576..c4e15ceb65 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -16,12 +16,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -53,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; @@ -77,10 +80,15 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents. import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitAsyncProcessor; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GpxRouteFileConverter; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarmLabel; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitAlarmSettings; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitDeviceSettings; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ConfigurationMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage; @@ -762,6 +770,99 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC sendOutgoingMessage("find device", findMyWatch); } + @Override + public void onSetAlarms(final ArrayList alarms) { + final int alarmSlotCount = getCoordinator().getAlarmSlotCount(getDevice()); + + final List dataRecords = new ArrayList<>(1 + alarmSlotCount); + + final int currentTime = (int) (System.currentTimeMillis() / 1000); + + dataRecords.add(new FitFileId.Builder() + .setType(FileType.FILETYPE.SETTINGS) + .setManufacturer(1) // garmin + .setProduct(65534) // connect + .setTimeCreated((long) currentTime) + .setSerialNumber(1L) + .setNumber(1) + .build()); + + final List deviceSettingsTimes = new ArrayList<>(); + final List deviceSettingsUnk5 = new ArrayList<>(); + final List deviceSettingsEnabled = new ArrayList<>(); + final List deviceSettingsRepeat = new ArrayList<>(); + + int numberEnabledAlarms = 0; + for (Alarm alarm : alarms) { + if (alarm.getUnused()) { + continue; + } + + final int soundCode = switch (Alarm.ALARM_SOUND.values()[alarm.getSoundCode()]) { + case OFF -> 0; + case TONE -> 1; + case VIBRATION -> 2; + case UNSET, TONE_AND_VIBRATION -> 3; + }; + final FieldDefinitionAlarmLabel.Label label; + + final String alarmTitle = alarm.getTitle(); + if (StringUtils.isBlank(alarmTitle)) { + label = FieldDefinitionAlarmLabel.Label.NONE; + } else { + FieldDefinitionAlarmLabel.Label alarmLabel; + try { + alarmLabel = FieldDefinitionAlarmLabel.Label.valueOf(alarmTitle); + } catch (final Exception e) { + LOG.error("Invalid alarm label {}", alarmTitle, e); + alarmLabel = FieldDefinitionAlarmLabel.Label.NONE; + } + label = alarmLabel; + } + + final long repetitionCode = alarm.getRepetition() != 0 ? alarm.getRepetition() : 128L; + + final FitAlarmSettings.Builder alarmBuilder = new FitAlarmSettings.Builder() + .setTime(LocalTime.of(alarm.getHour(), alarm.getMinute())) + .setRepeat(repetitionCode) + .setEnabled(alarm.getEnabled() ? 1 : 0) + .setSound(soundCode) + .setBacklight(alarm.getBacklight() ? 1 : 0) + .setSomeTimestamp((long) currentTime) + .setUnknown7(0) + .setLabel(label) + .setMessageIndex(numberEnabledAlarms); + + dataRecords.add(alarmBuilder.build()); + + deviceSettingsTimes.add(alarm.getHour() * 60 + alarm.getMinute()); + deviceSettingsUnk5.add(5); + deviceSettingsEnabled.add(alarm.getEnabled() ? 1 : 0); + deviceSettingsRepeat.add(repetitionCode); + + numberEnabledAlarms++; + } + + if (numberEnabledAlarms > 0) { + final FitDeviceSettings.Builder deviceSettingsBuilder = new FitDeviceSettings.Builder() + .setAlarmsTime(deviceSettingsTimes.toArray(new Number[0])) + .setAlarmsUnk5(deviceSettingsUnk5.toArray(new Number[0])) + .setAlarmsEnabled(deviceSettingsEnabled.toArray(new Number[0])) + .setAlarmsRepeat(deviceSettingsRepeat.toArray(new Number[0])); + + dataRecords.add(deviceSettingsBuilder.build()); + } + + final FitFile fitFile = new FitFile(dataRecords); + communicator.sendMessage( + "set alarms", + fileTransferHandler.initiateUpload( + fitFile.getOutgoingMessage(), + FileType.FILETYPE.SETTINGS + ).getOutgoingMessage() + ); + } + @Override public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { sendOutgoingMessage("set canned messages", protocolBufferHandler.setCannedMessages(cannedMessagesSpec)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java index 2aede47133..814644a925 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java @@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarm; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarmLabel; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionArray; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionCoordinate; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionDayOfWeek; @@ -26,48 +27,28 @@ public class FieldDefinitionFactory { if (null == field) { return new FieldDefinition(localNumber, size, baseType, name, scale, offset); } - switch (field) { - case ALARM: - return new FieldDefinitionAlarm(localNumber, size, baseType, name); - case ARRAY: - return new FieldDefinitionArray(localNumber, size, baseType, name, scale, offset); - case DAY_OF_WEEK: - return new FieldDefinitionDayOfWeek(localNumber, size, baseType, name); - case EXERCISE_CATEGORY: - return new FieldDefinitionExerciseCategory(localNumber, size, baseType, name); - case FILE_TYPE: - return new FieldDefinitionFileType(localNumber, size, baseType, name); - case GOAL_SOURCE: - return new FieldDefinitionGoalSource(localNumber, size, baseType, name); - case GOAL_TYPE: - return new FieldDefinitionGoalType(localNumber, size, baseType, name); - case HRV_STATUS: - return new FieldDefinitionHrvStatus(localNumber, size, baseType, name); - case HR_TIME_IN_ZONE: - return new FieldDefinitionHrTimeInZone(localNumber, size, baseType, name); - case HR_ZONE_HIGH_BOUNDARY: - return new FieldDefinitionHrZoneHighBoundary(localNumber, size, baseType, name); - case MEASUREMENT_SYSTEM: - return new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name); - case TEMPERATURE: - return new FieldDefinitionTemperature(localNumber, size, baseType, name); - case TIMESTAMP: - return new FieldDefinitionTimestamp(localNumber, size, baseType, name); - case WEATHER_CONDITION: - return new FieldDefinitionWeatherCondition(localNumber, size, baseType, name); - case LANGUAGE: - return new FieldDefinitionLanguage(localNumber, size, baseType, name); - case SLEEP_STAGE: - return new FieldDefinitionSleepStage(localNumber, size, baseType, name); - case WEATHER_AQI: - return new FieldDefinitionWeatherAqi(localNumber, size, baseType, name); - case COORDINATE: - return new FieldDefinitionCoordinate(localNumber, size, baseType, name); - case SWIM_STYLE: - return new FieldDefinitionSwimStyle(localNumber, size, baseType, name); - default: - return new FieldDefinition(localNumber, size, baseType, name); - } + return switch (field) { + case ALARM -> new FieldDefinitionAlarm(localNumber, size, baseType, name); + case ARRAY -> new FieldDefinitionArray(localNumber, size, baseType, name, scale, offset); + case DAY_OF_WEEK -> new FieldDefinitionDayOfWeek(localNumber, size, baseType, name); + case EXERCISE_CATEGORY -> new FieldDefinitionExerciseCategory(localNumber, size, baseType, name); + case ALARM_LABEL -> new FieldDefinitionAlarmLabel(localNumber, size, baseType, name); + case FILE_TYPE -> new FieldDefinitionFileType(localNumber, size, baseType, name); + case GOAL_SOURCE -> new FieldDefinitionGoalSource(localNumber, size, baseType, name); + case GOAL_TYPE -> new FieldDefinitionGoalType(localNumber, size, baseType, name); + case HRV_STATUS -> new FieldDefinitionHrvStatus(localNumber, size, baseType, name); + case HR_TIME_IN_ZONE -> new FieldDefinitionHrTimeInZone(localNumber, size, baseType, name); + case HR_ZONE_HIGH_BOUNDARY -> new FieldDefinitionHrZoneHighBoundary(localNumber, size, baseType, name); + case MEASUREMENT_SYSTEM -> new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name); + case TEMPERATURE -> new FieldDefinitionTemperature(localNumber, size, baseType, name); + case TIMESTAMP -> new FieldDefinitionTimestamp(localNumber, size, baseType, name); + case WEATHER_CONDITION -> new FieldDefinitionWeatherCondition(localNumber, size, baseType, name); + case LANGUAGE -> new FieldDefinitionLanguage(localNumber, size, baseType, name); + case SLEEP_STAGE -> new FieldDefinitionSleepStage(localNumber, size, baseType, name); + case WEATHER_AQI -> new FieldDefinitionWeatherAqi(localNumber, size, baseType, name); + case COORDINATE -> new FieldDefinitionCoordinate(localNumber, size, baseType, name); + case SWIM_STYLE -> new FieldDefinitionSwimStyle(localNumber, size, baseType, name); + }; } public enum FIELD { @@ -75,6 +56,7 @@ public class FieldDefinitionFactory { ARRAY, DAY_OF_WEEK, EXERCISE_CATEGORY, + ALARM_LABEL, FILE_TYPE, GOAL_SOURCE, GOAL_TYPE, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitRecordDataBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitRecordDataBuilder.java index 32b8d771fc..584c0924f2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitRecordDataBuilder.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitRecordDataBuilder.java @@ -47,7 +47,7 @@ public class FitRecordDataBuilder { new RecordHeader((byte) 0x40), ByteOrder.BIG_ENDIAN, globalMessage, - values.keySet().stream().map(globalMessage::getFieldDefinition).collect(Collectors.toList()), + values.entrySet().stream().map(e -> globalMessage.getFieldDefinition(e.getKey(), e.getValue().length)).collect(Collectors.toList()), null ), new RecordHeader((byte) 0x00) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java index 09be1316c1..978d4483ef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java @@ -31,7 +31,10 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(2, BaseType.UINT32, "time_offset"), new FieldDefinitionPrimitive(4, BaseType.ENUM, "time_mode"), new FieldDefinitionPrimitive(5, BaseType.SINT8, "time_zone_offset"), + new FieldDefinitionPrimitive(8, BaseType.UINT16, "alarms_time", FieldDefinitionFactory.FIELD.ARRAY), + new FieldDefinitionPrimitive(9, BaseType.ENUM, "alarms_unk5", FieldDefinitionFactory.FIELD.ARRAY), // [5, 5] new FieldDefinitionPrimitive(12, BaseType.ENUM, "backlight_mode"), + new FieldDefinitionPrimitive(28, BaseType.ENUM, "alarms_enabled", FieldDefinitionFactory.FIELD.ARRAY), // [1,1] new FieldDefinitionPrimitive(36, BaseType.ENUM, "activity_tracker_enabled"), new FieldDefinitionPrimitive(46, BaseType.ENUM, "move_alert_enabled"), new FieldDefinitionPrimitive(47, BaseType.ENUM, "date_mode"), @@ -41,7 +44,8 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(58, BaseType.UINT16, "autosync_min_steps"), new FieldDefinitionPrimitive(59, BaseType.UINT16, "autosync_min_time"), new FieldDefinitionPrimitive(86, BaseType.ENUM, "ble_auto_upload_enabled"), - new FieldDefinitionPrimitive(90, BaseType.UINT32, "auto_activity_detect") + new FieldDefinitionPrimitive(90, BaseType.UINT32, "auto_activity_detect"), + new FieldDefinitionPrimitive(92, BaseType.UINT32Z, "alarms_repeat", FieldDefinitionFactory.FIELD.ARRAY) )); public static GlobalFITMessage USER_PROFILE = new GlobalFITMessage(3, "USER_PROFILE", Arrays.asList( @@ -404,10 +408,10 @@ public class GlobalFITMessage { new FieldDefinitionPrimitive(1, BaseType.UINT32Z, "repeat"), // 31 weekday 96 weekend 126 all except mon 127 daily 128 once new FieldDefinitionPrimitive(2, BaseType.ENUM, "enabled"), // 0/1 new FieldDefinitionPrimitive(3, BaseType.ENUM, "sound"), // 0 none 1 sound 2 vibrate 3 sound+vibrate - new FieldDefinitionPrimitive(4, BaseType.ENUM, "unknown4"), // 1 + new FieldDefinitionPrimitive(4, BaseType.ENUM, "backlight"), // 1 new FieldDefinitionPrimitive(5, BaseType.UINT32, "some_timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP), new FieldDefinitionPrimitive(7, BaseType.UINT8, "unknown7"), // 0 - new FieldDefinitionPrimitive(8, BaseType.ENUM, "label"), // 0 none 2 workout 3 reminder 4 appointment 6 class 7 meditate 8 bedtime + new FieldDefinitionPrimitive(8, BaseType.ENUM, "label", FieldDefinitionFactory.FIELD.ALARM_LABEL), // 0 none 2 workout 3 reminder 4 appointment 6 class 7 meditate 8 bedtime new FieldDefinitionPrimitive(254, BaseType.UINT16, "message_index") )); @@ -684,13 +688,13 @@ public class GlobalFITMessage { return subset; } - public FieldDefinition getFieldDefinition(final String name) { + public FieldDefinition getFieldDefinition(final String name, final int count) { for (FieldDefinitionPrimitive fieldDefinitionPrimitive : fieldDefinitionPrimitives) { if (name.equals(fieldDefinitionPrimitive.name)) { return FieldDefinitionFactory.create( fieldDefinitionPrimitive.number, - fieldDefinitionPrimitive.size, + fieldDefinitionPrimitive.size * count, fieldDefinitionPrimitive.type, fieldDefinitionPrimitive.baseType, fieldDefinitionPrimitive.name, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java index 344531f03f..5c5a54ca47 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordData.java @@ -6,6 +6,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.Date; import java.util.List; @@ -28,9 +29,9 @@ public class RecordData { /** * The computed timestamp consists of the running timestamp for this record, which may come from - * a timestamp field 253, or from a compressed timestamp, or simply be the same timestamp as the - * previously seen sample. This does not take into account sample-specific timestamps such as - * timestamp16. + * a timestamp field 253, or from a compressed timestamp, or simply be the same timestamp as the + * previously seen sample. This does not take into account sample-specific timestamps such as + * timestamp16. */ public Long computedTimestamp = null; @@ -179,24 +180,25 @@ public class RecordData { tsb.append(new Date(getComputedTimestamp() * 1000L)); } - for (FieldData fieldData : fieldDataList) { - final String fieldName; - if (!StringUtils.isBlank(fieldData.getName())) { - fieldName = fieldData.getName(); - } else { - fieldName = "unknown_" + fieldData.getNumber() + fieldData; - } - Object o = fieldData.decode(); - final String fieldValueString; - if (o == null) { - fieldValueString = null; - } else if (o instanceof Object[]) { - fieldValueString = "[" + StringUtils.join((Object[]) o, ",") + "]"; - } else { - fieldValueString = o.toString(); - } - tsb.append(fieldName, fieldValueString); - } + fieldDataList.stream().sorted(Comparator.comparingInt(FieldData::getNumber)) + .forEach(fieldData -> { + final String fieldName; + if (!StringUtils.isBlank(fieldData.getName())) { + fieldName = fieldData.getName(); + } else { + fieldName = "unknown_" + fieldData.getNumber() + fieldData; + } + Object o = fieldData.decode(); + final String fieldValueString; + if (o == null) { + fieldValueString = null; + } else if (o instanceof Object[]) { + fieldValueString = "[" + StringUtils.join((Object[]) o, ",") + "]"; + } else { + fieldValueString = o.toString(); + } + tsb.append(fieldName, fieldValueString); + }); return tsb.build(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java index e85271d507..25463c64d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java @@ -27,6 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFIT import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarmLabel; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionExerciseCategory; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType; @@ -273,6 +274,7 @@ public class FitCodeGen { case ARRAY -> Number[].class; case DAY_OF_WEEK -> DayOfWeek.class; case EXERCISE_CATEGORY -> FieldDefinitionExerciseCategory.ExerciseCategory[].class; + case ALARM_LABEL -> FieldDefinitionAlarmLabel.Label.class; case FILE_TYPE -> FileType.FILETYPE.class; case GOAL_SOURCE -> FieldDefinitionGoalSource.Source.class; case GOAL_TYPE -> FieldDefinitionGoalType.Type.class; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarmLabel.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarmLabel.java new file mode 100644 index 0000000000..bc1904d67f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionAlarmLabel.java @@ -0,0 +1,67 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType; + +public class FieldDefinitionAlarmLabel extends FieldDefinition { + + public FieldDefinitionAlarmLabel(final int localNumber, final int size, final BaseType baseType, final String name) { + super(localNumber, size, baseType, name, 1, 0); + } + + @Override + public Object decode(final ByteBuffer byteBuffer) { + final Object rawObj = baseType.decode(byteBuffer, scale, offset); + if (rawObj != null) { + final int raw = (int) rawObj; + return Label.fromId(raw); + } + return null; + } + + @Override + public void encode(final ByteBuffer byteBuffer, final Object o) { + if (o instanceof Label) { + baseType.encode(byteBuffer, (((Label) o).getId()), scale, offset); + return; + } + baseType.encode(byteBuffer, o, scale, offset); + } + + public enum Label { + NONE(0), + WAKE_UP(1), + WORKOUT(2), + REMINDER(3), + APPOINTMENT(4), + TRAINING(5), + CLASS(6), + MEDITATE(7), + BEDTIME(8), + ; + + private final int id; + + Label(final int i) { + id = i; + } + + @Nullable + public static Label fromId(final int id) { + for (Label label : Label.values()) { + if (id == label.getId()) { + return label; + } + } + return null; + } + + public int getId() { + return id; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitAlarmSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitAlarmSettings.java index 4ba5e4effa..cd2cb3da2e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitAlarmSettings.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitAlarmSettings.java @@ -8,6 +8,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitRecord import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarmLabel.Label; // // WARNING: This class was auto-generated, please avoid modifying it directly. @@ -44,7 +45,7 @@ public class FitAlarmSettings extends RecordData { } @Nullable - public Integer getUnknown4() { + public Integer getBacklight() { return (Integer) getFieldByNumber(4); } @@ -59,8 +60,8 @@ public class FitAlarmSettings extends RecordData { } @Nullable - public Integer getLabel() { - return (Integer) getFieldByNumber(8); + public Label getLabel() { + return (Label) getFieldByNumber(8); } @Nullable @@ -93,7 +94,7 @@ public class FitAlarmSettings extends RecordData { return this; } - public Builder setUnknown4(final Integer value) { + public Builder setBacklight(final Integer value) { setFieldByNumber(4, value); return this; } @@ -108,7 +109,7 @@ public class FitAlarmSettings extends RecordData { return this; } - public Builder setLabel(final Integer value) { + public Builder setLabel(final Label value) { setFieldByNumber(8, value); return this; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitDeviceSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitDeviceSettings.java index 293ff8e9bd..756c4708c8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitDeviceSettings.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitDeviceSettings.java @@ -46,11 +46,47 @@ public class FitDeviceSettings extends RecordData { return (Integer) getFieldByNumber(5); } + @Nullable + public Number[] getAlarmsTime() { + final Object[] objectsArray = (Object[]) getFieldByNumber(8); + if (objectsArray == null) + return null; + final Number[] ret = new Number[objectsArray.length]; + for (int i = 0; i < objectsArray.length; i++) { + ret[i] = (Number) objectsArray[i]; + } + return ret; + } + + @Nullable + public Number[] getAlarmsUnk5() { + final Object[] objectsArray = (Object[]) getFieldByNumber(9); + if (objectsArray == null) + return null; + final Number[] ret = new Number[objectsArray.length]; + for (int i = 0; i < objectsArray.length; i++) { + ret[i] = (Number) objectsArray[i]; + } + return ret; + } + @Nullable public Integer getBacklightMode() { return (Integer) getFieldByNumber(12); } + @Nullable + public Number[] getAlarmsEnabled() { + final Object[] objectsArray = (Object[]) getFieldByNumber(28); + if (objectsArray == null) + return null; + final Number[] ret = new Number[objectsArray.length]; + for (int i = 0; i < objectsArray.length; i++) { + ret[i] = (Number) objectsArray[i]; + } + return ret; + } + @Nullable public Integer getActivityTrackerEnabled() { return (Integer) getFieldByNumber(36); @@ -101,6 +137,18 @@ public class FitDeviceSettings extends RecordData { return (Long) getFieldByNumber(90); } + @Nullable + public Number[] getAlarmsRepeat() { + final Object[] objectsArray = (Object[]) getFieldByNumber(92); + if (objectsArray == null) + return null; + final Number[] ret = new Number[objectsArray.length]; + for (int i = 0; i < objectsArray.length; i++) { + ret[i] = (Number) objectsArray[i]; + } + return ret; + } + public static class Builder extends FitRecordDataBuilder { public Builder() { super(2); @@ -131,11 +179,26 @@ public class FitDeviceSettings extends RecordData { return this; } + public Builder setAlarmsTime(final Number[] value) { + setFieldByNumber(8, value); + return this; + } + + public Builder setAlarmsUnk5(final Number[] value) { + setFieldByNumber(9, value); + return this; + } + public Builder setBacklightMode(final Integer value) { setFieldByNumber(12, value); return this; } + public Builder setAlarmsEnabled(final Number[] value) { + setFieldByNumber(28, value); + return this; + } + public Builder setActivityTrackerEnabled(final Integer value) { setFieldByNumber(36, value); return this; @@ -186,6 +249,11 @@ public class FitDeviceSettings extends RecordData { return this; } + public Builder setAlarmsRepeat(final Number[] value) { + setFieldByNumber(92, value); + return this; + } + @Override public FitDeviceSettings build() { return (FitDeviceSettings) super.build(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index fc6c89e890..a5654eb135 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -1004,7 +1004,7 @@ public class HuaweiSupportProvider { title = context.getString(R.string.alarm_smart_wakeup); description = context.getString(R.string.huawei_alarm_smart_description); } - return new Alarm(device.getId(), user.getId(), position, false, smartWakeup, null, false, 0, 6, 30, true, title, description); + return new Alarm(device.getId(), user.getId(), position, false, smartWakeup, null, false, 0, 6, 30, true, title, description, 0, true); } private void getAlarms() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java index a7fa49b5a1..940b65e75f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetEventAlarmList.java @@ -78,7 +78,9 @@ public class GetEventAlarmList extends Request { eventAlarm.startMinute, false, eventAlarm.name, - "" + "", + 0, + true )); usedBitmap |= 1 << eventAlarm.index; } @@ -99,7 +101,9 @@ public class GetEventAlarmList extends Request { 0, true, "", - "" + "", + 0, + true )); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java index 007c358654..b663abcae0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSmartAlarmList.java @@ -69,7 +69,9 @@ public class GetSmartAlarmList extends Request { smartAlarm.startMinute, false, "Smart alarm", - "" + "", + 0, + true ) }); } else { @@ -88,7 +90,9 @@ public class GetSmartAlarmList extends Request { 0, true, "Smart alarm", - "" + "", + 0, + true ) }); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AlarmUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AlarmUtils.java index f55df594e2..b1327b8b44 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AlarmUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AlarmUtils.java @@ -53,7 +53,7 @@ public class AlarmUtils { */ public static nodomain.freeyourgadget.gadgetbridge.model.Alarm createSingleShot(int index, boolean smartWakeup, boolean snooze, Calendar calendar) { // TODO: add interval setting? - return new Alarm(-1, -1, index, true, smartWakeup, null, snooze, Alarm.ALARM_ONCE, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), false, GBApplication.getContext().getString(R.string.quick_alarm), GBApplication.getContext().getString(R.string.quick_alarm_description)); + return new Alarm(-1, -1, index, true, smartWakeup, null, snooze, Alarm.ALARM_ONCE, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), false, GBApplication.getContext().getString(R.string.quick_alarm), GBApplication.getContext().getString(R.string.quick_alarm_description), 0, true); } /** @@ -80,7 +80,7 @@ public class AlarmUtils { public static Alarm createDefaultAlarm(DaoSession daoSession, GBDevice gbDevice, int position) { Device device = DBHelper.getDevice(gbDevice, daoSession); User user = DBHelper.getUser(daoSession); - return new Alarm(device.getId(), user.getId(), position, false, false, null, false, 0, 6, 30, false, null, null); + return new Alarm(device.getId(), user.getId(), position, false, false, null, false, 0, 6, 30, false, null, null, 0, true); } /** @@ -160,7 +160,7 @@ public class AlarmUtils { int hour = Integer.parseInt(tokens[4]); int minute = Integer.parseInt(tokens[5]); - return new Alarm(device.getId(), user.getId(), index, enabled, smartWakeup, null, false, repetition, hour, minute, false, null, null); + return new Alarm(device.getId(), user.getId(), index, enabled, smartWakeup, null, false, repetition, hour, minute, false, null, null, 0, true); } private static Comparator createComparator() { diff --git a/app/src/main/res/layout/activity_alarm_details.xml b/app/src/main/res/layout/activity_alarm_details.xml index 8f9b1a8ada..5c23fc45a2 100644 --- a/app/src/main/res/layout/activity_alarm_details.xml +++ b/app/src/main/res/layout/activity_alarm_details.xml @@ -2,6 +2,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical" tools:context="nodomain.freeyourgadget.gadgetbridge.activities.AlarmDetails"> @@ -23,8 +24,25 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingBottom="@dimen/activity_vertical_margin"> + + + + + - + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ae8189450..94e260c628 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,6 +216,7 @@ Generic Health Sound & Vibration + Sound + Vibration Offline Voice Sound Time @@ -3407,6 +3408,7 @@ Error deleting device: %s Folder name: Add new folder + Unset Unset folder Set or create new folder Auto reconnect to device @@ -3660,6 +3662,11 @@ Alerts Focus Bedtime + Reminder + Appointment + Training + Class + Meditate Wake Up Sleep Mode Schedule Send a reminder and enter sleep mode at bedtime. At the scheduled wake-up time, the wake-up alarm will sound.