Even Realities G1: Add Settings menu and silent mode toggle

This change adds in a toggle for "silent mode" which turns the displays
off and makes the glasses act like normal glasses.

This change also adds the following configuration settings:
 - Screen Location/height
 - Screen Depth Effect
 - Head Tilt Activation Angle
 - Toggle Auto Brightness
 - Brightness Level
 - Wear Detection

Also refactored Communications file to have a consistent naming style.
This commit is contained in:
jrthomas270
2025-05-26 21:43:19 +02:00
committed by José Rebelo
parent 0a0fba4179
commit 7021804db5
15 changed files with 954 additions and 90 deletions
@@ -625,4 +625,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_DISPLAY_ON_START = "display_on_start";
public static final String PREF_DISPLAY_ON_END = "display_on_end";
public static final String PREF_CONNECTION_PRIORITY_LOW_POWER = "connection_priority_low_power";
public static final String PREF_EVEN_REALITIES_SCREEN_HEIGHT = "pref_even_realities_g1_screen_height";
public static final String PREF_EVEN_REALITIES_SCREEN_DEPTH = "pref_even_realities_g1_screen_depth";
public static final String PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE = "pref_even_realities_g1_screen_activation_angle";
}
@@ -622,6 +622,10 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_WEAR_SENSOR_TOGGLE);
addPreferenceHandlerFor(PREF_BANDW_PSERIES_GUI_VPT_LEVEL);
addPreferenceHandlerFor(PREF_EVEN_REALITIES_SCREEN_HEIGHT);
addPreferenceHandlerFor(PREF_EVEN_REALITIES_SCREEN_DEPTH);
addPreferenceHandlerFor(PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE);
addPreferenceHandlerFor(PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES);
addPreferenceHandlerFor(PREF_HYBRID_HR_FORCE_WHITE_COLOR);
addPreferenceHandlerFor(PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES);
@@ -1,6 +1,8 @@
package nodomain.freeyourgadget.gadgetbridge.devices.evenrealities;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
@@ -9,12 +11,15 @@ import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -174,4 +179,36 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
return 1;
}
}
@Override
public List<DeviceCardAction> getCustomActions() {
return Collections.singletonList(new DeviceCardAction() {
@Override
public int getIcon(GBDevice device) {
return R.drawable.ic_dnd;
}
@Override
public String getDescription(final GBDevice device, final Context context) {
return context.getString(R.string.silent_mode);
}
@Override
public void onClick(final GBDevice device, final Context context) {
final Intent intent = new Intent(G1Constants.INTENT_TOGGLE_SILENT_MODE);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
intent.setPackage(context.getPackageName());
context.sendBroadcast(intent);
}
});
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
if (device.isConnected()) {
return new int[]{R.xml.devicesettings_even_realities_g1_display};
} else {
return new int[]{};
}
}
}
@@ -14,7 +14,7 @@ public class G1Communications {
public abstract static class CommandHandler {
private final boolean expectResponse;
private final Function<byte[], Boolean> callback;
protected short sequence;
protected byte sequence;
private byte[] responsePayload;
private int retryCount;
@@ -26,7 +26,7 @@ public class G1Communications {
}
public boolean needsGlobalSequence() { return false; }
public void setGlobalSequence(short sequence) {
public void setGlobalSequence(byte sequence) {
this.sequence = sequence;
}
public int getTimeout() {
@@ -95,8 +95,29 @@ public class G1Communications {
public abstract String getName();
}
public static class CommandFirmwareInfo extends CommandHandler {
public CommandFirmwareInfo(Function<byte[], Boolean> callback) {
public static class CommandSendInit extends CommandHandler {
public CommandSendInit() {
super(true, null);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.INIT.id, (byte)0xFB };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload[0] == G1Constants.CommandId.INIT.id;
}
@Override
public String getName() {
return "send_init";
}
}
public static class CommandGetFirmwareInfo extends CommandHandler {
public CommandGetFirmwareInfo(Function<byte[], Boolean> callback) {
super(true, callback);
}
@@ -115,12 +136,12 @@ public class G1Communications {
@Override
public String getName() {
return "firmware_info";
return "get_firmware_info";
}
}
public static class CommandBatteryLevel extends CommandHandler {
public CommandBatteryLevel(Function<byte[], Boolean> callback) {
public static class CommandGetBatteryInfo extends CommandHandler {
public CommandGetBatteryInfo(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
@@ -138,14 +159,14 @@ public class G1Communications {
@Override
public String getName() {
return "battery_level";
return "get_battery_info";
}
}
public static class CommandHeartBeat extends CommandHandler {
public CommandHeartBeat(short sequence) {
public static class CommandSendHeartBeat extends CommandHandler {
public CommandSendHeartBeat(byte sequence) {
super(false, null);
super.sequence = sequence;
setGlobalSequence(sequence);
}
@Override
@@ -154,11 +175,9 @@ public class G1Communications {
G1Constants.CommandId.HEARTBEAT.id,
0x00, // length is a short
0x06, // length
// TODO: What the heck is the 0x04 and why is the sequence split?
// Need to look at a real capture for this.
(byte) (sequence % 0xFF),
0x04,
(byte) (sequence % 0xFF)
sequence, // Sequence is included twice for some reason, also verified by the FW.
0x04, // Magic value that the FW looks for.
sequence
};
}
@@ -169,18 +188,18 @@ public class G1Communications {
@Override
public String getName() {
return "heart_beat";
return "send_heart_beat";
}
}
public static class CommandTimeAndWeather extends CommandHandler {
public static class CommandSetTimeAndWeather extends CommandHandler {
long timeMilliseconds;
boolean use12HourFormat;
byte tempInCelsius;
byte weatherIcon;
boolean useFahrenheit;
public CommandTimeAndWeather(long timeMilliseconds, boolean use12HourFormat, WeatherSpec weatherInfo, boolean useFahrenheit) {
public CommandSetTimeAndWeather(long timeMilliseconds, boolean use12HourFormat, WeatherSpec weatherInfo, boolean useFahrenheit) {
super(true, null);
this.timeMilliseconds = timeMilliseconds;
this.use12HourFormat = use12HourFormat;
@@ -194,15 +213,20 @@ public class G1Communications {
}
this.useFahrenheit = useFahrenheit;
}
public CommandSetTimeAndWeather(long timeMilliseconds, boolean use12HourFormat, boolean useFahrenheit) {
this(timeMilliseconds, use12HourFormat, null, useFahrenheit);
}
@Override
public boolean needsGlobalSequence() { return true; }
@Override
public byte[] serialize() {
byte[] packet = new byte[] {
G1Constants.CommandId.DASHBOARD_CONFIG.id,
G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id,
// Sequence place holder
0x00,
0x00,
sequence,
// Magic number?
0x01,
// Time 32bit place holders
@@ -227,7 +251,6 @@ public class G1Communications {
// 24H/12H
(byte)(use12HourFormat ? 0x01 : 0x00)
};
BLETypeConversions.writeUint16BE(packet, 2, sequence);
BLETypeConversions.writeUint32(packet, 5, (int)(timeMilliseconds / 1000));
BLETypeConversions.writeUint64(packet, 9, timeMilliseconds);
@@ -241,16 +264,300 @@ public class G1Communications {
}
// Command should match and the sequence should match.
// TODO actually check the sequence (need to confirm the offset).
return payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id &&
payload[1] == G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id;
// payload[2] == (byte)(sequence >> 8) &&
// payload[3] == (byte)sequence;
payload[1] == G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id &&
payload[3] == sequence;
}
@Override
public String getName() {
return "time_and_weather";
return "set_time_and_weather";
}
}
public static class CommandGetSilentModeSettings extends CommandHandler {
public CommandGetSilentModeSettings(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.GET_SILENT_MODE_SETTINGS.id };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 4 && payload[0] == G1Constants.CommandId.GET_SILENT_MODE_SETTINGS.id;
}
@Override
public String getName() {
return "get_silent_status";
}
public static boolean isEnabled(byte[] payload) {
return payload[2] == G1Constants.SilentStatus.ENABLE;
}
}
public static class CommandSetSilentModeSettings extends CommandHandler {
private final boolean enable;
public CommandSetSilentModeSettings(boolean enable) {
super(true, null);
this.enable = enable;
}
@Override
public byte[] serialize() {
return new byte[] {
G1Constants.CommandId.SET_SILENT_MODE_SETTINGS.id,
(byte)(enable ? G1Constants.SilentStatus.ENABLE : G1Constants.SilentStatus.DISABLE),
};
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length > 1 && payload[0] == G1Constants.CommandId.SET_SILENT_MODE_SETTINGS.id;
}
@Override
public String getName() {
return "set_silent_mode_settings_" + (enable ? "enabled" : "disabled");
}
}
public static class CommandGetDisplaySettings extends CommandHandler {
public CommandGetDisplaySettings(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.GET_DISPLAY_SETTINGS.id };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 4 && payload[0] == G1Constants.CommandId.GET_DISPLAY_SETTINGS.id;
}
@Override
public String getName() {
return "get_display_settings";
}
public static byte getHeight(byte[] payload) {
return payload[2];
}
public static byte getDepth(byte[] payload) {
return payload[3];
}
}
public static class CommandSetDisplaySettings extends CommandHandler {
private final boolean preview;
private final byte height;
private final byte depth;
public CommandSetDisplaySettings(boolean preview, byte height, byte depth) {
super(true, null);
this.preview = preview;
this.height = height;
this.depth = depth;
}
@Override
public boolean needsGlobalSequence() { return true; }
@Override
public byte[] serialize() {
return new byte[] {
G1Constants.CommandId.SET_DISPLAY_SETTINGS.id,
0x08, // Subcommand?
0x00,
sequence,
0x02, // Seems to be a magic number?
preview ? 0x01 : (byte)0x00,
height,
depth
};
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 6 &&
payload[0] == G1Constants.CommandId.SET_DISPLAY_SETTINGS.id &&
payload[1] == 0x06 && // Magic Number
payload[3] == sequence;
}
@Override
public String getName() {
return "set_display_settings_" + height + "_" + depth;
}
}
public static class CommandGetHeadGestureSettings extends CommandHandler {
public CommandGetHeadGestureSettings(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.GET_HEAD_GESTURE_SETTINGS.id };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 4 && payload[0] == G1Constants.CommandId.GET_HEAD_GESTURE_SETTINGS.id;
}
@Override
public String getName() {
return "get_head_gesture_settings";
}
public static byte getActivationAngle(byte[] payload) {
return payload[2];
}
}
public static class CommandSetHeadGestureSettings extends CommandHandler {
private final byte angle;
// Allowed Angles are 0-60.
public CommandSetHeadGestureSettings(byte angle) {
super(true, null);
this.angle = angle;
}
@Override
public byte[] serialize() {
return new byte[] {
G1Constants.CommandId.SET_HEAD_GESTURE_SETTINGS.id,
angle,
// Magic number, other project called it the "level setting".
// Maybe try sending 0x00 and see what happens?
0x01
};
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 1 && payload[0] == G1Constants.CommandId.SET_HEAD_GESTURE_SETTINGS.id;
}
@Override
public String getName() {
return "set_head_gesture_settings_" + angle;
}
}
public static class CommandGetBrightnessSettings extends CommandHandler {
public CommandGetBrightnessSettings(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.GET_BRIGHTNESS_SETTINGS.id };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 3 && payload[0] == G1Constants.CommandId.GET_BRIGHTNESS_SETTINGS.id;
}
@Override
public String getName() {
return "get_brightness_settings";
}
public static byte getBrightnessLevel(byte[] payload) {
return payload[2];
}
public static boolean isAutoBrightnessEnabled(byte[] payload) {
return payload[3] == 0x01;
}
}
public static class CommandSetBrightnessSettings extends CommandHandler {
private final boolean enableAutoBrightness;
private final byte brightnessLevel;
public CommandSetBrightnessSettings(boolean enableAutoBrightness, byte brightnessLevel) {
super(true, null);
this.enableAutoBrightness = enableAutoBrightness;
this.brightnessLevel = brightnessLevel;
}
@Override
public byte[] serialize() {
return new byte[] {
G1Constants.CommandId.SET_BRIGHTNESS_SETTINGS.id,
brightnessLevel,
enableAutoBrightness ? 0x01 : (byte)0x00
};
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length > 1 && payload[0] == G1Constants.CommandId.SET_BRIGHTNESS_SETTINGS.id;
}
@Override
public String getName() {
return "set_brightness_settings_" + enableAutoBrightness + "_" + brightnessLevel;
}
}
public static class CommandGetWearDetectionSettings extends CommandHandler {
public CommandGetWearDetectionSettings(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.GET_WEAR_DETECTION_SETTINGS.id };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 2 && payload[0] == G1Constants.CommandId.GET_WEAR_DETECTION_SETTINGS.id;
}
@Override
public String getName() {
return "get_wear_detection_settings";
}
public static boolean isEnabled(byte[] payload) {
return payload[2] == 0x01;
}
}
public static class CommandSetWearDetectionSettings extends CommandHandler {
private final boolean enable;
public CommandSetWearDetectionSettings(boolean enable) {
super(true, null);
this.enable = enable;
}
@Override
public byte[] serialize() {
return new byte[] {
G1Constants.CommandId.SET_WEAR_DETECTION_SETTINGS.id,
enable ? 0x01 : (byte)0x00
};
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 2 && payload[0] == G1Constants.CommandId.SET_WEAR_DETECTION_SETTINGS.id;
}
@Override
public String getName() {
return "set_wear_detection_settings_" + (enable ? "enabled" : "disabled");
}
}
}
@@ -12,7 +12,9 @@ public class G1Constants {
public static final int MTU = 251;
public static final int HEART_BEAT_DELAY_MS = 28000;
public static final int DEFAULT_COMMAND_TIMEOUT_MS = 5000;
public static final int DISPLAY_SETTINGS_PREVIEW_DELAY = 3000;
public static final int DEFAULT_RETRY_COUNT = 5;
public static final String INTENT_TOGGLE_SILENT_MODE = "nodomain.freeyourgadget.gadgetbridge.evenrealities.silent_mode";
// Extract the L or R at the end of the device prefix.
public static Side getSideFromFullName(String deviceName) {
@@ -71,12 +73,15 @@ public class G1Constants {
}
}
public static class CommandStatus {
public static final byte FAILED = (byte)0xCA;
public static final byte DATA_CONTINUES = (byte)0xCA;
public static final byte SUCCESS = (byte)0xC9;
}
// TODO: Lifted these from a different project, some of them are wrong.
public enum CommandId {
SILENT_MODE((byte) 0x03),
NOTIFICATION_CONFIG((byte) 0x04),
DASHBOARD_CONFIG((byte) 0x06),
DASHBOARD((byte) 0x22),
FW_INFO_REQUEST((byte) 0x23),
@@ -84,7 +89,18 @@ public class G1Constants {
BATTERY_LEVEL((byte) 0x2C),
INIT((byte) 0x4D),
NOTIFICATION((byte) 0x4B),
FW_INFO_RESPONSE((byte) 0x6E);
FW_INFO_RESPONSE((byte) 0x6E),
DEVICE_ACTION((byte) 0xF5),
GET_SILENT_MODE_SETTINGS((byte) 0x2B), // There is more info in this one
SET_SILENT_MODE_SETTINGS((byte) 0x03),
GET_DISPLAY_SETTINGS((byte) 0x3B),
SET_DISPLAY_SETTINGS((byte) 0x26),
GET_HEAD_GESTURE_SETTINGS((byte) 0x32),
SET_HEAD_GESTURE_SETTINGS((byte) 0x0B),
GET_BRIGHTNESS_SETTINGS((byte) 0x29),
SET_BRIGHTNESS_SETTINGS((byte) 0x01),
GET_WEAR_DETECTION_SETTINGS((byte) 0x3A),
SET_WEAR_DETECTION_SETTINGS((byte) 0x27);
final public byte id;
@@ -96,7 +112,9 @@ public class G1Constants {
public enum DashboardConfigSubCommand {
SET_MODE((byte) 0x07),
UNKNOWN_1((byte) 0x0C),
SET_TIME_AND_WEATHER((byte) 0x15);
SET_TIME_AND_WEATHER((byte) 0x15),
// Not sure why they use this one sometimes.
SET_TIME_AND_WEATHER_ALSO((byte) 0x16);
final public byte id;
@@ -104,4 +122,9 @@ public class G1Constants {
this.id = id;
}
}
public static class SilentStatus {
public static final byte ENABLE = 0x0C;
public static final byte DISABLE = 0x0A;
}
}
@@ -3,13 +3,18 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.icu.util.Calendar;
import android.icu.util.TimeZone;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,10 +48,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class);
private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
private G1SideManager leftSide;
private G1SideManager rightSide;
private BroadcastReceiver intentReceiver = null;
private final Object sendTimeLock = new Object();
private G1SideManager leftSide = null;
private G1SideManager rightSide = null;
public G1DeviceSupport() {
this(LOG);
}
@@ -97,6 +102,16 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// device before the devices are linked.
super.setContext(device, btAdapter, context);
}
// Register to receive silent mode intent calls from the UI.
if (intentReceiver == null) {
intentReceiver = new IntentReceiver();
ContextCompat.registerReceiver(
context,
intentReceiver,
new IntentFilter(G1Constants.INTENT_TOGGLE_SILENT_MODE),
ContextCompat.RECEIVER_NOT_EXPORTED);
}
}
@Override
@@ -142,17 +157,21 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
builder.setCallback(this);
builder.notify(rx, true);
// If the manager is initializing, then the whole device is initializing as well.
// If the side is in the connected state, it is ready to be initialized.
// IMPORTANT: use getDevice(deviceIdx), not getDevice(/* 0 */) here otherwise the device
// will lock up in a half initialized state because GB thinks the left side is initialized,
// after because the right ran first.
if (side.getState() == GBDevice.State.CONNECTED) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING,
builder.add(new SetDeviceStateAction(getDevice(deviceIdx), GBDevice.State.INITIALIZING,
getContext()));
side.initialize();
side.initialize(builder);
}
// Both sides are initialized. The whole device is initialized.
synchronized (this) {
if (leftSide != null && leftSide.getState() == GBDevice.State.INITIALIZED &&
rightSide != null && rightSide.getState() == GBDevice.State.INITIALIZED) {
// Both sides are initialized. The whole device is initialized, don't use a device
// index here. Device 0 is the device that the reset of GB sees.
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED,
getContext()));
// This means that both sides have been connected to and basic info has been collected.
@@ -160,8 +179,8 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// individual initialization. We don't know what thread we are handling the update state
// event on, so to be safe, schedule these as a background task.
backgroundTasksHandler.postDelayed(() -> {
leftSide.postInitialize();
rightSide.postInitialize();
leftSide.postInitializeLeft();
rightSide.postInitializeRight();
onSetTime();
}, 200);
}
@@ -180,6 +199,11 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
leftSide = null;
rightSide = null;
// Stop listening for intent actions
if (intentReceiver != null) {
getContext().unregisterReceiver(intentReceiver);
}
super.dispose();
}
@@ -211,8 +235,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
return leftSide;
} else if (deviceIdx == G1Constants.Side.RIGHT.getDeviceIndex()) {
rightSide = new G1SideManager(G1Constants.Side.RIGHT, backgroundTasksHandler,
getQueue, handleEvent, this::getDevicePrefs,
rx, tx);
getQueue, handleEvent, this::getDevicePrefs, rx, tx);
return rightSide;
}
@@ -229,6 +252,20 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
return null;
}
private class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action != null && action.equals(G1Constants.INTENT_TOGGLE_SILENT_MODE)) {
GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null && device.equals(getDevice())) {
// We don't know what thread is handling this event, schedule the BLE to run in
// the background.
backgroundTasksHandler.post(G1DeviceSupport.this::onToggleSilentMode);
}
}
}
}
////////////////////////////////////////////////////////////////////////
// Below are all the onXXX() handlers overridden from the base class. //
@@ -277,9 +314,17 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
*/
@Override
public void onSendConfiguration(String config) {
// Forward to both sides.
if (leftSide != null) leftSide.onSendConfiguration(config);
if (rightSide != null) rightSide.onSendConfiguration(config);
switch (config) {
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE:
// This setting is only sent to the right arm.
if (rightSide != null) rightSide.onSendConfiguration(config);
break;
default:
// Forward to both sides.
if (leftSide != null) leftSide.onSendConfiguration(config);
if (rightSide != null) rightSide.onSendConfiguration(config);
break;
}
}
@Override
@@ -289,31 +334,48 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
boolean use12HourFormat = getDevicePrefs().getTimeFormat().equals(
DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
long timeMilliseconds = c.getTimeInMillis();
long tzOffset = TimeZone.getDefault().getOffset(timeMilliseconds);
timeMilliseconds += tzOffset;
long currentMilliseconds = c.getTimeInMillis();
long tzOffset = TimeZone.getDefault().getOffset(currentMilliseconds);
long timeMilliseconds = currentMilliseconds + tzOffset;
// Check if the GB settings are set to metric, if not, set the temp to use Fahrenheit.
String metricString = GBApplication.getContext().getString(R.string.p_unit_metric);
boolean useFahrenheit = !GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, metricString).equals(metricString);
// This block is synchronized. We do not want two calls to overlap, otherwise the lenses
// could get skewed with different values.
synchronized (sendTimeLock) {
// Send the left the time synchronously, then once a response is received, send the right.
// The glasses will ignore the command on the right lens if it arrives before the left.
G1Communications.CommandHandler leftCommandHandler =
new G1Communications.CommandTimeAndWeather(
timeMilliseconds, use12HourFormat, /*weatherInfo=*/ null, useFahrenheit);
leftSide.send(leftCommandHandler);
if (!leftCommandHandler.waitForResponsePayload()) {
LOG.error("Set time on left lens timed out");
getDevice().setState(GBDevice.State.WAITING_FOR_RECONNECT);
getDevice().sendDeviceUpdateIntent(getContext());
}
// Run in the background in case the command hangs and this was run from the UI thread.
backgroundTasksHandler.post(() -> {
// This block is synchronized. We do not want two calls to overlap, otherwise the lenses
// could get skewed with different values.
synchronized (sendTimeLock) {
// Send the left the time synchronously, then once a response is received, send the right.
// The glasses will ignore the command on the right lens if it arrives before the left.
G1Communications.CommandHandler leftCommandHandler =
new G1Communications.CommandSetTimeAndWeather(timeMilliseconds,
use12HourFormat, useFahrenheit);
leftSide.send(leftCommandHandler);
if (!leftCommandHandler.waitForResponsePayload()) {
LOG.error("Set time on left lens timed out");
getDevice().setState(GBDevice.State.WAITING_FOR_RECONNECT);
getDevice().sendDeviceUpdateIntent(getContext());
}
rightSide.send(new G1Communications.CommandTimeAndWeather(
timeMilliseconds, use12HourFormat, /*weatherInfo=*/ null, useFahrenheit));
rightSide.send(new G1Communications.CommandSetTimeAndWeather(timeMilliseconds,
use12HourFormat,
useFahrenheit));
}
});
}
private void onToggleSilentMode() {
if (leftSide == null || rightSide == null) return;
// If both lenses are in sync on what the status is, set them both. Otherwise, only set the
// right one so they can be resynchronized.
if(leftSide.getSilentModeStatus() == rightSide.getSilentModeStatus()){
leftSide.onToggleSilentMode();
rightSide.onToggleSilentMode();
} else {
rightSide.onToggleSilentMode();
}
}
}
@@ -1,6 +1,8 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Handler;
import org.slf4j.Logger;
@@ -42,9 +44,10 @@ public class G1SideManager {
private final BluetoothGattCharacteristic tx;
private final Runnable batteryRunner;
private final Runnable heartBeatRunner;
private final Runnable displaySettingsPreviewCloserRunner;
private final Set<G1Communications.CommandHandler> commandHandlers;
private short heartBeatSequence;
private short globalSequence;
private byte globalSequence;
private boolean isSilentModeEnabled;
private GBDevice.State state;
public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler,
@@ -59,19 +62,29 @@ public class G1SideManager {
this.rx = rx;
this.tx = tx;
this.batteryRunner = () -> {
send(new G1Communications.CommandBatteryLevel(this::handleBatteryPayload));
send(new G1Communications.CommandGetBatteryInfo(this::handleBatteryPayload));
scheduleBatteryPolling();
};
this.heartBeatRunner = () -> {
send(new G1Communications.CommandHeartBeat(heartBeatSequence++));
// We can send any command as a heart beat. The official app uses this one.
send(new G1Communications.CommandGetSilentModeSettings(null));
scheduleHeatBeat();
};
this.displaySettingsPreviewCloserRunner = () -> {
DevicePrefs prefs = getDevicePrefs();
send(new G1Communications.CommandSetDisplaySettings(
false /* preview */,
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT, 0),
// Depth ranges from 1-9 instead of 0-8, so offset by one to convert from
// the slider space.
(byte)(prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH, 0) + 1)));
};
this.commandHandlers = new HashSet<>();
// Non Finals
this.heartBeatSequence = 0;
this.globalSequence = 0;
this.isSilentModeEnabled = false;
this.state = GBDevice.State.CONNECTED;
}
@@ -99,7 +112,7 @@ public class G1SideManager {
return state;
}
public void initialize() {
public void initialize(TransactionBuilder transaction) {
// The glasses will auto disconnect after 30 seconds of no data on the wire.
// Schedule a heartbeat task. If this is not enabled, the glasses will disconnect and be
// useless to the user.
@@ -111,21 +124,77 @@ public class G1SideManager {
state = GBDevice.State.INITIALIZED;
}
public void postInitialize() {
send(new G1Communications.CommandBatteryLevel(this::handleBatteryPayload));
send(new G1Communications.CommandFirmwareInfo(this::handleFirmwareInfoPayload));
public byte getSilentModeStatus() {
return isSilentModeEnabled ? G1Constants.SilentStatus.ENABLE : G1Constants.SilentStatus.DISABLE;
}
private void postInitializeCommon(TransactionBuilder transaction) {
sendInTransaction(transaction, new G1Communications.CommandGetBatteryInfo(this::handleBatteryPayload));
sendInTransaction(transaction, new G1Communications.CommandGetFirmwareInfo(this::handleFirmwareInfoPayload));
sendInTransaction(transaction, new G1Communications.CommandGetSilentModeSettings(this::handleSilentStatusPayload));
}
public void postInitializeLeft() {
TransactionBuilder transaction =
new TransactionBuilder("post_initialize_left_" + mySide.getDeviceIndex());
postInitializeCommon(transaction);
// These can be sent to both, but the left lens is used as the master for these settings.
sendInTransaction(transaction, new G1Communications.CommandGetDisplaySettings(this::handleDisplaySettingsPayload));
sendInTransaction(transaction, new G1Communications.CommandGetBrightnessSettings(this::handleBrightnessSettingsPayload));
transaction.queue(getQueue());
}
public void postInitializeRight() {
TransactionBuilder transaction =
new TransactionBuilder( "post_initialize_right_" + mySide.getDeviceIndex());
postInitializeCommon(transaction);
// This settings are only sent to the right lens in the official app, so we copy that.
sendInTransaction(transaction, new G1Communications.CommandGetHeadGestureSettings(this::handleHeadGestureSettingsPayload));
// This setting uses the right lens as the master for the setting simply to balance the amount
// of commands being sent to the left vs right.
sendInTransaction(transaction, new G1Communications.CommandGetWearDetectionSettings(this::handleWearDetectionSettingsPayload));
transaction.queue(getQueue());
}
public void onSendConfiguration(String config) {
DevicePrefs prefs = getDevicePrefs();
switch (config) {
// Reschedule battery polling. The new schedule may be disabled.
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL:
scheduleBatteryPolling();
break;
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT:
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH:
sendDisplaySettings(prefs);
break;
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE:
send(new G1Communications.CommandSetHeadGestureSettings(
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE, 40)));
break;
case DeviceSettingsPreferenceConst.PREF_SCREEN_AUTO_BRIGHTNESS:
case DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS:
send(new G1Communications.CommandSetBrightnessSettings(
prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SCREEN_AUTO_BRIGHTNESS, true),
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS, 0x2A)));// TODO: Add a constant for the max value?
break;
case DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE:
send(new G1Communications.CommandSetWearDetectionSettings(
prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE, true)));
break;
}
}
public void onToggleSilentMode() {
isSilentModeEnabled = !isSilentModeEnabled;
G1Communications.CommandHandler commandHandler =
new G1Communications.CommandSetSilentModeSettings(isSilentModeEnabled);
send(commandHandler);
}
private void scheduleHeatBeat() {
backgroundTasksHandler.removeCallbacksAndMessages(heartBeatRunner);
LOG.debug("Starting heartbeat runner delayed by {}ms", G1Constants.HEART_BEAT_DELAY_MS);
@@ -134,30 +203,56 @@ public class G1SideManager {
private void scheduleBatteryPolling() {
backgroundTasksHandler.removeCallbacksAndMessages(batteryRunner);
DevicePrefs prefs = getDevicePrefs();
if (prefs.getBatteryPollingEnabled()) {
int interval_minutes = prefs.getBatteryPollingIntervalMinutes();
int interval = interval_minutes * 60 * 1000;
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval,
interval_minutes);
backgroundTasksHandler.postDelayed(batteryRunner, interval);
DevicePrefs prefs = getDevicePrefs();
if (prefs.getBatteryPollingEnabled()) {
int interval_minutes = prefs.getBatteryPollingIntervalMinutes();
int interval = interval_minutes * 60 * 1000;
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval,
interval_minutes);
backgroundTasksHandler.postDelayed(batteryRunner, interval);
}
}
private synchronized short getNextSequence() {
private synchronized byte getNextSequence() {
// Synchronized so the sequence increments atomically.
// This number will eventually overflow, and that is fine. The sequence number is just to
// match the request and response together.
return globalSequence++;
}
public void send(G1Communications.CommandHandler command) {
TransactionBuilder builder =
new TransactionBuilder(command.getName() + "_" + mySide.getDeviceIndex());
sendInTransaction(builder, command);
builder.queue(getQueue());
private synchronized void sendDisplaySettings(DevicePrefs prefs) {
// Synchronized so that there can only ever be one background task.
// Clear any existing runner in case the user has changed the value multiple times
// before th delay expired.
backgroundTasksHandler.removeCallbacksAndMessages(displaySettingsPreviewCloserRunner);
// The glasses expect the setting to
send(new G1Communications.CommandSetDisplaySettings(
true /* preview */,
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT, 0),
// Depth ranges from 1-9 instead of 0-8, so offset by one to convert from
// the slider space.
(byte)(prefs.getInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH, 0) + 1)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// On newer APIs, use the runner as the token.
backgroundTasksHandler.postDelayed(displaySettingsPreviewCloserRunner,
displaySettingsPreviewCloserRunner,
G1Constants.DISPLAY_SETTINGS_PREVIEW_DELAY);
} else {
backgroundTasksHandler.postDelayed(displaySettingsPreviewCloserRunner,
G1Constants.DISPLAY_SETTINGS_PREVIEW_DELAY);
}
}
private void sendInTransaction(TransactionBuilder builder, G1Communications.CommandHandler command) {
public void send(G1Communications.CommandHandler command) {
TransactionBuilder transaction =
new TransactionBuilder(command.getName() + "_" + mySide.getDeviceIndex());
sendInTransaction(transaction, command);
transaction.queue(getQueue());
}
private void sendInTransaction(TransactionBuilder transaction, G1Communications.CommandHandler command) {
// Calling getNextSequence() will advance the global sequence, if the command doesn't need
// a sequence number, don't call it so we don't waste a sequence number.
if (command.needsGlobalSequence()) {
@@ -167,7 +262,7 @@ public class G1SideManager {
LOG.debug("Send command {} on side {}", command.getName(), mySide.getDeviceIndex());
// Write the packet to the BLE txn.
builder.write(tx, command.serialize());
transaction.write(tx, command.serialize());
// If this command expects a response, register the handler.
if (command.expectResponse()) {
@@ -206,8 +301,10 @@ public class G1SideManager {
public boolean handlePayload(byte[] payload) {
for (G1Communications.CommandHandler commandHandler : commandHandlers) {
LOG.debug("Got response payload for command {} on side {}: {}", commandHandler.getName(), mySide.getDeviceIndex(), Logging.formatBytes(payload));
if (commandHandler.responseMatches(payload)) {
LOG.debug("Got response payload for command {} on side {}: {}",
commandHandler.getName(), mySide.getDeviceIndex(),
Logging.formatBytes(payload));
synchronized (commandHandlers) {
commandHandlers.remove(commandHandler);
commandHandler.setResponsePayload(payload);
@@ -217,7 +314,19 @@ public class G1SideManager {
return callback != null && callback.apply(payload);
}
}
LOG.debug("Unhandled payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload));
// These can come in unprompted from the glasses, call the correct handler based on what the
// command is.
if (payload.length > 1) {
if (payload[0] == G1Constants.CommandId.DEVICE_ACTION.id) {
return handleDeviceActionPayload(payload);
} else if (payload[0] == G1Constants.CommandId.DASHBOARD.id) {
return handleDashboardPayload(payload);
}
}
LOG.debug("Unhandled payload on side {}: {}",
mySide.getDeviceIndex(), Logging.formatBytes(payload));
// Not handled by any handlers.
return false;
@@ -254,4 +363,55 @@ public class G1SideManager {
}
return false;
}
private boolean handleSilentStatusPayload(byte[] payload) {
isSilentModeEnabled = G1Communications.CommandGetSilentModeSettings.isEnabled(payload);
return true;
}
private boolean handleDisplaySettingsPayload(byte[] payload) {
SharedPreferences.Editor editor = getDevicePrefs().getPreferences().edit();
editor.putInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_HEIGHT,
G1Communications.CommandGetDisplaySettings.getHeight(payload));
// Depth is indexed is 1-9, so subtract 1 to map it to the 0-8 of the slider.
editor.putInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH,
G1Communications.CommandGetDisplaySettings.getDepth(payload) - 1);
editor.apply();
return true;
}
private boolean handleHeadGestureSettingsPayload(byte[] payload) {
SharedPreferences.Editor editor = getDevicePrefs().getPreferences().edit();
editor.putInt(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE,
G1Communications.CommandGetHeadGestureSettings.getActivationAngle(payload));
editor.apply();
return true;
}
private boolean handleBrightnessSettingsPayload(byte[] payload) {
SharedPreferences.Editor editor = getDevicePrefs().getPreferences().edit();
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SCREEN_AUTO_BRIGHTNESS,
G1Communications.CommandGetBrightnessSettings.isAutoBrightnessEnabled(payload));
editor.putInt(DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS,
G1Communications.CommandGetBrightnessSettings.getBrightnessLevel(payload));
editor.apply();
return true;
}
private boolean handleWearDetectionSettingsPayload(byte[] payload) {
SharedPreferences.Editor editor = getDevicePrefs().getPreferences().edit();
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE,
G1Communications.CommandGetWearDetectionSettings.isEnabled(payload));
editor.apply();
return true;
}
private boolean handleDeviceActionPayload(byte[] payload) {
LOG.debug("Device Action payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload));
return true;
}
private boolean handleDashboardPayload(byte[] payload) {
LOG.debug("Dashboard payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload));
return true;
}
}
+28
View File
@@ -0,0 +1,28 @@
<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:pathData="M2,21h20"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M2,21 L16,5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M7.5,15 A5,5 0 0,1 10.5,21"
android:strokeWidth="1.5"
android:strokeColor="#FFFFFFFF"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"/>
</vector>
@@ -0,0 +1,46 @@
<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:pathData="M2,21h20"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M2,21 L14,7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M7.5,15 A5,5 0 0,1 10.5,21"
android:strokeWidth="1.5"
android:strokeColor="#FFFFFFFF"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"/>
<path
android:pathData="M7.5,5 A5,5 0 0,1 17,15"
android:strokeWidth="2"
android:strokeColor="#FFFFFFFF"
android:fillColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round"/>
<path
android:pathData="M8,2 L7.5,5 L10,6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,46 @@
<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:pathData="M10.8,2.3 L12,1 L13.2,2.3"
android:strokeLineJoin="round"
android:strokeWidth="1"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round" />
<path
android:pathData="M11.7,1.4 L12.3,1.4 L12.8,7 L11.2,7 Z"
android:fillColor="#FFFFFFFF" />
<path
android:pathData="M12,7v0"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.6"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M12,10v0"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="2.1"
android:strokeLineCap="round"
android:fillColor="#00000000" />
<path
android:pathData="M10.95,10 L13.05,10 L14,21 L10,21 Z"
android:fillColor="#FFFFFFFF" />
<path
android:pathData="M7,16 L12,21 L17,16"
android:strokeLineJoin="round"
android:strokeWidth="3"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,36 @@
<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:pathData="M12,14v7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M12,3v7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M9,18l3,3l3,-3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M9,6l3,-3l3,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"/>
</vector>
+16
View File
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#7E7E7E"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"
/>
<path
android:fillColor="@android:color/white"
android:fillType="evenOdd"
android:pathData="M6,11h12v2h-12z" />
</vector>
@@ -0,0 +1,31 @@
<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="M16 20c-0.29 0-0.56-0.06-0.76-0.15-0.71-0.37-1.21-0.88-1.71-2.38-0.51-1.56-1.47-2.29-2.39-3-0.79-0.61-1.61-1.24-2.32-2.53C8.29 10.98 8 9.93 8 9c0-2.8 2.2-5 5-5s5 2.2 5 5h2c0-3.93-3.07-7-7-7S6 5.07 6 9c0 1.26 0.38 2.65 1.07 3.9 0.91 1.65 1.98 2.48 2.85 3.15 0.81 0.62 1.39 1.07 1.71 2.05 0.6 1.82 1.37 2.84 2.73 3.55C14.87 21.88 15.43 22 16 22c2.21 0 4-1.79 4-4h-2c0 1.1-0.9 2-2 2z"/>
<path
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#FFFFFFFF"
android:strokeLineCap="round"
android:pathData="M5,6h2 M5,6 l -3,4"/>
<path
android:strokeWidth="2"
android:strokeColor="#FFFFFFFF"
android:pathData="M18,6h5 "/>
<!-- Need to make this look nicer -->
<!-- <path-->
<!-- android:strokeLineJoin="round"-->
<!-- android:strokeWidth="3"-->
<!-- android:strokeColor="#FFFFFFFF"-->
<!-- android:strokeLineCap="round"-->
<!-- android:pathData="M13,9 l 2,2 M15,11 l 1,2"/>-->
</vector>
+9
View File
@@ -1124,6 +1124,7 @@
<string name="silent_mode_normal_vibrate">Normal / Vibrate</string>
<string name="silent_mode_normal_silent">Normal / Silent</string>
<string name="silent_mode_vibrate_silent">Vibrate / Silent</string>
<string name="silent_mode">Silent Mode</string>
<string name="prefs_always_on_display">Always On Display</string>
<string name="prefs_always_on_display_follow_watchface">Style follows Watchface</string>
<string name="prefs_always_on_display_style">Style</string>
@@ -3959,4 +3960,12 @@
<string name="pref_huawei_activity_reminder_goal_reached_title">Goal Reached</string>
<string name="pref_huawei_activity_reminder_goal_reached_summary">Get a reminder when you complete rings</string>
<string name="body_energy_legend_average">30-Day Average</string>
<string name="pref_even_realities_g1_screen_location_title">Screen Location</string>
<string name="pref_even_realities_g1_screen_location_summary">Changes the vertical location of the display</string>
<string name="pref_even_realities_g1_screen_depth_title">Screen Depth</string>
<string name="pref_even_realities_g1_screen_depth_summary">Changes the binocular separation and apparent depth of the display</string>
<string name="pref_even_realities_g1_screen_activation_angle_title">Activation Angle</string>
<string name="pref_even_realities_g1_screen_activation_angle_summary">Changes how high the wearer needs to lift their head for the dashboard to be shown</string>
<string name="pref_even_realities_g1_screen_calibrate_angle_title">Calibrate Angle</string>
<string name="pref_even_realities_g1_screen_calibrate_angle_summary">Zeroes the angle of the glasses, look straight ahead and tap here</string>
</resources>
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<!-- <PreferenceCategory android:title="@string/pref_header_display">-->
<SeekBarPreference
android:key="pref_even_realities_g1_screen_height"
android:defaultValue="0"
android:max="8"
android:icon="@drawable/ic_arrows_up_down"
android:title="@string/pref_even_realities_g1_screen_location_title"
android:summary="@string/pref_even_realities_g1_screen_location_summary" />
<SeekBarPreference
android:key="pref_even_realities_g1_screen_depth"
android:defaultValue="0"
android:icon="@drawable/ic_arrow_depth"
android:max="8"
android:title="@string/pref_even_realities_g1_screen_depth_title"
android:summary="@string/pref_even_realities_g1_screen_depth_summary" />
<SeekBarPreference
android:key="pref_even_realities_g1_screen_activation_angle"
android:defaultValue="40"
android:icon="@drawable/ic_angle"
android:max="60"
android:title="@string/pref_even_realities_g1_screen_activation_angle_title"
android:summary="@string/pref_even_realities_g1_screen_activation_angle_summary" />
<Preference
android:defaultValue="0"
android:icon="@drawable/ic_angle_calibrate"
android:enabled="false"
android:key="pref_even_realities_g1_screen_calibrate_angle"
android:summary="@string/pref_even_realities_g1_screen_calibrate_angle_summary"
android:title="@string/pref_even_realities_g1_screen_calibrate_angle_title" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:disableDependentsState="true"
android:icon="@drawable/ic_wb_sunny"
android:key="screen_auto_brightness"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_screen_auto_brightness_summary"
android:title="@string/pref_screen_auto_brightness_title" />
<SeekBarPreference
android:key="screen_brightness"
android:dependency="screen_auto_brightness"
android:defaultValue="42"
android:max="42"
android:icon="@drawable/ic_wb_sunny"
android:title="@string/pref_screen_brightness" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:icon="@drawable/ic_ear_glasses"
android:key="wear_sensor_toggle"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_wear_sensor_summary"
android:title="@string/pref_wear_sensor_title" />
<!-- </PreferenceCategory>-->
</PreferenceScreen>