Even Realities G1: Support more things

Add support for:
 - Charging status
 - Battery level of the case
 - Fetching Serial Number
 - Parsing hardware information from serial number
 - Add Hard Reset button
 - Toggle for device level debug logging
 - Bug Fix: kill heartbeat worker on disconnect
 - Weather
 - Toggle for 12H/24H time
 - Set the dashboard to minimal on connection
This commit is contained in:
jrthomas270
2025-05-29 16:21:57 -07:00
committed by José Rebelo
parent 3320203c23
commit 85f01ee6ca
17 changed files with 962 additions and 129 deletions

View File

@@ -324,6 +324,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_APP_LOGS_START = "pref_app_logs_start"; public static final String PREF_APP_LOGS_START = "pref_app_logs_start";
public static final String PREF_APP_LOGS_STOP = "pref_app_logs_stop"; public static final String PREF_APP_LOGS_STOP = "pref_app_logs_stop";
public static final String PREF_DEVICE_LOGS_TOGGLE = "device_logs_enabled";
public static final String MORNING_UPDATES_ENABLED = "morning_updates_enabled"; public static final String MORNING_UPDATES_ENABLED = "morning_updates_enabled";
public static final String MORNING_UPDATES_CATEGORIES_SORTABLE = "morning_updates_categories"; public static final String MORNING_UPDATES_CATEGORIES_SORTABLE = "morning_updates_categories";

View File

@@ -830,6 +830,8 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_DUAL_DEVICE_SUPPORT); addPreferenceHandlerFor(PREF_DUAL_DEVICE_SUPPORT);
addPreferenceHandlerFor(PREF_DEVICE_LOGS_TOGGLE);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL); addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_NOTIFICATION); addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_NOTIFICATION);
addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_SECONDARY); addPreferenceHandlerFor(PREF_USER_FITNESS_GOAL_SECONDARY);

View File

@@ -370,6 +370,7 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
int batteryIcon = device.getBatteryIcon(batteryIndex); int batteryIcon = device.getBatteryIcon(batteryIndex);
int batteryLabel = device.getBatteryLabel(batteryIndex); //unused for now int batteryLabel = device.getBatteryLabel(batteryIndex); //unused for now
batteryIcons[batteryIndex].setImageResource(R.drawable.level_list_battery); batteryIcons[batteryIndex].setImageResource(R.drawable.level_list_battery);
batteryStatusLabels[batteryIndex].setAlpha(1.0f);
if (batteryIcon != GBDevice.BATTERY_ICON_DEFAULT){ if (batteryIcon != GBDevice.BATTERY_ICON_DEFAULT){
batteryIcons[batteryIndex].setImageResource(batteryIcon); batteryIcons[batteryIndex].setImageResource(batteryIcon);
@@ -381,7 +382,16 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
BatteryState.BATTERY_CHARGING_FULL.equals(batteryState)) { BatteryState.BATTERY_CHARGING_FULL.equals(batteryState)) {
batteryIcons[batteryIndex].setImageLevel(device.getBatteryLevel(batteryIndex) + 100); batteryIcons[batteryIndex].setImageLevel(device.getBatteryLevel(batteryIndex) + 100);
} else { } else {
batteryIcons[batteryIndex].setImageLevel(device.getBatteryLevel(batteryIndex)); if (BatteryState.NO_BATTERY.equals(batteryState)) {
// There is a level to show, but the device has indicated that it does not
// know the status of the battery. This can be used to indicate the last
// known state of charge for things like a headphones case that is not
// actively connected but there is a previously known level.
batteryStatusLabels[batteryIndex].setAlpha(0.3f);
batteryIcons[batteryIndex].setImageLevel(300);
} else {
batteryIcons[batteryIndex].setImageLevel(device.getBatteryLevel(batteryIndex));
}
} }
} else if (BatteryState.NO_BATTERY.equals(batteryState) && batteryVoltage != GBDevice.BATTERY_UNKNOWN) { } else if (BatteryState.NO_BATTERY.equals(batteryState) && batteryVoltage != GBDevice.BATTERY_UNKNOWN) {
batteryStatusLabels[batteryIndex].setText(String.format(Locale.getDefault(), "%.2f", batteryVoltage)); batteryStatusLabels[batteryIndex].setText(String.format(Locale.getDefault(), "%.2f", batteryVoltage));

View File

@@ -0,0 +1,71 @@
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
/** This event is basically the same as GBDeviceEventBatteryInfo, but it will only update the fields
* that are explicitly set. This can be used when the caller that is sending the event doesn't have
* all of the battery state available to make the call. For example the device has a sent an event
* that indicates the device has started charging, but does not send the current level. If that
* caller tries to fetch the current state and resend it, there is a race condition that may cause a
* concurrent update to be dropped.
*/
public class GBDeviceEventBatteryIncrementalInfo extends GBDeviceEventBatteryInfo {
private static final Logger LOG = LoggerFactory.getLogger(GBDeviceEventBatteryIncrementalInfo.class);
private enum UpdateType {
LAST_CHARGE_TIME,
STATE,
LEVEL,
VOLTAGE;
}
private final UpdateType updateType;
public GBDeviceEventBatteryIncrementalInfo(int batteryIndex, GregorianCalendar lastChargeTime) {
super();
super.batteryIndex = batteryIndex;
super.lastChargeTime = lastChargeTime;
super.level = GBDevice.BATTERY_UNKNOWN;
this.updateType = UpdateType.LAST_CHARGE_TIME;
}
public GBDeviceEventBatteryIncrementalInfo(int batteryIndex, BatteryState state) {
super();
super.batteryIndex = batteryIndex;
super.state = state;
super.level = GBDevice.BATTERY_UNKNOWN;
this.updateType = UpdateType.STATE;
}
public GBDeviceEventBatteryIncrementalInfo(int batteryIndex, int level) {
super();
super.batteryIndex = batteryIndex;
super.level = level;
this.updateType = UpdateType.LEVEL;
}
public GBDeviceEventBatteryIncrementalInfo(int batteryIndex, float voltage) {
super();
super.batteryIndex = batteryIndex;
super.level = GBDevice.BATTERY_UNKNOWN;
super.voltage = voltage;
this.updateType = UpdateType.VOLTAGE;
}
@Override
protected void setDeviceValues(final GBDevice device) {
switch (updateType) {
case STATE -> device.setBatteryState(super.state, this.batteryIndex);
case LEVEL -> device.setBatteryLevel(super.level, this.batteryIndex);
case VOLTAGE -> device.setBatteryVoltage(super.voltage, this.batteryIndex);
}
}
}

View File

@@ -62,6 +62,12 @@ public class GBDeviceEventBatteryInfo extends GBDeviceEvent {
return super.toString() + "index: " + batteryIndex + ", level: " + level; return super.toString() + "index: " + batteryIndex + ", level: " + level;
} }
protected void setDeviceValues(final GBDevice device) {
device.setBatteryLevel(this.level, this.batteryIndex);
device.setBatteryState(this.state, this.batteryIndex);
device.setBatteryVoltage(this.voltage, this.batteryIndex);
}
@Override @Override
public void evaluate(final Context context, final GBDevice device) { public void evaluate(final Context context, final GBDevice device) {
if ((level < 0 || level > 100) && level != GBDevice.BATTERY_UNKNOWN) { if ((level < 0 || level > 100) && level != GBDevice.BATTERY_UNKNOWN) {
@@ -74,9 +80,7 @@ public class GBDeviceEventBatteryInfo extends GBDeviceEvent {
this.level != GBDevice.BATTERY_UNKNOWN && this.level != GBDevice.BATTERY_UNKNOWN &&
this.level > device.getBatteryLevel(this.batteryIndex); this.level > device.getBatteryLevel(this.batteryIndex);
device.setBatteryLevel(this.level, this.batteryIndex); setDeviceValues(device);
device.setBatteryState(this.state, this.batteryIndex);
device.setBatteryVoltage(this.voltage, this.batteryIndex);
final DevicePrefs devicePrefs = GBApplication.getDevicePrefs(device); final DevicePrefs devicePrefs = GBApplication.getDevicePrefs(device);
final BatteryConfig batteryConfig = device.getDeviceCoordinator().getBatteryConfig(device)[this.batteryIndex]; final BatteryConfig batteryConfig = device.getDeviceCoordinator().getBatteryConfig(device)[this.batteryIndex];

View File

@@ -60,7 +60,9 @@ public class GBDeviceEventVersionInfo extends GBDeviceEvent {
if (fwVersion2 != null) { if (fwVersion2 != null) {
device.setFirmwareVersion2(fwVersion2); device.setFirmwareVersion2(fwVersion2);
} }
device.setModel(hwVersion); if (hwVersion != null) {
device.setModel(hwVersion);
}
device.sendDeviceUpdateIntent(context); device.sendDeviceUpdateIntent(context);
} }
} }

View File

@@ -18,6 +18,8 @@ import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@@ -168,16 +170,15 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
@Override @Override
public int getBatteryCount(final GBDevice device) { public int getBatteryCount(final GBDevice device) {
ItemWithDetails right_name = return 3;
device.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey()); }
ItemWithDetails right_address =
device.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey()); @Override
if (right_name != null && !right_name.getDetails().isEmpty() && right_address != null && public BatteryConfig[] getBatteryConfig(final GBDevice device) {
!right_address.getDetails().isEmpty()) { BatteryConfig battery1 = new BatteryConfig(0, GBDevice.BATTERY_ICON_DEFAULT, R.string.even_realities_left_lens);
return 2; BatteryConfig battery2 = new BatteryConfig(1, GBDevice.BATTERY_ICON_DEFAULT, R.string.even_realities_right_lens);
} else { BatteryConfig battery3 = new BatteryConfig(2, R.drawable.level_list_even_realities_g1_case_battery, R.string.battery_case);
return 1; return new BatteryConfig[]{battery1, battery2, battery3};
}
} }
@Override @Override
@@ -203,12 +204,26 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
}); });
} }
@Override @Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) { public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
if (device.isConnected()) { if (device.isConnected()) {
return new int[]{R.xml.devicesettings_even_realities_g1_display}; deviceSpecificSettings.addRootScreen(R.xml.devicesettings_even_realities_g1_display);
} else { deviceSpecificSettings.addRootScreen(R.xml.devicesettings_timeformat);
return new int[]{}; final List<Integer> developer =
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
developer.add(R.xml.devicesettings_header_system);
developer.add(R.xml.devicesettings_debug_logs_toggle);
} }
return deviceSpecificSettings;
} }
////////////////////////////////////////////////
// Gadget bridge feature support declarations //
////////////////////////////////////////////////
@Override
public boolean supportsWeather() { return true; }
} }

View File

@@ -3,8 +3,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.function.Function; import java.util.function.Function;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
@@ -116,6 +118,30 @@ public class G1Communications {
} }
} }
public static class CommandSendReset extends CommandHandler {
public CommandSendReset() {
super(false, null);
}
@Override
public byte[] serialize() {
return new byte[] {
G1Constants.CommandId.SYSTEM.id,
G1Constants.SystemSubCommand.RESET.id
};
}
@Override
public boolean responseMatches(byte[] payload) {
return false;
}
@Override
public String getName() {
return "send_reset";
}
}
public static class CommandGetFirmwareInfo extends CommandHandler { public static class CommandGetFirmwareInfo extends CommandHandler {
public CommandGetFirmwareInfo(Function<byte[], Boolean> callback) { public CommandGetFirmwareInfo(Function<byte[], Boolean> callback) {
super(true, callback); super(true, callback);
@@ -123,7 +149,10 @@ public class G1Communications {
@Override @Override
public byte[] serialize() { public byte[] serialize() {
return new byte[] { G1Constants.CommandId.FW_INFO_REQUEST.id, 0x74 }; return new byte[] {
G1Constants.CommandId.SYSTEM.id,
G1Constants.SystemSubCommand.GET_FW_INFO.id
};
} }
@Override @Override
@@ -161,6 +190,10 @@ public class G1Communications {
public String getName() { public String getName() {
return "get_battery_info"; return "get_battery_info";
} }
public static byte getBatteryPercent(byte[] payload) {
return payload[2];
}
} }
public static class CommandSendHeartBeat extends CommandHandler { public static class CommandSendHeartBeat extends CommandHandler {
@@ -204,9 +237,14 @@ public class G1Communications {
this.timeMilliseconds = timeMilliseconds; this.timeMilliseconds = timeMilliseconds;
this.use12HourFormat = use12HourFormat; this.use12HourFormat = use12HourFormat;
if (weatherInfo != null) { if (weatherInfo != null) {
// TODO need to convert the weather spec enums to the ER enums. this.weatherIcon = G1Constants.fromOpenWeatherCondition(weatherInfo.currentConditionCode);
this.weatherIcon = 0x01; // Convert sunny to a moon if the current time stamp is between sunrise and sunset.
this.tempInCelsius = (byte) weatherInfo.currentTemp; if (timeMilliseconds / 1000 >= weatherInfo.sunSet &&
this.weatherIcon == G1Constants.WeatherId.SUNNY) {
this.weatherIcon = G1Constants.WeatherId.NIGHT;
}
// Convert Kelvin -> Celsius.
this.tempInCelsius = (byte) (weatherInfo.currentTemp - 273);
} else { } else {
this.weatherIcon = 0x00; this.weatherIcon = 0x00;
this.tempInCelsius = 0x00; this.tempInCelsius = 0x00;
@@ -224,11 +262,11 @@ public class G1Communications {
public byte[] serialize() { public byte[] serialize() {
byte[] packet = new byte[] { byte[] packet = new byte[] {
G1Constants.CommandId.DASHBOARD_CONFIG.id, G1Constants.CommandId.DASHBOARD_CONFIG.id,
G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id, 0x15, // Length = 21 bytes
0x00, 0x00,
sequence, sequence,
// Magic number? // Subcommand
0x01, G1Constants.DashboardConfig.SUB_COMMAND_SET_TIME_AND_WEATHER,
// Time 32bit place holders // Time 32bit place holders
(byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00,
@@ -246,10 +284,10 @@ public class G1Communications {
// Weather info // Weather info
this.weatherIcon, this.weatherIcon,
tempInCelsius, tempInCelsius,
// F/C useFahrenheit ? G1Constants.TemperatureUnit.FAHRENHEIT
(byte)(useFahrenheit ? 0x01 : 0x00), : G1Constants.TemperatureUnit.CELSIUS,
// 24H/12H use12HourFormat ? G1Constants.TimeFormat.TWELVE_HOUR
(byte)(use12HourFormat ? 0x01 : 0x00) : G1Constants.TimeFormat.TWENTY_FOUR_HOUR
}; };
BLETypeConversions.writeUint32(packet, 5, (int)(timeMilliseconds / 1000)); BLETypeConversions.writeUint32(packet, 5, (int)(timeMilliseconds / 1000));
BLETypeConversions.writeUint64(packet, 9, timeMilliseconds); BLETypeConversions.writeUint64(packet, 9, timeMilliseconds);
@@ -259,14 +297,11 @@ public class G1Communications {
@Override @Override
public boolean responseMatches(byte[] payload) { public boolean responseMatches(byte[] payload) {
if (payload.length < 4) {
return false;
}
// Command should match and the sequence should match. // Command should match and the sequence should match.
return payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id && return payload.length >= 5 &&
payload[1] == G1Constants.DashboardConfigSubCommand.SET_TIME_AND_WEATHER.id && payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id &&
payload[3] == sequence; payload[3] == sequence &&
payload[4] == G1Constants.DashboardConfig.SUB_COMMAND_SET_TIME_AND_WEATHER;
} }
@Override @Override
@@ -275,6 +310,46 @@ public class G1Communications {
} }
} }
public static class CommandSetDashboardModeSettings extends CommandHandler {
byte mode;
byte secondaryPaneMode;
public CommandSetDashboardModeSettings(byte mode, byte secondaryPaneMode) {
super(true, null);
this.mode = mode;
this.secondaryPaneMode = secondaryPaneMode;
}
@Override
public boolean needsGlobalSequence() { return true; }
@Override
public byte[] serialize() {
return new byte[]{
G1Constants.CommandId.DASHBOARD_CONFIG.id,
0x07, // Length
0x00, // pad
sequence,
G1Constants.DashboardConfig.SUB_COMMAND_SET_MODE,
mode,
secondaryPaneMode
};
}
@Override
public boolean responseMatches(byte[] payload) {
// Command should match and the sequence should match.
return payload.length >= 5 &&
payload[0] == G1Constants.CommandId.DASHBOARD_CONFIG.id &&
payload[3] == sequence &&
payload[4] == G1Constants.DashboardConfig.SUB_COMMAND_SET_MODE;
}
@Override
public String getName() {
return "set_dashboard_mode_settings";
}
}
public static class CommandGetSilentModeSettings extends CommandHandler { public static class CommandGetSilentModeSettings extends CommandHandler {
public CommandGetSilentModeSettings(Function<byte[], Boolean> callback) { public CommandGetSilentModeSettings(Function<byte[], Boolean> callback) {
super(true, callback); super(true, callback);
@@ -373,7 +448,7 @@ public class G1Communications {
public byte[] serialize() { public byte[] serialize() {
return new byte[] { return new byte[] {
G1Constants.CommandId.SET_DISPLAY_SETTINGS.id, G1Constants.CommandId.SET_DISPLAY_SETTINGS.id,
0x08, // Subcommand? 0x08, // Length
0x00, 0x00,
sequence, sequence,
0x02, // Seems to be a magic number? 0x02, // Seems to be a magic number?
@@ -560,4 +635,109 @@ public class G1Communications {
return "set_wear_detection_settings_" + (enable ? "enabled" : "disabled"); return "set_wear_detection_settings_" + (enable ? "enabled" : "disabled");
} }
} }
public static class CommandGetSerialNumber extends CommandHandler {
public CommandGetSerialNumber(Function<byte[], Boolean> callback) {
super(true, callback);
}
@Override
public byte[] serialize() {
return new byte[] { G1Constants.CommandId.GET_SERIAL_NUMBER.id };
}
@Override
public boolean responseMatches(byte[] payload) {
return payload.length >= 16 && payload[0] == G1Constants.CommandId.GET_SERIAL_NUMBER.id;
}
@Override
public String getName() {
return "get_serial_number";
}
public static int getFrameType(byte[] payload) {
String serialNumber = getSerialNumber(payload);
if (serialNumber.length() < 7) return -1;
switch(serialNumber.substring(4, 7)) {
case G1Constants.HardwareDescriptionKey.COLOR_GREY:
return R.string.even_realities_frame_color_grey;
case G1Constants.HardwareDescriptionKey.COLOR_BROWN:
return R.string.even_realities_frame_color_brown;
case G1Constants.HardwareDescriptionKey.COLOR_GREEN:
return R.string.even_realities_frame_color_green;
default:
return -1;
}
}
public static int getFrameColor(byte[] payload) {
String serialNumber = getSerialNumber(payload);
if (serialNumber.length() < 4) return -1;
switch(serialNumber.substring(0, 4)) {
case G1Constants.HardwareDescriptionKey.FRAME_ROUND:
return R.string.even_realities_frame_shape_G1A;
case G1Constants.HardwareDescriptionKey.FRAME_SQUARE:
return R.string.even_realities_frame_shape_G1B;
default:
return -1;
}
}
public static String getSerialNumber(byte[] payload) {
return new String(payload, 2, 16, StandardCharsets.US_ASCII);
}
}
public static class CommandSetDebugLogSettings extends CommandHandler {
private final boolean enable;
public CommandSetDebugLogSettings(boolean enable) {
super(false, null);
this.enable = enable;
}
@Override
public byte[] serialize() {
return new byte[]{
G1Constants.CommandId.SYSTEM.id,
G1Constants.SystemSubCommand.SET_DEBUG_LOGGING.id,
enable ? G1Constants.DebugLoggingStatus.ENABLE
: G1Constants.DebugLoggingStatus.DISABLE
};
}
@Override
public boolean responseMatches(byte[] payload) {
return false;
}
@Override
public String getName() {
return "set_debug_mode_settings_" + (enable ? "enabled" : "disabled");
}
}
public static class DebugLog {
public static boolean messageMatches(byte[] payload) {
return payload.length >= 1 && payload[0] == G1Constants.CommandId.DEBUG_LOG.id;
}
public static String getMessage(byte[] payload) {
return new String(payload, 1, payload.length-2, StandardCharsets.US_ASCII);
}
}
public static class DeviceEvent {
public static boolean messageMatches(byte[] payload) {
return payload.length >= 2 && payload[0] == G1Constants.CommandId.DEVICE_EVENT.id;
}
public static byte getEventId(byte[] payload) {
return payload[1];
}
public static byte getValue(byte[] payload) {
return payload[2];
}
}
} }

View File

@@ -14,6 +14,7 @@ public class G1Constants {
public static final int DEFAULT_COMMAND_TIMEOUT_MS = 5000; public static final int DEFAULT_COMMAND_TIMEOUT_MS = 5000;
public static final int DISPLAY_SETTINGS_PREVIEW_DELAY = 3000; public static final int DISPLAY_SETTINGS_PREVIEW_DELAY = 3000;
public static final int DEFAULT_RETRY_COUNT = 5; public static final int DEFAULT_RETRY_COUNT = 5;
public static final int CASE_BATTERY_INDEX = 2;
public static final String INTENT_TOGGLE_SILENT_MODE = "nodomain.freeyourgadget.gadgetbridge.evenrealities.silent_mode"; 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. // Extract the L or R at the end of the device prefix.
@@ -73,6 +74,14 @@ public class G1Constants {
} }
} }
public static class HardwareDescriptionKey {
public static final String FRAME_ROUND = "S100";
public static final String FRAME_SQUARE = "S110";
public static final String COLOR_GREY = "LAA";
public static final String COLOR_BROWN = "LBB";
public static final String COLOR_GREEN = "LCC";
}
public static class CommandStatus { public static class CommandStatus {
public static final byte FAILED = (byte)0xCA; public static final byte FAILED = (byte)0xCA;
public static final byte DATA_CONTINUES = (byte)0xCA; public static final byte DATA_CONTINUES = (byte)0xCA;
@@ -83,14 +92,16 @@ public class G1Constants {
public enum CommandId { public enum CommandId {
NOTIFICATION_CONFIG((byte) 0x04), NOTIFICATION_CONFIG((byte) 0x04),
DASHBOARD_CONFIG((byte) 0x06), DASHBOARD_CONFIG((byte) 0x06),
DASHBOARD((byte) 0x22), SYNC_SEQUENCE((byte) 0x22), // 0x05
FW_INFO_REQUEST((byte) 0x23), DASHBOARD_SHOWN((byte) 0x22), // 0x0A
SYSTEM((byte) 0x23),
HEARTBEAT((byte) 0x25), HEARTBEAT((byte) 0x25),
BATTERY_LEVEL((byte) 0x2C), BATTERY_LEVEL((byte) 0x2C),
INIT((byte) 0x4D), INIT((byte) 0x4D),
NOTIFICATION((byte) 0x4B), NOTIFICATION((byte) 0x4B),
FW_INFO_RESPONSE((byte) 0x6E), FW_INFO_RESPONSE((byte) 0x6E),
DEVICE_ACTION((byte) 0xF5), DEBUG_LOG((byte) 0xF4),
DEVICE_EVENT((byte) 0xF5),
GET_SILENT_MODE_SETTINGS((byte) 0x2B), // There is more info in this one GET_SILENT_MODE_SETTINGS((byte) 0x2B), // There is more info in this one
SET_SILENT_MODE_SETTINGS((byte) 0x03), SET_SILENT_MODE_SETTINGS((byte) 0x03),
GET_DISPLAY_SETTINGS((byte) 0x3B), GET_DISPLAY_SETTINGS((byte) 0x3B),
@@ -100,7 +111,8 @@ public class G1Constants {
GET_BRIGHTNESS_SETTINGS((byte) 0x29), GET_BRIGHTNESS_SETTINGS((byte) 0x29),
SET_BRIGHTNESS_SETTINGS((byte) 0x01), SET_BRIGHTNESS_SETTINGS((byte) 0x01),
GET_WEAR_DETECTION_SETTINGS((byte) 0x3A), GET_WEAR_DETECTION_SETTINGS((byte) 0x3A),
SET_WEAR_DETECTION_SETTINGS((byte) 0x27); SET_WEAR_DETECTION_SETTINGS((byte) 0x27),
GET_SERIAL_NUMBER((byte) 0x34);
final public byte id; final public byte id;
@@ -109,16 +121,31 @@ public class G1Constants {
} }
} }
public enum DashboardConfigSubCommand { public static class DashboardConfig {
SET_MODE((byte) 0x07), public static final byte SUB_COMMAND_SET_TIME_AND_WEATHER = 0x01;
UNKNOWN_1((byte) 0x0C), public static final byte SUB_COMMAND_SET_MODE = 0x06;
SET_TIME_AND_WEATHER((byte) 0x15),
// Not sure why they use this one sometimes. public static final byte MODE_FULL = 0x00;
SET_TIME_AND_WEATHER_ALSO((byte) 0x16); public static final byte MODE_DUAL = 0x01;
public static final byte MODE_MINIMAl = 0x02;
public static final byte PANE_NOTES = 0x00;
public static final byte PANE_STOCKS = 0x01;
public static final byte PANE_NEWS = 0x02;
public static final byte PANE_CALENDAR = 0x03;
public static final byte PANE_NAVIGATION = 0x04;
public static final byte PANE_EMPTY = 0x05;
}
public enum SystemSubCommand {
RESET((byte) 0x72),
GET_FW_INFO((byte) 0x74),
SET_DEBUG_LOGGING((byte) 0x6C);
final public byte id; final public byte id;
DashboardConfigSubCommand(byte id) { SystemSubCommand(byte id) {
this.id = id; this.id = id;
} }
} }
@@ -127,4 +154,185 @@ public class G1Constants {
public static final byte ENABLE = 0x0C; public static final byte ENABLE = 0x0C;
public static final byte DISABLE = 0x0A; public static final byte DISABLE = 0x0A;
} }
}
public static class DebugLoggingStatus {
public static final byte ENABLE = 0x00;
public static final byte DISABLE = (byte)0x31;
}
public static class DeviceEventId {
// Used to indicate a double tap, but it was used to close the dashboard.
public static final byte DOUBLE_TAP_FOR_EXIT = 0x00;
public static final byte UNKNOWN_1 = 0x01;
public static final byte HEAD_UP = 0x02;
public static final byte HEAD_DOWN = 0x03;
public static final byte SILENT_MODE_ENABLED = 0x04;
public static final byte SILENT_MODE_DISABLED = 0x05;
public static final byte GLASSES_WORN = 0x06;
public static final byte GLASSES_NOT_WORN_NO_CASE = 0x07;
public static final byte CASE_LID_OPEN = 0x08;
// Sent with a payload of 00 or 01 to indicate charging state.
public static final byte GLASSES_CHARGING = 0x09;
// Comes with a payload 00 - 64
public static final byte GLASSES_SIDE_BATTERY_LEVEL = 0x0A;
public static final byte CASE_LID_CLOSE = 0x0B;
public static final byte UNKNOWN_4 = 0x0C;
public static final byte UNKNOWN_5 = 0x0D;
// Sent with a payload of 00 or 01 to indicate charging state.
public static final byte CASE_CHARGING = 0x0E;
// Comes with a payload 00 - 64
public static final byte CASE_BATTERY_LEVEL = 0x0F;
public static final byte UNKNOWN_6 = 0x10;
public static final byte BINDING_SUCCESS = 0x11;
public static final byte DASHBOARD_SHOW = 0x1E;
public static final byte DASHBOARD_CLOSE = 0x1F;
// Used to initiate translate or transcribe in the official app.
// For us it's strictly a double tap that only sends the event.
public static final byte DOUBLE_TAP_FOR_ACTION = 0x20;
}
public static class TemperatureUnit {
public static final byte CELSIUS = 0x00;
public static final byte FAHRENHEIT = 0x01;
}
public static class TimeFormat {
public static final byte TWELVE_HOUR = 0x00;
public static final byte TWENTY_FOUR_HOUR = 0x01;
}
public static class WeatherId {
public static final byte NONE = 0x00;
public static final byte NIGHT = 0x01;
public static final byte CLOUDS = 0x02;
public static final byte DRIZZLE = 0x03;
public static final byte HEAVY_DRIZZLE = 0x04;
public static final byte RAIN = 0x05;
public static final byte HEAVY_RAIN = 0x06;
public static final byte THUNDER = 0x07;
public static final byte THUNDERSTORM = 0x08;
public static final byte SNOW = 0x09;
public static final byte MIST = 0x0A;
public static final byte FOG = 0x0B;
public static final byte SAND = 0x0C;
public static final byte SQUALLS = 0x0D;
public static final byte TORNADO = 0x0E;
public static final byte FREEZING_RAIN = 0x0F;
public static final byte SUNNY = 0x10;
}
public static byte fromOpenWeatherCondition(int openWeatherMapCondition) {
// http://openweathermap.org/weather-conditions
switch (openWeatherMapCondition) {
//Group 2xx: Thunderstorm
case 200: //thunderstorm with light rain:
case 201: //thunderstorm with rain:
case 202: //thunderstorm with heavy rain:
case 210: //light thunderstorm::
case 211: //thunderstorm:
case 230: //thunderstorm with light drizzle:
case 231: //thunderstorm with drizzle:
case 232: //thunderstorm with heavy drizzle:
case 212: //heavy thunderstorm:
case 221: //ragged thunderstorm:
return WeatherId.THUNDERSTORM;
//Group 3xx: Drizzle
case 300: //light intensity drizzle:
case 301: //drizzle:
case 310: //light intensity drizzle rain:
return WeatherId.DRIZZLE;
case 302: //heavy intensity drizzle:
case 311: //drizzle rain:
case 312: //heavy intensity drizzle rain:
case 313: //shower rain and drizzle:
case 314: //heavy shower rain and drizzle:
case 321: //shower drizzle:
return WeatherId.HEAVY_DRIZZLE;
//Group 5xx: Rain
case 500: //light rain:
case 501: //moderate rain:
return WeatherId.RAIN;
case 502: //heavy intensity rain:
case 503: //very heavy rain:
case 504: //extreme rain:
case 511: //freezing rain:
case 520: //light intensity shower rain:
case 521: //shower rain:
case 522: //heavy intensity shower rain:
case 531: //ragged shower rain:
return WeatherId.HEAVY_RAIN;
//Group 6xx: Snow
case 600: //light snow:
case 601: //snow:
case 602: //heavy snow:
return WeatherId.SNOW;
case 611: //sleet:
case 612: //shower sleet:
case 615: //light rain and snow:
case 616: //rain and snow:
case 620: //light shower snow:
case 621: //shower snow:
case 622: //heavy shower snow:
return WeatherId.FREEZING_RAIN;
//Group 7xx: Atmosphere
case 701: //mist:
return WeatherId.MIST;
case 711: //smoke:
return WeatherId.FOG;
case 721: //haze:
return WeatherId.MIST;
case 731: //sandcase dust whirls:
return WeatherId.SAND;
case 741: //fog:
return WeatherId.FOG;
case 751: //sand:
case 761: //dust:
case 762: //volcanic ash:
return WeatherId.SAND;
case 771: //squalls:
return WeatherId.SQUALLS;
case 781: //tornado:
case 900: //tornado
return WeatherId.TORNADO;
//Group 800: Clear
case 800: //clear sky:
return WeatherId.SUNNY;
//Group 80x: Clouds
case 801: //few clouds:
case 802: //scattered clouds:
case 803: //broken clouds:
case 804: //overcast clouds:
return WeatherId.CLOUDS;
//Group 90x: Extreme
case 903: //cold
return WeatherId.SNOW;
case 904: //hot
return WeatherId.SUNNY;
case 905: //windy
return WeatherId.NONE;
case 906: //hail
return WeatherId.THUNDERSTORM;
//Group 9xx: Additional
case 951: //calm
return WeatherId.SUNNY;
case 952: //light breeze
case 953: //gentle breeze
case 954: //moderate breeze
case 955: //fresh breeze
case 956: //strong breeze
case 957: //high windcase near gale
case 958: //gale
return WeatherId.SQUALLS;
case 901: //tropical storm
case 959: //severe gale
case 960: //storm
case 961: //violent storm
case 902: //hurricane
case 962: //hurricane
return WeatherId.TORNADO;
default:
return WeatherId.SUNNY;
}
}
}

View File

@@ -13,11 +13,13 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.function.Function; import java.util.function.Function;
@@ -27,13 +29,15 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16Constants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails; import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEMultiDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEMultiDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
/** /**
@@ -49,9 +53,10 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class); private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class);
private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper()); private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
private BroadcastReceiver intentReceiver = null; private BroadcastReceiver intentReceiver = null;
private final Object sendTimeLock = new Object(); private final Object lensSkewLock = new Object();
private G1SideManager leftSide = null; private G1SideManager leftSide = null;
private G1SideManager rightSide = null; private G1SideManager rightSide = null;
public G1DeviceSupport() { public G1DeviceSupport() {
this(LOG); this(LOG);
} }
@@ -60,17 +65,9 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
super(logger, 2); super(logger, 2);
addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART, addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART,
G1Constants.Side.LEFT.getDeviceIndex()); G1Constants.Side.LEFT.getDeviceIndex());
addSupportedService(BFH16Constants.BFH16_GENERIC_ACCESS_SERVICE,
G1Constants.Side.LEFT.getDeviceIndex());
addSupportedService(BFH16Constants.BFH16_GENERIC_ATTRIBUTE_SERVICE,
G1Constants.Side.LEFT.getDeviceIndex());
addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART, addSupportedService(G1Constants.UUID_SERVICE_NORDIC_UART,
G1Constants.Side.RIGHT.getDeviceIndex()); G1Constants.Side.RIGHT.getDeviceIndex());
addSupportedService(BFH16Constants.BFH16_GENERIC_ACCESS_SERVICE,
G1Constants.Side.RIGHT.getDeviceIndex());
addSupportedService(BFH16Constants.BFH16_GENERIC_ATTRIBUTE_SERVICE,
G1Constants.Side.RIGHT.getDeviceIndex());
} }
@Override @Override
@@ -79,8 +76,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// Ignore any context sets from non-left devices. // Ignore any context sets from non-left devices.
G1Constants.Side side = G1Constants.getSideFromFullName(device.getName()); G1Constants.Side side = G1Constants.getSideFromFullName(device.getName());
if (side == G1Constants.Side.LEFT) { if (side == G1Constants.Side.LEFT) {
ItemWithDetails right_name = ItemWithDetails right_name = device.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey());
device.getDeviceInfo(G1Constants.Side.RIGHT.getNameKey());
ItemWithDetails right_address = ItemWithDetails right_address =
device.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey()); device.getDeviceInfo(G1Constants.Side.RIGHT.getAddressKey());
if (right_name != null && !right_name.getDetails().isEmpty() && right_address != null && if (right_name != null && !right_name.getDetails().isEmpty() && right_address != null &&
@@ -106,11 +102,9 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// Register to receive silent mode intent calls from the UI. // Register to receive silent mode intent calls from the UI.
if (intentReceiver == null) { if (intentReceiver == null) {
intentReceiver = new IntentReceiver(); intentReceiver = new IntentReceiver();
ContextCompat.registerReceiver( ContextCompat.registerReceiver(context, intentReceiver,
context, new IntentFilter(G1Constants.INTENT_TOGGLE_SILENT_MODE),
intentReceiver, ContextCompat.RECEIVER_NOT_EXPORTED);
new IntentFilter(G1Constants.INTENT_TOGGLE_SILENT_MODE),
ContextCompat.RECEIVER_NOT_EXPORTED);
} }
} }
@@ -121,6 +115,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
getCharacteristic(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_RX, deviceIdx); getCharacteristic(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_RX, deviceIdx);
BluetoothGattCharacteristic tx = BluetoothGattCharacteristic tx =
getCharacteristic(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_TX, deviceIdx); getCharacteristic(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_TX, deviceIdx);
if (rx == null || tx == null) { if (rx == null || tx == null) {
// If the characteristics are not received from the device reconnect and try again. // If the characteristics are not received from the device reconnect and try again.
LOG.warn("RX/TX characteristics are null, will attempt to reconnect"); LOG.warn("RX/TX characteristics are null, will attempt to reconnect");
@@ -161,15 +156,22 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// IMPORTANT: use getDevice(deviceIdx), not getDevice(/* 0 */) here otherwise the device // 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, // will lock up in a half initialized state because GB thinks the left side is initialized,
// after because the right ran first. // after because the right ran first.
if (side.getState() == GBDevice.State.CONNECTED) { if (side.getConnectingState() == GBDevice.State.CONNECTED) {
builder.add(new SetDeviceStateAction(getDevice(deviceIdx), GBDevice.State.INITIALIZING, builder.add(new SetDeviceStateAction(getDevice(deviceIdx), GBDevice.State.INITIALIZING,
getContext())); getContext()));
side.initialize(builder); side.initialize(builder);
} }
synchronized (this) { synchronized (this) {
if (leftSide != null && leftSide.getState() == GBDevice.State.INITIALIZED && if (leftSide != null && leftSide.getConnectingState() == GBDevice.State.INITIALIZED &&
rightSide != null && rightSide.getState() == GBDevice.State.INITIALIZED) { rightSide != null && rightSide.getConnectingState() == GBDevice.State.INITIALIZED) {
// set device firmware to prevent the following error when data is saved to the
// database and device firmware has not been set yet.
// java.lang.IllegalArgumentException: the bind value at index 2 is null.
// Must be called before the PostInitialize down below.
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
// Both sides are initialized. The whole device is initialized, don't use a device // 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. // index here. Device 0 is the device that the reset of GB sees.
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED,
@@ -181,6 +183,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
backgroundTasksHandler.postDelayed(() -> { backgroundTasksHandler.postDelayed(() -> {
leftSide.postInitializeLeft(); leftSide.postInitializeLeft();
rightSide.postInitializeRight(); rightSide.postInitializeRight();
onSetDashboardMode();
onSetTime(); onSetTime();
}, 200); }, 200);
} }
@@ -222,6 +225,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// passing in "this" because we don't want to forward ALL functionality of the device // passing in "this" because we don't want to forward ALL functionality of the device
// support and we don't want a hard dependency on G1DeviceSupport in G1SideManager. // support and we don't want a hard dependency on G1DeviceSupport in G1SideManager.
Callable<BtLEQueue> getQueue = () -> this.getQueue(deviceIdx); Callable<BtLEQueue> getQueue = () -> this.getQueue(deviceIdx);
Callable<GBDevice> getDevice = () -> this.getDevice(deviceIdx);
Function<GBDeviceEvent, Void> handleEvent = (GBDeviceEvent event) -> { Function<GBDeviceEvent, Void> handleEvent = (GBDeviceEvent event) -> {
this.evaluateGBDeviceEvent(event); this.evaluateGBDeviceEvent(event);
return null; return null;
@@ -229,13 +233,12 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// Create the desired side. // Create the desired side.
if (deviceIdx == G1Constants.Side.LEFT.getDeviceIndex()) { if (deviceIdx == G1Constants.Side.LEFT.getDeviceIndex()) {
leftSide = leftSide = new G1SideManager(G1Constants.Side.LEFT, backgroundTasksHandler, getQueue,
new G1SideManager(G1Constants.Side.LEFT, backgroundTasksHandler, getQueue, getDevice, handleEvent, this::getDevicePrefs, rx, tx);
handleEvent, this::getDevicePrefs, rx, tx);
return leftSide; return leftSide;
} else if (deviceIdx == G1Constants.Side.RIGHT.getDeviceIndex()) { } else if (deviceIdx == G1Constants.Side.RIGHT.getDeviceIndex()) {
rightSide = new G1SideManager(G1Constants.Side.RIGHT, backgroundTasksHandler, rightSide = new G1SideManager(G1Constants.Side.RIGHT, backgroundTasksHandler, getQueue,
getQueue, handleEvent, this::getDevicePrefs, rx, tx); getDevice, handleEvent, this::getDevicePrefs, rx, tx);
return rightSide; return rightSide;
} }
@@ -285,8 +288,7 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
if (characteristic.getUuid().equals(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_RX)) { if (characteristic.getUuid().equals(G1Constants.UUID_CHARACTERISTIC_NORDIC_UART_RX)) {
String address = gatt.getDevice().getAddress(); String address = gatt.getDevice().getAddress();
if (getDevice(G1Constants.Side.LEFT.getDeviceIndex()) != null) { if (getDevice(G1Constants.Side.LEFT.getDeviceIndex()) != null) {
String leftAddress = String leftAddress = getDevice(G1Constants.Side.LEFT.getDeviceIndex()).getAddress();
getDevice(G1Constants.Side.LEFT.getDeviceIndex()).getAddress();
if (address.equals(leftAddress) && leftSide != null) { if (address.equals(leftAddress) && leftSide != null) {
return leftSide.handlePayload(characteristic.getValue()); return leftSide.handlePayload(characteristic.getValue());
} }
@@ -317,22 +319,30 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
switch (config) { switch (config) {
case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE: case DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE:
// This setting is only sent to the right arm. // This setting is only sent to the right arm.
if (rightSide != null) rightSide.onSendConfiguration(config); if (rightSide != null)
rightSide.onSendConfiguration(config);
break;
case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT:
// Units or time format updated, update the time and weather on the glasses to match
onSetTimeOrWeather();
break; break;
default: default:
// Forward to both sides. // Forward to both sides.
if (leftSide != null) leftSide.onSendConfiguration(config); if (leftSide != null)
if (rightSide != null) rightSide.onSendConfiguration(config); leftSide.onSendConfiguration(config);
if (rightSide != null)
rightSide.onSendConfiguration(config);
break; break;
} }
} }
@Override private void onSetTimeOrWeather() {
public void onSetTime() { if (leftSide == null || rightSide == null)
if (leftSide == null || rightSide == null) return; return;
boolean use12HourFormat = getDevicePrefs().getTimeFormat().equals( boolean use12HourFormat = getDevicePrefs().getTimeFormat()
DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H); .equals(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC")); Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
long currentMilliseconds = c.getTimeInMillis(); long currentMilliseconds = c.getTimeInMillis();
long tzOffset = TimeZone.getDefault().getOffset(currentMilliseconds); long tzOffset = TimeZone.getDefault().getOffset(currentMilliseconds);
@@ -340,18 +350,25 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
// Check if the GB settings are set to metric, if not, set the temp to use Fahrenheit. // 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); String metricString = GBApplication.getContext().getString(R.string.p_unit_metric);
boolean useFahrenheit = !GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, metricString).equals(metricString); boolean useFahrenheit = !GBApplication.getPrefs()
.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM,
metricString).equals(metricString);
// Pull the weather into a local variable so that if it changes between the two lenses being
// updated, we won't end up with a skewed value.
@Nullable WeatherSpec weather = Weather.getInstance().getWeatherSpec();
// Run in the background in case the command hangs and this was run from the UI thread. // Run in the background in case the command hangs and this was run from the UI thread.
backgroundTasksHandler.post(() -> { backgroundTasksHandler.post(() -> {
// This block is synchronized. We do not want two calls to overlap, otherwise the lenses // This block is synchronized. We do not want two calls to overlap, otherwise the lenses
// could get skewed with different values. // could get skewed with different values.
synchronized (sendTimeLock) { synchronized (lensSkewLock) {
// Send the left the time synchronously, then once a response is received, send the right. // 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. // The glasses will ignore the command on the right lens if it arrives before the left.
G1Communications.CommandHandler leftCommandHandler = G1Communications.CommandHandler leftCommandHandler =
new G1Communications.CommandSetTimeAndWeather(timeMilliseconds, new G1Communications.CommandSetTimeAndWeather(timeMilliseconds,
use12HourFormat, useFahrenheit); use12HourFormat, weather,
useFahrenheit);
leftSide.send(leftCommandHandler); leftSide.send(leftCommandHandler);
if (!leftCommandHandler.waitForResponsePayload()) { if (!leftCommandHandler.waitForResponsePayload()) {
LOG.error("Set time on left lens timed out"); LOG.error("Set time on left lens timed out");
@@ -361,21 +378,72 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
rightSide.send(new G1Communications.CommandSetTimeAndWeather(timeMilliseconds, rightSide.send(new G1Communications.CommandSetTimeAndWeather(timeMilliseconds,
use12HourFormat, use12HourFormat,
weather,
useFahrenheit)); useFahrenheit));
} }
}); });
} }
private void onToggleSilentMode() { private void onToggleSilentMode() {
if (leftSide == null || rightSide == null) return; if (leftSide == null || rightSide == null)
return;
// If both lenses are in sync on what the status is, set them both. Otherwise, only set the // 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. // right one so they can be resynchronized.
if(leftSide.getSilentModeStatus() == rightSide.getSilentModeStatus()){ if (leftSide.getSilentModeStatus() == rightSide.getSilentModeStatus()) {
leftSide.onToggleSilentMode(); leftSide.onToggleSilentMode();
rightSide.onToggleSilentMode(); rightSide.onToggleSilentMode();
} else { } else {
rightSide.onToggleSilentMode(); rightSide.onToggleSilentMode();
} }
} }
private void onSetDashboardMode() {
// 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 (lensSkewLock) {
// Send to the left 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.
// TODO: Pull these values from the settings and build a UI to configure it.
G1Communications.CommandHandler leftCommandHandler =
new G1Communications.CommandSetDashboardModeSettings(
G1Constants.DashboardConfig.MODE_MINIMAl,
G1Constants.DashboardConfig.PANE_EMPTY);
leftSide.send(leftCommandHandler);
if (!leftCommandHandler.waitForResponsePayload()) {
LOG.error("Set dashboard on right lens timed out");
getDevice().setState(GBDevice.State.WAITING_FOR_RECONNECT);
getDevice().sendDeviceUpdateIntent(getContext());
}
rightSide.send(new G1Communications.CommandSetDashboardModeSettings(
G1Constants.DashboardConfig.MODE_MINIMAl,
G1Constants.DashboardConfig.PANE_EMPTY));
}
});
}
@Override
public void onReset(int flags) {
if (flags == GBDeviceProtocol.RESET_FLAGS_REBOOT) {
leftSide.send(new G1Communications.CommandSendReset());
rightSide.send(new G1Communications.CommandSendReset());
}
}
@Override
public void onSendWeather(ArrayList<WeatherSpec> weatherSpecs) {
// onSetTimeAndWeather() fetches the weather directly from the global state, so no need to
// pass in the weatherSpecs.
onSetTimeOrWeather();
}
@Override
public void onSetTime() {
onSetTimeOrWeather();
}
} }

View File

@@ -14,10 +14,12 @@ import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.function.Function; import java.util.function.Function;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryIncrementalInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
@@ -38,6 +40,7 @@ public class G1SideManager {
private final G1Constants.Side mySide; private final G1Constants.Side mySide;
private final Handler backgroundTasksHandler; private final Handler backgroundTasksHandler;
private final Callable<BtLEQueue> getQueueHandler; private final Callable<BtLEQueue> getQueueHandler;
private final Callable<GBDevice> getDeviceHandler;
private final Function<GBDeviceEvent, Void> sendEventHandler; private final Function<GBDeviceEvent, Void> sendEventHandler;
private final Callable<DevicePrefs> getPrefsHandler; private final Callable<DevicePrefs> getPrefsHandler;
private final BluetoothGattCharacteristic rx; private final BluetoothGattCharacteristic rx;
@@ -48,15 +51,17 @@ public class G1SideManager {
private final Set<G1Communications.CommandHandler> commandHandlers; private final Set<G1Communications.CommandHandler> commandHandlers;
private byte globalSequence; private byte globalSequence;
private boolean isSilentModeEnabled; private boolean isSilentModeEnabled;
private GBDevice.State state; private GBDevice.State connectingState;
private boolean debugEnabled;
public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler, public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler,
Callable<BtLEQueue> getQueue, Function<GBDeviceEvent, Void> sendEvent, Callable<BtLEQueue> getQueue, Callable<GBDevice> getDevice,
Callable<DevicePrefs> getPrefs, Function<GBDeviceEvent, Void> sendEvent, Callable<DevicePrefs> getPrefs,
BluetoothGattCharacteristic rx, BluetoothGattCharacteristic tx) { BluetoothGattCharacteristic rx, BluetoothGattCharacteristic tx) {
this.mySide = mySide; this.mySide = mySide;
this.backgroundTasksHandler = backgroundTasksHandler; this.backgroundTasksHandler = backgroundTasksHandler;
this.getQueueHandler = getQueue; this.getQueueHandler = getQueue;
this.getDeviceHandler = getDevice;
this.sendEventHandler = sendEvent; this.sendEventHandler = sendEvent;
this.getPrefsHandler = getPrefs; this.getPrefsHandler = getPrefs;
this.rx = rx; this.rx = rx;
@@ -67,9 +72,14 @@ public class G1SideManager {
}; };
this.heartBeatRunner = () -> { this.heartBeatRunner = () -> {
// We can send any command as a heart beat. The official app uses this one. if (getDevice().isConnected()) {
send(new G1Communications.CommandGetSilentModeSettings(null)); // We can send any command as a heart beat. The official app uses this one.
scheduleHeatBeat(); send(new G1Communications.CommandGetSilentModeSettings(null));
scheduleHeatBeat();
} else {
// Don't reschedule if the device is disconnected.
LOG.debug("Stopping heartbeat runner since side is in state: {}", getDevice().getState());
}
}; };
this.displaySettingsPreviewCloserRunner = () -> { this.displaySettingsPreviewCloserRunner = () -> {
DevicePrefs prefs = getDevicePrefs(); DevicePrefs prefs = getDevicePrefs();
@@ -85,7 +95,8 @@ public class G1SideManager {
// Non Finals // Non Finals
this.globalSequence = 0; this.globalSequence = 0;
this.isSilentModeEnabled = false; this.isSilentModeEnabled = false;
this.state = GBDevice.State.CONNECTED; this.connectingState = GBDevice.State.CONNECTED;
this.debugEnabled = false;
} }
private BtLEQueue getQueue() { private BtLEQueue getQueue() {
@@ -96,6 +107,14 @@ public class G1SideManager {
} }
} }
private GBDevice getDevice() {
try {
return getDeviceHandler.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void evaluateGBDeviceEvent(GBDeviceEvent event) { private void evaluateGBDeviceEvent(GBDeviceEvent event) {
sendEventHandler.apply(event); sendEventHandler.apply(event);
} }
@@ -108,11 +127,19 @@ public class G1SideManager {
} }
} }
public GBDevice.State getState() { public GBDevice.State getConnectingState() {
return state; return connectingState;
} }
public void initialize(TransactionBuilder transaction) { public void initialize(TransactionBuilder transaction) {
// Disable device logging in the prefs. There is no way to query this state from the device
// so instead, it is always disabled on connection, and then if a debug message arrives, the
// setting will be flipped to true.
this.debugEnabled = false;
getDevicePrefs().getPreferences().edit()
.putBoolean(DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, this.debugEnabled)
.apply();
// The glasses will auto disconnect after 30 seconds of no data on the wire. // 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 // Schedule a heartbeat task. If this is not enabled, the glasses will disconnect and be
// useless to the user. // useless to the user.
@@ -121,7 +148,7 @@ public class G1SideManager {
// Schedule the battery polling. // Schedule the battery polling.
scheduleBatteryPolling(); scheduleBatteryPolling();
state = GBDevice.State.INITIALIZED; connectingState = GBDevice.State.INITIALIZED;
} }
public byte getSilentModeStatus() { public byte getSilentModeStatus() {
@@ -142,6 +169,7 @@ public class G1SideManager {
// These can be sent to both, but the left lens is used as the master for these settings. // 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.CommandGetDisplaySettings(this::handleDisplaySettingsPayload));
sendInTransaction(transaction, new G1Communications.CommandGetBrightnessSettings(this::handleBrightnessSettingsPayload)); sendInTransaction(transaction, new G1Communications.CommandGetBrightnessSettings(this::handleBrightnessSettingsPayload));
sendInTransaction(transaction, new G1Communications.CommandGetSerialNumber(this::handleSerialNumberPayload));
transaction.queue(getQueue()); transaction.queue(getQueue());
} }
@@ -184,15 +212,17 @@ public class G1SideManager {
send(new G1Communications.CommandSetWearDetectionSettings( send(new G1Communications.CommandSetWearDetectionSettings(
prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE, true))); prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE, true)));
break; break;
case DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE:
this.debugEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, false);
send(new G1Communications.CommandSetDebugLogSettings(this.debugEnabled));
break;
} }
} }
public void onToggleSilentMode() { public void onToggleSilentMode() {
isSilentModeEnabled = !isSilentModeEnabled; isSilentModeEnabled = !isSilentModeEnabled;
G1Communications.CommandHandler commandHandler = send(new G1Communications.CommandSetSilentModeSettings(isSilentModeEnabled));
new G1Communications.CommandSetSilentModeSettings(isSilentModeEnabled);
send(commandHandler);
} }
private void scheduleHeatBeat() { private void scheduleHeatBeat() {
@@ -299,6 +329,22 @@ public class G1SideManager {
} }
} }
private void updateBatteryLevel(int level, int index) {
evaluateGBDeviceEvent(new GBDeviceEventBatteryIncrementalInfo(index, level));
}
private void updateBatteryLevel(int level) {
updateBatteryLevel(level, mySide.getDeviceIndex());
}
private void updateBatteryState(BatteryState state, int index) {
evaluateGBDeviceEvent(new GBDeviceEventBatteryIncrementalInfo(index, state));
}
private void updateBatteryState(BatteryState state) {
updateBatteryState(state, mySide.getDeviceIndex());
}
public boolean handlePayload(byte[] payload) { public boolean handlePayload(byte[] payload) {
for (G1Communications.CommandHandler commandHandler : commandHandlers) { for (G1Communications.CommandHandler commandHandler : commandHandlers) {
if (commandHandler.responseMatches(payload)) { if (commandHandler.responseMatches(payload)) {
@@ -315,14 +361,14 @@ public class G1SideManager {
} }
} }
// These can come in unprompted from the glasses, call the correct handler based on what the // The glasses will send unprompted messages indicating certain events happening.
// command is. // ex. glasses are taken off, glasses are charging, or touch pad was pressed.
if (payload.length > 1) { if (G1Communications.DeviceEvent.messageMatches(payload)) {
if (payload[0] == G1Constants.CommandId.DEVICE_ACTION.id) { return handleDeviceEventPayload(payload);
return handleDeviceActionPayload(payload); }
} else if (payload[0] == G1Constants.CommandId.DASHBOARD.id) {
return handleDashboardPayload(payload); if (G1Communications.DebugLog.messageMatches(payload)) {
} return handleDebugLogPayload(payload);
} }
LOG.debug("Unhandled payload on side {}: {}", LOG.debug("Unhandled payload on side {}: {}",
@@ -333,11 +379,7 @@ public class G1SideManager {
} }
private boolean handleBatteryPayload(byte[] payload) { private boolean handleBatteryPayload(byte[] payload) {
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); updateBatteryLevel(G1Communications.CommandGetBatteryInfo.getBatteryPercent(payload));
batteryInfo.state = BatteryState.BATTERY_NORMAL;
batteryInfo.level = payload[2];
batteryInfo.batteryIndex = mySide.getDeviceIndex();
evaluateGBDeviceEvent(batteryInfo);
return true; return true;
} }
@@ -349,15 +391,32 @@ public class G1SideManager {
int versionEnd = fwString.indexOf(',', versionStart); int versionEnd = fwString.indexOf(',', versionStart);
if (versionStart > -1 && versionEnd > versionStart) { if (versionStart > -1 && versionEnd > versionStart) {
String version = fwString.substring(versionStart, versionEnd); String version = fwString.substring(versionStart, versionEnd);
LOG.debug("Parsed fw version: {}", version);
GBDeviceEventVersionInfo fwInfo = new GBDeviceEventVersionInfo(); GBDeviceEventVersionInfo fwInfo = new GBDeviceEventVersionInfo();
if (mySide == G1Constants.Side.LEFT) { fwInfo.hwVersion = null;
fwInfo.fwVersion = version; fwInfo.fwVersion = mySide == G1Constants.Side.LEFT ? version : null;
} else if (mySide == G1Constants.Side.RIGHT) { fwInfo.fwVersion2 = mySide == G1Constants.Side.RIGHT ? version : null;
fwInfo.fwVersion2 = version; evaluateGBDeviceEvent(fwInfo);
} return true;
// Actually get this some how? }
fwInfo.hwVersion = "G1A"; return false;
}
private boolean handleSerialNumberPayload(byte[] payload) {
String serialNumber = G1Communications.CommandGetSerialNumber.getSerialNumber(payload);
// Parse the hardware information out of the serial number.
int shape = G1Communications.CommandGetSerialNumber.getFrameType(payload);
int color = G1Communications.CommandGetSerialNumber.getFrameColor(payload);
if (shape != -1 && color != -1) {
GBDeviceEventVersionInfo fwInfo = new GBDeviceEventVersionInfo();
fwInfo.hwVersion = GBApplication.getContext().getString(
R.string.even_realities_frame_description,
GBApplication.getContext().getString(color),
GBApplication.getContext().getString(shape),
GBApplication.getContext().getString(R.string.serial_number),
serialNumber);
fwInfo.fwVersion = null;
fwInfo.fwVersion2 = null;
evaluateGBDeviceEvent(fwInfo); evaluateGBDeviceEvent(fwInfo);
return true; return true;
} }
@@ -405,13 +464,48 @@ public class G1SideManager {
return true; return true;
} }
private boolean handleDeviceActionPayload(byte[] payload) { private boolean handleDeviceEventPayload(byte[] payload) {
LOG.debug("Device Action payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload)); switch (G1Communications.DeviceEvent.getEventId(payload)) {
case G1Constants.DeviceEventId.GLASSES_CHARGING:
updateBatteryState(
G1Communications.DeviceEvent.getValue(payload) == 0x01
? BatteryState.BATTERY_CHARGING
: BatteryState.BATTERY_NORMAL);
break;
case G1Constants.DeviceEventId.GLASSES_SIDE_BATTERY_LEVEL:
updateBatteryLevel(G1Communications.DeviceEvent.getValue(payload));
break;
case G1Constants.DeviceEventId.CASE_CHARGING:
updateBatteryState(
G1Communications.DeviceEvent.getValue(payload) == 0x01
? BatteryState.BATTERY_CHARGING
: BatteryState.BATTERY_NORMAL,
G1Constants.CASE_BATTERY_INDEX);
break;
case G1Constants.DeviceEventId.CASE_BATTERY_LEVEL:
updateBatteryLevel(G1Communications.DeviceEvent.getValue(payload),
G1Constants.CASE_BATTERY_INDEX);
break;
case G1Constants.DeviceEventId.GLASSES_NOT_WORN_NO_CASE:
updateBatteryState(BatteryState.NO_BATTERY, G1Constants.CASE_BATTERY_INDEX);
break;
default:
LOG.debug("Device Event on side {}: {}", mySide.getDeviceIndex(),
Logging.formatBytes(payload));
return false;
}
return true; return true;
} }
private boolean handleDashboardPayload(byte[] payload) { private boolean handleDebugLogPayload(byte[] payload) {
LOG.debug("Dashboard payload on side {}: {}", mySide.getDeviceIndex(), Logging.formatBytes(payload)); // Use the local boolean so that we aren't constantly committing the same value to the prefs
if (!this.debugEnabled) {
this.debugEnabled = true;
// Mark the pref as enabled so that the Setting UI reflects the true state.
getDevicePrefs().getPreferences().edit().putBoolean(
DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, this.debugEnabled).apply();
}
LOG.info("{}: {}", mySide, G1Communications.DebugLog.getMessage(payload));
return true; return true;
} }
} }

View File

@@ -0,0 +1,40 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Back fill -->
<path
android:pathData="M2,11h20"
android:strokeWidth="12"
android:strokeColor="#000000"
android:strokeAlpha=".3" />
<!-- Side Lines -->
<path
android:strokeLineCap="round"
android:strokeWidth="2"
android:pathData="
M2,6v12
M22,6v12"
android:strokeColor="#000000"/>
<!-- Horizontal Lines -->
<path
android:fillType="evenOdd"
android:pathData="
M2,17 L22,17 L22,19 L2,19 Z
M2,13.5 L22,13.5 L22,14.5 L2,14.5 Z
M2,5 L22,5 L22,7 L2,7 Z
"
android:fillColor="#000000"/>
<!-- Center LED -->
<path
android:strokeLineCap="round"
android:strokeWidth=".5"
android:pathData="M11.5,15.5 h1"
android:strokeColor="#000000"/>
</vector>

View File

@@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Back fill -->
<path
android:fillType="evenOdd"
android:fillAlpha=".3"
android:pathData="
M2,5h20v13H2V6z
M11,18v-5.5H9L13,5v5.5h2L11,18z"
android:fillColor="#000000" />
<!-- Side Lines -->
<path
android:strokeLineCap="round"
android:strokeWidth="2"
android:pathData="
M2,6v12
M22,6v12"
android:strokeColor="#000000"/>
<!-- Horizontal Lines -->
<path
android:fillType="evenOdd"
android:pathData="
M2,17 L22,17 L22,19 L2,19 Z
M13,5 L13,7 L11.935,7 Z
M2,13.5 L22,13.5 L22,14.5 L2,14.5 Z
M11,13.5 L13.4,13.5 L12.86,14.5 L11,14.5 Z
M2,5 L22,5 L22,7 L2,7 Z
M11,18 L11,17 L11.533,17 Z
"
android:fillColor="#000000"/>
<!-- Center LED -->
<path
android:pathData="
M 12.5 15.75 A 0.25 0.25 0 0 0 12.5 15.25 Z
M12.465,15.25 L12.2,15.75 L12.5,15.75 L12.5,15.25 Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:alpha=".3"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Side Lines -->
<path
android:strokeLineCap="round"
android:strokeWidth="2"
android:pathData="
M2,6v12
M22,6v12"
android:strokeColor="#000000"/>
<!-- Horizontal Lines -->
<path
android:pathData="
M2,17 L22,17 L22,19 L2,19 Z
M2,13.5 L22,13.5 L22,14.5 L2,14.5 Z
M2,5 L22,5 L22,7 L2,7 Z
"
android:fillColor="#000000"/>
<!-- Center LED -->
<path
android:strokeLineCap="round"
android:strokeWidth=".5"
android:pathData="M11.5,15.5 h1"
android:strokeColor="#000000"/>
</vector>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_even_realities_g1_case"
android:maxLevel="10" />
<item
android:drawable="@drawable/ic_even_realities_g1_case"
android:maxLevel="30" />
<item
android:drawable="@drawable/ic_even_realities_g1_case"
android:maxLevel="60" />
<item
android:drawable="@drawable/ic_even_realities_g1_case"
android:maxLevel="80" />
<item
android:drawable="@drawable/ic_even_realities_g1_case"
android:maxLevel="100" />
<item
android:drawable="@drawable/ic_even_realities_g1_case_charging"
android:maxLevel="110" />
<item
android:drawable="@drawable/ic_even_realities_g1_case_charging"
android:maxLevel="130" />
<item
android:drawable="@drawable/ic_even_realities_g1_case_charging"
android:maxLevel="160" />
<item
android:drawable="@drawable/ic_even_realities_g1_case_charging"
android:maxLevel="180" />
<item
android:drawable="@drawable/ic_even_realities_g1_case_charging"
android:maxLevel="200" />
<item
android:drawable="@drawable/ic_even_realities_g1_case_unknown"
android:maxLevel="300" />
</level-list>

View File

@@ -3330,6 +3330,8 @@
<string name="pref_app_logs_summary">Enable logs from watch apps</string> <string name="pref_app_logs_summary">Enable logs from watch apps</string>
<string name="pref_app_logs_start_summary">Start logging from watch apps</string> <string name="pref_app_logs_start_summary">Start logging from watch apps</string>
<string name="pref_app_logs_stop_summary">Stop logging from watch apps</string> <string name="pref_app_logs_stop_summary">Stop logging from watch apps</string>
<string name="pref_device_logs_summary">Enable logging from the device</string>
<string name="pref_device_logs_title">Device Logs</string>
<string name="pref_app_connection_duration">App connection duration</string> <string name="pref_app_connection_duration">App connection duration</string>
<string name="title">Title</string> <string name="title">Title</string>
<string name="description">Description</string> <string name="description">Description</string>
@@ -3968,4 +3970,13 @@
<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_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_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> <string name="pref_even_realities_g1_screen_calibrate_angle_summary">Zeroes the angle of the glasses, look straight ahead and tap here</string>
<string name="even_realities_frame_shape_G1A">G1A/Round</string>
<string name="even_realities_frame_shape_G1B">G1B/Square</string>
<string name="even_realities_frame_color_grey">Grey</string>
<string name="even_realities_frame_color_brown">Brown</string>
<string name="even_realities_frame_color_green">Green</string>
<string name="even_realities_frame_description">%1s %2s (%3s: %4s)</string>
<string name="even_realities_left_lens">Left Lens</string>
<string name="even_realities_right_lens">Right Lens</string>
</resources> </resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_developer_mode"
android:key="device_logs_enabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_device_logs_summary"
android:title="@string/pref_device_logs_title" />
</androidx.preference.PreferenceScreen>