diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bd42dd7d8..e9d2fc015 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -387,6 +387,12 @@
+
+
createBLEScanFilters() {
+ ParcelUuid watch9Service = new ParcelUuid(Watch9Constants.UUID_SERVICE_WATCH9);
+ ScanFilter filter = new ScanFilter.Builder().setServiceUuid(watch9Service).build();
+ return Collections.singletonList(filter);
+ }
+
+ @NonNull
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String macAddress = candidate.getMacAddress().toUpperCase();
+ String deviceName = candidate.getName().toUpperCase();
+ if (candidate.supportsService(Watch9Constants.UUID_SERVICE_WATCH9)) {
+ return DeviceType.WATCH9;
+ } else if (macAddress.startsWith("1C:87:79")) {
+ return DeviceType.WATCH9;
+ } else if (deviceName.equals("WATCH 9")) {
+ return DeviceType.WATCH9;
+ }
+ return DeviceType.UNKNOWN;
+ }
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.WATCH9;
+ }
+
+ @Override
+ public int getBondingStyle(GBDevice deviceCandidate) {
+ return BONDING_STYLE_NONE;
+ }
+
+ @Nullable
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return Watch9PairingActivity.class;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return false;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return null;
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsScreenshots() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsAlarmConfiguration() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsSmartWakeup(GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return false;
+ }
+
+ @Override
+ public String getManufacturer() {
+ return "Lenovo";
+ }
+
+ @Override
+ public boolean supportsAppsManagement() {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsCalendarEvents() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsRealtimeData() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsWeather() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsFindDevice() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java
new file mode 100644
index 000000000..fe768304d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java
@@ -0,0 +1,113 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.watch9;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.v4.content.LocalBroadcastManager;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
+import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
+import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class Watch9PairingActivity extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(Watch9PairingActivity.class);
+
+ private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate";
+
+ private TextView message;
+ private GBDeviceCandidate deviceCandidate;
+
+ private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) {
+ GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ LOG.debug("pairing activity: device changed: " + device);
+ if (deviceCandidate.getMacAddress().equals(device.getAddress())) {
+ if (device.isInitialized()) {
+ pairingFinished();
+ } else if (device.isConnecting() || device.isInitializing()) {
+ LOG.info("still connecting/initializing device...");
+ }
+ }
+ }
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_watch9_pairing);
+
+ message = findViewById(R.id.watch9_pair_message);
+ Intent intent = getIntent();
+ deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
+ if (deviceCandidate == null && savedInstanceState != null) {
+ deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
+ }
+ if (deviceCandidate == null) {
+ Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
+ startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
+ finish();
+ return;
+ }
+ startPairing();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE);
+ }
+
+ @Override
+ protected void onDestroy() {
+ AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
+ super.onDestroy();
+ }
+
+ private void startPairing() {
+ message.setText(getString(R.string.pairing, deviceCandidate));
+
+ IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED);
+ LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter);
+
+ GBApplication.deviceService().disconnect();
+ GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
+ if (device != null) {
+ GBApplication.deviceService().connect(device, true);
+ } else {
+ GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR);
+ }
+ }
+
+ private void pairingFinished() {
+ AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver);
+
+ Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+
+ finish();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index 4486c739d..598704945 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -48,6 +48,7 @@ public enum DeviceType {
XWATCH(70, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_xwatch),
ZETIME(80, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_mykronoz_zetime),
ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115),
+ WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
index 32257f69a..10f3381d1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
@@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DeviceSupportFactory {
@@ -160,6 +161,9 @@ public class DeviceSupportFactory {
case ID115:
deviceSupport = new ServiceDeviceSupport(new ID115Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
+ case WATCH9:
+ deviceSupport = new ServiceDeviceSupport(new Watch9DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
+ break;
}
if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/Watch9DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/Watch9DeviceSupport.java
new file mode 100644
index 000000000..ba778cea5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/Watch9DeviceSupport.java
@@ -0,0 +1,605 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.watch9;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.support.annotation.IntRange;
+import android.support.v4.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
+import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
+import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
+import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
+import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.operations.InitOperation;
+import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
+
+public class Watch9DeviceSupport extends AbstractBTLEDeviceSupport {
+
+ private boolean needsAuth;
+ private int sequenceNumber = 0;
+ private boolean isCalibrationActive = false;
+
+ private byte ACK_CALIBRATION = 0;
+
+ private final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
+ private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
+
+ private static final Logger LOG = LoggerFactory.getLogger(Watch9DeviceSupport.class);
+
+ private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String broadcastAction = intent.getAction();
+ switch (broadcastAction) {
+ case Watch9Constants.ACTION_CALIBRATION:
+ enableCalibration(intent.getBooleanExtra(Watch9Constants.ACTION_ENABLE, false));
+ break;
+ case Watch9Constants.ACTION_CALIBRATION_SEND:
+ int hour = intent.getIntExtra(Watch9Constants.VALUE_CALIBRATION_HOUR, -1);
+ int minute = intent.getIntExtra(Watch9Constants.VALUE_CALIBRATION_MINUTE, -1);
+ int second = intent.getIntExtra(Watch9Constants.VALUE_CALIBRATION_SECOND, -1);
+ if (hour != -1 && minute != -1 && second != -1) {
+ sendCalibrationData(hour, minute, second);
+ }
+ break;
+ case Watch9Constants.ACTION_CALIBRATION_HOLD:
+ holdCalibration();
+ break;
+ }
+ }
+ };
+
+ public Watch9DeviceSupport() {
+ super(LOG);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
+ addSupportedService(Watch9Constants.UUID_SERVICE_WATCH9);
+
+ LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Watch9Constants.ACTION_CALIBRATION);
+ intentFilter.addAction(Watch9Constants.ACTION_CALIBRATION_SEND);
+ intentFilter.addAction(Watch9Constants.ACTION_CALIBRATION_HOLD);
+ broadcastManager.registerReceiver(broadcastReceiver, intentFilter);
+ }
+
+ @Override
+ protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
+ try {
+ boolean auth = needsAuth;
+ needsAuth = false;
+ new InitOperation(auth, this, builder).perform();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return builder;
+ }
+
+ @Override
+ public boolean connectFirstTime() {
+ needsAuth = true;
+ return super.connect();
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return true;
+ }
+
+ @Override
+ public void onNotification(NotificationSpec notificationSpec) {
+ sendNotification(Watch9Constants.NOTIFICATION_CHANNEL_DEFAULT, false);
+ }
+
+ private void sendNotification(int notificationChannel, boolean isStopNotification) {
+ try {
+ TransactionBuilder builder = performInitialized("showNotification");
+ byte[] command = Watch9Constants.CMD_NOTIFICATION_TASK;
+ command[1] = (byte) (isStopNotification ? 0x04 : 0x01);
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(command,
+ Watch9Constants.TASK,
+ Conversion.toByteArr32(notificationChannel)));
+ performImmediately(builder);
+ } catch (IOException e) {
+ LOG.warn("Unable to send notification", e);
+ }
+ }
+
+ private Watch9DeviceSupport enableNotificationChannels(TransactionBuilder builder) {
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_NOTIFICATION_SETTINGS,
+ Watch9Constants.WRITE_VALUE,
+ new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}));
+
+ return this;
+ }
+
+ public Watch9DeviceSupport authorizationRequest(TransactionBuilder builder, boolean firstConnect) {
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_AUTHORIZATION_TASK,
+ Watch9Constants.TASK,
+ new byte[]{(byte) (firstConnect ? 0x00 : 0x01)})); //possibly not the correct meaning
+
+ return this;
+ }
+
+ private Watch9DeviceSupport enableDoNotDisturb(TransactionBuilder builder, boolean active) {
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_DO_NOT_DISTURB_SETTINGS,
+ Watch9Constants.WRITE_VALUE,
+ new byte[]{(byte) (active ? 0x01 : 0x00)}));
+
+ return this;
+ }
+
+ private void enableCalibration(boolean enable) {
+ try {
+ TransactionBuilder builder = performInitialized("enableCalibration");
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_CALIBRATION_INIT_TASK,
+ Watch9Constants.TASK,
+ new byte[]{(byte) (enable ? 0x01 : 0x00)}));
+ performImmediately(builder);
+ } catch (IOException e) {
+ LOG.warn("Unable to start/stop calibration mode", e);
+ }
+ }
+
+ private void holdCalibration() {
+ try {
+ TransactionBuilder builder = performInitialized("holdCalibration");
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_CALIBRATION_KEEP_ALIVE,
+ Watch9Constants.KEEP_ALIVE));
+ performImmediately(builder);
+ } catch (IOException e) {
+ LOG.warn("Unable to keep calibration mode alive", e);
+ }
+ }
+
+ private void sendCalibrationData(@IntRange(from=0,to=23)int hour, @IntRange(from=0,to=59)int minute, @IntRange(from=0,to=59)int second) {
+ try {
+ isCalibrationActive = true;
+ TransactionBuilder builder = performInitialized("calibrate");
+ int handsPosition = ((hour % 12) * 60 + minute) * 60 + second;
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_CALIBRATION_TASK,
+ Watch9Constants.TASK,
+ Conversion.toByteArr16(handsPosition)));
+ performImmediately(builder);
+ } catch (IOException e) {
+ isCalibrationActive = false;
+ LOG.warn("Unable to send calibration data", e);
+ }
+ }
+
+ private void getTime() {
+ try {
+ TransactionBuilder builder = performInitialized("getTime");
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_TIME_SETTINGS,
+ Watch9Constants.READ_VALUE));
+ performImmediately(builder);
+ } catch (IOException e) {
+ LOG.warn("Unable to get device time", e);
+ }
+ }
+
+ private void handleTime(byte[] time) {
+ GregorianCalendar now = BLETypeConversions.createCalendar();
+ GregorianCalendar nowDevice = BLETypeConversions.createCalendar();
+ int year = (nowDevice.get(Calendar.YEAR) / 100) * 100 + Conversion.fromBcd8(time[8]);
+ nowDevice.set(year,
+ Conversion.fromBcd8(time[9]) - 1,
+ Conversion.fromBcd8(time[10]),
+ Conversion.fromBcd8(time[11]),
+ Conversion.fromBcd8(time[12]),
+ Conversion.fromBcd8(time[13]));
+ nowDevice.set(Calendar.DAY_OF_WEEK, Conversion.fromBcd8(time[16]) + 1);
+
+ long timeDiff = (Math.abs(now.getTimeInMillis() - nowDevice.getTimeInMillis())) / 1000;
+ if (10 < timeDiff && timeDiff < 120) {
+ enableCalibration(true);
+ setTime(BLETypeConversions.createCalendar());
+ enableCalibration(false);
+ }
+ }
+
+ private void setTime(Calendar calendar) {
+ try {
+ TransactionBuilder builder = performInitialized("setTime");
+ int timezoneOffsetMinutes = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
+ int timezoneOffsetIndustrialMinutes = Math.round((Math.abs(timezoneOffsetMinutes) % 60) * 100f / 60f);
+ byte[] time = new byte[]{Conversion.toBcd8(calendar.get(Calendar.YEAR) % 100),
+ Conversion.toBcd8(calendar.get(Calendar.MONTH) + 1),
+ Conversion.toBcd8(calendar.get(Calendar.DAY_OF_MONTH)),
+ Conversion.toBcd8(calendar.get(Calendar.HOUR_OF_DAY)),
+ Conversion.toBcd8(calendar.get(Calendar.MINUTE)),
+ Conversion.toBcd8(calendar.get(Calendar.SECOND)),
+ (byte) (timezoneOffsetMinutes / 60),
+ (byte) timezoneOffsetIndustrialMinutes,
+ (byte) (calendar.get(Calendar.DAY_OF_WEEK) - 1)
+ };
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_TIME_SETTINGS,
+ Watch9Constants.WRITE_VALUE,
+ time));
+ performImmediately(builder);
+ } catch (IOException e) {
+ LOG.warn("Unable to set time", e);
+ }
+ }
+
+ public Watch9DeviceSupport getFirmwareVersion(TransactionBuilder builder) {
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_FIRMWARE_INFO,
+ Watch9Constants.READ_VALUE));
+
+ return this;
+ }
+
+ private Watch9DeviceSupport getBatteryState(TransactionBuilder builder) {
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_BATTERY_INFO,
+ Watch9Constants.READ_VALUE));
+
+ return this;
+ }
+
+ private Watch9DeviceSupport setFitnessGoal(TransactionBuilder builder) {
+ int fitnessGoal = new ActivityUser().getStepsGoal();
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_FITNESS_GOAL_SETTINGS,
+ Watch9Constants.WRITE_VALUE,
+ Conversion.toByteArr16(fitnessGoal)));
+
+ return this;
+ }
+
+ public Watch9DeviceSupport initialize(TransactionBuilder builder) {
+ getFirmwareVersion(builder)
+ .getBatteryState(builder)
+ .enableNotificationChannels(builder)
+ .enableDoNotDisturb(builder, false)
+ .setFitnessGoal(builder);
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
+ builder.setGattCallback(this);
+
+ return this;
+ }
+
+ @Override
+ public void onDeleteNotification(int id) {
+
+ }
+
+ @Override
+ public void onSetTime() {
+ getTime();
+ }
+
+ @Override
+ public void onSetAlarms(ArrayList extends Alarm> alarms) {
+ try {
+ TransactionBuilder builder = performInitialized("setAlarms");
+ for (Alarm alarm : alarms) {
+ setAlarm(alarm, alarm.getIndex() + 1, builder);
+ }
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ LOG.warn("Unable to set alarms", e);
+ }
+ }
+
+ // No useful use case at the moment, used to clear alarm slots for testing.
+ private void deleteAlarm(TransactionBuilder builder, int index) {
+ if (0 < index && index < 4) {
+ byte[] alarmValue = new byte[]{(byte) index, 0x00, 0x00, 0x00, 0x00, 0x00};
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_ALARM_SETTINGS,
+ Watch9Constants.WRITE_VALUE,
+ alarmValue));
+ }
+ }
+
+ private void setAlarm(Alarm alarm, int index, TransactionBuilder builder) {
+ // Shift the GB internal repetition mask to match the device specific one.
+ byte repetitionMask = (byte) ((alarm.getRepetitionMask() << 1) | (alarm.isRepetitive() ? 0x80 : 0x00));
+ repetitionMask |= (alarm.getRepetition(Alarm.ALARM_SUN) ? 0x01 : 0x00);
+ if (0 < index && index < 4) {
+ byte[] alarmValue = new byte[]{(byte) index,
+ Conversion.toBcd8(alarm.getAlarmCal().get(Calendar.HOUR_OF_DAY)),
+ Conversion.toBcd8(alarm.getAlarmCal().get(Calendar.MINUTE)),
+ repetitionMask,
+ (byte) (alarm.isEnabled() ? 0x01 : 0x00),
+ 0x00 // TODO: Unknown
+ };
+ builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE),
+ buildCommand(Watch9Constants.CMD_ALARM_SETTINGS,
+ Watch9Constants.WRITE_VALUE,
+ alarmValue));
+ }
+ }
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+ switch (callSpec.command) {
+ case CallSpec.CALL_INCOMING:
+ sendNotification(Watch9Constants.NOTIFICATION_CHANNEL_PHONE_CALL, false);
+ break;
+ case CallSpec.CALL_START:
+ case CallSpec.CALL_END:
+ sendNotification(Watch9Constants.NOTIFICATION_CHANNEL_PHONE_CALL, true);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
+
+ }
+
+ @Override
+ public void onSetMusicState(MusicStateSpec stateSpec) {
+
+ }
+
+ @Override
+ public void onSetMusicInfo(MusicSpec musicSpec) {
+
+ }
+
+ @Override
+ public void onEnableRealtimeSteps(boolean enable) {
+
+ }
+
+ @Override
+ public void onInstallApp(Uri uri) {
+
+ }
+
+ @Override
+ public void onAppInfoReq() {
+
+ }
+
+ @Override
+ public void onAppStart(UUID uuid, boolean start) {
+
+ }
+
+ @Override
+ public void onAppDelete(UUID uuid) {
+
+ }
+
+ @Override
+ public void onAppConfiguration(UUID appUuid, String config, Integer id) {
+
+ }
+
+ @Override
+ public void onAppReorder(UUID[] uuids) {
+
+ }
+
+ @Override
+ public void onFetchRecordedData(int dataTypes) {
+
+ }
+
+ @Override
+ public void onReboot() {
+
+ }
+
+ @Override
+ public void onHeartRateTest() {
+
+ }
+
+ @Override
+ public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
+
+ }
+
+ @Override
+ public void onFindDevice(boolean start) {
+
+ }
+
+ @Override
+ public void onSetConstantVibration(int integer) {
+
+ }
+
+ @Override
+ public void onScreenshotReq() {
+
+ }
+
+ @Override
+ public void onEnableHeartRateSleepSupport(boolean enable) {
+
+ }
+
+ @Override
+ public void onSetHeartRateMeasurementInterval(int seconds) {
+
+ }
+
+ @Override
+ public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
+
+ }
+
+ @Override
+ public void onDeleteCalendarEvent(byte type, long id) {
+
+ }
+
+ @Override
+ public void onSendConfiguration(String config) {
+ TransactionBuilder builder;
+ try {
+ builder = performInitialized("sendConfig: " + config);
+ switch (config) {
+ case ActivityUser.PREF_USER_STEPS_GOAL:
+ setFitnessGoal(builder);
+ break;
+ }
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onTestNewFunction() {
+
+ }
+
+ @Override
+ public void onSendWeather(WeatherSpec weatherSpec) {
+
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ super.onCharacteristicChanged(gatt, characteristic);
+
+ UUID characteristicUUID = characteristic.getUuid();
+ if (Watch9Constants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) {
+ byte[] value = characteristic.getValue();
+ if (ArrayUtils.equals(value, Watch9Constants.RESP_FIRMWARE_INFO, 5)) {
+ handleFirmwareInfo(value);
+ } else if (ArrayUtils.equals(value, Watch9Constants.RESP_BATTERY_INFO, 5)) {
+ handleBatteryState(value);
+ } else if (ArrayUtils.equals(value, Watch9Constants.RESP_TIME_SETTINGS, 5)) {
+ handleTime(value);
+ } else if (ArrayUtils.equals(value, Watch9Constants.RESP_BUTTON_INDICATOR, 5)) {
+ LOG.info("Unhandled action: Button pressed");
+ } else if (ArrayUtils.equals(value, Watch9Constants.RESP_ALARM_INDICATOR, 5)) {
+ LOG.info("Alarm active: id=" + value[8]);
+ } else if (isCalibrationActive && value.length == 7 && value[4] == ACK_CALIBRATION) {
+ setTime(BLETypeConversions.createCalendar());
+ isCalibrationActive = false;
+ }
+
+ return true;
+ } else {
+ LOG.info("Unhandled characteristic changed: " + characteristicUUID);
+ logMessageContent(characteristic.getValue());
+ }
+
+ return false;
+ }
+
+ private byte[] buildCommand(byte[] command, byte action) {
+ return buildCommand(command, action, null);
+ }
+
+ private byte[] buildCommand(byte[] command, byte action, byte[] value) {
+ if (Arrays.equals(command, Watch9Constants.CMD_CALIBRATION_TASK)) {
+ ACK_CALIBRATION = (byte) sequenceNumber;
+ }
+ command = BLETypeConversions.join(command, value);
+ byte[] result = new byte[7 + command.length];
+ System.arraycopy(Watch9Constants.CMD_HEADER, 0, result, 0, 5);
+ System.arraycopy(command, 0, result, 6, command.length);
+ result[2] = (byte) (command.length + 1);
+ result[3] = Watch9Constants.REQUEST;
+ result[4] = (byte) sequenceNumber++;
+ result[5] = action;
+ result[result.length - 1] = calculateChecksum(result);
+
+ return result;
+ }
+
+ private byte calculateChecksum(byte[] bytes) {
+ byte checksum = 0x00;
+ for (int i = 0; i < bytes.length - 1; i++) {
+ checksum += (bytes[i] ^ i) & 0xFF;
+ }
+ return (byte) (checksum & 0xFF);
+ }
+
+ private void handleFirmwareInfo(byte[] value) {
+ versionInfo.fwVersion = String.format(Locale.US,"%d.%d.%d", value[8], value[9], value[10]);
+ handleGBDeviceEvent(versionInfo);
+ }
+
+ private void handleBatteryState(byte[] value) {
+ batteryInfo.state = value[9] == 1 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_LOW;
+ batteryInfo.level = GBDevice.BATTERY_UNKNOWN;
+ handleGBDeviceEvent(batteryInfo);
+ }
+
+ @Override
+ public void dispose() {
+ LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext());
+ broadcastManager.unregisterReceiver(broadcastReceiver);
+ super.dispose();
+ }
+
+ private static class Conversion {
+ static byte toBcd8(@IntRange(from = 0, to = 99) int value) {
+ int high = (value / 10) << 4;
+ int low = value % 10;
+ return (byte) (high | low);
+ }
+
+ static int fromBcd8(byte value) {
+ int high = ((value & 0xF0) >> 4) * 10;
+ int low = value & 0x0F;
+ return high + low;
+ }
+
+ static byte[] toByteArr16(int value) {
+ return new byte[]{(byte) (value >> 8), (byte) value};
+ }
+
+ static byte[] toByteArr32(int value) {
+ return new byte[]{(byte) (value >> 24),
+ (byte) (value >> 16),
+ (byte) (value >> 8),
+ (byte) value};
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/operations/InitOperation.java
new file mode 100644
index 000000000..a2c87d958
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/operations/InitOperation.java
@@ -0,0 +1,77 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.operations;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class InitOperation extends AbstractBTLEOperation{
+
+ private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class);
+
+ private final TransactionBuilder builder;
+ private final boolean needsAuth;
+ private final BluetoothGattCharacteristic cmdCharacteristic = getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE);
+
+ public InitOperation(boolean needsAuth, Watch9DeviceSupport support, TransactionBuilder builder) {
+ super(support);
+ this.needsAuth = needsAuth;
+ this.builder = builder;
+ builder.setGattCallback(this);
+ }
+
+ @Override
+ protected void doPerform() throws IOException {
+ builder.notify(cmdCharacteristic, true);
+ if (needsAuth) {
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
+ getSupport().authorizationRequest(builder, needsAuth);
+ } else {
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
+ getSupport().initialize(builder);
+ getSupport().performImmediately(builder);
+ }
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ UUID characteristicUUID = characteristic.getUuid();
+ if (Watch9Constants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID) && needsAuth) {
+ try {
+ byte[] value = characteristic.getValue();
+ getSupport().logMessageContent(value);
+ if (ArrayUtils.equals(value, Watch9Constants.RESP_AUTHORIZATION_TASK, 5) && value[8] == 0x01) {
+ TransactionBuilder builder = getSupport().createTransactionBuilder("authInit");
+ builder.setGattCallback(this);
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
+ getSupport().initialize(builder).performImmediately(builder);
+ } else {
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+ } catch (Exception e) {
+ GB.toast(getContext(), "Error authenticating Watch 9", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ return true;
+ } else {
+ LOG.info("Unhandled characteristic changed: " + characteristicUUID);
+ return super.onCharacteristicChanged(gatt, characteristic);
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
index 1f3cdb531..8caf2974f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
@@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@@ -215,6 +216,7 @@ public class DeviceHelper {
result.add(new XWatchCoordinator());
result.add(new ZeTimeCoordinator());
result.add(new ID115Coordinator());
+ result.add(new Watch9DeviceCoordinator());
return result;
}
diff --git a/app/src/main/res/layout/activity_watch9_calibration.xml b/app/src/main/res/layout/activity_watch9_calibration.xml
new file mode 100644
index 000000000..48caf1eca
--- /dev/null
+++ b/app/src/main/res/layout/activity_watch9_calibration.xml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_watch9_pairing.xml b/app/src/main/res/layout/activity_watch9_pairing.xml
new file mode 100644
index 000000000..2add84aa2
--- /dev/null
+++ b/app/src/main/res/layout/activity_watch9_pairing.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/device_itemv2.xml b/app/src/main/res/layout/device_itemv2.xml
index 4ad19fb84..f8ff97f1b 100644
--- a/app/src/main/res/layout/device_itemv2.xml
+++ b/app/src/main/res/layout/device_itemv2.xml
@@ -269,6 +269,21 @@
card_view:srcCompat="@drawable/ic_action_find_lost_device"
android:focusable="true" />
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1de993644..33cda9a30 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -542,5 +542,13 @@
Das Abrufen erfolgt beim Entsperren des Bildschirms. Funktioniert nur, wenn ein Sperrmechanismus eingestellt ist!
+ Sobald die Uhr vibriert, den Knopf betätigen oder kurz schütteln.
+ Kalibrieren
+ Watch 9 koppeln
+ Minuten:
+ Stunden:
+ Sekunden:
+ Watch 9 kalibrieren
+ Stelle die Uhrzeit ein, die aktuell auf der Uhr zusehen ist.
-
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 29cab8301..5a8d22d89 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -21,6 +21,7 @@
Disconnecting
Connecting
Taking a screenshot of the device
+ Calibrate Device
Debug
@@ -269,6 +270,7 @@
Device image
Name/Alias
Vibration count
+ When your watch vibrates, shake the device or press its button.
Sleep monitor
Write log files
@@ -577,6 +579,7 @@
XWatch
MyKronoz ZeTime
ID115
+ Watch 9
Choose export location
Gadgetbridge notifications
@@ -595,4 +598,12 @@
Alipay
Music
More
+
+ Minutes:
+ Hours:
+ Seconds:
+ Set the time your device is showing to you right now.
+ Calibrate
+ Watch 9 pairing
+ Watch 9 calibration