mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-12 10:55:49 +01:00
Garmin: Realtime settings
This commit is contained in:
parent
0f8889498e
commit
b7aec071ff
@ -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"
|
||||||
|
@ -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) {
|
||||||
|
@ -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";
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
12
app/src/main/res/drawable/ic_add_gray.xml
Normal file
12
app/src/main/res/drawable/ic_add_gray.xml
Normal 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>
|
17
app/src/main/res/menu/menu_garmin_realtime_settings.xml
Normal file
17
app/src/main/res/menu/menu_garmin_realtime_settings.xml
Normal 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>
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
5
app/src/main/res/xml/garmin_realtime_settings.xml
Normal file
5
app/src/main/res/xml/garmin_realtime_settings.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user