1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-23 23:32:23 +02:00

Garmin: Realtime settings

This commit is contained in:
José Rebelo 2024-06-07 23:39:12 +01:00
parent 0f8889498e
commit b7aec071ff
27 changed files with 1703 additions and 68 deletions

View File

@ -179,6 +179,10 @@
android:name=".activities.loyaltycards.LoyaltyCardsSettingsActivity" android:name=".activities.loyaltycards.LoyaltyCardsSettingsActivity"
android:label="@string/loyalty_cards" android:label="@string/loyalty_cards"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" /> android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".devices.garmin.GarminRealtimeSettingsActivity"
android:label="@string/loading"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity <activity
android:name=".devices.pebble.PebbleSettingsActivity" android:name=".devices.pebble.PebbleSettingsActivity"
android:label="@string/pref_title_pebble_settings" android:label="@string/pref_title_pebble_settings"

View File

@ -20,6 +20,7 @@ import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.preference.EditTextPreference; import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
@ -45,6 +46,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.XDatePreference;
import nodomain.freeyourgadget.gadgetbridge.util.XDatePreferenceFragment;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference; import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment; import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment;
import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialEditTextPreferenceDialogFragment; import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialEditTextPreferenceDialogFragment;
@ -94,10 +97,12 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragmentCompa
} }
@Override @Override
public void onDisplayPreferenceDialog(final Preference preference) { public void onDisplayPreferenceDialog(@NonNull final Preference preference) {
DialogFragment dialogFragment; DialogFragment dialogFragment;
if (preference instanceof XTimePreference) { if (preference instanceof XTimePreference) {
dialogFragment = new XTimePreferenceFragment(); dialogFragment = new XTimePreferenceFragment();
} else if (preference instanceof XDatePreference) {
dialogFragment = new XDatePreferenceFragment();
} else if (preference instanceof DragSortListPreference) { } else if (preference instanceof DragSortListPreference) {
dialogFragment = new DragSortListPreferenceFragment(); dialogFragment = new DragSortListPreferenceFragment();
} else if (preference instanceof EditTextPreference) { } else if (preference instanceof EditTextPreference) {

View File

@ -465,6 +465,4 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL = "pref_cycling_persistence_interval"; public static final String PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL = "pref_cycling_persistence_interval";
public static final String PREF_CYCLING_SENSOR_WHEEL_DIAMETER = "pref_cycling_wheel_diameter"; public static final String PREF_CYCLING_SENSOR_WHEEL_DIAMETER = "pref_cycling_wheel_diameter";
public static final String PREF_GARMIN_DEFAULT_REPLY_SUFFIX = "pref_key_garmin_default_reply_suffix";
} }

View File

@ -648,8 +648,6 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE); addPreferenceHandlerFor(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE);
addPreferenceHandlerFor(PREF_GARMIN_DEFAULT_REPLY_SUFFIX);
addPreferenceHandlerFor("lock"); addPreferenceHandlerFor("lock");
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF); String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);

View File

@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.List; import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -13,6 +14,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@ -98,12 +100,15 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
if (supports(device, GarminCapability.REALTIME_SETTINGS)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_garmin_realtime_settings);
}
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
notifications.add(R.xml.devicesettings_send_app_notifications); notifications.add(R.xml.devicesettings_send_app_notifications);
if (getCannedRepliesSlotCount(device) > 0) { 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_reply_16);
notifications.add(R.xml.devicesettings_canned_dismisscall_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) { public boolean supportsAgpsUpdates(final GBDevice device) {
return !getPrefs(device).getString(GarminPreferences.PREF_AGPS_KNOWN_URLS, "").isEmpty(); 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());
}
} }

View File

@ -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_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_FOLDER = "garmin_agps_folder";
public static final String PREF_GARMIN_AGPS_FILENAME = "garmin_agps_filename_%s"; 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) { public static String agpsStatus(final String url) {
return String.format(GarminPreferences.PREF_GARMIN_AGPS_STATUS, CheckSums.md5(url)); return String.format(GarminPreferences.PREF_GARMIN_AGPS_STATUS, CheckSums.md5(url));

View File

@ -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<GarminRealtimeSettingsFragment> 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();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<Integer, GdiSettingsService.EntryState> 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<String> sortableOptions = new ArrayList<>(3);
if (i > 0) {
sortableOptions.add(moveUpStr);
}
if (i < entry.getSortOptions().getEntriesCount() - 1) {
sortableOptions.add(moveDownStr);
}
sortableOptions.add(deleteStr);
final ArrayAdapter<String> 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);
}
}
}
}

View File

@ -34,6 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; 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.service.devices.garmin.agps.GarminAgpsStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -49,6 +50,16 @@ public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomize
@Override @Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) { 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); final PreferenceCategory prefAgpsHeader = handler.findPreference(DeviceSettingsPreferenceConst.PREF_HEADER_AGPS);
if (prefAgpsHeader != null) { if (prefAgpsHeader != null) {
final List<String> urls = prefs.getList(GarminPreferences.PREF_AGPS_KNOWN_URLS, Collections.emptyList(), "\n"); final List<String> urls = prefs.getList(GarminPreferences.PREF_AGPS_KNOWN_URLS, Collections.emptyList(), "\n");

View File

@ -103,25 +103,50 @@ public enum GarminCapability {
GOLF_9_PLUS_9, GOLF_9_PLUS_9,
ANTI_THEFT_ALARM, ANTI_THEFT_ALARM,
INREACH, 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<GarminCapability> ALL_CAPABILITIES = new HashSet<>(values().length); public static final Set<GarminCapability> ALL_CAPABILITIES = new HashSet<>(values().length);
private static final Map<Integer, GarminCapability> FROM_ORDINAL = new HashMap<>(values().length); private static final Map<Integer, GarminCapability> FROM_ORDINAL = new HashMap<>(values().length);
static { static {
for (GarminCapability cap : values()) { for (final GarminCapability cap : values()) {
FROM_ORDINAL.put(cap.ordinal(), cap); FROM_ORDINAL.put(cap.ordinal(), cap);
ALL_CAPABILITIES.add(cap); ALL_CAPABILITIES.add(cap);
} }
} }
public static Set<GarminCapability> setFromBinary(byte[] bytes) { public static Set<GarminCapability> setFromBinary(final byte[] bytes) {
final Set<GarminCapability> result = new HashSet<>(GarminCapability.values().length); final Set<GarminCapability> result = new HashSet<>(GarminCapability.values().length);
int current = 0; int current = 0;
for (int b : bytes) { for (int b : bytes) {
for (int curr = 1; curr < 0x100; curr <<= 1) { for (int i = 0; i < 8; i++) {
if ((b & curr) != 0) { if ((b & (1 << i)) != 0) {
result.add(FROM_ORDINAL.get(current)); if (FROM_ORDINAL.containsKey(current)) {
result.add(FROM_ORDINAL.get(current));
}
} }
++current; ++current;
} }
@ -129,7 +154,7 @@ public enum GarminCapability {
return result; return result;
} }
public static byte[] setToBinary(Set<GarminCapability> capabilities) { public static byte[] setToBinary(final Set<GarminCapability> capabilities) {
final GarminCapability[] values = values(); final GarminCapability[] values = values();
final byte[] result = new byte[(values.length + 7) / 8]; final byte[] result = new byte[(values.length + 7) / 8];
int bytePos = 0; int bytePos = 0;
@ -147,9 +172,9 @@ public enum GarminCapability {
return result; return result;
} }
public static String setToString(Set<GarminCapability> capabilities) { public static String setToString(final Set<GarminCapability> capabilities) {
final StringBuilder result = new StringBuilder(); final StringBuilder result = new StringBuilder();
for (GarminCapability cap : capabilities) { for (final GarminCapability cap : capabilities) {
if (result.length() > 0) result.append(", "); if (result.length() > 0) result.append(", ");
result.append(cap.name()); result.append(cap.name());
} }

View File

@ -25,6 +25,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.UUID; 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.ICommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1; 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.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.FileDownloadedDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent; 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 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_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; 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) { if (weather != null) {
sendWeatherConditions(weather); sendWeatherConditions(weather);
} }
} else if (deviceEvent instanceof CapabilitiesDeviceEvent) {
final Set<GarminCapability> 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) { } else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable; final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
notificationsHandler.setEnabled(enable); notificationsHandler.setEnabled(enable);
@ -327,13 +346,8 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
communicator.sendMessage(taskName, message.getOutgoingMessage()); 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) { 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 // Device does not support sending weather as fit
return; return;
} }
@ -444,39 +458,34 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
sendOutgoingMessage("request supported file types", new SupportedFileTypesMessage()); sendOutgoingMessage("request supported file types", new SupportedFileTypesMessage());
sendOutgoingMessage("toggle default reply suffix", toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
if (mFirstConnect) { if (mFirstConnect) {
sendOutgoingMessage("set sync complete", new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0)); sendOutgoingMessage("set sync complete", new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0));
this.mFirstConnect = false; 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 @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) { 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: case PREF_SEND_APP_NOTIFICATIONS:
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent(); NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
notificationSubscriptionDeviceEvent.enable = true; // actual status is fetched from preferences notificationSubscriptionDeviceEvent.enable = true; // actual status is fetched from preferences
evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent); evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent);
break; return;
} }
} }
private void processDownloadQueue() { private void processDownloadQueue() {
@ -754,6 +763,41 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
return super.connect(); 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 @Override
public void onTestNewFunction() { public void onTestNewFunction() {
parseAllFitFilesFromStorage(); parseAllFitFilesFromStorage();

View File

@ -1,7 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin; package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.content.Intent;
import android.location.Location; import android.location.Location;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang3.ArrayUtils; 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.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; 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.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService; import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; 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.GdiDeviceStatus;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiFindMyWatch;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiHttpService; 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.GdiSmartProto;
import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmsNotification; import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GdiSmsNotification;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.DataTransferHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.DataTransferHandler;
@ -119,9 +124,29 @@ public class ProtocolBufferHandler implements MessageHandler {
processed = true; processed = true;
processProtobufFindMyWatchResponse(smart.getFindMyWatchService()); 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); 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; return null;
@ -387,6 +412,33 @@ public class ProtocolBufferHandler implements MessageHandler {
LOG.warn("Unknown FindMyWatchService response: {}", findMyWatchService); 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) { public ProtobufMessage prepareProtobufRequest(GdiSmartProto.Smart protobufPayload) {
if (null == protobufPayload) if (null == protobufPayload)
return null; return null;

View File

@ -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<GarminCapability> capabilities;
public CapabilitiesDeviceEvent(final Set<GarminCapability> capabilities) {
this.capabilities = capabilities;
}
}

View File

@ -1,5 +1,6 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -9,6 +10,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.CapabilitiesDeviceEvent;
public class ConfigurationMessage extends GFDIMessage { public class ConfigurationMessage extends GFDIMessage {
@ -29,9 +31,8 @@ public class ConfigurationMessage extends GFDIMessage {
} }
public static ConfigurationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) { public static ConfigurationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final int endOfPayload = reader.readByte(); final int numBytes = reader.readByte();
ConfigurationMessage configurationMessage = new ConfigurationMessage(garminMessage, reader.readBytes(endOfPayload - reader.getPosition())); return new ConfigurationMessage(garminMessage, reader.readBytes(numBytes));
return configurationMessage;
} }
@Override @Override
@ -40,7 +41,8 @@ public class ConfigurationMessage extends GFDIMessage {
for (final GarminCapability capability : capabilities) { for (final GarminCapability capability : capabilities) {
capabilitiesPref.add(capability.name()); capabilitiesPref.add(capability.name());
} }
return Collections.singletonList( return Arrays.asList(
new CapabilitiesDeviceEvent(capabilities),
new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_GARMIN_CAPABILITIES, capabilitiesPref) new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_GARMIN_CAPABILITIES, capabilitiesPref)
); );
} }

View File

@ -52,6 +52,12 @@ public final class Optional<T> {
return value != null ? value : other; return value != null ? value : other;
} }
public void ifPresent(final Consumer<T> consumer) {
if (value != null) {
consumer.consume(value);
}
}
public static <T> Optional<T> empty() { public static <T> Optional<T> empty() {
return new Optional<>(); return new Optional<>();
} }
@ -63,4 +69,8 @@ public final class Optional<T> {
public static <T> Optional<T> ofNullable(final T value) { public static <T> Optional<T> ofNullable(final T value) {
return value == null ? empty() : of(value); return value == null ? empty() : of(value);
} }
public static interface Consumer<T> {
void consume(final T value);
}
} }

View File

@ -128,11 +128,11 @@ public class StringUtils {
return ""; return "";
} }
public static boolean isNullOrEmpty(String string){ public static boolean isNullOrEmpty(CharSequence string){
return string == null || string.isEmpty(); return string == null || string.length() == 0;
} }
public static boolean isEmpty(String string) { public static boolean isEmpty(CharSequence string) {
return string != null && string.length() == 0; return string != null && string.length() == 0;
} }

View File

@ -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 <https://www.gnu.org/licenses/>. */
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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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();
}
}

View File

@ -23,10 +23,14 @@ import android.util.AttributeSet;
import androidx.preference.DialogPreference; import androidx.preference.DialogPreference;
import java.util.Locale;
public class XTimePreference extends DialogPreference { public class XTimePreference extends DialogPreference {
protected int hour = 0; protected int hour = 0;
protected int minute = 0; protected int minute = 0;
protected Format format = Format.AUTO;
public XTimePreference(Context context, AttributeSet attrs) { public XTimePreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
} }
@ -62,8 +66,19 @@ public class XTimePreference extends DialogPreference {
updateSummary(); 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() { void updateSummary() {
if (DateFormat.is24HourFormat(getContext())) if (is24HourFormat())
setSummary(getTime24h()); setSummary(getTime24h());
else else
setSummary(getTime12h()); setSummary(getTime12h());
@ -80,7 +95,34 @@ public class XTimePreference extends DialogPreference {
return h + ":" + String.format("%02d", minute) + suffix; 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) { void persistStringValue(String value) {
persistString(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,
;
}
} }

View File

@ -32,7 +32,7 @@ public class XTimePreferenceFragment extends MaterialPreferenceDialogFragment im
@Override @Override
protected View onCreateDialogView(Context context) { protected View onCreateDialogView(Context context) {
picker = new TimePicker(context); picker = new TimePicker(context);
picker.setIs24HourView(DateFormat.is24HourFormat(getContext())); picker.setIs24HourView(((XTimePreference) getPreference()).is24HourFormat());
picker.setPadding(0, 50, 0, 50); picker.setPadding(0, 50, 0, 50);
return picker; return picker;

View File

@ -5,25 +5,224 @@ package garmin_vivomovehr;
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin"; option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin";
message SettingsService { message SettingsService {
optional ChangeRequest change_request = 5; optional ScreenDefinitionRequest definitionRequest = 1;
optional ChangeResponse change_response = 6; 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 { message ChangeRequest {
optional uint32 pointer1 = 1; optional uint32 screenId = 1;
optional uint32 pointer2 = 2; optional uint32 entryId = 2;
optional Switch enable = 3; 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 { message Switch {
required bool value = 1; 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 { message ChangeResponse {
optional ResponseStatus status = 1; 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 { enum ResponseStatus {
SUCCESS = 0; SUCCESS = 0;
GENERIC_ERROR = 1; GENERIC_ERROR = 1;
} }

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#7E7E7E"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</vector>

View File

@ -0,0 +1,17 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity">
<item
android:id="@+id/garmin_rt_debug_toggle"
android:icon="@drawable/ic_developer_mode"
android:title="@string/toggle_debug_mode"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
<item
android:id="@+id/garmin_rt_debug_share"
android:icon="@drawable/ic_share"
android:title="@string/share_debug_info"
app:iconTint="?attr/actionmenu_icon_color"
app:showAsAction="never" />
</menu>

View File

@ -1103,6 +1103,7 @@
<string name="activity_prefs_year_birth">Year of birth</string> <string name="activity_prefs_year_birth">Year of birth</string>
<string name="activity_prefs_gender">Gender</string> <string name="activity_prefs_gender">Gender</string>
<string name="activity_prefs_height_cm">Height in cm</string> <string name="activity_prefs_height_cm">Height in cm</string>
<string name="activity_prefs_height_inches">Height in inches</string>
<string name="activity_prefs_weight_kg">Weight in kg</string> <string name="activity_prefs_weight_kg">Weight in kg</string>
<string name="activity_prefs_target_weight_kg">Target weight in kg</string> <string name="activity_prefs_target_weight_kg">Target weight in kg</string>
<string name="activity_prefs_step_length_cm">Step length in cm</string> <string name="activity_prefs_step_length_cm">Step length in cm</string>
@ -2890,4 +2891,11 @@
<string name="garmin_agps_local_file">Local file</string> <string name="garmin_agps_local_file">Local file</string>
<string name="pref_garmin_agps_help">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.</string> <string name="pref_garmin_agps_help">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.</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
<string name="loading">Loading…</string>
<string name="toggle_debug_mode">Toggle debug mode</string>
<string name="share_debug_info">Share debug info</string>
<string name="realtime_settings">Realtime settings</string>
<string name="unsupported">Unsupported</string>
<string name="min_val">Minimum: %d</string>
<string name="max_val">Maximum: %d</string>
</resources> </resources>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:key="pref_key_garmin_default_reply_suffix"
android:summary="@string/pref_summary_garmin_default_reply_suffix"
android:title="@string/pref_title_garmin_default_reply_suffix">
</SwitchPreferenceCompat>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_settings"
android:key="garmin_realtime_settings"
android:title="@string/realtime_settings" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="garmin_realtime_settings">
</androidx.preference.PreferenceScreen>