Garmin: Set alarms from phone

This commit is contained in:
José Rebelo
2025-08-10 23:16:09 +01:00
parent d6daab8d0d
commit eef224ca4e
27 changed files with 725 additions and 278 deletions
@@ -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);
}
@@ -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<String, ALARM_SOUND> textToAlarmSound = new HashMap<>(ALARM_SOUND.values().length);
private final Map<String, ALARM_LABEL> 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<ALARM_LABEL> 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<String> 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<String> 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);
private boolean supportsSnoozing() {
return device.getDeviceCoordinator().supportsAlarmSnoozing(device);
}
return false;
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<ALARM_LABEL> 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);
}
@@ -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));
}
}
@@ -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++) {
@@ -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);
@@ -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) {
}
}
@@ -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<Alarm.ALARM_LABEL> getAlarmTitlePresets(@NonNull GBDevice device) {
return Collections.emptyList();
}
@Override
public boolean supportsMusicInfo(@NonNull GBDevice device) {
return false;
@@ -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<Alarm.ALARM_LABEL> getAlarmTitlePresets(@NonNull GBDevice device);
/**
* Returns true if the given device supports heart rate measurements.
* @return
@@ -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<Alarm.ALARM_LABEL> 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;
@@ -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);
}
}
@@ -17,8 +17,12 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
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();
int getSoundCode();
boolean getBacklight();
}
@@ -237,7 +237,9 @@ public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYP
private UploadRequestMessage setCreateFileStatusMessage(CreateFileStatusMessage createFileStatusMessage) {
if (createFileStatusMessage.canProceed()) {
LOG.info("SENDING UPLOAD FILE");
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");
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!!!");
if (showNotification) {
updateUploadProgress(100);
}
return new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0);
} else {
if (fileTransferDataStatusMessage.canProceed()) {
LOG.info("SENDING NEXT CHUNK!!!");
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();
@@ -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<? extends Alarm> alarms) {
final int alarmSlotCount = getCoordinator().getAlarmSlotCount(getDevice());
final List<RecordData> 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<Number> deviceSettingsTimes = new ArrayList<>();
final List<Number> deviceSettingsUnk5 = new ArrayList<>();
final List<Number> deviceSettingsEnabled = new ArrayList<>();
final List<Number> 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));
@@ -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,
@@ -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)
@@ -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,
@@ -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;
@@ -179,7 +180,8 @@ public class RecordData {
tsb.append(new Date(getComputedTimestamp() * 1000L));
}
for (FieldData fieldData : fieldDataList) {
fieldDataList.stream().sorted(Comparator.comparingInt(FieldData::getNumber))
.forEach(fieldData -> {
final String fieldName;
if (!StringUtils.isBlank(fieldData.getName())) {
fieldName = fieldData.getName();
@@ -196,7 +198,7 @@ public class RecordData {
fieldValueString = o.toString();
}
tsb.append(fieldName, fieldValueString);
}
});
return tsb.build();
}
@@ -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;
@@ -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;
}
}
}
@@ -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;
}
@@ -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();
@@ -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() {
@@ -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
));
}
}
@@ -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
)
});
}
@@ -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<Alarm> createComparator() {
@@ -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">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/preset_label_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/world_clock_label"
app:endIconMode="dropdown_menu">
<AutoCompleteTextView
android:id="@+id/preset_label_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false" />
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/alarm_title"
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
@@ -33,7 +51,7 @@
android:maxLength="32" />
<EditText
android:id="@+id/alarm_description"
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
@@ -44,19 +62,46 @@
android:singleLine="false" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_snooze"
android:id="@+id/cb_snooze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:drawableStart="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_snooze"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_smart_wakeup"
android:layout_width="wrap_content"
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sound_mode_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="@string/pref_header_sound_vibration"
app:endIconMode="dropdown_menu">
<AutoCompleteTextView
android:id="@+id/sound_mode_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:focusable="false" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/cb_backlight"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:drawableStart="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/watchface_setting_button_toggle_backlight"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/cb_smart_wakeup"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:drawableStart="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
@@ -64,7 +109,7 @@
android:textAppearance="?android:attr/textAppearanceSmall" />
<EditText
android:id="@+id/alarm_cb_smart_wakeup_interval"
android:id="@+id/cb_smart_wakeup_interval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
@@ -72,7 +117,7 @@
android:hint="@string/alarm_smart_wakeup_interval_default" />
<TimePicker
android:id="@+id/alarm_time_picker"
android:id="@+id/time_picker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
@@ -87,7 +132,7 @@
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_monday"
android:id="@+id/cb_monday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -95,10 +140,11 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_mon_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_tuesday"
android:id="@+id/cb_tuesday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -106,10 +152,11 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_tue_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_wednesday"
android:id="@+id/cb_wednesday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -117,10 +164,11 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_wed_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_thursday"
android:id="@+id/cb_thursday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -128,10 +176,11 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_thu_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_friday"
android:id="@+id/cb_friday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -139,10 +188,11 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_fri_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_saturday"
android:id="@+id/cb_saturday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -150,10 +200,11 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_sat_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
<androidx.appcompat.widget.AppCompatCheckedTextView
android:id="@+id/alarm_cb_sunday"
android:id="@+id/cb_sunday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
@@ -161,6 +212,7 @@
android:drawableTop="?android:attr/listChoiceIndicatorMultiple"
android:gravity="center"
android:text="@string/alarm_sun_short"
android:textAlignment="gravity"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
+7
View File
@@ -216,6 +216,7 @@
<string name="pref_header_generic">Generic</string>
<string name="pref_header_health">Health</string>
<string name="pref_header_sound_vibration">Sound &amp; Vibration</string>
<string name="sound_and_vibration">Sound + Vibration</string>
<string name="pref_header_offline_voice">Offline Voice</string>
<string name="pref_header_sound">Sound</string>
<string name="pref_header_time">Time</string>
@@ -3407,6 +3408,7 @@
<string name="error_deleting_device">Error deleting device: %s</string>
<string name="controlcenter_folder_name">Folder name:</string>
<string name="controlcenter_add_new_folder">Add new folder</string>
<string name="unset">Unset</string>
<string name="controlcenter_unset_folder">Unset folder</string>
<string name="controlcenter_set_folder_title">Set or create new folder</string>
<string name="auto_reconnect_ble_title">Auto reconnect to device</string>
@@ -3660,6 +3662,11 @@
<string name="menuitem_alerts">Alerts</string>
<string name="menuitem_focus">Focus</string>
<string name="bedtime">Bedtime</string>
<string name="reminder">Reminder</string>
<string name="alarm_label_appointment">Appointment</string>
<string name="alarm_label_training">Training</string>
<string name="alarm_label_class">Class</string>
<string name="alarm_label_meditate">Meditate</string>
<string name="wake_up_time">Wake Up</string>
<string name="pref_sleep_mode_schedule_title">Sleep Mode Schedule</string>
<string name="pref_sleep_mode_schedule_summary">Send a reminder and enter sleep mode at bedtime. At the scheduled wake-up time, the wake-up alarm will sound.</string>