mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Even Realites G1: Support Notifications and improve disconnections
This change helps improve the connection experience when GadgetBridge is working as a background task. The glasses must be send a heartbeat packet every 32 seconds, otherwise they will disconnect. When GadgetBridge is running in the background there is a 15-20 second delay between the background handler waking up. This means that when the wake up time is sceduled for 30 seconds, it often won't be woken up for 45-50 seconds leading to almost immediate disconnect after the user turns off the screen. The official app gets around this by using a 5 second delay leading to ~25 seconds of delay in sleep mode, but this over sends when the phone is awake. Instead of doing that, I maintain the 28 second delay but I change the background thread to a higher priority so that it more consistently wakes up within th 32 second BLE limit. Also added support for Notifications being sent to the glasses.
This commit is contained in:
+1
@@ -293,6 +293,7 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_NOTIFICATION_WAKE_ON_OPEN = "notification_wake_on_open";
|
||||
public static final String PREF_AUTOREMOVE_NOTIFICATIONS = "autoremove_notifications";
|
||||
public static final String PREF_SCREEN_ON_ON_NOTIFICATIONS = "screen_on_on_notifications";
|
||||
public static final String PREF_SCREEN_ON_ON_NOTIFICATIONS_TIMEOUT = "screen_on_on_notifications_timeout";
|
||||
public static final String PREF_WORKOUT_KEEP_SCREEN_ON = "workout_keep_screen_on";
|
||||
public static final String PREF_OPERATING_SOUNDS = "operating_sounds";
|
||||
public static final String PREF_KEY_VIBRATION = "key_vibration";
|
||||
|
||||
+1
@@ -659,6 +659,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_CASIO_ALERT_CALENDAR);
|
||||
addPreferenceHandlerFor(PREF_CASIO_ALERT_OTHER);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_ON_ON_NOTIFICATIONS);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_ON_ON_NOTIFICATIONS_TIMEOUT);
|
||||
addPreferenceHandlerFor(PREF_WORKOUT_KEEP_SCREEN_ON);
|
||||
addPreferenceHandlerFor(PREF_KEY_VIBRATION);
|
||||
addPreferenceHandlerFor(PREF_OPERATING_SOUNDS);
|
||||
|
||||
+3
@@ -198,8 +198,11 @@ public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||
if (device.isConnected()) {
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_screen_on_on_notifications);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_screen_on_on_notifications_timeout);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_even_realities_g1_display);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_timeformat);
|
||||
|
||||
final List<Integer> developer =
|
||||
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
|
||||
developer.add(R.xml.devicesettings_header_system);
|
||||
|
||||
+308
-9
@@ -1,12 +1,21 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
|
||||
@@ -73,7 +82,7 @@ public class G1Communications {
|
||||
return hasRetryRemaining();
|
||||
}
|
||||
|
||||
public byte[] getResponsePayload(){
|
||||
public byte[] getResponsePayload() {
|
||||
if (responsePayload == null) {
|
||||
throw new RuntimeException("Null payload. Did you call waitForPayload()?");
|
||||
}
|
||||
@@ -97,6 +106,79 @@ public class G1Communications {
|
||||
public abstract String getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Certain payloads are too large for one packet, so this class is a simple extension of the
|
||||
* CommandHandler that allows the subclass to send multiple packets.
|
||||
* This works by forcing the caller to pass in a callback to the "send" function they are using
|
||||
* and the ChunkCommandHandler will intercept calls to the Command callback to send the next
|
||||
* chunk in the packet. Once the response for the last chunk is sent, the passed in callback
|
||||
* will be sent.
|
||||
*/
|
||||
public abstract static class ChunkedCommandHandler extends CommandHandler {
|
||||
private final Consumer<CommandHandler> sendCallback;
|
||||
private byte currentChunk;
|
||||
protected final byte[] payload;
|
||||
protected final byte chunkCount;
|
||||
|
||||
public ChunkedCommandHandler(Consumer<CommandHandler> sendCallback, Function<byte[], Boolean> callback, byte[] payload) {
|
||||
super(true, callback);
|
||||
this.sendCallback = sendCallback;
|
||||
this.currentChunk = 0;
|
||||
this.payload = payload;
|
||||
int maxChunkSize = G1Constants.MAX_PACKET_SIZE_BYTES - getHeaderSize();
|
||||
this.chunkCount = (byte)((this.payload.length / maxChunkSize) + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
final public Function<byte[], Boolean> getCallback() {
|
||||
if (currentChunk < chunkCount) {
|
||||
// Return the callback which sends the next chunk.
|
||||
return this::sendNextChunk;
|
||||
} else {
|
||||
// Now that all the chunks have been received, return the user callback.
|
||||
return super.getCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
final public byte[] serialize() {
|
||||
// Calculate the size, begin and end of the chunk.
|
||||
int maxPayloadSize = G1Constants.MAX_PACKET_SIZE_BYTES - getHeaderSize();
|
||||
int chunkBegin = this.currentChunk * maxPayloadSize;
|
||||
int chunkEnd = Math.min(this.payload.length,
|
||||
(this.currentChunk + 1) * maxPayloadSize);
|
||||
int payloadSize = chunkEnd - chunkBegin;
|
||||
|
||||
// Create the packet with space for the header.
|
||||
byte[] packet = new byte[getHeaderSize() + payloadSize];
|
||||
|
||||
// Let the subclass write the header.
|
||||
writeHeader(this.currentChunk, this.chunkCount, packet);
|
||||
|
||||
// Copy the chunk of the payload into the packet.
|
||||
System.arraycopy(this.payload, chunkBegin, packet, getHeaderSize(), payloadSize);
|
||||
|
||||
// Advance the chunk.
|
||||
currentChunk++;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
final public boolean responseMatches(byte[] payload) {
|
||||
return chunkMatches(currentChunk, payload);
|
||||
}
|
||||
|
||||
private boolean sendNextChunk(byte[] payload) {
|
||||
sendCallback.accept(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract boolean chunkMatches(byte currentChunk, byte[] payload);
|
||||
protected abstract void writeHeader(byte currentChunk, byte chunkCount, byte[] chunk);
|
||||
protected abstract int getHeaderSize();
|
||||
}
|
||||
|
||||
public static class CommandSendInit extends CommandHandler {
|
||||
public CommandSendInit() {
|
||||
super(true, null);
|
||||
@@ -191,7 +273,7 @@ public class G1Communications {
|
||||
return "get_battery_info";
|
||||
}
|
||||
|
||||
public static byte getBatteryPercent(byte[] payload) {
|
||||
public static int getBatteryPercent(byte[] payload) {
|
||||
return payload[2];
|
||||
}
|
||||
}
|
||||
@@ -239,8 +321,12 @@ public class G1Communications {
|
||||
if (weatherInfo != null) {
|
||||
this.weatherIcon = G1Constants.fromOpenWeatherCondition(weatherInfo.getCurrentConditionCode());
|
||||
// Convert sunny to a moon if the current time stamp is between sunrise and sunset.
|
||||
if (timeMilliseconds / 1000 >= weatherInfo.getSunSet() &&
|
||||
this.weatherIcon == G1Constants.WeatherId.SUNNY) {
|
||||
// At midnight, the sunrise and sunset time move forward.
|
||||
boolean isNight = (weatherInfo.getTimestamp() >= weatherInfo.getSunSet() &&
|
||||
weatherInfo.getTimestamp() >= weatherInfo.getSunRise()) ||
|
||||
(weatherInfo.getTimestamp() < weatherInfo.getSunRise() &&
|
||||
weatherInfo.getTimestamp() < weatherInfo.getSunSet());
|
||||
if (this.weatherIcon == G1Constants.WeatherId.SUNNY && isNight) {
|
||||
this.weatherIcon = G1Constants.WeatherId.NIGHT;
|
||||
}
|
||||
// Convert Kelvin -> Celsius.
|
||||
@@ -421,11 +507,11 @@ public class G1Communications {
|
||||
return "get_display_settings";
|
||||
}
|
||||
|
||||
public static byte getHeight(byte[] payload) {
|
||||
public static int getHeight(byte[] payload) {
|
||||
return payload[2];
|
||||
}
|
||||
|
||||
public static byte getDepth(byte[] payload) {
|
||||
public static int getDepth(byte[] payload) {
|
||||
return payload[3];
|
||||
}
|
||||
}
|
||||
@@ -492,7 +578,7 @@ public class G1Communications {
|
||||
return "get_head_gesture_settings";
|
||||
}
|
||||
|
||||
public static byte getActivationAngle(byte[] payload) {
|
||||
public static int getActivationAngle(byte[] payload) {
|
||||
return payload[2];
|
||||
}
|
||||
}
|
||||
@@ -547,7 +633,7 @@ public class G1Communications {
|
||||
return "get_brightness_settings";
|
||||
}
|
||||
|
||||
public static byte getBrightnessLevel(byte[] payload) {
|
||||
public static int getBrightnessLevel(byte[] payload) {
|
||||
return payload[2];
|
||||
}
|
||||
|
||||
@@ -685,7 +771,220 @@ public class G1Communications {
|
||||
}
|
||||
|
||||
public static String getSerialNumber(byte[] payload) {
|
||||
return new String(payload, 2, 16, StandardCharsets.US_ASCII);
|
||||
return new String(payload, 2, 14, StandardCharsets.US_ASCII);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandGetNotificationDisplaySettings extends CommandHandler {
|
||||
public CommandGetNotificationDisplaySettings(Function<byte[], Boolean> callback) {
|
||||
super(true, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[] { G1Constants.CommandId.GET_NOTIFICATION_DISPLAY_SETTINGS.id };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
return payload.length >= 4 && payload[0] == G1Constants.CommandId.GET_NOTIFICATION_DISPLAY_SETTINGS.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "get_notification_display_settings";
|
||||
}
|
||||
|
||||
public static boolean isEnabled(byte[] payload) {
|
||||
return payload[2] == 0x01;
|
||||
}
|
||||
public static int getTimeout(byte[] payload) {
|
||||
return payload[3];
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandSetNotificationDisplaySettings extends CommandHandler {
|
||||
private final boolean enable;
|
||||
private final byte timeout;
|
||||
public CommandSetNotificationDisplaySettings(boolean enable, byte timeout) {
|
||||
super(true, null);
|
||||
this.enable = enable;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[] {
|
||||
G1Constants.CommandId.SET_NOTIFICATION_DISPLAY_SETTINGS.id,
|
||||
enable ? 0x01 : (byte)0x00,
|
||||
timeout
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
return payload.length >= 2 && payload[0] == G1Constants.CommandId.SET_NOTIFICATION_DISPLAY_SETTINGS.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "set_notification_display_settings_" + (enable ? "enabled" : "disabled") + "_" + timeout;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandSetAppNotificationSettings extends ChunkedCommandHandler {
|
||||
public CommandSetAppNotificationSettings(Consumer<CommandHandler> sendCallback,
|
||||
List<Pair<String, String>> appIdentifiers,
|
||||
boolean enableCalendar,
|
||||
boolean enableCalls,
|
||||
boolean enableSMS) {
|
||||
super(sendCallback, null,
|
||||
generatePayload(appIdentifiers, enableCalendar, enableCalls, enableSMS));
|
||||
}
|
||||
|
||||
private static byte[] generatePayload(List<Pair<String, String>> appIdentifiers,
|
||||
boolean enableCalendar,
|
||||
boolean enableCalls,
|
||||
boolean enableSMS) {
|
||||
try {
|
||||
JSONObject appJson = new JSONObject();
|
||||
JSONArray appList = new JSONArray();
|
||||
for (Pair<String, String> appInfo : appIdentifiers) {
|
||||
JSONObject app = new JSONObject();
|
||||
app.put("id", appInfo.first);
|
||||
app.put("name", appInfo.second);
|
||||
appList.put(app);
|
||||
}
|
||||
appJson.put("list", appList);
|
||||
appJson.put("enable", true);
|
||||
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("calendar_enable", enableCalendar);
|
||||
json.put("call_enable", enableCalls);
|
||||
json.put("msg_enable", enableSMS);
|
||||
json.put("ios_mail_enable", false);
|
||||
json.put("app", appJson);
|
||||
|
||||
// Need to allocate one larger in order to null terminate.
|
||||
String jsonString = json.toString();
|
||||
byte[] bytes = new byte[jsonString.length() + 1];
|
||||
System.arraycopy(jsonString.getBytes(StandardCharsets.US_ASCII), 0, bytes, 0, jsonString.length());
|
||||
bytes[jsonString.length()] = 0;
|
||||
return bytes;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean chunkMatches(byte chunk, byte[] payload) {
|
||||
return payload.length >= 1 && payload[0] == G1Constants.CommandId.SET_NOTIFICATION_APP_SETTINGS.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHeader(byte currentChunk, byte chunkCount, byte[] chunk) {
|
||||
chunk[0] = G1Constants.CommandId.SET_NOTIFICATION_APP_SETTINGS.id;
|
||||
chunk[1] = chunkCount;
|
||||
chunk[2] = currentChunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHeaderSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "set_notification_app_settings";
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandSendNotification extends ChunkedCommandHandler {
|
||||
private final int messageId;
|
||||
|
||||
public CommandSendNotification(Consumer<CommandHandler> sendCallback, NotificationSpec notificationSpec) {
|
||||
super(sendCallback, null, generatePayload(notificationSpec));
|
||||
this.messageId = notificationSpec.getId();
|
||||
}
|
||||
|
||||
private static byte[] generatePayload(NotificationSpec notificationSpec) {
|
||||
|
||||
try {
|
||||
JSONObject notificationJson = new JSONObject();
|
||||
notificationJson.put("msg_id", notificationSpec.getId());
|
||||
notificationJson.put("action", 0);
|
||||
notificationJson.put("app_identifier", notificationSpec.sourceAppId.substring(0,Math.min(notificationSpec.sourceAppId.length(), 31)));
|
||||
if (notificationSpec.title != null) notificationJson.put("title", notificationSpec.title);
|
||||
if (notificationSpec.subject != null) notificationJson.put("subtitle", notificationSpec.subject);
|
||||
if (notificationSpec.body != null) notificationJson.put("message", notificationSpec.body);
|
||||
notificationJson.put("time_s", notificationSpec.when / 1000);
|
||||
notificationJson.put("date", new Date(notificationSpec.when).toString());
|
||||
notificationJson.put("display_name", notificationSpec.sourceName);
|
||||
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("ncs_notification", notificationJson);
|
||||
|
||||
// Need to allocate one larger in order to null terminate.
|
||||
String jsonString = json.toString();
|
||||
byte[] bytes = new byte[jsonString.length() + 1];
|
||||
System.arraycopy(jsonString.getBytes(StandardCharsets.US_ASCII), 0, bytes, 0, jsonString.length());
|
||||
bytes[jsonString.length()] = 0;
|
||||
return bytes;
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean chunkMatches(byte chunk, byte[] payload) {
|
||||
return payload.length >= 1 && payload[0] == G1Constants.CommandId.SEND_NOTIFICATION.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeHeader(byte currentChunk, byte chunkCount, byte[] chunk) {
|
||||
chunk[0] = G1Constants.CommandId.SEND_NOTIFICATION.id;
|
||||
chunk[1] = 0x0;
|
||||
chunk[2] = chunkCount;
|
||||
chunk[3] = currentChunk;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHeaderSize() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "send_notification_" + messageId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandSendClearNotification extends CommandHandler {
|
||||
private final int messageId;
|
||||
|
||||
public CommandSendClearNotification(int messageId) {
|
||||
super(true, null);
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
byte[] packet = new byte[] {
|
||||
G1Constants.CommandId.SEND_CLEAR_NOTIFICATION.id,
|
||||
0x0,0x0,0x0,0x0
|
||||
};
|
||||
BLETypeConversions.writeUint32BE(packet, 1, messageId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean responseMatches(byte[] payload) {
|
||||
return payload.length >= 1 && payload[0] == G1Constants.CommandId.SEND_CLEAR_NOTIFICATION.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "send_clear_notification_" + messageId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -1,5 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class G1Constants {
|
||||
@@ -10,12 +12,26 @@ public class G1Constants {
|
||||
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_RX =
|
||||
UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final int MTU = 251;
|
||||
// The MTU is set to 251, which suggests there should be a larger packet size, however,
|
||||
// (excluding headers) the glasses only ever send a payload of 180 bytes.
|
||||
// TODO: Try out a larger MTU and a larger packet size to see if these numbers are flexible.
|
||||
// It could be that a 180 byte buffer is allocated in the FW for the payload and sending
|
||||
// more will cause the glasses to crash.
|
||||
public static final int MAX_PACKET_SIZE_BYTES = 180;
|
||||
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 int CASE_BATTERY_INDEX = 2;
|
||||
public static final String INTENT_TOGGLE_SILENT_MODE = "nodomain.freeyourgadget.gadgetbridge.evenrealities.silent_mode";
|
||||
// The glasses have a filter based on a whitelist of apps and it will only display
|
||||
// notifications from apps that are on that list. GadgetBridge already filters the
|
||||
// notifications before sending to the glasses and GadgetBridge can work as either a
|
||||
// blacklist or a whitelist. To get around this, a fixed application id is used since the
|
||||
// glasses don't display it, it doesn't matter to the user experience and it allows all of
|
||||
// the notification filtering to happen on the phone side.
|
||||
public static final Pair<String, String>
|
||||
FIXED_NOTIFICATION_APP_ID = new Pair<>("nodomain.freeyourgadget.gadget", "Name");
|
||||
|
||||
// Extract the L or R at the end of the device prefix.
|
||||
public static Side getSideFromFullName(String deviceName) {
|
||||
@@ -90,7 +106,6 @@ public class G1Constants {
|
||||
|
||||
// TODO: Lifted these from a different project, some of them are wrong.
|
||||
public enum CommandId {
|
||||
NOTIFICATION_CONFIG((byte) 0x04),
|
||||
DASHBOARD_CONFIG((byte) 0x06),
|
||||
SYNC_SEQUENCE((byte) 0x22), // 0x05
|
||||
DASHBOARD_SHOWN((byte) 0x22), // 0x0A
|
||||
@@ -112,7 +127,12 @@ public class G1Constants {
|
||||
SET_BRIGHTNESS_SETTINGS((byte) 0x01),
|
||||
GET_WEAR_DETECTION_SETTINGS((byte) 0x3A),
|
||||
SET_WEAR_DETECTION_SETTINGS((byte) 0x27),
|
||||
GET_SERIAL_NUMBER((byte) 0x34);
|
||||
GET_SERIAL_NUMBER((byte) 0x34),
|
||||
GET_NOTIFICATION_DISPLAY_SETTINGS((byte) 0x3C),
|
||||
SET_NOTIFICATION_DISPLAY_SETTINGS((byte) 0x4F),
|
||||
SET_NOTIFICATION_APP_SETTINGS((byte) 0x04),
|
||||
SEND_NOTIFICATION((byte) 0x4B),
|
||||
SEND_CLEAR_NOTIFICATION((byte) 0x4C);
|
||||
|
||||
final public byte id;
|
||||
|
||||
|
||||
+43
-15
@@ -8,7 +8,8 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Process;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -21,18 +22,17 @@ import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.weather.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEMultiDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
@@ -50,7 +50,8 @@ 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 final HandlerThread backgroundThread = new HandlerThread("even_g1_background_thread", Process.THREAD_PRIORITY_DEFAULT);
|
||||
private Handler backgroundTasksHandler = null;
|
||||
private BroadcastReceiver intentReceiver = null;
|
||||
private final Object lensSkewLock = new Object();
|
||||
private G1SideManager leftSide = null;
|
||||
@@ -98,6 +99,11 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
super.setContext(device, btAdapter, context);
|
||||
}
|
||||
|
||||
if (backgroundTasksHandler == null) {
|
||||
backgroundThread.start();
|
||||
backgroundTasksHandler = new Handler(backgroundThread.getLooper());
|
||||
}
|
||||
|
||||
// Register to receive silent mode intent calls from the UI.
|
||||
if (intentReceiver == null) {
|
||||
intentReceiver = new IntentReceiver();
|
||||
@@ -191,8 +197,14 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
@Override
|
||||
public void dispose() {
|
||||
synchronized (ConnectionMonitor) {
|
||||
// Remove all background tasks.
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(null);
|
||||
if (backgroundTasksHandler != null) {
|
||||
// Remove all background tasks.
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
// Shutdown the background handler.
|
||||
backgroundThread.quitSafely();
|
||||
backgroundTasksHandler = null;
|
||||
}
|
||||
|
||||
// Kill both sides.
|
||||
leftSide = null;
|
||||
@@ -223,20 +235,18 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
// support and we don't want a hard dependency on G1DeviceSupport in G1SideManager.
|
||||
Callable<BtLEQueue> getQueue = () -> this.getQueue(deviceIdx);
|
||||
Callable<GBDevice> getDevice = () -> this.getDevice(deviceIdx);
|
||||
Function<GBDeviceEvent, Void> handleEvent = (GBDeviceEvent event) -> {
|
||||
this.evaluateGBDeviceEvent(event);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Create the desired side.
|
||||
if (deviceIdx == G1Constants.Side.LEFT.getDeviceIndex()) {
|
||||
leftSide = new G1SideManager(G1Constants.Side.LEFT, backgroundTasksHandler, getQueue,
|
||||
getDevice, handleEvent, this::getDevicePrefs, rx, tx,
|
||||
getDevice, this::evaluateGBDeviceEvent,
|
||||
this::getDevicePrefs, rx, tx,
|
||||
this::createTransactionBuilder);
|
||||
return leftSide;
|
||||
} else if (deviceIdx == G1Constants.Side.RIGHT.getDeviceIndex()) {
|
||||
rightSide = new G1SideManager(G1Constants.Side.RIGHT, backgroundTasksHandler, getQueue,
|
||||
getDevice, handleEvent, this::getDevicePrefs, rx, tx,
|
||||
getDevice, this::evaluateGBDeviceEvent,
|
||||
this::getDevicePrefs, rx, tx,
|
||||
this::createTransactionBuilder);
|
||||
return rightSide;
|
||||
}
|
||||
@@ -340,8 +350,13 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
if (leftSide == null || rightSide == null)
|
||||
return;
|
||||
|
||||
boolean use12HourFormat = getDevicePrefs().getTimeFormat()
|
||||
.equals(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
|
||||
// In FW v1.6.0, they flipped this boolean.
|
||||
boolean use12HourFormat =
|
||||
getDevicePrefs().getTimeFormat()
|
||||
.equals(getDevice().getFirmwareVersion().compareTo("1.6.0") >= 0
|
||||
? DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H
|
||||
: DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_12H);
|
||||
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
long currentMilliseconds = c.getTimeInMillis();
|
||||
long tzOffset = TimeZone.getDefault().getOffset(currentMilliseconds);
|
||||
@@ -443,4 +458,17 @@ public class G1DeviceSupport extends AbstractBTLEMultiDeviceSupport {
|
||||
public void onSetTime() {
|
||||
onSetTimeOrWeather();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
// Rewrite the App Id to the fixed one used for all notifications. See the comment in
|
||||
// G1Constants.java for more information.
|
||||
notificationSpec.sourceAppId = G1Constants.FIXED_NOTIFICATION_APP_ID.first;
|
||||
leftSide.send(new G1Communications.CommandSendNotification(leftSide::send, notificationSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
leftSide.send(new G1Communications.CommandSendClearNotification(id));
|
||||
}
|
||||
}
|
||||
|
||||
+62
-27
@@ -1,7 +1,8 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.SharedPreferences;
|
||||
import android.icu.util.Calendar;
|
||||
import android.icu.util.TimeZone;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
|
||||
@@ -10,9 +11,11 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
@@ -21,6 +24,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryIncrementalInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
@@ -42,7 +46,7 @@ public class G1SideManager {
|
||||
private final Handler backgroundTasksHandler;
|
||||
private final Callable<BtLEQueue> getQueueHandler;
|
||||
private final Callable<GBDevice> getDeviceHandler;
|
||||
private final Function<GBDeviceEvent, Void> sendEventHandler;
|
||||
private final Consumer<GBDeviceEvent> sendEventHandler;
|
||||
private final Callable<DevicePrefs> getPrefsHandler;
|
||||
private final BiFunction<String, Integer, TransactionBuilder> createTransactionBuilder;
|
||||
private final BluetoothGattCharacteristic rx;
|
||||
@@ -55,10 +59,11 @@ public class G1SideManager {
|
||||
private boolean isSilentModeEnabled;
|
||||
private GBDevice.State connectingState;
|
||||
private boolean debugEnabled;
|
||||
private long lastHeartBeatTime;
|
||||
|
||||
public G1SideManager(G1Constants.Side mySide, Handler backgroundTasksHandler,
|
||||
Callable<BtLEQueue> getQueue, Callable<GBDevice> getDevice,
|
||||
Function<GBDeviceEvent, Void> sendEvent, Callable<DevicePrefs> getPrefs,
|
||||
Consumer<GBDeviceEvent> sendEvent, Callable<DevicePrefs> getPrefs,
|
||||
BluetoothGattCharacteristic rx, BluetoothGattCharacteristic tx,
|
||||
BiFunction<String, Integer, TransactionBuilder> createTransactionBuilder) {
|
||||
this.mySide = mySide;
|
||||
@@ -76,9 +81,13 @@ public class G1SideManager {
|
||||
};
|
||||
|
||||
this.heartBeatRunner = () -> {
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
long currentMilliseconds = c.getTimeInMillis();
|
||||
LOG.info("Side {}: {}ms since the last heartbeat", mySide, currentMilliseconds - lastHeartBeatTime);
|
||||
lastHeartBeatTime = currentMilliseconds;
|
||||
if (getDevice().isConnected()) {
|
||||
// We can send any command as a heart beat. The official app uses this one.
|
||||
send(new G1Communications.CommandGetSilentModeSettings(null));
|
||||
send(new G1Communications.CommandSendHeartBeat(getNextSequence()));
|
||||
scheduleHeatBeat();
|
||||
} else {
|
||||
// Don't reschedule if the device is disconnected.
|
||||
@@ -101,6 +110,9 @@ public class G1SideManager {
|
||||
this.isSilentModeEnabled = false;
|
||||
this.connectingState = GBDevice.State.CONNECTED;
|
||||
this.debugEnabled = false;
|
||||
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
this.lastHeartBeatTime = c.getTimeInMillis();
|
||||
|
||||
}
|
||||
|
||||
private BtLEQueue getQueue() {
|
||||
@@ -120,7 +132,7 @@ public class G1SideManager {
|
||||
}
|
||||
|
||||
private void evaluateGBDeviceEvent(GBDeviceEvent event) {
|
||||
sendEventHandler.apply(event);
|
||||
sendEventHandler.accept(event);
|
||||
}
|
||||
|
||||
private DevicePrefs getDevicePrefs() {
|
||||
@@ -171,10 +183,15 @@ public class G1SideManager {
|
||||
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));
|
||||
sendInTransaction(transaction, new G1Communications.CommandGetSerialNumber(this::handleSerialNumberPayload));
|
||||
transaction.queue();
|
||||
|
||||
// Sent to the left only and it's own transaction, this is a large piece of data and can
|
||||
// cause GB to time out the initialization and get stuck in a loop.
|
||||
send(new G1Communications.CommandSetAppNotificationSettings(
|
||||
this::send, List.of(G1Constants.FIXED_NOTIFICATION_APP_ID),
|
||||
false, false, false));
|
||||
}
|
||||
|
||||
public void postInitializeRight() {
|
||||
@@ -186,7 +203,9 @@ public class G1SideManager {
|
||||
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.CommandGetDisplaySettings(this::handleDisplaySettingsPayload));
|
||||
sendInTransaction(transaction, new G1Communications.CommandGetWearDetectionSettings(this::handleWearDetectionSettingsPayload));
|
||||
sendInTransaction(transaction, new G1Communications.CommandGetNotificationDisplaySettings(this::handleNotificationDisplaySettingsPayload));
|
||||
transaction.queue();
|
||||
}
|
||||
|
||||
@@ -220,7 +239,12 @@ public class G1SideManager {
|
||||
this.debugEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_DEVICE_LOGS_TOGGLE, false);
|
||||
send(new G1Communications.CommandSetDebugLogSettings(this.debugEnabled));
|
||||
break;
|
||||
|
||||
case DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS:
|
||||
case DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS_TIMEOUT:
|
||||
send(new G1Communications.CommandSetNotificationDisplaySettings(
|
||||
prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS, true),
|
||||
(byte)prefs.getInt(DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS_TIMEOUT, 5)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,42 +456,53 @@ public class G1SideManager {
|
||||
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));
|
||||
GBDeviceEventUpdatePreferences prefsEvent = new GBDeviceEventUpdatePreferences();
|
||||
prefsEvent.preferences.put(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();
|
||||
prefsEvent.preferences.put(DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_DEPTH,
|
||||
G1Communications.CommandGetDisplaySettings.getDepth(payload) - 1);
|
||||
evaluateGBDeviceEvent(prefsEvent);
|
||||
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();
|
||||
GBDeviceEventUpdatePreferences prefsEvent = new GBDeviceEventUpdatePreferences();
|
||||
prefsEvent.preferences.put(
|
||||
DeviceSettingsPreferenceConst.PREF_EVEN_REALITIES_SCREEN_ACTIVATION_ANGLE,
|
||||
G1Communications.CommandGetHeadGestureSettings.getActivationAngle(payload));
|
||||
evaluateGBDeviceEvent(prefsEvent);
|
||||
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();
|
||||
GBDeviceEventUpdatePreferences prefsEvent = new GBDeviceEventUpdatePreferences();
|
||||
prefsEvent.preferences.put(DeviceSettingsPreferenceConst.PREF_SCREEN_AUTO_BRIGHTNESS,
|
||||
G1Communications.CommandGetBrightnessSettings.isAutoBrightnessEnabled(payload));
|
||||
prefsEvent.preferences.put(DeviceSettingsPreferenceConst.PREF_SCREEN_BRIGHTNESS,
|
||||
G1Communications.CommandGetBrightnessSettings.getBrightnessLevel(payload));
|
||||
evaluateGBDeviceEvent(prefsEvent);
|
||||
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();
|
||||
GBDeviceEventUpdatePreferences prefsEvent = new GBDeviceEventUpdatePreferences();
|
||||
prefsEvent.preferences.put(DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE,
|
||||
G1Communications.CommandGetWearDetectionSettings.isEnabled(payload));
|
||||
evaluateGBDeviceEvent(prefsEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleNotificationDisplaySettingsPayload(byte[] payload) {
|
||||
GBDeviceEventUpdatePreferences prefsEvent = new GBDeviceEventUpdatePreferences();
|
||||
prefsEvent.preferences.put(DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS,
|
||||
G1Communications.CommandGetNotificationDisplaySettings.isEnabled(payload));
|
||||
prefsEvent.preferences.put(
|
||||
DeviceSettingsPreferenceConst.PREF_SCREEN_ON_ON_NOTIFICATIONS_TIMEOUT,
|
||||
Integer.toString(G1Communications.CommandGetNotificationDisplaySettings.getTimeout(payload)));
|
||||
evaluateGBDeviceEvent(prefsEvent);
|
||||
return true;
|
||||
}
|
||||
private boolean handleDeviceEventPayload(byte[] payload) {
|
||||
switch (G1Communications.DeviceEvent.getEventId(payload)) {
|
||||
case G1Constants.DeviceEventId.GLASSES_CHARGING:
|
||||
|
||||
@@ -390,7 +390,9 @@
|
||||
<string name="pref_title_autoremove_notifications">Autoremove dismissed notifications</string>
|
||||
<string name="pref_summary_autoremove_notifications">Notifications are automatically removed from the device when dismissed from the phone</string>
|
||||
<string name="pref_title_screen_on_on_notifications">Screen On on Notifications</string>
|
||||
<string name="pref_summary_screen_on_on_notifications">Turn on the band\'s screen when a notification arrives</string>
|
||||
<string name="pref_summary_screen_on_on_notifications">Turn on the device\'s screen when a notification arrives</string>
|
||||
<string name="pref_title_screen_on_on_notifications_timeout">Notification Display Time</string>
|
||||
<string name="pref_summary_screen_on_on_notifications_timeout">Delay %s before turning the display back off after a notification</string>
|
||||
<string name="pref_title_send_app_notifications">Send notifications</string>
|
||||
<string name="pref_summary_send_app_notifications">Send app notifications to the device</string>
|
||||
<string name="pref_title_pebble_privacy_mode">Privacy mode</string>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<ListPreference
|
||||
android:dependency="screen_on_on_notifications"
|
||||
android:defaultValue="5"
|
||||
android:entries="@array/screen_timeout"
|
||||
android:entryValues="@array/screen_timeout_values"
|
||||
android:key="screen_on_on_notifications_timeout"
|
||||
android:icon="@drawable/ic_hourglass_empty"
|
||||
android:summary="@string/pref_summary_screen_on_on_notifications_timeout"
|
||||
android:title="@string/pref_title_screen_on_on_notifications_timeout" />
|
||||
</androidx.preference.PreferenceScreen>
|
||||
@@ -209,4 +209,4 @@
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user