diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6c5219698..82e08c474 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -179,6 +179,10 @@
android:name=".activities.loyaltycards.LoyaltyCardsSettingsActivity"
android:label="@string/loyalty_cards"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
+
notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
notifications.add(R.xml.devicesettings_send_app_notifications);
if (getCannedRepliesSlotCount(device) > 0) {
- notifications.add(R.xml.devicesettings_garmin_default_reply_suffix);
notifications.add(R.xml.devicesettings_canned_reply_16);
notifications.add(R.xml.devicesettings_canned_dismisscall_16);
}
@@ -221,4 +226,9 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
public boolean supportsAgpsUpdates(final GBDevice device) {
return !getPrefs(device).getString(GarminPreferences.PREF_AGPS_KNOWN_URLS, "").isEmpty();
}
+
+ public boolean supports(final GBDevice device, final GarminCapability capability) {
+ return getPrefs(device).getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet())
+ .contains(capability.name());
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java
index d3f614b54..1e5a88579 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java
@@ -11,6 +11,7 @@ public class GarminPreferences {
public static final String PREF_GARMIN_AGPS_UPDATE_TIME = "garmin_agps_update_time_%s";
public static final String PREF_GARMIN_AGPS_FOLDER = "garmin_agps_folder";
public static final String PREF_GARMIN_AGPS_FILENAME = "garmin_agps_filename_%s";
+ public static final String PREF_GARMIN_REALTIME_SETTINGS = "garmin_realtime_settings";
public static String agpsStatus(final String url) {
return String.format(GarminPreferences.PREF_GARMIN_AGPS_STATUS, CheckSums.md5(url));
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminRealtimeSettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminRealtimeSettingsActivity.java
new file mode 100644
index 000000000..52b756599
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminRealtimeSettingsActivity.java
@@ -0,0 +1,93 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.preference.PreferenceFragmentCompat;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Optional;
+
+public class GarminRealtimeSettingsActivity extends AbstractSettingsActivityV2 {
+ private GBDevice device;
+ private int screenId;
+
+ public static final String EXTRA_SCREEN_ID = "screenId";
+
+ @Override
+ protected String fragmentTag() {
+ return GarminRealtimeSettingsFragment.FRAGMENT_TAG;
+ }
+
+ @Override
+ protected PreferenceFragmentCompat newFragment() {
+ return GarminRealtimeSettingsFragment.newInstance(device, screenId);
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ screenId = getIntent().getIntExtra(EXTRA_SCREEN_ID, GarminRealtimeSettingsFragment.ROOT_SCREEN_ID);
+
+ super.onCreate(savedInstanceState);
+
+ if (device == null || !device.isInitialized()) {
+ GB.toast(getString(R.string.watch_not_connected), Toast.LENGTH_SHORT, GB.INFO);
+ finish();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(final Menu menu) {
+ final MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_garmin_realtime_settings, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
+ final int itemId = item.getItemId();
+
+ if (itemId == R.id.garmin_rt_debug_toggle) {
+ getFragment().ifPresent(GarminRealtimeSettingsFragment::toggleDebug);
+ return true;
+ } else if (itemId == R.id.garmin_rt_debug_share) {
+ getFragment().ifPresent(GarminRealtimeSettingsFragment::shareDebug);
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode, final @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ getFragment().ifPresent(GarminRealtimeSettingsFragment::refreshFromDevice);
+ }
+
+ private Optional getFragment() {
+ final FragmentManager fragmentManager = getSupportFragmentManager();
+ final Fragment fragment = fragmentManager.findFragmentByTag(GarminRealtimeSettingsFragment.FRAGMENT_TAG);
+ if (fragment == null) {
+ return Optional.empty();
+ }
+
+ if (fragment instanceof GarminRealtimeSettingsFragment) {
+ return Optional.of((GarminRealtimeSettingsFragment) fragment);
+ }
+
+ return Optional.empty();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminRealtimeSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminRealtimeSettingsFragment.java
new file mode 100644
index 000000000..b68ab95de
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminRealtimeSettingsFragment.java
@@ -0,0 +1,893 @@
+/* Copyright (C) 2024 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
+
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.annotation.DrawableRes;
+import androidx.fragment.app.FragmentActivity;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.preference.DialogPreference;
+import androidx.preference.EditTextPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreferenceCompat;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSettingsService;
+import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.XDatePreference;
+import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
+
+public class GarminRealtimeSettingsFragment extends AbstractPreferenceFragment {
+ private static final Logger LOG = LoggerFactory.getLogger(GarminRealtimeSettingsFragment.class);
+
+ public static final String EXTRA_SCREEN_ID = "screenId";
+ public static final String PREF_DEBUG = "garmin_rt_debug_mode";
+
+ public static final int ROOT_SCREEN_ID = 36352;
+
+ static final String FRAGMENT_TAG = "GARMIN_REALTIME_SETTINGS_FRAGMENT";
+
+ private GBDevice device;
+ private int screenId = ROOT_SCREEN_ID;
+
+ private GdiSettingsService.ScreenDefinition screenDefinition;
+ private GdiSettingsService.ScreenState screenState;
+
+ public static final String EXTRA_PROTOBUF = "protobuf";
+
+ public static final String ACTION_SCREEN_DEFINITION = "nodomain.freeyourgadget.gadgetbridge.garmin.realtime_settings.screen_definition";
+ public static final String ACTION_SCREEN_STATE = "nodomain.freeyourgadget.gadgetbridge.garmin.realtime_settings.screen_state";
+ public static final String ACTION_CHANGE = "nodomain.freeyourgadget.gadgetbridge.garmin.realtime_settings.change";
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) {
+ LOG.error("Got null action");
+ return;
+ }
+
+ switch (action) {
+ case ACTION_SCREEN_DEFINITION:
+ final GdiSettingsService.ScreenDefinition incomingScreen;
+ try {
+ incomingScreen = GdiSettingsService.ScreenDefinition.parseFrom(intent.getByteArrayExtra(EXTRA_PROTOBUF));
+ } catch (final InvalidProtocolBufferException e) {
+ // should never happen
+ LOG.error("Failed to parse protobuf for screen definition on {}", screenId, e);
+ return;
+ }
+ if (incomingScreen.getScreenId() != screenId) {
+ return;
+ }
+ LOG.debug("Got screen definition for screenId={}", screenId);
+ screenDefinition = incomingScreen;
+ break;
+ case ACTION_SCREEN_STATE:
+ final GdiSettingsService.ScreenState incomingState;
+ try {
+ incomingState = GdiSettingsService.ScreenState.parseFrom(intent.getByteArrayExtra(EXTRA_PROTOBUF));
+ } catch (final InvalidProtocolBufferException e) {
+ // should never happen
+ LOG.error("Failed to parse protobuf for screen state on {}", screenId, e);
+ return;
+ }
+ if (incomingState.getScreenId() != screenId) {
+ return;
+ }
+ LOG.debug("Got screen state for screenId={}", screenId);
+ screenState = incomingState;
+ break;
+ case ACTION_CHANGE:
+ final GdiSettingsService.ChangeResponse incomingChange;
+ try {
+ incomingChange = GdiSettingsService.ChangeResponse.parseFrom(intent.getByteArrayExtra(EXTRA_PROTOBUF));
+ } catch (final InvalidProtocolBufferException e) {
+ // should never happen
+ LOG.error("Failed to parse protobuf for change", e);
+ return;
+ }
+ if (incomingChange.getState().getScreenId() != screenId) {
+ return;
+ }
+
+ if (incomingChange.getShouldReturn()) {
+ LOG.debug("Returning from {}", screenId);
+ requireActivity().finish();
+ return;
+ }
+
+ LOG.debug("Got screen change for screenId={}", screenId);
+
+ GBApplication.deviceService(device).onReadConfiguration("screenId:" + screenId);
+ return;
+ default:
+ LOG.error("Unknown action {}", action);
+ return;
+ }
+
+ reload();
+ }
+ };
+
+ private void setDevice(final GBDevice device) {
+ final Bundle args = getArguments() != null ? getArguments() : new Bundle();
+ args.putParcelable("device", device);
+ setArguments(args);
+ }
+
+ private void setScreenId(final int screenId) {
+ final Bundle args = getArguments() != null ? getArguments() : new Bundle();
+ args.putInt("screenId", screenId);
+ setArguments(args);
+ }
+
+ @Override
+ public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
+ final Bundle arguments = getArguments();
+ if (arguments == null) {
+ return;
+ }
+ this.device = arguments.getParcelable(GBDevice.EXTRA_DEVICE);
+ if (device == null) {
+ return;
+ }
+ this.screenId = arguments.getInt(EXTRA_SCREEN_ID, ROOT_SCREEN_ID);
+ if (screenId == 0) {
+ return;
+ }
+
+ LOG.info("Opened realtime preferences screen for {}", screenId);
+
+ getPreferenceManager().setSharedPreferencesName("garmin_rt_" + device.getAddress());
+ setPreferencesFromResource(R.xml.garmin_realtime_settings, rootKey);
+
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_SCREEN_DEFINITION);
+ filter.addAction(ACTION_SCREEN_STATE);
+ filter.addAction(ACTION_CHANGE);
+ LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filter);
+
+ GBApplication.deviceService(device).onReadConfiguration("screenId:" + screenId);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ reload();
+ }
+
+ @Override
+ public void onDestroyView() {
+ LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver);
+ super.onDestroyView();
+ }
+
+ static GarminRealtimeSettingsFragment newInstance(final GBDevice device, final int screenId) {
+ final GarminRealtimeSettingsFragment fragment = new GarminRealtimeSettingsFragment();
+ fragment.setDevice(device);
+ fragment.setScreenId(screenId);
+
+ return fragment;
+ }
+
+ void refreshFromDevice() {
+ screenDefinition = null;
+ screenState = null;
+ reload();
+ GBApplication.deviceService(device).onReadConfiguration("screenId:" + screenId);
+ }
+
+ void reload() {
+ final boolean debug = GBApplication.getDevicePrefs(device.getAddress()).getBoolean(PREF_DEBUG, BuildConfig.DEBUG);
+
+ final FragmentActivity activity = getActivity();
+ if (activity == null) {
+ LOG.error("Activity is null");
+ return;
+ }
+
+ final PreferenceScreen prefScreen = findPreference(GarminPreferences.PREF_GARMIN_REALTIME_SETTINGS);
+ if (prefScreen == null) {
+ LOG.error("Preference screen for {} is null", GarminPreferences.PREF_GARMIN_REALTIME_SETTINGS);
+ activity.finish();
+ return;
+ }
+
+ if (screenDefinition == null || screenState == null) {
+ ((GarminRealtimeSettingsActivity) activity).setActionBarTitle(activity.getString(R.string.loading));
+
+ // Disable all existing preferences while loading
+ for (int i = 0; i < prefScreen.getPreferenceCount(); i++) {
+ prefScreen.getPreference(i).setEnabled(false);
+ }
+
+ return;
+ }
+
+ prefScreen.removeAll();
+
+ if (debug) {
+ final Preference pref = new PreferenceCategory(activity);
+ pref.setIconSpaceReserved(false);
+ pref.setTitle("Screen ID: " + screenId);
+ pref.setPersistent(false);
+ pref.setKey("rt_pref_header_" + screenId);
+ prefScreen.addPreference(pref);
+ }
+
+ // Update the screen title, if any
+ if (screenDefinition.hasTitle()) {
+ final String title = screenDefinition.getTitle().getText();
+
+ ((GarminRealtimeSettingsActivity) activity).setActionBarTitle(title);
+ }
+
+ final Map stateById = new HashMap<>();
+ for (final GdiSettingsService.EntryState state : screenState.getStateList()) {
+ stateById.put(state.getId(), state);
+ }
+
+ for (final GdiSettingsService.ScreenEntry entry : screenDefinition.getEntryList()) {
+ final GdiSettingsService.EntryState state = stateById.get(entry.getId());
+
+ final Preference pref;
+ boolean supported = true;
+
+ if (entry.hasTarget()) {
+ switch (entry.getTarget().getType()) {
+ case 0: // subscreen
+ case 9: // subscreen with options for a specific preference
+ pref = new Preference(activity);
+ pref.setOnPreferenceClickListener(preference -> {
+ final Intent newIntent = new Intent(requireContext(), GarminRealtimeSettingsActivity.class);
+ newIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
+ newIntent.putExtra(GarminRealtimeSettingsActivity.EXTRA_SCREEN_ID, entry.getTarget().getSubscreen());
+ activity.startActivityForResult(newIntent, 0);
+ return true;
+ });
+ break;
+ case 1: // list preference
+ pref = new ListPreference(activity);
+ final CharSequence[] entries = new String[entry.getTarget().getOptions().getOptionList().size()];
+ int optionIndex = 0;
+ for (final GdiSettingsService.TargetOptionEntry option : entry.getTarget().getOptions().getOptionList()) {
+ entries[optionIndex++] = option.getTitle().getText();
+ }
+ final ListPreference listPreference = (ListPreference) pref;
+ listPreference.setEntries(entries);
+ listPreference.setEntryValues(entries);
+ listPreference.setValue(entries[Objects.requireNonNull(state).getSummary().getValueList().getIndex()].toString());
+ listPreference.setOnPreferenceChangeListener((preference, newValue) -> {
+ int newValueIdx = -1;
+ for (int i = 0; i < entries.length; i++) {
+ if (entries[i].equals(newValue.toString())) {
+ newValueIdx = i;
+ break;
+ }
+ }
+ if (newValueIdx < 0) {
+ LOG.error("Failed to find index for {}", newValue);
+ return false;
+ }
+
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setOption(GdiSettingsService.ChangeRequest.Option.newBuilder()
+ .setIndex(newValueIdx)
+ )
+ );
+ return true;
+ });
+ break;
+
+ case 3: // time
+ pref = new XTimePreference(activity, null);
+ ((XTimePreference) pref).setValue(
+ Objects.requireNonNull(state).getSummary().getValueTime().getSeconds() / 3600,
+ (Objects.requireNonNull(state).getSummary().getValueTime().getSeconds() % 3600) / 60
+ );
+ if (state.getSummary().getValueTime().hasTimeFormat()) {
+ final int timeFormat = state.getSummary().getValueTime().getTimeFormat();
+ switch (timeFormat) {
+ case 0: // 12h
+ ((XTimePreference) pref).setFormat(XTimePreference.Format.FORMAT_12H);
+ break;
+ case 1: // 24h
+ ((XTimePreference) pref).setFormat(XTimePreference.Format.FORMAT_24H);
+ break;
+ }
+ }
+ pref.setSummary(state.getSummary().getValueDate().getSubtitle().getText());
+ pref.setOnPreferenceChangeListener((preference, newValue) -> {
+ final String[] pieces = newValue.toString().split(":");
+
+ final int hour = Integer.parseInt(pieces[0]);
+ final int minute = Integer.parseInt(pieces[1]);
+
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setTime(GdiSettingsService.ChangeRequest.Time.newBuilder()
+ .setSeconds(hour * 3600 + minute * 60)
+ )
+ );
+ return true;
+ });
+ break;
+ case 5: // number picker
+ pref = new EditTextPreference(activity);
+ ((EditTextPreference) pref).setText(String.valueOf(state.getSummary().getValueNumber().getValue()));
+ ((EditTextPreference) pref).setSummary(state.getSummary().getValueNumber().getSubtitle().getText());
+
+ ((EditTextPreference) pref).setOnBindEditTextListener(p -> {
+ p.setInputType(InputType.TYPE_CLASS_NUMBER);
+ int minValue = Integer.MIN_VALUE;
+ int maxValue = Integer.MAX_VALUE;
+ if (entry.getTarget().getNumberPicker().hasMin()) {
+ minValue = entry.getTarget().getNumberPicker().getMin();
+ }
+ if (entry.getTarget().getNumberPicker().hasMax()) {
+ maxValue = entry.getTarget().getNumberPicker().getMax();
+ }
+ p.addTextChangedListener(new MinMaxTextWatcher(p, minValue, maxValue));
+ p.setSelection(p.getText().length());
+ });
+ ((EditTextPreference) pref).setOnPreferenceChangeListener((preference, newValue) -> {
+ final int newValueInt = Integer.parseInt(newValue.toString());
+
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setNumber(GdiSettingsService.ChangeRequest.Number.newBuilder()
+ .setValue(newValueInt)
+ )
+ );
+ return true;
+ });
+ break;
+ case 6: // activity
+ switch (entry.getTarget().getActivity()) {
+ case 2: // garmin pay
+ case 7: // text responses
+ case 8: // music providers
+ pref = new Preference(activity);
+ pref.setVisible(debug);
+ pref.setEnabled(false);
+ break;
+ default:
+ supported = false;
+ pref = new Preference(activity);
+ break;
+ }
+
+ break;
+ case 7: // hidden?
+ pref = new Preference(activity);
+ pref.setVisible(debug);
+ pref.setEnabled(false);
+ break;
+ case 10: // date picker
+ pref = new XDatePreference(activity, null);
+ ((XDatePreference) pref).setValue(
+ Objects.requireNonNull(state).getSummary().getValueDate().getCurrentDate().getYear(),
+ Objects.requireNonNull(state).getSummary().getValueDate().getCurrentDate().getMonth(),
+ Objects.requireNonNull(state).getSummary().getValueDate().getCurrentDate().getDay()
+ );
+ if (state.getSummary().getValueDate().hasMinDate()) {
+ final Calendar calendar = GregorianCalendar.getInstance();
+ calendar.set(
+ state.getSummary().getValueDate().getMinDate().getYear(),
+ state.getSummary().getValueDate().getMinDate().getMonth() - 1,
+ state.getSummary().getValueDate().getMinDate().getDay()
+ );
+ ((XDatePreference) pref).setMinDate(calendar.getTimeInMillis());
+ }
+ if (state.getSummary().getValueDate().hasMaxDate()) {
+ final Calendar calendar = GregorianCalendar.getInstance();
+ calendar.set(
+ state.getSummary().getValueDate().getMaxDate().getYear(),
+ state.getSummary().getValueDate().getMaxDate().getMonth() - 1,
+ state.getSummary().getValueDate().getMaxDate().getDay()
+ );
+ ((XDatePreference) pref).setMaxDate(calendar.getTimeInMillis());
+ }
+ pref.setSummary(state.getSummary().getValueDate().getSubtitle().getText());
+ pref.setOnPreferenceChangeListener((preference, newValue) -> {
+ final String[] pieces = newValue.toString().split("-");
+
+ final int year = Integer.parseInt(pieces[0]);
+ final int month = Integer.parseInt(pieces[1]);
+ final int day = Integer.parseInt(pieces[2]);
+
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setNewDate(GdiSettingsService.ChangeRequest.NewDate.newBuilder()
+ .setValue(GdiSettingsService.Date.newBuilder()
+ .setYear(year).setMonth(month).setDay(day)
+ )
+ )
+ );
+
+ return true;
+ });
+ break;
+ case 12: // Connect IQ Store
+ pref = new Preference(activity);
+ pref.setVisible(debug);
+ pref.setEnabled(false);
+ break;
+ case 13: // height
+ pref = new EditTextPreference(activity);
+ ((EditTextPreference) pref).setText(String.valueOf(state.getSummary().getValueHeight().getValue()));
+ ((EditTextPreference) pref).setSummary(state.getSummary().getValueHeight().getSubtitle().getText());
+ if (state.getSummary().getValueHeight().getUnit() == 0) {
+ ((EditTextPreference) pref).setDialogTitle(R.string.activity_prefs_height_cm);
+ ((EditTextPreference) pref).setTitle(R.string.activity_prefs_height_cm);
+ } else {
+ ((EditTextPreference) pref).setDialogTitle(R.string.activity_prefs_height_inches);
+ ((EditTextPreference) pref).setTitle(R.string.activity_prefs_height_inches);
+ }
+ ((EditTextPreference) pref).setOnBindEditTextListener(p -> {
+ p.setInputType(InputType.TYPE_CLASS_NUMBER);
+ p.addTextChangedListener(new MinMaxTextWatcher(p, 0, 300));
+ p.setSelection(p.getText().length());
+ });
+ ((EditTextPreference) pref).setOnPreferenceChangeListener((preference, newValue) -> {
+ final int newValueInt = Integer.parseInt(newValue.toString());
+
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setHeight(GdiSettingsService.ChangeRequest.Height.newBuilder()
+ .setValue(newValueInt)
+ .setUnit(state.getSummary().getValueHeight().getUnit())
+ )
+ );
+ return true;
+ });
+ break;
+ default:
+ supported = false;
+ pref = new Preference(activity);
+ }
+ } else { // No target
+ switch (entry.getType()) {
+ case 0: // notice
+ pref = new Preference(activity);
+ pref.setSummary(entry.getTitle().getText());
+ break;
+ case 1: // category
+ pref = new PreferenceCategory(activity);
+ break;
+ case 2: // space
+ pref = new PreferenceCategory(activity);
+ pref.setTitle("");
+ break;
+ case 3: // switch
+ pref = new SwitchPreferenceCompat(activity);
+ pref.setLayoutResource(R.layout.preference_checkbox);
+ ((SwitchPreferenceCompat) pref).setChecked(Objects.requireNonNull(state).getSwitch().getEnabled());
+ ((SwitchPreferenceCompat) pref).setSummary(Objects.requireNonNull(state).getSwitch().getTitle().getText());
+ pref.setOnPreferenceChangeListener((preference, newValue) -> {
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setSwitch(GdiSettingsService.ChangeRequest.Switch.newBuilder()
+ .setValue((Boolean) newValue)
+ )
+ );
+ return true;
+ });
+
+ break;
+ case 4: // single line + optional icon
+ case 5: // double line
+ pref = new Preference(activity);
+ break;
+ case 18: // single line with action (eg. glances)
+ case 7: // single line, normally in list for selection?
+ pref = new Preference(activity);
+ pref.setOnPreferenceClickListener(preference -> {
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ );
+ return true;
+ });
+ break;
+ case 8: // device + status?
+ case 9: // finish setup
+ case 10: // find my device
+ case 11: // preferred activity tracker
+ case 13: // help & info
+ pref = new Preference(activity);
+ pref.setVisible(debug);
+ pref.setEnabled(false);
+ break;
+ case 15: // sortable + delete
+ // Add all sortable items and then continue
+ final String moveUpStr = activity.getString(R.string.widget_move_up);
+ final String moveDownStr = activity.getString(R.string.widget_move_down);
+ final String deleteStr = activity.getString(R.string.appmananger_app_delete);
+
+ for (int i = 0; i < entry.getSortOptions().getEntriesCount(); i++) {
+ final GdiSettingsService.SortEntry sortEntry = entry.getSortOptions().getEntries(i);
+ final List sortableOptions = new ArrayList<>(3);
+ if (i > 0) {
+ sortableOptions.add(moveUpStr);
+ }
+ if (i < entry.getSortOptions().getEntriesCount() - 1) {
+ sortableOptions.add(moveDownStr);
+ }
+ sortableOptions.add(deleteStr);
+ final ArrayAdapter sortOptionsAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, sortableOptions);
+ final int iFinal = i;
+ final Preference sortPref = new Preference(activity);
+ sortPref.setTitle(sortEntry.getTitle().getText());
+ sortPref.setPersistent(false);
+ sortPref.setIconSpaceReserved(false);
+ sortPref.setKey("rt_pref_" + screenId + "_" + entry.getId() + "__" + sortEntry.getId());
+ sortPref.setOnPreferenceClickListener(preference -> {
+ new MaterialAlertDialogBuilder(activity)
+ .setTitle(sortPref.getTitle())
+ .setAdapter(sortOptionsAdapter, (dialogInterface, j) -> {
+ final String option = sortableOptions.get(j);
+ int moveOffset = 0;
+ if (option.equals(moveUpStr)) {
+ moveOffset = -1;
+ } else if (option.equals(moveDownStr)) {
+ moveOffset = 1;
+ }
+
+ if (moveOffset != 0) {
+ sortPref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(sortEntry.getId())
+ .setPosition(GdiSettingsService.ChangeRequest.Position.newBuilder()
+ .setIndex(iFinal + moveOffset)
+ )
+ );
+ return;
+ }
+
+ if (option.equals(deleteStr)) {
+ sortPref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(sortEntry.getId())
+ .setPosition(GdiSettingsService.ChangeRequest.Position.newBuilder()
+ .setDelete(true)
+ )
+ );
+ }
+ }).setNegativeButton(android.R.string.cancel, null)
+ .create().show();
+ return true;
+ });
+ prefScreen.addPreference(sortPref);
+ }
+
+ continue; // We already added all options above, continue
+ case 16: // text
+ pref = new EditTextPreference(activity);
+
+ ((EditTextPreference) pref).setOnBindEditTextListener(p -> {
+ int maxValue = Integer.MAX_VALUE;
+ if (entry.getTextOption().hasLimits() && entry.getTextOption().getLimits().hasMaxLength()) {
+ p.setFilters(new InputFilter[]{new InputFilter.LengthFilter(entry.getTextOption().getLimits().getMaxLength())});
+ }
+ p.setSelection(p.getText().length());
+ });
+ ((EditTextPreference) pref).setOnPreferenceChangeListener((preference, newValue) -> {
+ if (StringUtils.isNullOrEmpty(newValue.toString())) {
+ return true;
+ }
+ pref.setEnabled(false);
+ sendChangeRequest(
+ GdiSettingsService.ChangeRequest.newBuilder()
+ .setScreenId(screenId)
+ .setEntryId(entry.getId())
+ .setText(GdiSettingsService.ChangeRequest.Text.newBuilder()
+ .setValue(newValue.toString())
+ )
+ );
+ return true;
+ });
+ break;
+ default:
+ supported = false;
+ pref = new Preference(activity);
+ }
+ }
+
+ if (StringUtils.isNullOrEmpty(pref.getTitle()) && entry.getType() != 0 && entry.getType() != 2) {
+ pref.setTitle(!StringUtils.isEmpty(entry.getTitle().getText()) ? entry.getTitle().getText() : activity.getString(R.string.unknown));
+
+ if (pref instanceof DialogPreference) {
+ ((DialogPreference) pref).setDialogTitle(pref.getTitle());
+ }
+ }
+
+ final int icon = getIcon(entry);
+ if (icon != 0) {
+ pref.setIcon(icon);
+ } else {
+ pref.setIconSpaceReserved(false);
+ }
+
+ if (state != null && !StringUtils.isEmpty(state.getSummary().getTitle().getText())) {
+ pref.setSummary(state.getSummary().getTitle().getText());
+ }
+
+ if (state != null && state.hasState()) {
+ switch (state.getState()) {
+ case 1:
+ pref.setVisible(false);
+ break;
+ case 2:
+ pref.setEnabled(false);
+ break;
+ default:
+ LOG.warn("Unknown state value {}", state.getState());
+ }
+ }
+
+ if (!supported) {
+ pref.setEnabled(false);
+
+ if (StringUtils.isNullOrEmpty(pref.getSummary())) {
+ pref.setSummary(R.string.unsupported);
+ } else {
+ pref.setSummary(activity.getString(R.string.menuitem_unsupported, pref.getSummary()));
+ }
+ }
+
+ if (debug) {
+ final StringBuilder sb = new StringBuilder();
+
+ if (pref.getSummary() != null && pref.getSummary().length() != 0) {
+ sb.append(pref.getSummary()).append("\n");
+ }
+
+ sb.append("id=").append(entry.getId());
+ sb.append(", type=").append(entry.getType());
+
+ if (entry.hasTarget()) {
+ sb.append(", targetType=").append(entry.getTarget().getType());
+ }
+
+ pref.setSummary(sb.toString());
+ }
+
+ pref.setPersistent(false);
+ pref.setKey("rt_pref_" + screenId + "_" + entry.getId());
+ prefScreen.addPreference(pref);
+ }
+
+ // If no preferences after the last visible preference category are visible, hide it
+ for (int i = prefScreen.getPreferenceCount() - 1; i >= 0; i--) {
+ final Preference lastVisiblePreference = prefScreen.getPreference(i);
+ if (lastVisiblePreference.isVisible()) {
+ break;
+ }
+ if (lastVisiblePreference instanceof PreferenceCategory) {
+ lastVisiblePreference.setVisible(false);
+ break;
+ }
+ }
+ }
+
+ @DrawableRes
+ private int getIcon(final GdiSettingsService.ScreenEntry entry) {
+ if (entry.hasIcon()) {
+ switch (entry.getIcon()) {
+ //
+ // Main menu
+ case 20: // Garmin Pay
+ return 0;
+ case 21: // Text Responses
+ return R.drawable.ic_reply;
+ case 4: // Clocks
+ return R.drawable.ic_access_time;
+ case 2: // Glances
+ return R.drawable.ic_widgets;
+ case 3: // Controls
+ return R.drawable.ic_menu;
+ case 1: // Activities / Apps, have the same icon
+ return R.drawable.ic_activity_unknown_small;
+ case 39: // Shortcut
+ return R.drawable.ic_shortcut;
+ case 27: // Notifications & Alerts
+ return R.drawable.ic_notifications;
+ case 46: // Watch Sensors
+ return R.drawable.ic_sensor_calibration;
+ case 47: // Accessories
+ return R.drawable.ic_bluetooth_searching;
+ case 7: // Music
+ return R.drawable.ic_music_note;
+ case 13: // Audio Prompts
+ return R.drawable.ic_volume_up;
+ case 14: // User Profile
+ return R.drawable.ic_person;
+ case 15: // Safety & Tracking
+ return R.drawable.ic_health;
+ case 16: // Activity Tracking
+ return R.drawable.ic_activity_unknown_small;
+ case 19: // System
+ return R.drawable.ic_settings;
+
+ //
+ // Sortable screens (glances, apps, etc)
+ case 33:
+ return R.drawable.ic_add_gray;
+ }
+ }
+
+ return 0;
+ }
+
+ void toggleDebug() {
+ final Prefs prefs = GBApplication.getDevicePrefs(device.getAddress());
+ prefs.getPreferences().edit()
+ .putBoolean(PREF_DEBUG, !prefs.getBoolean(PREF_DEBUG, BuildConfig.DEBUG))
+ .apply();
+
+ reload();
+ }
+
+ void shareDebug() {
+ final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
+ intent.setType("text/plain");
+
+ final StringBuilder sb = new StringBuilder();
+
+ sb.append("screenId: ").append(screenId);
+ sb.append("\n");
+
+ sb.append("settingsScreen: ");
+ if (screenDefinition != null) {
+ sb.append(GB.hexdump(screenDefinition.toByteArray()));
+ } else {
+ sb.append("null");
+ }
+ sb.append("\n");
+
+ sb.append("settingsState: ");
+ if (screenState != null) {
+ sb.append(GB.hexdump(screenState.toByteArray()));
+ } else {
+ sb.append("null");
+ }
+ sb.append("\n");
+
+ intent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Garmin Settings Screen " + screenId);
+ intent.putExtra(android.content.Intent.EXTRA_TEXT, sb.toString());
+
+ try {
+ startActivity(Intent.createChooser(intent, "Share debug info"));
+ } catch (final ActivityNotFoundException e) {
+ Toast.makeText(requireContext(), "Failed to share text", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void sendChangeRequest(final GdiSettingsService.ChangeRequest.Builder changeRequest) {
+ screenDefinition = null;
+ screenState = null;
+ final GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder()
+ .setSettingsService(GdiSettingsService.SettingsService.newBuilder()
+ .setChangeRequest(changeRequest)
+ ).build();
+ GBApplication.deviceService(device).onSendConfiguration("protobuf:" + GB.hexdump(smart.toByteArray()));
+ }
+
+ private static class MinMaxTextWatcher implements TextWatcher {
+ private final EditText editText;
+ private final int min;
+ private final int max;
+
+ private MinMaxTextWatcher(final EditText editText, final int min, final int max) {
+ this.editText = editText;
+ this.min = min;
+ this.max = max;
+ }
+
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable editable) {
+ try {
+ final int val = Integer.parseInt(editable.toString());
+ editText.getRootView().findViewById(android.R.id.button1)
+ .setEnabled(val >= min && val <= max);
+ if (val < min) {
+ editText.setError(editText.getContext().getString(R.string.min_val, min));
+ } else if (val > max) {
+ editText.setError(editText.getContext().getString(R.string.max_val, max));
+ } else {
+ editText.setError(null);
+ }
+ } catch (final NumberFormatException e) {
+ editText.getRootView().findViewById(android.R.id.button1)
+ .setEnabled(false);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java
index a0b69005e..78e426b2f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminSettingsCustomizer.java
@@ -34,6 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.GarminAgpsStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@@ -49,6 +50,16 @@ public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomize
@Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
+ final Preference realtimeSettings = handler.findPreference(GarminPreferences.PREF_GARMIN_REALTIME_SETTINGS);
+ if (realtimeSettings != null) {
+ realtimeSettings.setOnPreferenceClickListener(preference -> {
+ final Intent intent = new Intent(handler.getContext(), GarminRealtimeSettingsActivity.class);
+ intent.putExtra(GBDevice.EXTRA_DEVICE, handler.getDevice());
+ handler.getContext().startActivity(intent);
+ return true;
+ });
+ }
+
final PreferenceCategory prefAgpsHeader = handler.findPreference(DeviceSettingsPreferenceConst.PREF_HEADER_AGPS);
if (prefAgpsHeader != null) {
final List urls = prefs.getList(GarminPreferences.PREF_AGPS_KNOWN_URLS, Collections.emptyList(), "\n");
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java
index 081cdd9ca..25663c63b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vivomovehr/GarminCapability.java
@@ -103,25 +103,50 @@ public enum GarminCapability {
GOLF_9_PLUS_9,
ANTI_THEFT_ALARM,
INREACH,
- EVENT_SHARING;
+ EVENT_SHARING,
+ UNK_82,
+ UNK_83,
+ UNK_84,
+ UNK_85,
+ UNK_86,
+ UNK_87,
+ UNK_88,
+ UNK_89,
+ UNK_90,
+ UNK_91,
+ REALTIME_SETTINGS,
+ UNK_93,
+ UNK_94,
+ UNK_95,
+ UNK_96,
+ UNK_97,
+ UNK_98,
+ UNK_99,
+ UNK_100,
+ UNK_101,
+ UNK_102,
+ UNK_103,
+ ;
public static final Set ALL_CAPABILITIES = new HashSet<>(values().length);
private static final Map FROM_ORDINAL = new HashMap<>(values().length);
static {
- for (GarminCapability cap : values()) {
+ for (final GarminCapability cap : values()) {
FROM_ORDINAL.put(cap.ordinal(), cap);
ALL_CAPABILITIES.add(cap);
}
}
- public static Set setFromBinary(byte[] bytes) {
+ public static Set setFromBinary(final byte[] bytes) {
final Set result = new HashSet<>(GarminCapability.values().length);
int current = 0;
for (int b : bytes) {
- for (int curr = 1; curr < 0x100; curr <<= 1) {
- if ((b & curr) != 0) {
- result.add(FROM_ORDINAL.get(current));
+ for (int i = 0; i < 8; i++) {
+ if ((b & (1 << i)) != 0) {
+ if (FROM_ORDINAL.containsKey(current)) {
+ result.add(FROM_ORDINAL.get(current));
+ }
}
++current;
}
@@ -129,7 +154,7 @@ public enum GarminCapability {
return result;
}
- public static byte[] setToBinary(Set capabilities) {
+ public static byte[] setToBinary(final Set capabilities) {
final GarminCapability[] values = values();
final byte[] result = new byte[(values.length + 7) / 8];
int bytePos = 0;
@@ -147,9 +172,9 @@ public enum GarminCapability {
return result;
}
- public static String setToString(Set capabilities) {
+ public static String setToString(final Set capabilities) {
final StringBuilder result = new StringBuilder();
- for (GarminCapability cap : capabilities) {
+ for (final GarminCapability cap : capabilities) {
if (result.length() > 0) result.append(", ");
result.append(cap.name());
}
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 21d669eca..3572518f6 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
@@ -25,6 +25,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
+import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
@@ -60,6 +61,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateA
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2.CommunicatorV2;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.CapabilitiesDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
@@ -84,7 +86,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
-import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_GARMIN_DEFAULT_REPLY_SUFFIX;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SEND_APP_NOTIFICATIONS;
@@ -251,6 +252,24 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
if (weather != null) {
sendWeatherConditions(weather);
}
+ } else if (deviceEvent instanceof CapabilitiesDeviceEvent) {
+ final Set capabilities = ((CapabilitiesDeviceEvent) deviceEvent).capabilities;
+ if (capabilities.contains(GarminCapability.REALTIME_SETTINGS)) {
+ final String language = Locale.getDefault().getLanguage();
+ final String country = Locale.getDefault().getCountry();
+ final String localeString = language + "_" + country.toUpperCase();
+ final ProtobufMessage realtimeSettingsInit = protocolBufferHandler.prepareProtobufRequest(GdiSmartProto.Smart.newBuilder()
+ .setSettingsService(
+ GdiSettingsService.SettingsService.newBuilder()
+ .setInitRequest(
+ GdiSettingsService.InitRequest.newBuilder()
+ .setLanguage(localeString.length() == 5 ? localeString : "en_US")
+ .setRegion("us") // FIXME choose region
+ )
+ )
+ .build());
+ sendOutgoingMessage("init realtime settings", realtimeSettingsInit);
+ }
} else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
notificationsHandler.setEnabled(enable);
@@ -327,13 +346,8 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
communicator.sendMessage(taskName, message.getOutgoingMessage());
}
- private boolean supports(final GarminCapability capability) {
- return getDevicePrefs().getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet())
- .contains(capability.name());
- }
-
private void sendWeatherConditions(WeatherSpec weather) {
- if (!supports(GarminCapability.WEATHER_CONDITIONS)) {
+ if (!getCoordinator().supports(getDevice(), GarminCapability.WEATHER_CONDITIONS)) {
// Device does not support sending weather as fit
return;
}
@@ -444,39 +458,34 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
sendOutgoingMessage("request supported file types", new SupportedFileTypesMessage());
- sendOutgoingMessage("toggle default reply suffix", toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
-
if (mFirstConnect) {
sendOutgoingMessage("set sync complete", new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0));
this.mFirstConnect = false;
}
}
- private ProtobufMessage toggleDefaultReplySuffix(boolean value) {
- final GdiSettingsService.SettingsService.Builder enableSignature = GdiSettingsService.SettingsService.newBuilder()
- .setChangeRequest(
- GdiSettingsService.ChangeRequest.newBuilder()
- .setPointer1(65566) //TODO: this might be device specific, tested on Instinct 2s
- .setPointer2(3) //TODO: this might be device specific, tested on Instinct 2s
- .setEnable(GdiSettingsService.ChangeRequest.Switch.newBuilder().setValue(value)));
-
- return protocolBufferHandler.prepareProtobufRequest(
- GdiSmartProto.Smart.newBuilder()
- .setSettingsService(enableSignature).build());
- }
-
@Override
- public void onSendConfiguration(String config) {
+ public void onSendConfiguration(final String config) {
+ if (config.startsWith("protobuf:")) {
+ try {
+ final GdiSmartProto.Smart smart = GdiSmartProto.Smart.parseFrom(GB.hexStringToByteArray(config.replaceFirst("protobuf:", "")));
+ sendOutgoingMessage("send config", protocolBufferHandler.prepareProtobufRequest(smart));
+ } catch (final Exception e) {
+ LOG.error("Failed to send {} as protobuf", config, e);
+ }
+
+ return;
+ }
+
switch (config) {
- case PREF_GARMIN_DEFAULT_REPLY_SUFFIX:
- sendOutgoingMessage("toggle default reply suffix", toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
- break;
case PREF_SEND_APP_NOTIFICATIONS:
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
notificationSubscriptionDeviceEvent.enable = true; // actual status is fetched from preferences
evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent);
- break;
+ return;
}
+
+
}
private void processDownloadQueue() {
@@ -754,6 +763,41 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
return super.connect();
}
+ @Override
+ public void onReadConfiguration(final String config) {
+ if (config.startsWith("screenId:")) {
+ final int screenId = Integer.parseInt(config.replaceFirst("screenId:", ""));
+
+ LOG.debug("Requesting screen {}", screenId);
+
+ final String language = Locale.getDefault().getLanguage();
+ final String country = Locale.getDefault().getCountry();
+ final String localeString = language + "_" + country.toUpperCase();
+
+ sendOutgoingMessage("get settings screen " + screenId, protocolBufferHandler.prepareProtobufRequest(
+ GdiSmartProto.Smart.newBuilder()
+ .setSettingsService(GdiSettingsService.SettingsService.newBuilder()
+ .setDefinitionRequest(
+ GdiSettingsService.ScreenDefinitionRequest.newBuilder()
+ .setScreenId(screenId)
+ .setUnk2(0)
+ .setLanguage(localeString.length() == 5 ? localeString : "en_US")
+ )
+ ).build()
+ ));
+
+ sendOutgoingMessage("get settings state " + screenId, protocolBufferHandler.prepareProtobufRequest(
+ GdiSmartProto.Smart.newBuilder()
+ .setSettingsService(GdiSettingsService.SettingsService.newBuilder()
+ .setStateRequest(
+ GdiSettingsService.ScreenStateRequest.newBuilder()
+ .setScreenId(screenId)
+ )
+ ).build()
+ ));
+ }
+ }
+
@Override
public void onTestNewFunction() {
parseAllFitFilesFromStorage();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
index c24d432f3..10f60d8e3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
@@ -1,7 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
+import android.content.Intent;
import android.location.Location;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang3.ArrayUtils;
@@ -19,6 +22,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
+import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRealtimeSettingsFragment;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
@@ -28,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDataTransferService;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiDeviceStatus;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService;
+import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSettingsService;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmartProto;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmsNotification;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.DataTransferHandler;
@@ -119,9 +124,29 @@ public class ProtocolBufferHandler implements MessageHandler {
processed = true;
processProtobufFindMyWatchResponse(smart.getFindMyWatchService());
}
- if (!processed) {
+ if (smart.hasSettingsService()) {
+ processed = true;
+ processProtobufSettingsService(smart.getSettingsService());
+ }
+ if (processed) {
+ message.setStatusMessage(new ProtobufStatusMessage(
+ message.getMessageType(),
+ GFDIMessage.Status.ACK,
+ message.getRequestId(),
+ message.getDataOffset(),
+ ProtobufStatusMessage.ProtobufChunkStatus.KEPT,
+ ProtobufStatusMessage.ProtobufStatusCode.NO_ERROR
+ ));
+ } else {
LOG.warn("Unknown protobuf request: {}", smart);
- message.setStatusMessage(new ProtobufStatusMessage(message.getMessageType(), GFDIMessage.Status.ACK, message.getRequestId(), message.getDataOffset(), ProtobufStatusMessage.ProtobufChunkStatus.DISCARDED, ProtobufStatusMessage.ProtobufStatusCode.UNKNOWN_REQUEST_ID));
+ message.setStatusMessage(new ProtobufStatusMessage(
+ message.getMessageType(),
+ GFDIMessage.Status.ACK,
+ message.getRequestId(),
+ message.getDataOffset(),
+ ProtobufStatusMessage.ProtobufChunkStatus.DISCARDED,
+ ProtobufStatusMessage.ProtobufStatusCode.UNKNOWN_REQUEST_ID
+ ));
}
}
return null;
@@ -387,6 +412,33 @@ public class ProtocolBufferHandler implements MessageHandler {
LOG.warn("Unknown FindMyWatchService response: {}", findMyWatchService);
}
+ private boolean processProtobufSettingsService(final GdiSettingsService.SettingsService settingsService) {
+ boolean processed = false;
+
+ if (settingsService.hasDefinitionResponse()) {
+ processed = true;
+ final Intent intent = new Intent(GarminRealtimeSettingsFragment.ACTION_SCREEN_DEFINITION);
+ intent.putExtra(GarminRealtimeSettingsFragment.EXTRA_PROTOBUF, settingsService.getDefinitionResponse().getDefinition().toByteArray());
+ LocalBroadcastManager.getInstance(deviceSupport.getContext()).sendBroadcast(intent);
+ }
+
+ if (settingsService.hasStateResponse()) {
+ processed = true;
+ final Intent intent = new Intent(GarminRealtimeSettingsFragment.ACTION_SCREEN_STATE);
+ intent.putExtra(GarminRealtimeSettingsFragment.EXTRA_PROTOBUF, settingsService.getStateResponse().getState().toByteArray());
+ LocalBroadcastManager.getInstance(deviceSupport.getContext()).sendBroadcast(intent);
+ }
+
+ if (settingsService.hasChangeResponse()) {
+ processed = true;
+ final Intent intent = new Intent(GarminRealtimeSettingsFragment.ACTION_CHANGE);
+ intent.putExtra(GarminRealtimeSettingsFragment.EXTRA_PROTOBUF, settingsService.getChangeResponse().toByteArray());
+ LocalBroadcastManager.getInstance(deviceSupport.getContext()).sendBroadcast(intent);
+ }
+
+ return processed;
+ }
+
public ProtobufMessage prepareProtobufRequest(GdiSmartProto.Smart protobufPayload) {
if (null == protobufPayload)
return null;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/CapabilitiesDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/CapabilitiesDeviceEvent.java
new file mode 100644
index 000000000..538252e7f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/CapabilitiesDeviceEvent.java
@@ -0,0 +1,14 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
+
+import java.util.Set;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
+
+public class CapabilitiesDeviceEvent extends GBDeviceEvent {
+ public Set capabilities;
+
+ public CapabilitiesDeviceEvent(final Set capabilities) {
+ this.capabilities = capabilities;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java
index 415fb19ca..a6a77b685 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java
@@ -1,5 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -9,6 +10,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.CapabilitiesDeviceEvent;
public class ConfigurationMessage extends GFDIMessage {
@@ -29,9 +31,8 @@ public class ConfigurationMessage extends GFDIMessage {
}
public static ConfigurationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
- final int endOfPayload = reader.readByte();
- ConfigurationMessage configurationMessage = new ConfigurationMessage(garminMessage, reader.readBytes(endOfPayload - reader.getPosition()));
- return configurationMessage;
+ final int numBytes = reader.readByte();
+ return new ConfigurationMessage(garminMessage, reader.readBytes(numBytes));
}
@Override
@@ -40,7 +41,8 @@ public class ConfigurationMessage extends GFDIMessage {
for (final GarminCapability capability : capabilities) {
capabilitiesPref.add(capability.name());
}
- return Collections.singletonList(
+ return Arrays.asList(
+ new CapabilitiesDeviceEvent(capabilities),
new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_GARMIN_CAPABILITIES, capabilitiesPref)
);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Optional.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Optional.java
index 1d1bf4e34..89438d193 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Optional.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/Optional.java
@@ -52,6 +52,12 @@ public final class Optional {
return value != null ? value : other;
}
+ public void ifPresent(final Consumer consumer) {
+ if (value != null) {
+ consumer.consume(value);
+ }
+ }
+
public static Optional empty() {
return new Optional<>();
}
@@ -63,4 +69,8 @@ public final class Optional {
public static Optional ofNullable(final T value) {
return value == null ? empty() : of(value);
}
+
+ public static interface Consumer {
+ void consume(final T value);
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java
index 9e7296ce4..994f0e951 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java
@@ -128,11 +128,11 @@ public class StringUtils {
return "";
}
- public static boolean isNullOrEmpty(String string){
- return string == null || string.isEmpty();
+ public static boolean isNullOrEmpty(CharSequence string){
+ return string == null || string.length() == 0;
}
- public static boolean isEmpty(String string) {
+ public static boolean isEmpty(CharSequence string) {
return string != null && string.length() == 0;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XDatePreference.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XDatePreference.java
new file mode 100644
index 000000000..87217673d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XDatePreference.java
@@ -0,0 +1,114 @@
+/* Copyright (C) 2024 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.util;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+import androidx.preference.DialogPreference;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class XDatePreference extends DialogPreference {
+ private int year;
+ private int month;
+ private int day;
+ private long minDate; // TODO actually read minDate
+ private long maxDate; // TODO actually read maxDate
+
+ public XDatePreference(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected Object onGetDefaultValue(final TypedArray a, final int index) {
+ return a.getString(index);
+ }
+
+ @Override
+ protected void onSetInitialValue(final Object defaultValue) {
+ final String persistedString = getPersistedString((String) defaultValue);
+
+ final String dateStr;
+
+ if (StringUtils.isNullOrEmpty(persistedString)) {
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT);
+ dateStr = getPersistedString(sdf.format(new Date()));
+ } else {
+ dateStr = persistedString;
+ }
+
+ final String[] pieces = dateStr.split("-");
+
+ year = Integer.parseInt(pieces[0]);
+ month = Integer.parseInt(pieces[1]);
+ day = Integer.parseInt(pieces[2]);
+
+ updateSummary();
+ }
+
+ public void setMinDate(final long minDate) {
+ this.minDate = minDate;
+ }
+
+ public void setMaxDate(final long maxDate) {
+ this.maxDate = maxDate;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public int getDay() {
+ return day;
+ }
+
+ public long getMinDate() {
+ return minDate;
+ }
+
+ public long getMaxDate() {
+ return maxDate;
+ }
+
+ String getPrefValue() {
+ return String.format(Locale.ROOT, "%04d-%02d-%02d", year, month, day);
+ }
+
+ public void setValue(final int year, final int month, final int day) {
+ this.year = year;
+ this.month = month;
+ this.day = day;
+
+ persistStringValue(getPrefValue());
+ }
+
+ void updateSummary() {
+ setSummary(String.format(Locale.ROOT, "%04d-%02d-%02d", year, month, day));
+ }
+
+ void persistStringValue(final String value) {
+ persistString(value);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XDatePreferenceFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XDatePreferenceFragment.java
new file mode 100644
index 000000000..74d6ae246
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XDatePreferenceFragment.java
@@ -0,0 +1,80 @@
+/* Copyright (C) 2024 José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.util;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.DatePicker;
+
+import androidx.annotation.NonNull;
+import androidx.preference.DialogPreference;
+import androidx.preference.Preference;
+
+import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialPreferenceDialogFragment;
+
+public class XDatePreferenceFragment extends MaterialPreferenceDialogFragment implements DialogPreference.TargetFragment {
+ private DatePicker picker = null;
+
+ @Override
+ protected View onCreateDialogView(final Context context) {
+ picker = new DatePicker(context);
+ picker.setPadding(0, 50, 0, 50);
+
+ return picker;
+ }
+
+ @Override
+ protected void onBindDialogView(final View v) {
+ super.onBindDialogView(v);
+ final XDatePreference pref = (XDatePreference) getPreference();
+
+ picker.init(pref.getYear(), pref.getMonth() - 1, pref.getDay(), null);
+
+ if (pref.getMinDate() != 0) {
+ picker.setMinDate(pref.getMinDate());
+ }
+
+ if (pref.getMaxDate() != 0) {
+ picker.setMaxDate(pref.getMaxDate());
+ }
+ }
+
+ @Override
+ public void onDialogClosed(final boolean positiveResult) {
+ if (!positiveResult) {
+ return;
+ }
+
+ final XDatePreference pref = (XDatePreference) getPreference();
+ pref.setValue(
+ picker.getYear(),
+ picker.getMonth() + 1,
+ picker.getDayOfMonth()
+ );
+
+ final String date = pref.getPrefValue();
+ if (pref.callChangeListener(date)) {
+ pref.persistStringValue(date);
+ pref.updateSummary();
+ }
+ }
+
+ @Override
+ public Preference findPreference(@NonNull final CharSequence key) {
+ return getPreference();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreference.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreference.java
index 17d4e7bac..ac3349d2c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreference.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreference.java
@@ -23,10 +23,14 @@ import android.util.AttributeSet;
import androidx.preference.DialogPreference;
+import java.util.Locale;
+
public class XTimePreference extends DialogPreference {
protected int hour = 0;
protected int minute = 0;
+ protected Format format = Format.AUTO;
+
public XTimePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -62,8 +66,19 @@ public class XTimePreference extends DialogPreference {
updateSummary();
}
+ public String getPrefValue() {
+ return String.format(Locale.ROOT, "%02d:%02d", hour, minute);
+ }
+
+ public void setValue(final int hour, final int minute) {
+ this.hour = hour;
+ this.minute = minute;
+
+ persistStringValue(getPrefValue());
+ }
+
void updateSummary() {
- if (DateFormat.is24HourFormat(getContext()))
+ if (is24HourFormat())
setSummary(getTime24h());
else
setSummary(getTime12h());
@@ -80,7 +95,34 @@ public class XTimePreference extends DialogPreference {
return h + ":" + String.format("%02d", minute) + suffix;
}
+ public void setFormat(final Format format) {
+ this.format = format;
+ }
+
+ public Format getFormat() {
+ return format;
+ }
+
void persistStringValue(String value) {
persistString(value);
}
+
+ public boolean is24HourFormat() {
+ switch (format) {
+ case FORMAT_24H:
+ return true;
+ case FORMAT_12H:
+ return false;
+ case AUTO:
+ default:
+ return DateFormat.is24HourFormat(getContext());
+ }
+ }
+
+ public enum Format {
+ AUTO,
+ FORMAT_24H,
+ FORMAT_12H,
+ ;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreferenceFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreferenceFragment.java
index dfb616b05..b8f6bf91e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreferenceFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/XTimePreferenceFragment.java
@@ -32,7 +32,7 @@ public class XTimePreferenceFragment extends MaterialPreferenceDialogFragment im
@Override
protected View onCreateDialogView(Context context) {
picker = new TimePicker(context);
- picker.setIs24HourView(DateFormat.is24HourFormat(getContext()));
+ picker.setIs24HourView(((XTimePreference) getPreference()).is24HourFormat());
picker.setPadding(0, 50, 0, 50);
return picker;
diff --git a/app/src/main/proto/garmin/gdi_settings_service.proto b/app/src/main/proto/garmin/gdi_settings_service.proto
index 88bbcc998..4ebbb08b8 100644
--- a/app/src/main/proto/garmin/gdi_settings_service.proto
+++ b/app/src/main/proto/garmin/gdi_settings_service.proto
@@ -5,25 +5,224 @@ package garmin_vivomovehr;
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin";
message SettingsService {
- optional ChangeRequest change_request = 5;
- optional ChangeResponse change_response = 6;
+ optional ScreenDefinitionRequest definitionRequest = 1;
+ optional ScreenDefinitionResponse definitionResponse = 2;
+
+ optional ScreenStateRequest stateRequest = 3;
+ optional ScreenStateResponse stateResponse = 4;
+
+ optional ChangeRequest changeRequest = 5;
+ optional ChangeResponse changeResponse = 6;
+
+ optional InitRequest initRequest = 8;
+ optional InitResponse initResponse = 9;
+}
+
+message ScreenDefinitionRequest {
+ optional uint32 screenId = 1;
+ optional uint32 unk2 = 2; // 0
+ optional string language = 3; // en_US
+}
+
+message ScreenDefinitionResponse {
+ optional uint32 unk1 = 1; // 0, status?
+ optional ScreenDefinition definition = 2;
+}
+
+message ScreenDefinition {
+ optional uint32 screenId = 1;
+ optional uint32 unk2 = 2; // 0
+ optional uint32 unk3 = 3; // 928002
+ optional Label title = 4;
+ repeated ScreenEntry entry = 5;
+}
+
+message Label {
+ optional string id = 1;
+ optional string text = 2;
+}
+
+message ScreenEntry {
+ optional uint32 id = 1;
+ optional uint32 type = 2;
+ optional Label title = 3;
+ optional uint32 icon = 5;
+ optional Target target = 9;
+ optional SortOptions sortOptions = 12;
+ optional TextOption textOption = 13;
+}
+
+message SortOptions {
+ optional uint32 unk3 = 3; // 1
+ repeated SortEntry entries = 5;
+}
+
+message SortEntry {
+ optional uint32 id = 1;
+ optional Label title = 2;
+ optional uint32 unk5 = 5; // 1
+}
+
+message TextOption {
+ optional TextLimits limits = 1;
+ optional uint32 unk2 = 2; // 0
+}
+
+message TextLimits {
+ optional uint32 maxLength = 1;
+}
+
+message Target {
+ optional uint32 type = 1; // 0 subscreen, 1 list preference, 6 other activity, 7 hidden, 9 subscreen with options
+ optional uint32 subscreen = 2; // when 0
+ optional uint32 activity = 3; // when 6
+ optional TargetOptions options = 4; // when 1
+ optional TargetNumberPicker numberPicker = 8;
+}
+
+message ScreenStateRequest {
+ optional uint32 screenId = 1;
+}
+
+message TargetOptions {
+ repeated TargetOptionEntry option = 1;
+}
+
+message TargetOptionEntry {
+ optional Label title = 3;
+}
+
+message TargetNumberPicker {
+ optional uint32 min = 1;
+ optional uint32 max = 2;
+ optional uint32 step = 3; // maybe? 1 on weight
+}
+
+message ScreenStateResponse {
+ optional uint32 unk1 = 1; // 0
+ optional ScreenState state = 2;
+}
+
+message ScreenState {
+ optional uint32 screenId = 1;
+ optional uint32 unk2 = 2; // 0
+ optional uint32 unk3 = 3; // 928002
+ repeated EntryState state = 4;
+}
+
+message EntryState {
+ optional uint32 id = 1;
+ optional uint32 state = 2;
+ optional Switch switch = 3;
+ optional Summary summary = 4;
+}
+
+message Switch {
+ optional bool enabled = 1;
+ optional Label title = 2;
+}
+
+message Summary {
+ optional Label title = 1;
+ optional ValueList valueList = 2;
+ optional ValueTime valueTime = 4;
+ optional ValueNumber valueNumber = 6;
+ optional ValueDate valueDate = 8;
+ optional ValueHeight valueHeight = 10;
+}
+
+message ValueList {
+ optional uint32 index = 1;
+}
+
+message ValueTime {
+ optional uint32 seconds = 1;
+ optional uint32 timeFormat = 3; // 0 12h, 1 24h
+}
+
+message ValueNumber {
+ optional uint32 value = 1;
+ optional Label subtitle = 2;
+ optional Label title = 3;
+ optional Label unit = 4;
+}
+
+message ValueDate {
+ optional Label subtitle = 1;
+ optional Date currentDate = 2;
+ optional Date minDate = 3;
+ optional Date maxDate = 4;
+}
+
+message ValueHeight {
+ optional Label subtitle = 1;
+ optional uint32 value = 2;
+ optional uint32 unit = 3; // 0 cm
+}
+
+message Date {
+ optional uint32 month = 1;
+ optional uint32 day = 2;
+ optional uint32 year = 3;
}
message ChangeRequest {
- optional uint32 pointer1 = 1;
- optional uint32 pointer2 = 2;
- optional Switch enable = 3;
+ optional uint32 screenId = 1;
+ optional uint32 entryId = 2;
+ optional Switch switch = 3;
+ optional Option option = 4;
+ optional Time time = 6;
+ optional Number number = 8;
+ optional Position position = 11;
+ optional NewDate newDate = 12;
+ optional Text text = 14;
+ optional Height height = 15;
message Switch {
required bool value = 1;
}
+ message Option {
+ optional uint32 index = 1;
+ }
+ message Time {
+ optional uint32 seconds = 1;
+ }
+ message Number {
+ optional uint32 value = 1;
+ }
+ message Position {
+ optional uint32 index = 1;
+ optional bool delete = 2;
+ }
+ message NewDate {
+ optional Date value = 1;
+ }
+ message Text {
+ optional string value = 1;
+ }
+ message Height {
+ optional uint32 value = 1;
+ optional uint32 unit = 2;
+ }
}
message ChangeResponse {
optional ResponseStatus status = 1;
+ optional ScreenState state = 3;
+ optional bool shouldReturn = 5;
+}
+
+message InitRequest {
+ optional string language = 1; // en_US
+ optional string region = 2; // us
+}
+
+message InitResponse {
+ optional string unk1 = 1;
+ optional string unk2 = 2;
}
enum ResponseStatus {
SUCCESS = 0;
GENERIC_ERROR = 1;
-}
\ No newline at end of file
+}
diff --git a/app/src/main/res/drawable/ic_add_gray.xml b/app/src/main/res/drawable/ic_add_gray.xml
new file mode 100644
index 000000000..3732fe476
--- /dev/null
+++ b/app/src/main/res/drawable/ic_add_gray.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_garmin_realtime_settings.xml b/app/src/main/res/menu/menu_garmin_realtime_settings.xml
new file mode 100644
index 000000000..7bc674916
--- /dev/null
+++ b/app/src/main/res/menu/menu_garmin_realtime_settings.xml
@@ -0,0 +1,17 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c35a7e925..dec49cb1a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1103,6 +1103,7 @@
Year of birth
Gender
Height in cm
+ Height in inches
Weight in kg
Target weight in kg
Step length in cm
@@ -2890,4 +2891,11 @@
Local file
The list below contains all URLs requested by the watch for AGPS updates. You can select a file from the phone\'s storage that will be sent to the watch when it requests an update.
Copied to clipboard
+ Loading…
+ Toggle debug mode
+ Share debug info
+ Realtime settings
+ Unsupported
+ Minimum: %d
+ Maximum: %d
diff --git a/app/src/main/res/xml/devicesettings_garmin_default_reply_suffix.xml b/app/src/main/res/xml/devicesettings_garmin_default_reply_suffix.xml
deleted file mode 100644
index ab3d63e4d..000000000
--- a/app/src/main/res/xml/devicesettings_garmin_default_reply_suffix.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/xml/devicesettings_garmin_realtime_settings.xml b/app/src/main/res/xml/devicesettings_garmin_realtime_settings.xml
new file mode 100644
index 000000000..89c718c71
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_garmin_realtime_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app/src/main/res/xml/garmin_realtime_settings.xml b/app/src/main/res/xml/garmin_realtime_settings.xml
new file mode 100644
index 000000000..f94b2efca
--- /dev/null
+++ b/app/src/main/res/xml/garmin_realtime_settings.xml
@@ -0,0 +1,5 @@
+
+
+
+