mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-27 12:26:48 +01:00
Merge pull request #1190 from maxirnilian/watch9
Watch 9: Initial support
This commit is contained in:
commit
95e39cabba
@ -387,6 +387,12 @@
|
||||
<activity
|
||||
android:name=".devices.pebble.PebblePairingActivity"
|
||||
android:label="@string/title_activity_pebble_pairing" />
|
||||
<activity
|
||||
android:name=".devices.watch9.Watch9PairingActivity"
|
||||
android:label="@string/title_activity_watch9_pairing" />
|
||||
<activity
|
||||
android:name=".devices.watch9.Watch9CalibrationActivity"
|
||||
android:label="@string/title_activity_watch9_calibration" />
|
||||
<activity
|
||||
android:name=".activities.charts.ChartsActivity"
|
||||
android:label="@string/title_activity_charts"
|
||||
|
@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9CalibrationActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
@ -290,6 +291,16 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
|
||||
|
||||
);
|
||||
|
||||
holder.calibrateDevice.setVisibility(device.isInitialized() && device.getType() == DeviceType.WATCH9 ? View.VISIBLE : View.GONE);
|
||||
holder.calibrateDevice.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent startIntent = new Intent(context, Watch9CalibrationActivity.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
context.startActivity(startIntent);
|
||||
}
|
||||
});
|
||||
|
||||
//remove device, hidden under details
|
||||
holder.removeDevice.setOnClickListener(new View.OnClickListener()
|
||||
|
||||
@ -354,6 +365,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
|
||||
ImageView setAlarmsView;
|
||||
ImageView showActivityGraphs;
|
||||
ImageView showActivityTracks;
|
||||
ImageView calibrateDevice;
|
||||
|
||||
ImageView deviceInfoView;
|
||||
//overflow
|
||||
@ -383,6 +395,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
|
||||
showActivityGraphs = view.findViewById(R.id.device_action_show_activity_graphs);
|
||||
showActivityTracks = view.findViewById(R.id.device_action_show_activity_tracks);
|
||||
deviceInfoView = view.findViewById(R.id.device_info_image);
|
||||
calibrateDevice = view.findViewById(R.id.device_action_calibrate);
|
||||
|
||||
deviceInfoBox = view.findViewById(R.id.device_item_infos_box);
|
||||
//overflow
|
||||
|
@ -0,0 +1,107 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.watch9;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.NumberPicker;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class Watch9CalibrationActivity extends AbstractGBActivity {
|
||||
|
||||
private static final String STATE_DEVICE = "stateDevice";
|
||||
GBDevice device;
|
||||
|
||||
NumberPicker pickerHour, pickerMinute, pickerSecond;
|
||||
|
||||
Handler handler;
|
||||
Runnable holdCalibration;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_watch9_calibration);
|
||||
|
||||
pickerHour = findViewById(R.id.np_hour);
|
||||
pickerMinute = findViewById(R.id.np_minute);
|
||||
pickerSecond = findViewById(R.id.np_second);
|
||||
|
||||
pickerHour.setMinValue(1);
|
||||
pickerHour.setMaxValue(12);
|
||||
pickerHour.setValue(12);
|
||||
pickerMinute.setMinValue(0);
|
||||
pickerMinute.setMaxValue(59);
|
||||
pickerMinute.setValue(0);
|
||||
pickerSecond.setMinValue(0);
|
||||
pickerSecond.setMaxValue(59);
|
||||
pickerSecond.setValue(0);
|
||||
|
||||
handler = new Handler();
|
||||
holdCalibration = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(Watch9Constants.ACTION_CALIBRATION_HOLD));
|
||||
handler.postDelayed(this, 10000);
|
||||
}
|
||||
};
|
||||
|
||||
Intent intent = getIntent();
|
||||
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (device == null && savedInstanceState != null) {
|
||||
device = savedInstanceState.getParcelable(STATE_DEVICE);
|
||||
}
|
||||
if (device == null) {
|
||||
finish();
|
||||
}
|
||||
|
||||
final Button btCalibrate = findViewById(R.id.watch9_bt_calibrate);
|
||||
btCalibrate.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
btCalibrate.setEnabled(false);
|
||||
handler.removeCallbacks(holdCalibration);
|
||||
Intent calibrationData = new Intent(Watch9Constants.ACTION_CALIBRATION_SEND);
|
||||
calibrationData.putExtra(Watch9Constants.VALUE_CALIBRATION_HOUR, pickerHour.getValue());
|
||||
calibrationData.putExtra(Watch9Constants.VALUE_CALIBRATION_MINUTE, pickerMinute.getValue());
|
||||
calibrationData.putExtra(Watch9Constants.VALUE_CALIBRATION_SECOND, pickerSecond.getValue());
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibrationData);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable(STATE_DEVICE, device);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
device = savedInstanceState.getParcelable(STATE_DEVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
Intent calibration = new Intent(Watch9Constants.ACTION_CALIBRATION);
|
||||
calibration.putExtra(Watch9Constants.ACTION_ENABLE, true);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
|
||||
handler.postDelayed(holdCalibration, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
Intent calibration = new Intent(Watch9Constants.ACTION_CALIBRATION);
|
||||
calibration.putExtra(Watch9Constants.ACTION_ENABLE, false);
|
||||
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration);
|
||||
handler.removeCallbacks(holdCalibration);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.watch9;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class Watch9Constants {
|
||||
public static final UUID UUID_SERVICE_WATCH9 = UUID.fromString("0000a800-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
public static final UUID UUID_UNKNOWN_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
public static final UUID UUID_CHARACTERISTIC_WRITE = UUID.fromString("0000a801-0000-1000-8000-00805f9b34fb");
|
||||
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_2 = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb");
|
||||
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_3 = UUID.fromString("0000a803-0000-1000-8000-00805f9b34fb");
|
||||
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_4 = UUID.fromString("0000a804-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
public static final int NOTIFICATION_CHANNEL_DEFAULT = 128;
|
||||
public static final int NOTIFICATION_CHANNEL_PHONE_CALL = 1024;
|
||||
|
||||
public static final byte RESPONSE = 0x13;
|
||||
public static final byte REQUEST = 0x31;
|
||||
|
||||
public static final byte WRITE_VALUE = 0x01;
|
||||
public static final byte READ_VALUE = 0x02;
|
||||
public static final byte TASK = 0x04;
|
||||
public static final byte KEEP_ALIVE = -0x80;
|
||||
|
||||
public static final byte[] CMD_HEADER = new byte[]{0x23, 0x01, 0x00, 0x00, 0x00};
|
||||
|
||||
// byte[] COMMAND = new byte[]{0x23, 0x01, 0x00, 0x31, 0x00, ... , 0x00}
|
||||
// | | | | | | └ Checksum
|
||||
// | | | | | └ Command + value
|
||||
// | | | | └ Sequence number
|
||||
// | | | └ Response/Request indicator
|
||||
// | | └ Value length
|
||||
// | |
|
||||
// └-----└ Header
|
||||
|
||||
public static final byte[] CMD_FIRMWARE_INFO = new byte[]{0x01, 0x02};
|
||||
public static final byte[] CMD_AUTHORIZATION_TASK = new byte[]{0x01, 0x05};
|
||||
public static final byte[] CMD_TIME_SETTINGS = new byte[]{0x01, 0x08};
|
||||
public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A};
|
||||
public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14};
|
||||
|
||||
public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01};
|
||||
public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02};
|
||||
public static final byte[] CMD_CALIBRATION_INIT_TASK = new byte[]{0x03, 0x31};
|
||||
public static final byte[] CMD_CALIBRATION_TASK = new byte[]{0x03, 0x33, 0x01};
|
||||
public static final byte[] CMD_CALIBRATION_KEEP_ALIVE = new byte[]{0x03, 0x34};
|
||||
public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61};
|
||||
|
||||
public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02};
|
||||
|
||||
public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05};
|
||||
public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11};
|
||||
public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A};
|
||||
|
||||
public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02};
|
||||
public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08};
|
||||
public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14};
|
||||
public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x08, 0x03, 0x02};
|
||||
|
||||
public static final String ACTION_ENABLE = "action.watch9.enable";
|
||||
|
||||
public static final String ACTION_CALIBRATION
|
||||
= "nodomain.freeyourgadget.gadgetbridge.devices.action.watch9.start_calibration";
|
||||
public static final String ACTION_CALIBRATION_SEND
|
||||
= "nodomain.freeyourgadget.gadgetbridge.devices.action.watch9.send_calibration";
|
||||
public static final String ACTION_CALIBRATION_HOLD
|
||||
= "nodomain.freeyourgadget.gadgetbridge.devices.action.watch9.keep_calibrating";
|
||||
public static final String VALUE_CALIBRATION_HOUR
|
||||
= "value.watch9.calibration_hour";
|
||||
public static final String VALUE_CALIBRATION_MINUTE
|
||||
= "value.watch9.calibration_minute";
|
||||
public static final String VALUE_CALIBRATION_SECOND
|
||||
= "value.watch9.calibration_second";
|
||||
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.watch9;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public Collection<? extends ScanFilter> 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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};
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Watch9DeviceSupport>{
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
124
app/src/main/res/layout/activity_watch9_calibration.xml
Normal file
124
app/src/main/res/layout/activity_watch9_calibration.xml
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_note"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/discovery_note"
|
||||
android:textColor="@color/accent"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignStart="@+id/tv_note"
|
||||
android:layout_below="@+id/tv_note"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/watch9_calibration_hint" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@id/watch9_bt_calibrate"
|
||||
android:layout_below="@id/tv_hint"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/watch9_time_hours"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/accent" />
|
||||
|
||||
<NumberPicker
|
||||
android:id="@+id/np_hour"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:descendantFocusability="blocksDescendants" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/watch9_time_minutes"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/accent" />
|
||||
|
||||
<NumberPicker
|
||||
android:id="@+id/np_minute"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:descendantFocusability="blocksDescendants" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/watch9_time_seconds"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/accent" />
|
||||
|
||||
<NumberPicker
|
||||
android:id="@+id/np_second"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:descendantFocusability="blocksDescendants" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/watch9_bt_calibrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:padding="16dp"
|
||||
android:text="@string/watch9_calibration_button" />
|
||||
|
||||
</RelativeLayout>
|
33
app/src/main/res/layout/activity_watch9_pairing.xml
Normal file
33
app/src/main/res/layout/activity_watch9_pairing.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9PairingActivity">
|
||||
|
||||
<TextView
|
||||
android:text="@string/pairing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/watch9_pair_message" />
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_marginTop="25dp"
|
||||
android:layout_below="@+id/watch9_pair_message"
|
||||
android:layout_centerHorizontal="true" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/watch9_pairing_tap_hint"
|
||||
android:id="@+id/watch9_pair_hint"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true" />
|
||||
|
||||
</RelativeLayout>
|
@ -269,6 +269,21 @@
|
||||
card_view:srcCompat="@drawable/ic_action_find_lost_device"
|
||||
android:focusable="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_action_calibrate"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_below="@id/device_image"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_toEndOf="@id/device_action_find"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/controlcenter_calibrate_device"
|
||||
android:padding="4dp"
|
||||
android:scaleType="fitXY"
|
||||
android:tint="@color/secondarytext"
|
||||
card_view:srcCompat="@drawable/ic_activity_unknown"
|
||||
android:focusable="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</android.support.v7.widget.CardView>
|
||||
|
@ -542,5 +542,13 @@
|
||||
|
||||
|
||||
<string name="pref_auto_fetch_summary">Das Abrufen erfolgt beim Entsperren des Bildschirms. Funktioniert nur, wenn ein Sperrmechanismus eingestellt ist!</string>
|
||||
<string name="watch9_pairing_tap_hint">Sobald die Uhr vibriert, den Knopf betätigen oder kurz schütteln.</string>
|
||||
<string name="watch9_calibration_button">Kalibrieren</string>
|
||||
<string name="title_activity_watch9_pairing">Watch 9 koppeln</string>
|
||||
<string name="watch9_time_minutes">Minuten:</string>
|
||||
<string name="watch9_time_hours">Stunden:</string>
|
||||
<string name="watch9_time_seconds">Sekunden:</string>
|
||||
<string name="title_activity_watch9_calibration">Watch 9 kalibrieren</string>
|
||||
<string name="watch9_calibration_hint">Stelle die Uhrzeit ein, die aktuell auf der Uhr zusehen ist.</string>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
@ -21,6 +21,7 @@
|
||||
<string name="controlcenter_snackbar_disconnecting">Disconnecting</string>
|
||||
<string name="controlcenter_snackbar_connecting">Connecting</string>
|
||||
<string name="controlcenter_snackbar_requested_screenshot">Taking a screenshot of the device</string>
|
||||
<string name="controlcenter_calibrate_device">Calibrate Device</string>
|
||||
|
||||
|
||||
<string name="title_activity_debug">Debug</string>
|
||||
@ -269,6 +270,7 @@
|
||||
<string name="candidate_item_device_image">Device image</string>
|
||||
<string name="miband_prefs_alias">Name/Alias</string>
|
||||
<string name="pref_header_vibration_count">Vibration count</string>
|
||||
<string name="watch9_pairing_tap_hint">When your watch vibrates, shake the device or press its button.</string>
|
||||
|
||||
<string name="title_activity_sleepmonitor">Sleep monitor</string>
|
||||
<string name="pref_write_logfiles">Write log files</string>
|
||||
@ -577,6 +579,7 @@
|
||||
<string name="devicetype_xwatch">XWatch</string>
|
||||
<string name="devicetype_mykronoz_zetime">MyKronoz ZeTime</string>
|
||||
<string name="devicetype_id115">ID115</string>
|
||||
<string name="devicetype_watch9">Watch 9</string>
|
||||
|
||||
<string name="choose_auto_export_location">Choose export location</string>
|
||||
<string name="notification_channel_name">Gadgetbridge notifications</string>
|
||||
@ -595,4 +598,12 @@
|
||||
<string name="menuitem_alipay">Alipay</string>
|
||||
<string name="menuitem_music">Music</string>
|
||||
<string name="menuitem_more">More</string>
|
||||
|
||||
<string name="watch9_time_minutes">Minutes:</string>
|
||||
<string name="watch9_time_hours">Hours:</string>
|
||||
<string name="watch9_time_seconds">Seconds:</string>
|
||||
<string name="watch9_calibration_hint">Set the time your device is showing to you right now.</string>
|
||||
<string name="watch9_calibration_button">Calibrate</string>
|
||||
<string name="title_activity_watch9_pairing">Watch 9 pairing</string>
|
||||
<string name="title_activity_watch9_calibration">Watch 9 calibration</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user