1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-09-26 16:26:52 +02:00

Merge branch 'dakhnod-fossil-q-hybrid'

This commit is contained in:
Andreas Shimokawa 2019-12-05 22:14:58 +01:00
commit fc69a25ac4
108 changed files with 8185 additions and 31 deletions

View File

@ -54,6 +54,7 @@ vendor's servers.
* XWatch (Affordable Chinese Casio-like smartwatches)
* Vibratissimo (experimental)
* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
* Fossil Q Hybrid (not officially supported by Fossil)
## Features

View File

@ -85,12 +85,21 @@ dependencies {
implementation "net.e175.klaus:solarpositioning:0.0.9"
// use pristine greendao instead of our custom version, since our custom jitpack-packaged
// version contains way too much and our custom patches are in the generator only.
implementation 'com.twofortyfouram:android-plugin-client-sdk-for-locale:[4.0.3, 5.0['
implementation 'com.twofortyfouram:android-plugin-host-sdk-for-locale:[2.0.3,3.0['
implementation 'com.twofortyfouram:android-plugin-api-for-locale:[1.0.2,2.0['
implementation "org.greenrobot:greendao:2.2.1"
implementation "org.apache.commons:commons-lang3:3.7"
implementation "org.cyanogenmod:platform.sdk:6.0"
implementation 'com.jaredrummler:colorpicker:1.0.2'
// implementation project(":DaoCore")
implementation 'com.github.wax911:android-emojify:0.1.7'
implementation 'com.android.support:support-v4:28.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
preBuild.dependsOn(":GBDaoGenerator:genSources")

View File

@ -15,7 +15,7 @@
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
@ -499,7 +499,32 @@
<data android:scheme="gadgetbridge" />
</intent-filter>
</activity>
<activity
android:name=".devices.qhybrid.ConfigActivity"
android:exported="true" />
<activity
android:name=".devices.qhybrid.QHybridAppChoserActivity"
android:exported="true" />
<activity
android:name=".devices.qhybrid.TaskerPluginActivity"
android:exported="true"
android:icon="@drawable/ic_device_pebble"
android:label="@string/tasker_notification">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<receiver
android:name=".devices.qhybrid.TaskerPluginReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -80,7 +80,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
private ScanCallback newLeScanCallback = null;
// Disabled for testing, it seems worse for a few people
private final boolean disableNewBLEScanning = true;
private final boolean disableNewBLEScanning = false;
private final Handler handler = new Handler();
@ -394,7 +394,11 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
LOG.warn("Not starting discovery, because already scanning.");
return;
}
startDiscovery(Scanning.SCANNING_BT);
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
startDiscovery(Scanning.SCANNING_NEW_BTLE);
} else {
startDiscovery(Scanning.SCANNING_BTLE);
}
}
private void startDiscovery(Scanning what) {

View File

@ -55,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
@ -116,6 +117,15 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
pref = findPreference("pref_key_qhybrid");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivity(new Intent(SettingsActivity.this, ConfigActivity.class));
return true;
}
});
pref = findPreference("pref_key_zetime");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
@ -296,16 +306,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
pref = findPreference("weather_city");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
// reset city id and force a new lookup
GBApplication.getPrefs().getPreferences().edit().putString("weather_cityid", null).apply();
preference.setSummary(newVal.toString());
Intent intent = new Intent("GB_UPDATE_WEATHER");
intent.setPackage(BuildConfig.APPLICATION_ID);
sendBroadcast(intent);
return true;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
// reset city id and force a new lookup
GBApplication.getPrefs().getPreferences().edit().putString("weather_cityid", null).apply();
preference.setSummary(newVal.toString());
Intent intent = new Intent("GB_UPDATE_WEATHER");
intent.setPackage(BuildConfig.APPLICATION_ID);
sendBroadcast(intent);
return true;
}
});
pref = findPreference(GBPrefs.AUTO_EXPORT_LOCATION);
@ -435,8 +445,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
}
}
catch (Exception fdfsdfds) {
} catch (Exception fdfsdfds) {
LOG.warn("fuck");
}
}

View File

@ -0,0 +1,641 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.DeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ConfigActivity extends AbstractGBActivity {
PackageAdapter adapter;
ArrayList<NotificationConfiguration> list;
PackageConfigHelper helper;
final int REQUEST_CODE_ADD_APP = 0;
private boolean hasControl = false;
SharedPreferences prefs;
TextView timeOffsetView, timezoneOffsetView;
GBDevice device;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qhybrid_settings);
findViewById(R.id.buttonOverwriteButtons).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
}
});
prefs = getSharedPreferences(getPackageName(), MODE_PRIVATE);
timeOffsetView = findViewById(R.id.qhybridTimeOffset);
timeOffsetView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int timeOffset = prefs.getInt("QHYBRID_TIME_OFFSET", 0);
LinearLayout layout2 = new LinearLayout(ConfigActivity.this);
layout2.setOrientation(LinearLayout.HORIZONTAL);
final NumberPicker hourPicker = new NumberPicker(ConfigActivity.this);
hourPicker.setMinValue(0);
hourPicker.setMaxValue(23);
hourPicker.setValue(timeOffset / 60);
final NumberPicker minPicker = new NumberPicker(ConfigActivity.this);
minPicker.setMinValue(0);
minPicker.setMaxValue(59);
minPicker.setValue(timeOffset % 60);
layout2.addView(hourPicker);
TextView tw = new TextView(ConfigActivity.this);
tw.setText(":");
layout2.addView(tw);
layout2.addView(minPicker);
layout2.setGravity(Gravity.CENTER);
new AlertDialog.Builder(ConfigActivity.this)
.setTitle(getString(R.string.qhybrid_offset_time_by))
.setView(layout2)
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
prefs.edit().putInt("QHYBRID_TIME_OFFSET", hourPicker.getValue() * 60 + minPicker.getValue()).apply();
updateTimeOffset();
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE));
GB.toast(getString(R.string.qhybrid_changes_delay_prompt), Toast.LENGTH_SHORT, GB.INFO);
}
})
.setNegativeButton("cancel", null)
.show();
}
});
updateTimeOffset();
timezoneOffsetView = findViewById(R.id.timezoneOffset);
timezoneOffsetView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int timeOffset = prefs.getInt("QHYBRID_TIMEZONE_OFFSET", 0);
LinearLayout layout2 = new LinearLayout(ConfigActivity.this);
layout2.setOrientation(LinearLayout.HORIZONTAL);
final NumberPicker hourPicker = new NumberPicker(ConfigActivity.this);
hourPicker.setMinValue(0);
hourPicker.setMaxValue(23);
hourPicker.setValue(timeOffset / 60);
final NumberPicker minPicker = new NumberPicker(ConfigActivity.this);
minPicker.setMinValue(0);
minPicker.setMaxValue(59);
minPicker.setValue(timeOffset % 60);
layout2.addView(hourPicker);
TextView tw = new TextView(ConfigActivity.this);
tw.setText(":");
layout2.addView(tw);
layout2.addView(minPicker);
layout2.setGravity(Gravity.CENTER);
new AlertDialog.Builder(ConfigActivity.this)
.setTitle(getString(R.string.qhybrid_offset_timezone))
.setView(layout2)
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
prefs.edit().putInt("QHYBRID_TIMEZONE_OFFSET", hourPicker.getValue() * 60 + minPicker.getValue()).apply();
updateTimezoneOffset();
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_TIMEZONE));
GB.toast(getString(R.string.qhybrid_changes_delay_prompt), Toast.LENGTH_SHORT, GB.INFO);
}
})
.setNegativeButton("cancel", null)
.show();
}
});
updateTimezoneOffset();
setTitle(R.string.preferences_qhybrid_settings);
ListView appList = findViewById(R.id.qhybrid_appList);
try {
helper = new PackageConfigHelper(getApplicationContext());
list = helper.getNotificationConfigurations();
} catch (GBException e) {
GB.log("error getting configurations", GB.ERROR, e);
GB.toast("error getting configurations", Toast.LENGTH_SHORT, GB.ERROR, e);
list = new ArrayList<>();
}
// null is added to indicate the plus button added handled in PackageAdapter#getView
list.add(null);
appList.setAdapter(adapter = new PackageAdapter(this, R.layout.qhybrid_package_settings_item, list));
appList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(final AdapterView<?> adapterView, View view, final int i, long l) {
PopupMenu menu = new PopupMenu(ConfigActivity.this, view);
menu.getMenu().add(0, 0, 0, "edit");
menu.getMenu().add(0, 1, 1, "delete");
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case 0: {
TimePicker picker = new TimePicker(ConfigActivity.this, (NotificationConfiguration) adapterView.getItemAtPosition(i));
picker.finishListener = new TimePicker.OnFinishListener() {
@Override
public void onFinish(boolean success, NotificationConfiguration config) {
setControl(false, null);
if (success) {
try {
helper.saveNotificationConfiguration(config);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
} catch (GBException e) {
GB.log("error saving configuration", GB.ERROR, e);
GB.toast("error saving notification", Toast.LENGTH_SHORT, GB.ERROR, e);
}
refreshList();
}
}
};
picker.handsListener = new TimePicker.OnHandsSetListener() {
@Override
public void onHandsSet(NotificationConfiguration config) {
setHands(config);
}
};
picker.vibrationListener = new TimePicker.OnVibrationSetListener() {
@Override
public void onVibrationSet(NotificationConfiguration config) {
vibrate(config);
}
};
setControl(true, picker.getSettings());
break;
}
case 1: {
try {
helper.deleteNotificationConfiguration((NotificationConfiguration) adapterView.getItemAtPosition(i));
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
} catch (GBException e) {
GB.log("error deleting configuration", GB.ERROR, e);
GB.toast("error deleting setting", Toast.LENGTH_SHORT, GB.ERROR, e);
}
refreshList();
break;
}
}
return true;
}
});
menu.show();
return true;
}
});
appList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
Intent notificationIntent = new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION);
notificationIntent.putExtra("CONFIG", (NotificationConfiguration) adapterView.getItemAtPosition(i));
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(notificationIntent);
}
});
SeekBar vibeBar = findViewById(R.id.vibrationStrengthBar);
vibeBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
int start;
@Override
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
start = seekBar.getProgress();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int progress;
if ((progress = seekBar.getProgress()) == start) return;
String[] values = {"25", "50", "100"};
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_VIBRATION_STRENGTH, values[progress]));
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_VIBRATION_STRENGTH);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
}
});
device = GBApplication.app().getDeviceManager().getSelectedDevice();
if (device == null || device.getType() != DeviceType.FOSSILQHYBRID) {
setSettingsError(getString(R.string.watch_not_connected));
} else {
updateSettings();
}
}
private void updateTimeOffset() {
int timeOffset = prefs.getInt("QHYBRID_TIME_OFFSET", 0);
DecimalFormat format = new DecimalFormat("00");
timeOffsetView.setText(
format.format(timeOffset / 60) + ":" +
format.format(timeOffset % 60)
);
}
private void updateTimezoneOffset() {
int timeOffset = prefs.getInt("QHYBRID_TIMEZONE_OFFSET", 0);
DecimalFormat format = new DecimalFormat("00");
timezoneOffsetView.setText(
format.format(timeOffset / 60) + ":" +
format.format(timeOffset % 60)
);
}
private void setSettingsEnabled(boolean enables) {
findViewById(R.id.settingsLayout).setAlpha(enables ? 1f : 0.2f);
}
private void updateSettings() {
runOnUiThread(new Runnable() {
@Override
public void run() {
EditText et = findViewById(R.id.stepGoalEt);
et.setOnEditorActionListener(null);
final String text = device.getDeviceInfo(QHybridSupport.ITEM_STEP_GOAL).getDetails();
et.setText(text);
et.setSelection(text.length());
et.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
if (i == EditorInfo.IME_ACTION_DONE || i == EditorInfo.IME_ACTION_NEXT) {
String t = textView.getText().toString();
if (!t.isEmpty()) {
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_GOAL, t));
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_STEP_GOAL);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
updateSettings();
}
((InputMethodManager) getApplicationContext().getSystemService(Activity.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
return true;
}
});
if ("true".equals(device.getDeviceInfo(QHybridSupport.ITEM_EXTENDED_VIBRATION_SUPPORT).getDetails())) {
final int strengthProgress = (int) (Math.log(Double.parseDouble(device.getDeviceInfo(QHybridSupport.ITEM_VIBRATION_STRENGTH).getDetails()) / 25) / Math.log(2));
setSettingsEnabled(true);
SeekBar seekBar = findViewById(R.id.vibrationStrengthBar);
seekBar.setProgress(strengthProgress);
} else {
findViewById(R.id.vibrationStrengthBar).setEnabled(false);
findViewById(R.id.vibrationStrengthLayout).setAlpha(0.5f);
}
CheckBox activityHandCheckbox = findViewById(R.id.checkBoxUserActivityHand);
if (device.getDeviceInfo(QHybridSupport.ITEM_HAS_ACTIVITY_HAND).getDetails().equals("true")) {
if (device.getDeviceInfo(QHybridSupport.ITEM_USE_ACTIVITY_HAND).getDetails().equals("true")) {
activityHandCheckbox.setChecked(true);
}
activityHandCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean checked) {
if (!device.getDeviceInfo(QHybridSupport.ITEM_STEP_GOAL).getDetails().equals("1000000")) {
new AlertDialog.Builder(ConfigActivity.this)
.setMessage(getString(R.string.qhybrid_prompt_million_steps))
.setPositiveButton("ok", null)
.show();
buttonView.setChecked(false);
return;
}
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_USE_ACTIVITY_HAND, String.valueOf(checked)));
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_USE_ACTIVITY_HAND);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
}
});
} else {
// activityHandCheckbox.setEnabled(false);
activityHandCheckbox.setAlpha(0.2f);
activityHandCheckbox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GB.toast("nah.", Toast.LENGTH_SHORT, GB.INFO);
((CheckBox) v).setChecked(false);
}
});
}
final String buttonJson = device.getDeviceInfo(FossilWatchAdapter.ITEM_BUTTONS).getDetails();
try {
JSONArray buttonConfig_;
if (buttonJson == null || buttonJson.isEmpty()) {
buttonConfig_ = new JSONArray(new String[]{"", "", ""});
}else{
buttonConfig_ = new JSONArray(buttonJson);
}
final JSONArray buttonConfig = buttonConfig_;
LinearLayout buttonLayout = findViewById(R.id.buttonConfigLayout);
buttonLayout.removeAllViews();
findViewById(R.id.buttonOverwriteButtons).setVisibility(View.GONE);
final ConfigPayload[] payloads = ConfigPayload.values();
final String[] names = new String[payloads.length];
for (int i = 0; i < payloads.length; i++)
names[i] = payloads[i].getDescription();
for (int i = 0; i < buttonConfig.length(); i++) {
final int currentIndex = i;
String configName = buttonConfig.getString(i);
TextView buttonTextView = new TextView(ConfigActivity.this);
buttonTextView.setTextColor(Color.WHITE);
buttonTextView.setTextSize(20);
try {
ConfigPayload payload = ConfigPayload.valueOf(configName);
buttonTextView.setText("Button " + (i + 1) + ": " + payload.getDescription());
} catch (IllegalArgumentException e) {
buttonTextView.setText("Button " + (i + 1) + ": Unknown");
}
buttonTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog dialog = new AlertDialog.Builder(ConfigActivity.this)
.setItems(names, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
ConfigPayload selected = payloads[which];
try {
buttonConfig.put(currentIndex, selected.toString());
device.addDeviceInfo(new GenericItem(FossilWatchAdapter.ITEM_BUTTONS, buttonConfig.toString()));
updateSettings();
Intent buttonIntent = new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS);
buttonIntent.putExtra(FossilWatchAdapter.ITEM_BUTTONS, buttonConfig.toString());
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(buttonIntent);
} catch (JSONException e) {
GB.log("error", GB.ERROR, e);
}
}
})
.setTitle("Button " + (currentIndex + 1))
.create();
dialog.show();
}
});
buttonLayout.addView(buttonTextView);
}
} catch (JSONException e) {
GB.log("error parsing button config", GB.ERROR, e);
GB.toast("error parsing button config", Toast.LENGTH_LONG, GB.ERROR);
}
}
});
}
private void setControl(boolean control, NotificationConfiguration config) {
if (hasControl == control) return;
Intent intent = new Intent(control ? QHybridSupport.QHYBRID_COMMAND_CONTROL : QHybridSupport.QHYBRID_COMMAND_UNCONTROL);
intent.putExtra("CONFIG", config);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
this.hasControl = control;
}
private void setHands(NotificationConfiguration config) {
sendControl(config, QHybridSupport.QHYBRID_COMMAND_SET);
}
private void vibrate(NotificationConfiguration config) {
sendControl(config, QHybridSupport.QHYBRID_COMMAND_VIBRATE);
}
private void sendControl(NotificationConfiguration config, String request) {
Intent intent = new Intent(request);
intent.putExtra("CONFIG", config);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
private void refreshList() {
list.clear();
try {
list.addAll(helper.getNotificationConfigurations());
} catch (GBException e) {
GB.log("error getting configurations", GB.ERROR, e);
GB.toast("error getting configurations", Toast.LENGTH_SHORT, GB.ERROR, e);
}
// null is added to indicate the plus button added handled in PackageAdapter#getView
list.add(null);
adapter.notifyDataSetChanged();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
refreshList();
registerReceiver(buttonReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS));
LocalBroadcastManager.getInstance(this).registerReceiver(settingsReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_SETTINGS_UPDATED));
LocalBroadcastManager.getInstance(this).registerReceiver(fileReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_FILE_UPLOADED));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(buttonReceiver);
LocalBroadcastManager.getInstance(this).unregisterReceiver(settingsReceiver);
LocalBroadcastManager.getInstance(this).unregisterReceiver(fileReceiver);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
private void setSettingsError(final String error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setSettingsEnabled(false);
((TextView) findViewById(R.id.settingsErrorText)).setVisibility(View.VISIBLE);
((TextView) findViewById(R.id.settingsErrorText)).setText(error);
}
});
}
class PackageAdapter extends ArrayAdapter<NotificationConfiguration> {
PackageManager manager;
PackageAdapter(@NonNull Context context, int resource, @NonNull List<NotificationConfiguration> objects) {
super(context, resource, objects);
manager = context.getPackageManager();
}
@NonNull
@Override
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
if (!(view instanceof RelativeLayout))
view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.qhybrid_package_settings_item, null);
NotificationConfiguration settings = getItem(position);
if (settings == null) {
Button addButton = new Button(ConfigActivity.this);
addButton.setText("+");
addButton.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
addButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivityForResult(new Intent(ConfigActivity.this, QHybridAppChoserActivity.class), REQUEST_CODE_ADD_APP);
}
});
return addButton;
}
try {
((ImageView) view.findViewById(R.id.packageIcon)).setImageDrawable(manager.getApplicationIcon(settings.getPackageName()));
} catch (PackageManager.NameNotFoundException e) {
GB.log("error", GB.ERROR, e);
}
final int width = 100;
((TextView) view.findViewById(R.id.packageName)).setText(settings.getAppName());
Bitmap bitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
Paint black = new Paint();
black.setColor(Color.BLACK);
black.setStyle(Paint.Style.STROKE);
black.setStrokeWidth(5);
c.drawCircle(width / 2, width / 2, width / 2 - 3, black);
int center = width / 2;
if (settings.getHour() != -1) {
c.drawLine(
center,
center,
(float) (center + Math.sin(Math.toRadians(settings.getHour())) * (width / 4)),
(float) (center - Math.cos(Math.toRadians(settings.getHour())) * (width / 4)),
black
);
}
if (settings.getMin() != -1) {
c.drawLine(
center,
center,
(float) (center + Math.sin(Math.toRadians(settings.getMin())) * (width / 3)),
(float) (center - Math.cos(Math.toRadians(settings.getMin())) * (width / 3)),
black
);
}
((ImageView) view.findViewById(R.id.packageClock)).setImageBitmap(bitmap);
return view;
}
}
BroadcastReceiver fileReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
boolean error = intent.getBooleanExtra("EXTRA_ERROR", false);
if (error) {
GB.toast(getString(R.string.qhybrid_buttons_overwrite_error), Toast.LENGTH_SHORT, GB.ERROR);
return;
}
GB.toast(getString(R.string.qhybrid_buttons_overwrite_success), Toast.LENGTH_SHORT, GB.INFO);
}
};
BroadcastReceiver buttonReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
GB.toast("Button " + intent.getIntExtra("BUTTON", -1) + " pressed", Toast.LENGTH_SHORT, GB.INFO);
}
};
BroadcastReceiver settingsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
GB.toast("Setting updated", Toast.LENGTH_SHORT, GB.INFO);
updateSettings();
}
};
}

View File

@ -0,0 +1,115 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.util.Log;
import java.io.Serializable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
public class NotificationConfiguration implements Serializable {
private short min, hour, subEye = -1;
private String packageName, appName;
private PlayNotificationRequest.VibrationType vibration;
private boolean respectSilentMode;
private long id = -1;
NotificationConfiguration(short min, short hour, String packageName, String appName, boolean respectSilentMode, PlayNotificationRequest.VibrationType vibration) {
this.min = min;
this.hour = hour;
this.packageName = packageName;
this.appName = appName;
this.respectSilentMode = respectSilentMode;
this.vibration = vibration;
}
public NotificationConfiguration(short min, short hour, short subEye, PlayNotificationRequest.VibrationType vibration) {
this.min = min;
this.hour = hour;
this.subEye = subEye;
this.vibration = vibration;
}
public NotificationConfiguration(short min, short hour, String packageName, String appName, boolean respectSilentMode, PlayNotificationRequest.VibrationType vibration, long id) {
this.min = min;
this.hour = hour;
this.packageName = packageName;
this.appName = appName;
this.respectSilentMode = respectSilentMode;
this.vibration = vibration;
this.id = id;
}
NotificationConfiguration(String packageName, String appName) {
this.min = -1;
this.hour = -1;
this.packageName = packageName;
this.appName = appName;
this.respectSilentMode = false;
this.vibration = PlayNotificationRequest.VibrationType.SINGLE_NORMAL;
this.id = -1;
}
public PlayNotificationRequest.VibrationType getVibration() {
return vibration;
}
public void setVibration(PlayNotificationRequest.VibrationType vibration) {
this.vibration = vibration;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public boolean getRespectSilentMode() {
Log.d("Config", "respect: " + respectSilentMode);
return respectSilentMode;
}
public void setRespectSilentMode(boolean respectSilentMode) {
this.respectSilentMode = respectSilentMode;
}
public void setMin(short min) {
this.min = min;
}
public void setHour(short hour) {
this.hour = hour;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public short getMin() {
return min;
}
public short getHour() {
return hour;
}
public short getSubEye() {
return subEye;
}
public void setSubEye(short subEye) {
this.subEye = subEye;
}
public String getPackageName() {
return packageName;
}
public String getAppName() {
return appName;
}
}

View File

@ -0,0 +1,142 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class PackageConfigHelper {
public static final String DB_NAME = "qhybridNotifications.db";
public static final String DB_ID = "id";
public static final String DB_TABLE = "notifications";
public static final String DB_PACKAGE = "package";
public static final String DB_APPNAME = "appName";
public static final String DB_VIBRATION = "vibrationTtype";
public static final String DB_MINUTE = "minDegress";
public static final String DB_HOUR = "hourDegrees";
public static final String DB_RESPECT_SILENT = "respectSilent";
public PackageConfigHelper(Context context) throws GBException {
initDB();
}
public void saveNotificationConfiguration(NotificationConfiguration settings) throws GBException {
ContentValues values = new ContentValues(6);
values.put(DB_PACKAGE, settings.getPackageName());
values.put(DB_APPNAME, settings.getAppName());
values.put(DB_HOUR, settings.getHour());
values.put(DB_MINUTE, settings.getMin());
values.put(DB_VIBRATION, settings.getVibration().getValue());
values.put(DB_RESPECT_SILENT, settings.getRespectSilentMode());
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
if(settings.getId() == -1) {
settings.setId(database.insert(DB_TABLE, null, values));
}else{
database.update(DB_TABLE, values, DB_ID + "=?", new String[]{String.valueOf(settings.getId())});
}
GBApplication.releaseDB();
//LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent());
}
public ArrayList<NotificationConfiguration> getNotificationConfigurations() throws GBException {
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
Cursor cursor = database.query(DB_TABLE, new String[]{"*"}, null, null, null, null, null);
GBApplication.releaseDB();
int size = cursor.getCount();
ArrayList<NotificationConfiguration> list = new ArrayList<>(size);
if(size > 0){
int appNamePos = cursor.getColumnIndex(DB_APPNAME);
int packageNamePos = cursor.getColumnIndex(DB_PACKAGE);
int hourPos = cursor.getColumnIndex(DB_HOUR);
int minPos = cursor.getColumnIndex(DB_MINUTE);
int silentPos = cursor.getColumnIndex(DB_RESPECT_SILENT);
int vibrationPos = cursor.getColumnIndex(DB_VIBRATION);
int idPos = cursor.getColumnIndex(DB_ID);
cursor.moveToFirst();
do {
list.add(new NotificationConfiguration(
(short)cursor.getInt(minPos),
(short)cursor.getInt(hourPos),
cursor.getString(packageNamePos),
cursor.getString(appNamePos),
cursor.getInt(silentPos) == 1,
PlayNotificationRequest.VibrationType.fromValue((byte)cursor.getInt(vibrationPos)),
cursor.getInt(idPos)
));
Log.d("Settings", "setting #" + cursor.getPosition() + ": " + cursor.getInt(silentPos));
}while (cursor.moveToNext());
}
cursor.close();
return list;
}
public NotificationConfiguration getNotificationConfiguration(String appName) throws GBException {
if(appName == null) return null;
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
Cursor c = database.query(DB_TABLE, new String[]{"*"}, DB_APPNAME + "=?", new String[]{appName}, null, null, null);
GBApplication.releaseDB();
if(c.getCount() == 0){
c.close();
return null;
}
c.moveToFirst();
NotificationConfiguration settings = new NotificationConfiguration(
(short)c.getInt(c.getColumnIndex(DB_MINUTE)),
(short)c.getInt(c.getColumnIndex(DB_HOUR)),
c.getString(c.getColumnIndex(DB_PACKAGE)),
c.getString(c.getColumnIndex(DB_APPNAME)),
c.getInt(c.getColumnIndex(DB_RESPECT_SILENT)) == 1,
PlayNotificationRequest.VibrationType.fromValue((byte)c.getInt(c.getColumnIndex(DB_VIBRATION))),
c.getInt(c.getColumnIndex(DB_ID))
);
c.close();
return settings;
}
private void initDB() throws GBException {
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
database.execSQL("CREATE TABLE IF NOT EXISTS notifications(" +
DB_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
DB_PACKAGE + " TEXT, " +
DB_VIBRATION + " INTEGER, " +
DB_MINUTE + " INTEGER DEFAULT -1, " +
DB_APPNAME + " TEXT," +
DB_RESPECT_SILENT + " INTEGER," +
DB_HOUR + " INTEGER DEFAULT -1);");
GBApplication.releaseDB();
}
public void deleteNotificationConfiguration(NotificationConfiguration packageSettings) throws GBException {
Log.d("DB", "deleting id " + packageSettings.getId());
if(packageSettings.getId() == -1) return;
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
database.delete(DB_TABLE, DB_ID + "=?", new String[]{String.valueOf(packageSettings.getId())});
GBApplication.releaseDB();
}
}

View File

@ -0,0 +1,190 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.Collections;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static android.view.View.GONE;
public class QHybridAppChoserActivity extends AbstractGBActivity {
boolean hasControl = false;
PackageConfigHelper helper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qhybrid_app_choser);
try {
helper = new PackageConfigHelper(getApplicationContext());
} catch (GBException e) {
GB.log("database error", GB.ERROR, e);
GB.toast("error getting database helper", Toast.LENGTH_SHORT, GB.ERROR, e);
finish();
return;
}
final ListView appList = findViewById(R.id.qhybrid_appChooserList);
final PackageManager manager = getPackageManager();
final List<PackageInfo> packages = manager.getInstalledPackages(0);
new Thread(new Runnable() {
@Override
public void run() {
final IdentityHashMap<PackageInfo, String> nameMap = new IdentityHashMap(packages.size());
for(PackageInfo info : packages){
CharSequence label = manager.getApplicationLabel(info.applicationInfo);
if(label == null) label = info.packageName;
nameMap.put(info, label.toString());
}
Collections.sort(packages, new Comparator<PackageInfo>() {
@Override
public int compare(PackageInfo packageInfo, PackageInfo t1) {
return nameMap.get(packageInfo)
.compareToIgnoreCase(
nameMap.get(t1)
);
}
});
runOnUiThread(new Runnable() {
@Override
public void run() {
appList.setAdapter(new ConfigArrayAdapter(QHybridAppChoserActivity.this, R.layout.qhybrid_app_view, packages, manager));
findViewById(R.id.qhybrid_packageChooserLoading).setVisibility(GONE);
}
});
}
}).start();
appList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
showPackageDialog(packages.get(i));
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
}
private void setControl(boolean control) {
if (hasControl == control) return;
Intent intent = new Intent(control ? QHybridSupport.QHYBRID_COMMAND_CONTROL : QHybridSupport.QHYBRID_COMMAND_UNCONTROL);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
this.hasControl = control;
}
private void setHands(NotificationConfiguration config){
sendControl(config, QHybridSupport.QHYBRID_COMMAND_SET);
}
private void vibrate(NotificationConfiguration config){
sendControl(config, QHybridSupport.QHYBRID_COMMAND_VIBRATE);
}
private void sendControl(NotificationConfiguration config, String request){
Intent intent = new Intent(request);
intent.putExtra("CONFIG", config);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
@SuppressLint("ClickableViewAccessibility")
private void showPackageDialog(PackageInfo info) {
TimePicker picker = new TimePicker(this, info);
picker.finishListener = new TimePicker.OnFinishListener() {
@Override
public void onFinish(boolean success, NotificationConfiguration config) {
setControl(false);
if(success){
try {
helper.saveNotificationConfiguration(config);
LocalBroadcastManager.getInstance(QHybridAppChoserActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
} catch (GBException e) {
GB.log("error saving config", GB.ERROR, e);
GB.toast("error saving configuration", Toast.LENGTH_SHORT, GB.ERROR, e);
}
finish();
}
}
};
picker.handsListener = new TimePicker.OnHandsSetListener() {
@Override
public void onHandsSet(NotificationConfiguration config) {
setHands(config);
}
};
picker.vibrationListener = new TimePicker.OnVibrationSetListener() {
@Override
public void onVibrationSet(NotificationConfiguration config) {
vibrate(config);
}
};
setControl(true);
}
@Override
protected void onPause() {
super.onPause();
setControl(false);
finish();
}
class ConfigArrayAdapter extends ArrayAdapter<PackageInfo> {
PackageManager manager;
public ConfigArrayAdapter(@NonNull Context context, int resource, @NonNull List<PackageInfo> objects, PackageManager manager) {
super(context, resource, objects);
this.manager = manager;
}
@NonNull
@Override
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
if (view == null)
view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.qhybrid_app_view, null);
ApplicationInfo info = getItem(position).applicationInfo;
((ImageView) view.findViewById(R.id.qhybrid_appChooserItemIcon)).setImageDrawable(manager.getApplicationIcon(info));
((TextView) view.findViewById(R.id.qhybrid_appChooserItemText)).setText(manager.getApplicationLabel(info));
return view;
}
}
}

View File

@ -0,0 +1,168 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
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;
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
public class QHybridCoordinator extends AbstractDeviceCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
for(ParcelUuid uuid : candidate.getServiceUuids()){
if(uuid.getUuid().toString().equals("3dda0001-957f-7d4a-34a6-74696673696d")){
return DeviceType.FOSSILQHYBRID;
}
}
return DeviceType.UNKNOWN;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@NonNull
@Override
public Collection<? extends ScanFilter> createBLEScanFilters() {
return Collections.singletonList(new ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString("3dda0001-957f-7d4a-34a6-74696673696d")).build());
}
@Override
public DeviceType getDeviceType() {
return DeviceType.FOSSILQHYBRID;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@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;
}
public boolean supportsAlarmConfiguration() {
GBDevice connectedDevice = GBApplication.app().getDeviceManager().getSelectedDevice();
if(connectedDevice == null || connectedDevice.getType() != DeviceType.FOSSILQHYBRID || connectedDevice.getState() != GBDevice.State.INITIALIZED){
return false;
}
return true;
}
@Override
public int getAlarmSlotCount() {
return this.supportsAlarmConfiguration() ? 5 : 0;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public String getManufacturer() {
return "Fossil";
}
@Override
public boolean supportsAppsManagement() {
return true;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return ConfigActivity.class;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
GBDevice connectedDevice = GBApplication.app().getDeviceManager().getSelectedDevice();
if(connectedDevice == null || connectedDevice.getType() != DeviceType.FOSSILQHYBRID){
return true;
}
ItemWithDetails vibration = connectedDevice.getDeviceInfo(QHybridSupport.ITEM_EXTENDED_VIBRATION_SUPPORT);
if(vibration == null){
return true;
}
return vibration.getDetails().equals("true");
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
}

View File

@ -0,0 +1,93 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.Intent;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.twofortyfouram.locale.sdk.client.ui.activity.AbstractPluginActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
public class TaskerPluginActivity extends AbstractPluginActivity {
public static final String key_hours = "qhybrid_hours";
public static final String key_minute = "qhybrid_minutes";
public static final String key_vibration = "qhybrid_vibration";
RadioGroup group;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tasker_plugin);
group = findViewById(R.id.qhybrid_tasker_vibration);
for(PlayNotificationRequest.VibrationType type : PlayNotificationRequest.VibrationType.values()){
RadioButton button = new RadioButton(this);
button.setText(type.name() + " (" + type.name() + ")");
button.setId(type.getValue());
group.addView(button);
}
group.check(PlayNotificationRequest.VibrationType.NO_VIBE.getValue());
RadioButton custom = new RadioButton(this);
custom.setText("variable %vibration");
custom.setId(10);
group.addView(custom);
Intent intent = getIntent();
if(intent.hasExtra(key_hours)){
((TextView) findViewById(R.id.qhybrid_hour_degrees)).setText(intent.getStringExtra(key_hours));
}
if(intent.hasExtra(key_minute)){
((TextView) findViewById(R.id.qhybrid_minute_degrees)).setText(intent.getStringExtra(key_minute));
}
if(intent.hasExtra(key_vibration)){
String vibe = intent.getStringExtra(key_vibration);
if(vibe.equals("%vibration")){
group.check(10);
}else {
group.check(Integer.parseInt(vibe));
}
}
}
@Override
public boolean isBundleValid(@NonNull Bundle bundle) {
return true;
}
@Override
public void onPostCreateWithPreviousResult(@NonNull Bundle bundle, @NonNull String s) {
}
@Nullable
@Override
public Bundle getResultBundle() {
int vibration = group.getCheckedRadioButtonId();
Bundle bundle = new Bundle();
bundle.putString(key_hours, ((EditText) findViewById(R.id.qhybrid_hour_degrees)).getText().toString());
bundle.putString(key_minute, ((EditText) findViewById(R.id.qhybrid_minute_degrees)).getText().toString());
if(vibration == 10){
bundle.putString(key_vibration, "%vibration");
}else{
bundle.putString(key_vibration, String.valueOf(vibration));
}
TaskerPlugin.Setting.setVariableReplaceKeys(bundle, new String[]{key_hours, key_minute, key_vibration});
return bundle;
}
@NonNull
@Override
public String getResultBlurb(@NonNull Bundle bundle) {
return "nope";
}
}

View File

@ -0,0 +1,35 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
public class TaskerPluginReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String min = intent.getStringExtra(TaskerPluginActivity.key_minute);
String hour = intent.getStringExtra(TaskerPluginActivity.key_hours);
String vibration = intent.getStringExtra(TaskerPluginActivity.key_vibration);
int minDegrees = (int)Float.parseFloat(min);
int hourDegrees = (int)Float.parseFloat(hour);
NotificationConfiguration config = new NotificationConfiguration(
(short)minDegrees,
(short)hourDegrees,
null,
null,
false,
PlayNotificationRequest.VibrationType.fromValue(Byte.parseByte(vibration))
);
Intent send = new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION);
send.putExtra("CONFIG", config);
LocalBroadcastManager.getInstance(context).sendBroadcast(send);
}
}

View File

@ -0,0 +1,278 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.ScrollView;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
public class TimePicker extends AlertDialog.Builder {
ImageView pickerView;
Canvas pickerCanvas;
Bitmap pickerBitmap;
NotificationConfiguration settings;
int height, width, radius;
int radius1, radius2, radius3;
int controlledHand = 0;
int handRadius;
AlertDialog dialog;
OnFinishListener finishListener;
OnHandsSetListener handsListener;
OnVibrationSetListener vibrationListener;
protected TimePicker(@NonNull Context context, PackageInfo info) {
super(context);
settings = new NotificationConfiguration(info.packageName, context.getApplicationContext().getPackageManager().getApplicationLabel(info.applicationInfo).toString());
initGraphics(context);
}
protected TimePicker(Context context, NotificationConfiguration config){
super(context);
settings = config;
initGraphics(context);
}
private void initGraphics(Context context){
int w = (int) (((WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getWidth() * 0.8);
height = w;
width = w;
radius = (int) (w * 0.06);
radius1 = 0;
radius2 = (int) (radius * 2.3);
radius3 = (int)(radius2 * 2.15);
int offset = (int) (w * 0.1);
radius1 += offset;
radius2 += offset;
radius3 += offset;
pickerView = new ImageView(context);
pickerBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
pickerCanvas = new Canvas(pickerBitmap);
drawClock();
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.addView(pickerView);
CheckBox box = new CheckBox(context);
box.setText("Respect silent mode");
box.setChecked(settings.getRespectSilentMode());
box.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
settings.setRespectSilentMode(b);
}
});
layout.addView(box);
RadioGroup group = new RadioGroup(context);
for(PlayNotificationRequest.VibrationType vibe: PlayNotificationRequest.VibrationType.values()){
RadioButton button = new RadioButton(context);
button.setText(vibe.toString());
button.setId(vibe.getValue());
group.addView(button);
}
group.check(settings.getVibration().getValue());
group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
settings.setVibration(PlayNotificationRequest.VibrationType.fromValue((byte)i));
if(TimePicker.this.vibrationListener != null) TimePicker.this.vibrationListener.onVibrationSet(settings);
}
});
ScrollView scrollView = new ScrollView(context);
scrollView.addView(group);
layout.addView(scrollView);
setView(layout);
setNegativeButton("cancel", null);
setPositiveButton("ok", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
if(finishListener == null) return;
finishListener.onFinish(true, settings);
}
});
setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
if(finishListener == null) return;
finishListener.onFinish(false, settings);
}
});
setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialogInterface) {
if(finishListener == null) return;
finishListener.onFinish(false, settings);
}
});
dialog = show();
pickerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
handleTouch(dialog, motionEvent);
return true;
}
});
}
public NotificationConfiguration getSettings() {
return settings;
}
private void handleTouch(AlertDialog dialog, MotionEvent event) {
int centerX = width / 2;
int centerY = height / 2;
int difX = centerX - (int) event.getX();
int difY = (int) event.getY() - centerY;
int dist = (int) Math.sqrt(Math.abs(difX) * Math.abs(difX) + Math.abs(difY) * Math.abs(difY));
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
int radiusHalf = radius;
if (dist < (radius1 + radiusHalf) && dist > (radius1 - radiusHalf)) {
Log.d("Settings", "hit sub");
handRadius = (int) (height / 2f - radius1);
controlledHand = 3;
} else if (dist < (radius2 + radiusHalf) && dist > (radius2 - radiusHalf)) {
Log.d("Settings", "hit hour");
controlledHand = 1;
handRadius = (int) (height / 2f - radius2);
} else if (dist < (radius3 + radiusHalf) && dist > (radius3 - radiusHalf)) {
Log.d("Settings", "hit minute");
controlledHand = 2;
handRadius = (int) (height / 2f - radius3);
} else {
Log.d("Settings", "hit nothing");
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (controlledHand == 0) return;
double degree = difY == 0 ? (difX < 0 ? 90 : 270) : Math.toDegrees(Math.atan((float) difX / (float) difY));
if (difY > 0) degree = 180 + degree;
if (degree < 0) degree = 360 + degree;
switch (controlledHand) {
case 1: {
settings.setHour((short) (((int)(degree + 15) / 30) * 30 % 360));
break;
}
case 2: {
settings.setMin((short) (((int)(degree + 15) / 30) * 30 % 360));
break;
}
}
break;
}
case MotionEvent.ACTION_UP: {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setClickable(true);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setAlpha(1f);
if(handsListener != null) handsListener.onHandsSet(settings);
break;
}
}
drawClock();
}
private void drawClock() {
//pickerCanvas.drawColor(Color.WHITE);
Paint white = new Paint();
white.setColor(Color.WHITE);
white.setStyle(Paint.Style.FILL);
pickerCanvas.drawCircle(width / 2, width / 2, width / 2, white);
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setColor(Color.BLUE);
Paint text = new Paint();
text.setStyle(Paint.Style.FILL);
text.setTextSize(radius * 1.5f);
text.setColor(Color.BLACK);
text.setTextAlign(Paint.Align.CENTER);
int textShiftY = (int) ((text.descent() + text.ascent()) / 2);
Paint linePaint = new Paint();
linePaint.setStrokeWidth(10);
linePaint.setStyle(Paint.Style.FILL_AND_STROKE);
linePaint.setColor(Color.BLACK);
if (settings.getMin() != -1) {
paint.setAlpha(255);
float x = (float) (width / 2f + Math.sin(Math.toRadians(settings.getMin())) * (float) radius3);
float y = (float) (height / 2f - Math.cos(Math.toRadians(settings.getMin())) * (float) radius3);
linePaint.setAlpha(255);
pickerCanvas.drawLine(width / 2, height / 2, x, y, linePaint);
pickerCanvas.drawCircle(
x,
y,
radius,
paint
);
pickerCanvas.drawText(String.valueOf(settings.getMin() / 6), x, y - textShiftY, text);
}
if (settings.getHour() != -1) {
paint.setAlpha(255);
float x = (float) (width / 2f + Math.sin(Math.toRadians(settings.getHour())) * (float) radius2);
float y = (float) (height / 2f - Math.cos(Math.toRadians(settings.getHour())) * (float) radius2);
linePaint.setAlpha(255);
pickerCanvas.drawLine(width / 2, height / 2, x, y, linePaint);
pickerCanvas.drawCircle(
x,
y,
radius,
paint
);
pickerCanvas.drawText(settings.getHour() == 0 ? "12" : String.valueOf(settings.getHour() / 30), x, y - textShiftY, text);
}
Paint paint2 = new Paint();
paint2.setColor(Color.BLACK);
paint2.setStyle(Paint.Style.FILL_AND_STROKE);
pickerCanvas.drawCircle(width / 2, height / 2, 5, paint2);
pickerView.setImageBitmap(pickerBitmap);
}
interface OnFinishListener{
public void onFinish(boolean success, NotificationConfiguration config);
}
interface OnHandsSetListener{
public void onHandsSet(NotificationConfiguration config);
}
interface OnVibrationSetListener{
public void onVibrationSet(NotificationConfiguration config);
}
}

View File

@ -32,6 +32,7 @@ import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadata;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.RemoteException;
@ -43,6 +44,7 @@ import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -106,6 +108,8 @@ public class NotificationListener extends NotificationListenerService {
private HashMap<String, Long> notificationBurstPrevention = new HashMap<>();
private HashMap<String, Long> notificationOldRepeatPrevention = new HashMap<>();
public static ArrayList<String> notificationStack = new ArrayList<>();
private long activeCallPostTime;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@ -223,6 +227,7 @@ public class NotificationListener extends NotificationListenerService {
@Override
public void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
notificationStack.clear();
super.onDestroy();
}
@ -241,6 +246,9 @@ public class NotificationListener extends NotificationListenerService {
public void onNotificationPosted(StatusBarNotification sbn) {
Prefs prefs = GBApplication.getPrefs();
notificationStack.remove(sbn.getPackageName());
notificationStack.add(sbn.getPackageName());
if (GBApplication.isRunningLollipopOrLater()) {
if ("call".equals(sbn.getNotification().category) && prefs.getBoolean("notification_support_voip_calls", false)) {
handleCallNotification(sbn);
@ -384,7 +392,6 @@ public class NotificationListener extends NotificationListenerService {
}else {
LOG.info("This app might show old/duplicate notifications. notification.when is 0 for " + source);
}
GBApplication.deviceService().onNotification(notificationSpec);
}
@ -626,29 +633,30 @@ public class NotificationListener extends NotificationListenerService {
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
LOG.info("Notification removed: " + sbn.getPackageName());
if (GBApplication.isRunningLollipopOrLater()) {
LOG.info("Notification removed: " + sbn.getPackageName() + ", category: " + sbn.getNotification().category);
if (Notification.CATEGORY_CALL.equals(sbn.getNotification().category) && activeCallPostTime == sbn.getPostTime()) {
activeCallPostTime = 0;
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_END;
GBApplication.deviceService().onSetCallState(callSpec);
}
LOG.info("Notification removed: " + sbn.getPackageName() + ": " + sbn.getNotification().category);
notificationStack.remove(sbn.getPackageName());
if(Notification.CATEGORY_CALL.equals(sbn.getNotification().category) && activeCallPostTime == sbn.getPostTime()) {
activeCallPostTime = 0;
CallSpec callSpec = new CallSpec();
callSpec.command = CallSpec.CALL_END;
GBApplication.deviceService().onSetCallState(callSpec);
}
// FIXME: DISABLED for now
/*
if (shouldIgnore(sbn))
return;
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("autoremove_notifications", false)) {
if (prefs.getBoolean("autoremove_notifications", true)) {
LOG.info("notification removed, will ask device to delete it");
GBApplication.deviceService().onDeleteNotification((int) sbn.getPostTime());
}
*/
}

View File

@ -51,6 +51,7 @@ public enum DeviceType {
NO1F1(50, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_no1_f1),
TECLASTH30(60, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_teclast_h30),
XWATCH(70, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_xwatch),
FOSSILQHYBRID(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_qhybrid),
ZETIME(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_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),

View File

@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaL
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
@ -183,7 +184,10 @@ public class DeviceSupportFactory {
case XWATCH:
deviceSupport = new ServiceDeviceSupport(new XWatchSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ZETIME:
case FOSSILQHYBRID:
deviceSupport = new ServiceDeviceSupport(new QHybridSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ZETIME:
deviceSupport = new ServiceDeviceSupport(new ZeTimeDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ID115:

View File

@ -351,6 +351,11 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
}
@Override
public void onSetFmFrequency(float frequency) {

View File

@ -209,4 +209,9 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLEDeviceSupport>
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
mSupport.onReadRemoteRssi(gatt, rssi, status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
mSupport.onMtuChanged(gatt, mtu, status);
}
}

View File

@ -60,4 +60,8 @@ public abstract class AbstractGattCallback implements GattCallback {
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
}
}

View File

@ -532,6 +532,19 @@ public final class BtLEQueue {
checkWaitingCharacteristic(characteristic, status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if(getCallbackToUse() != null){
getCallbackToUse().onMtuChanged(gatt, mtu, status);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,

View File

@ -104,6 +104,8 @@ public interface GattCallback {
*/
void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status);
void onMtuChanged(BluetoothGatt gatt, int mtu, int status);
// /**
// * @see BluetoothGattCallback#onMtuChanged(BluetoothGatt, int, int)
// * @param gatt

View File

@ -18,13 +18,17 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestMtuAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction;
@ -56,6 +60,13 @@ public class TransactionBuilder {
return add(action);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TransactionBuilder requestMtu(int mtu){
return add(
new RequestMtuAction(mtu)
);
}
public TransactionBuilder notify(BluetoothGattCharacteristic characteristic, boolean enable) {
if (characteristic == null) {
LOG.warn("Unable to notify characteristic: null");

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import androidx.annotation.RequiresApi;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
public class RequestMtuAction extends BtLEAction {
private int mtu;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public RequestMtuAction(int mtu) {
super(null);
this.mtu = mtu;
}
@Override
public boolean expectsResult() {
return false;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean run(BluetoothGatt gatt) {
return gatt.requestMtu(this.mtu);
}
}

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
import java.util.zip.Checksum;
public final class CRC32C implements Checksum {
private static final int[] crcTable = new int[]{0, (int) 4067132163L, (int) 3778769143L, 324072436, (int) 3348797215L, 904991772, 648144872, (int) 3570033899L, (int) 2329499855L, 2024987596, 1809983544, (int) 2575936315L, 1296289744, (int) 3207089363L, (int) 2893594407L, 1578318884, 274646895, (int) 3795141740L, (int) 4049975192L, 51262619, (int) 3619967088L, 632279923, 922689671, (int) 3298075524L, (int) 2592579488L, 1760304291, 2075979607, (int) 2312596564L, 1562183871, (int) 2943781820L, (int) 3156637768L, 1313733451, 549293790, (int) 3537243613L, (int) 3246849577L, 871202090, (int) 3878099393L, 357341890, 102525238, (int) 4101499445L, (int) 2858735121L, 1477399826, 1264559846, (int) 3107202533L, 1845379342, (int) 2677391885L, (int) 2361733625L, 2125378298, 820201905, (int) 3263744690L, (int) 3520608582L, 598981189, (int) 4151959214L, 85089709, 373468761, (int) 3827903834L, (int) 3124367742L, 1213305469, 1526817161, (int) 2842354314L, 2107672161, (int) 2412447074L, (int) 2627466902L, 1861252501, 1098587580, (int) 3004210879L, (int) 2688576843L, 1378610760, (int) 2262928035L, 1955203488, 1742404180, (int) 2511436119L, (int) 3416409459L, 969524848, 714683780, (int) 3639785095L, 205050476, (int) 4266873199L, (int) 3976438427L, 526918040, 1361435347, (int) 2739821008L, (int) 2954799652L, 1114974503, (int) 2529119692L, 1691668175, 2005155131, (int) 2247081528L, (int) 3690758684L, 697762079, 986182379, (int) 3366744552L, 476452099, (int) 3993867776L, (int) 4250756596L, 255256311, 1640403810, (int) 2477592673L, (int) 2164122517L, 1922457750, (int) 2791048317L, 1412925310, 1197962378, (int) 3037525897L, (int) 3944729517L, 427051182, 170179418, (int) 4165941337L, 746937522, (int) 3740196785L, (int) 3451792453L, 1070968646, 1905808397, (int) 2213795598L, (int) 2426610938L, 1657317369, (int) 3053634322L, 1147748369, 1463399397, (int) 2773627110L, (int) 4215344322L, 153784257, 444234805, (int) 3893493558L, 1021025245, (int) 3467647198L, (int) 3722505002L, 797665321, (int) 2197175160L, 1889384571, 1674398607, (int) 2443626636L, 1164749927, (int) 3070701412L, (int) 2757221520L, 1446797203, 137323447, (int) 4198817972L, (int) 3910406976L, 461344835, (int) 3484808360L, 1037989803, 781091935, (int) 3705997148L, (int) 2460548119L, 1623424788, 1939049696, (int) 2180517859L, 1429367560, (int) 2807687179L, (int) 3020495871L, 1180866812, 410100952, (int) 3927582683L, (int) 4182430767L, 186734380, (int) 3756733383L, 763408580, 1053836080, (int) 3434856499L, (int) 2722870694L, 1344288421, 1131464017, (int) 2971354706L, 1708204729, (int) 2545590714L, (int) 2229949006L, 1988219213, 680717673, (int) 3673779818L, (int) 3383336350L, 1002577565, (int) 4010310262L, 493091189, 238226049, (int) 4233660802L, (int) 2987750089L, 1082061258, 1395524158, (int) 2705686845L, 1972364758, (int) 2279892693L, (int) 2494862625L, 1725896226, 952904198, (int) 3399985413L, (int) 3656866545L, 731699698, (int) 4283874585L, 222117402, 510512622, (int) 3959836397L, (int) 3280807620L, 837199303, 582374963, (int) 3504198960L, 68661723, (int) 4135334616L, (int) 3844915500L, 390545967, 1230274059, (int) 3141532936L, (int) 2825850620L, 1510247935, (int) 2395924756L, 2091215383, 1878366691, (int) 2644384480L, (int) 3553878443L, 565732008, 854102364, (int) 3229815391L, 340358836, (int) 3861050807L, (int) 4117890627L, 119113024, 1493875044, (int) 2875275879L, (int) 3090270611L, 1247431312, (int) 2660249211L, 1828433272, 2141937292, (int) 2378227087L, (int) 3811616794L, 291187481, 34330861, (int) 4032846830L, 615137029, (int) 3603020806L, (int) 3314634738L, 939183345, 1776939221, (int) 2609017814L, (int) 2295496738L, 2058945313, (int) 2926798794L, 1545135305, 1330124605, (int) 3173225534L, (int) 4084100981L, 17165430, 307568514, (int) 3762199681L, 888469610, (int) 3332340585L, (int) 3587147933L, 665062302, 2042050490, (int) 2346497209L, (int) 2559330125L, 1793573966, (int) 3190661285L, 1279665062, 1595330642, (int) 2910671697L};
private int crc = 0xFFFFFFFF;
public long getValue() {
return ~this.crc;
}
public void reset() {
this.crc = 0xFFFFFFFF;
}
public void update(int data) {
crc = crcTable[(data & 255 ^ crc) & 255] ^ crc >>> 8;
}
public void update(byte[] data, int offset, int length) {
for (int index = offset; index < length + offset; index++) {
this.crc = crcTable[(this.crc ^ data[index]) & 255] ^ this.crc >>> 8;
}
}
public void update(byte[] data){
update(data, 0, data.length);
}
}

View File

@ -0,0 +1,179 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;
import android.net.Uri;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
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;
class QHybridBaseSupport extends AbstractBTLEDeviceSupport {
QHybridBaseSupport(Logger logger) {
super(logger);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
}
@Override
public void onSetCallState(CallSpec callSpec) {
}
@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 onReset(int flags) {
}
@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) {
}
@Override
public void onReadConfiguration(String config) {
}
@Override
public void onTestNewFunction() {
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
}

View File

@ -0,0 +1,561 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
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.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;
import androidx.annotation.RequiresApi;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapterFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class QHybridSupport extends QHybridBaseSupport {
public static final String QHYBRID_COMMAND_CONTROL = "qhybrid_command_control";
public static final String QHYBRID_COMMAND_UNCONTROL = "qhybrid_command_uncontrol";
public static final String QHYBRID_COMMAND_SET = "qhybrid_command_set";
public static final String QHYBRID_COMMAND_VIBRATE = "qhybrid_command_vibrate";
public static final String QHYBRID_COMMAND_UPDATE = "qhybrid_command_update";
public static final String QHYBRID_COMMAND_UPDATE_TIMEZONE = "qhybrid_command_update_timezone";
public static final String QHYBRID_COMMAND_NOTIFICATION = "qhybrid_command_notification";
public static final String QHYBRID_COMMAND_UPDATE_SETTINGS = "nodomain.freeyourgadget.gadgetbridge.Q_UPDATE_SETTINGS";
public static final String QHYBRID_COMMAND_OVERWRITE_BUTTONS = "nodomain.freeyourgadget.gadgetbridge.Q_OVERWRITE_BUTTONS";
private static final String QHYBRID_ACTION_SET_ACTIVITY_HAND = "nodomain.freeyourgadget.gadgetbridge.Q_SET_ACTIVITY_HAND";
public static final String QHYBRID_EVENT_SETTINGS_UPDATED = "nodomain.freeyourgadget.gadgetbridge.Q_SETTINGS_UPDATED";
public static final String QHYBRID_EVENT_FILE_UPLOADED = "nodomain.freeyourgadget.gadgetbridge.Q_FILE_UPLOADED";
public static final String QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED = "nodomain.freeyourgadget.gadgetbridge.Q_NOTIFICATION_CONFIG_CHANGED";
public static final String QHYBRID_EVENT_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_BUTTON_PRESSED";
public static final String QHYBRID_EVENT_MULTI_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_MULTI_BUTTON_PRESSED";
public static final String ITEM_STEP_GOAL = "STEP_GOAL";
public static final String ITEM_STEP_COUNT = "STEP_COUNT";
public static final String ITEM_VIBRATION_STRENGTH = "VIBRATION_STRENGTH";
public static final String ITEM_ACTIVITY_POINT = "ACTIVITY_POINT";
public static final String ITEM_EXTENDED_VIBRATION_SUPPORT = "EXTENDED_VIBRATION";
public static final String ITEM_HAS_ACTIVITY_HAND = "HAS_ACTIVITY_HAND";
public static final String ITEM_USE_ACTIVITY_HAND = "USE_ACTIVITY_HAND";
public static final String ITEM_LAST_HEARTBEAT = "LAST_HEARTBEAT";
public static final String ITEM_TIMEZONE_OFFSET = "STEPTIMEZONE_OFFSET_COUNT";
private static final Logger logger = LoggerFactory.getLogger(QHybridSupport.class);
private PackageConfigHelper helper;
private volatile boolean searchDevice = false;
private long timeOffset;
private boolean useActivityHand;
private WatchAdapter watchAdapter;
public QHybridSupport() {
super(logger);
addSupportedService(UUID.fromString("3dda0001-957f-7d4a-34a6-74696673696d"));
addSupportedService(UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb"));
addSupportedService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"));
addSupportedService(UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"));
IntentFilter commandFilter = new IntentFilter(QHYBRID_COMMAND_CONTROL);
commandFilter.addAction(QHYBRID_COMMAND_UNCONTROL);
commandFilter.addAction(QHYBRID_COMMAND_SET);
commandFilter.addAction(QHYBRID_COMMAND_VIBRATE);
commandFilter.addAction(QHYBRID_COMMAND_UPDATE);
commandFilter.addAction(QHYBRID_COMMAND_UPDATE_TIMEZONE);
commandFilter.addAction(QHYBRID_COMMAND_NOTIFICATION);
commandFilter.addAction(QHYBRID_COMMAND_UPDATE_SETTINGS);
commandFilter.addAction(QHYBRID_COMMAND_OVERWRITE_BUTTONS);
commandFilter.addAction(QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED);
BroadcastReceiver commandReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle extras = intent.getExtras();
NotificationConfiguration config = extras == null ? null : (NotificationConfiguration) intent.getExtras().get("CONFIG");
switch (intent.getAction()) {
case QHYBRID_COMMAND_CONTROL: {
log("sending control request");
watchAdapter.requestHandsControl();
if (config != null) {
watchAdapter.setHands(config.getHour(), config.getMin());
} else {
watchAdapter.setHands((short) 0, (short) 0);
}
break;
}
case QHYBRID_COMMAND_UNCONTROL: {
watchAdapter.releaseHandsControl();
break;
}
case QHYBRID_COMMAND_SET: {
watchAdapter.setHands(config.getHour(), config.getMin());
break;
}
case QHYBRID_COMMAND_VIBRATE: {
watchAdapter.vibrate(config.getVibration());
break;
}
case QHYBRID_COMMAND_NOTIFICATION: {
watchAdapter.playNotification(config);
break;
}
case QHYBRID_COMMAND_UPDATE: {
loadTimeOffset();
onSetTime();
break;
}
case QHYBRID_COMMAND_UPDATE_TIMEZONE:{
loadTimezoneOffset();
break;
}
case QHYBRID_COMMAND_UPDATE_SETTINGS: {
String newSetting = intent.getStringExtra("EXTRA_SETTING");
switch (newSetting) {
case ITEM_VIBRATION_STRENGTH: {
watchAdapter.setVibrationStrength(Short.parseShort(gbDevice.getDeviceInfo(ITEM_VIBRATION_STRENGTH).getDetails()));
break;
}
case ITEM_STEP_GOAL: {
watchAdapter.setStepGoal(Integer.parseInt(gbDevice.getDeviceInfo(ITEM_STEP_GOAL).getDetails()));
break;
}
case ITEM_USE_ACTIVITY_HAND: {
QHybridSupport.this.useActivityHand = gbDevice.getDeviceInfo(ITEM_USE_ACTIVITY_HAND).getDetails().equals("true");
GBApplication.getPrefs().getPreferences().edit().putBoolean("QHYBRID_USE_ACTIVITY_HAND", useActivityHand).apply();
break;
}
}
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(QHYBRID_EVENT_SETTINGS_UPDATED));
break;
}
case QHYBRID_COMMAND_OVERWRITE_BUTTONS: {
String buttonConfig = intent.getStringExtra(FossilWatchAdapter.ITEM_BUTTONS);
watchAdapter.overwriteButtons(buttonConfig);
break;
}
case QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED: {
watchAdapter.syncNotificationSettings();
break;
}
}
}
};
LocalBroadcastManager.getInstance(getContext()).registerReceiver(commandReceiver, commandFilter);
try {
helper = new PackageConfigHelper(GBApplication.getContext());
} catch (GBException e) {
GB.log("error getting database", GB.ERROR, e);
GB.toast("error getting database", Toast.LENGTH_SHORT, GB.ERROR, e);
try {
throw e;
} catch (GBException ex) {
ex.printStackTrace();
}
}
IntentFilter globalFilter = new IntentFilter();
globalFilter.addAction(QHYBRID_ACTION_SET_ACTIVITY_HAND);
BroadcastReceiver globalCommandReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//noinspection SwitchStatementWithTooFewBranches
switch (intent.getAction()) {
case QHYBRID_ACTION_SET_ACTIVITY_HAND: {
try {
String extra = String.valueOf(intent.getExtras().get("EXTRA_PROGRESS"));
float progress = Float.parseFloat(extra);
watchAdapter.setActivityHand(progress);
watchAdapter.playNotification(new NotificationConfiguration(
(short) -1,
(short) -1,
(short) (progress * 180),
PlayNotificationRequest.VibrationType.NO_VIBE
));
} catch (Exception e) {
GB.log("wrong number format", GB.ERROR, e);
logger.debug("trash extra should be number 0.0-1.0");
}
break;
}
}
}
};
GBApplication.getContext().registerReceiver(globalCommandReceiver, globalFilter);
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
super.onSetAlarms(alarms);
if(this.watchAdapter == null){
GB.toast("watch not connected", Toast.LENGTH_LONG, GB.ERROR);
return;
}
this.watchAdapter.onSetAlarms(alarms);
}
private void loadTimeOffset() {
timeOffset = getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIME_OFFSET", 0);
}
private void loadTimezoneOffset(){
short offset = (short) getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIMEZONE_OFFSET", 0);
this.watchAdapter.setTimezoneOffsetMinutes(offset);
}
public long getTimeOffset(){
return this.timeOffset;
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
this.useActivityHand = GBApplication.getPrefs().getBoolean("QHYBRID_USE_ACTIVITY_HAND", false);
getDevice().addDeviceInfo(new GenericItem(ITEM_USE_ACTIVITY_HAND, String.valueOf(this.useActivityHand)));
getDevice().setNotificationIconConnected(R.drawable.ic_notification_qhybrid);
getDevice().setNotificationIconDisconnected(R.drawable.ic_notification_disconnected_qhybrid);
for (int i = 2; i <= 7; i++)
builder.notify(getCharacteristic(UUID.fromString("3dda000" + i + "-957f-7d4a-34a6-74696673696d")), true);
builder
.read(getCharacteristic(UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")))
.read(getCharacteristic(UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")))
.read(getCharacteristic(UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb")))
;
loadTimeOffset();
return builder;
}
@Override
public void onFetchRecordedData(int dataTypes) {
if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) {
this.watchAdapter.onFetchActivityData();
}
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
log("notif from " + notificationSpec.sourceAppId + " " + notificationSpec.sender + " " + notificationSpec.phoneNumber);
//new Exception().printStackTrace();
String packageName = notificationSpec.sourceName;
NotificationConfiguration config = null;
try {
config = helper.getNotificationConfiguration(packageName);
} catch (GBException e) {
GB.log("error getting notification configuration", GB.ERROR, e);
GB.toast("error getting notification configuration", Toast.LENGTH_SHORT, GB.ERROR, e);
}
if (config == null) return;
log("handling notification");
if (config.getRespectSilentMode()) {
int mode = ((AudioManager) getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE)).getRingerMode();
if (mode == AudioManager.RINGER_MODE_SILENT) return;
}
boolean enforceActivityHandNotification = config.getHour() == -1 && config.getMin() == -1;
playNotification(config);
showNotificationsByAllActive(enforceActivityHandNotification);
}
private void log(String message){
logger.debug(message);
}
@Override
public void onDeleteNotification(int id) {
super.onDeleteNotification(id);
showNotificationsByAllActive(true);
}
private void showNotificationsByAllActive(boolean enforceByNotification) {
if (!this.useActivityHand) return;
double progress = calculateNotificationProgress();
showNotificationCountOnActivityHand(progress);
if (enforceByNotification) {
watchAdapter.playNotification(new NotificationConfiguration(
(short) -1,
(short) -1,
(short) (progress * 180),
PlayNotificationRequest.VibrationType.NO_VIBE
));
}
}
public double calculateNotificationProgress() {
HashMap<NotificationConfiguration, Boolean> configs = new HashMap<>(0);
try {
for (NotificationConfiguration config : helper.getNotificationConfigurations()) {
configs.put(config, false);
}
} catch (GBException e) {
GB.log("error getting notification configuration", GB.ERROR, e);
GB.toast("error getting notification configs", Toast.LENGTH_SHORT, GB.ERROR, e);
}
double notificationProgress = 0;
for (String notificationPackage : NotificationListener.notificationStack) {
for (NotificationConfiguration notificationConfiguration : configs.keySet()) {
if (configs.get(notificationConfiguration)) continue;
if (notificationConfiguration.getPackageName().equals(notificationPackage)) {
notificationProgress += 0.25;
configs.put(notificationConfiguration, true);
}
}
}
return notificationProgress;
}
//TODO toggle "Notifications when screen on" options on this check
private void showNotificationCountOnActivityHand(double progress) {
if (useActivityHand) {
watchAdapter.setActivityHand(progress);
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
if(watchAdapter == null) return;
watchAdapter.onMtuChanged(gatt, mtu, status);
}
private void playNotification(NotificationConfiguration config) {
if (config.getMin() == -1 && config.getHour() == -1 && config.getVibration() == PlayNotificationRequest.VibrationType.NO_VIBE)
return;
watchAdapter.playNotification(config);
}
@Override
public void onSetTime() {
watchAdapter.setTime();
}
@Override
public void onFindDevice(boolean start) {
try {
if (watchAdapter.supportsExtendedVibration()) {
GB.toast("Device does not support brr brr", Toast.LENGTH_SHORT, GB.INFO);
}
} catch (UnsupportedOperationException e) {
notifiyException(e);
GB.toast("Please contact dakhnod@gmail.com\n", Toast.LENGTH_SHORT, GB.INFO);
}
if (start && searchDevice) return;
searchDevice = start;
if (start) {
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (searchDevice) {
QHybridSupport.this.watchAdapter.vibrateFindMyDevicePattern();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
GB.log("error", GB.ERROR, e);
}
}
}
}).start();
}
}
@Override
public void onTestNewFunction() {
watchAdapter.onTestNewFunction();
}
private void backupFile(DownloadFileRequest request) {
try {
File f = new File("/sdcard/qFiles/");
if (!f.exists()) f.mkdir();
File file = new File("/sdcard/qFiles/" + request.timeStamp);
if (file.exists()) {
throw new Exception("file " + file.getPath() + " exists");
}
logger.debug("Writing file " + file.getPath());
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
fos.write(request.file);
fos.close();
logger.debug("file written.");
FileOutputStream fos2 = new FileOutputStream("/sdcard/qFiles/steps", true);
fos2.write(("file " + request.timeStamp + " cut\n\n").getBytes());
fos2.close();
//TODO file stuff
// queueWrite(new EraseFileRequest((short) request.fileHandle));
} catch (Exception e) {
GB.log("error", GB.ERROR, e);
if (request.fileHandle > 257) {
// queueWrite(new DownloadFileRequest((short) (request.fileHandle - 1)));
}
}
}
@Override
public void handleGBDeviceEvent(GBDeviceEventBatteryInfo deviceEvent){
super.handleGBDeviceEvent(deviceEvent);
}
public void notifiyException(Exception e){
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
String sStackTrace = sw.toString();
Notification.Builder notificationBuilder = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationBuilder = new Notification.Builder(getContext(), GB.NOTIFICATION_CHANNEL_ID);
} else {
notificationBuilder = new Notification.Builder(getContext());
}
notificationBuilder
.setContentTitle("Q Error")
.setSmallIcon(R.drawable.ic_notification_qhybrid)
.setContentText(sStackTrace)
.setStyle(new Notification.BigTextStyle())
.build();
Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(
"mailto","dakhnod@gmail.com", null));
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Exception Report");
emailIntent.putExtra(Intent.EXTRA_TEXT, "Here's a crash from your stupid app: \n\n" + sStackTrace);
PendingIntent intent = PendingIntent.getActivity(getContext(), 0, emailIntent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
notificationBuilder.addAction(new Notification.Action(0, "report", intent));
}else{
notificationBuilder.addAction(0, "report", intent);
}
((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)).notify((int) System.currentTimeMillis(), notificationBuilder.build());
}
@Override
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
switch (characteristic.getUuid().toString()) {
case "00002a26-0000-1000-8000-00805f9b34fb": {
String firmwareVersion = characteristic.getStringValue(0);
gbDevice.setFirmwareVersion(firmwareVersion);
this.watchAdapter = new WatchAdapterFactory().createWatchAdapter(firmwareVersion, this);
this.watchAdapter.initialize();
showNotificationsByAllActive(false);
break;
}
case "00002a24-0000-1000-8000-00805f9b34fb": {
String modelNumber = characteristic.getStringValue(0);
gbDevice.setModel(modelNumber);
gbDevice.setName(watchAdapter.getModelName());
try {
gbDevice.addDeviceInfo(new GenericItem(ITEM_EXTENDED_VIBRATION_SUPPORT, String.valueOf(watchAdapter.supportsExtendedVibration())));
gbDevice.addDeviceInfo(new GenericItem(ITEM_HAS_ACTIVITY_HAND, String.valueOf(watchAdapter.supportsActivityHand())));
} catch (UnsupportedOperationException e) {
notifiyException(e);
GB.toast("Please contact dakhnod@gmail.com\n", Toast.LENGTH_SHORT, GB.INFO);
gbDevice.addDeviceInfo(new GenericItem(ITEM_EXTENDED_VIBRATION_SUPPORT, "false"));
}
break;
}
case "00002a19-0000-1000-8000-00805f9b34fb": {
short level = characteristic.getValue()[0];
gbDevice.setBatteryLevel(level);
gbDevice.setBatteryThresholdPercent((short) 2);
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = gbDevice.getBatteryLevel();
batteryInfo.state = BatteryState.BATTERY_NORMAL;
handleGBDeviceEvent(batteryInfo);
break;
}
}
return true;
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic
characteristic) {
if(watchAdapter == null) return super.onCharacteristicChanged(gatt, characteristic);
return watchAdapter.onCharacteristicChanged(gatt, characteristic);
}
}

View File

@ -0,0 +1,81 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import java.util.ArrayList;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
public abstract class WatchAdapter {
private QHybridSupport deviceSupport;
public WatchAdapter(QHybridSupport deviceSupport){
this.deviceSupport = deviceSupport;
}
public QHybridSupport getDeviceSupport(){
return this.deviceSupport;
}
public Context getContext(){
return getDeviceSupport().getContext();
}
public abstract void initialize();
public abstract void playPairingAnimation();
public abstract void playNotification(NotificationConfiguration config);
public abstract void setTime();
public abstract void overwriteButtons(String buttonConfigJson);
public abstract void setActivityHand(double progress);
public abstract void setHands(short hour, short minute);
public abstract void vibrate(PlayNotificationRequest.VibrationType vibration);
public abstract void vibrateFindMyDevicePattern();
public abstract void requestHandsControl();
public abstract void releaseHandsControl();
public abstract void setStepGoal(int stepGoal);
public abstract void setVibrationStrength(short strength);
public abstract void syncNotificationSettings();
public abstract void onTestNewFunction();
public abstract void setTimezoneOffsetMinutes(short offset);
public abstract boolean supportsFindDevice();
public abstract boolean supportsExtendedVibration();
public abstract boolean supportsActivityHand();
public String getModelName() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return "Q Commuter";
case "HL.0.0":
return "Q Activist";
case "DN.1.0":
return "Hybrid HR Collider";
}
return "unknwon Q";
}
public abstract void onFetchActivityData();
public abstract void onSetAlarms(ArrayList<? extends Alarm> alarms);
public abstract boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status){};
public String arrayToString(byte[] bytes) {
if (bytes.length == 0) return "";
StringBuilder s = new StringBuilder();
final String chars = "0123456789ABCDEF";
for (byte b : bytes) {
s.append(chars.charAt((b >> 4) & 0xF)).append(chars.charAt(b & 0xF)).append(" ");
}
return s.substring(0, s.length() - 1) + "\n";
}
}

View File

@ -0,0 +1,17 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.misfit.MisfitWatchAdapter;
public final class WatchAdapterFactory {
public final WatchAdapter createWatchAdapter(String firmwareVersion, QHybridSupport deviceSupport){
char major = firmwareVersion.charAt(6);
switch (major){
case '1': return new MisfitWatchAdapter(deviceSupport);
case '2': return new FossilWatchAdapter(deviceSupport);
}
throw new UnsupportedOperationException("Firmware " + firmwareVersion + " not supported");
}
}

View File

@ -0,0 +1,595 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.json.JSONArray;
import org.json.JSONException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigFileBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.AlarmsSetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.AnimationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ReleaseHandsControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.RequestHandControlRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_TIMEZONE_OFFSET;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_VIBRATION_STRENGTH;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_MULTI_BUTTON_PRESS;
public class FossilWatchAdapter extends WatchAdapter {
private ArrayList<Request> requestQueue = new ArrayList<>();
private FossilRequest fossilRequest;
private int MTU = 23;
private final String ITEM_MTU = "MTU";
static public final String ITEM_BUTTONS = "BUTTONS";
private final String CONFIG_ITEM_STEP_GOAL = "step_goal";
private final String CONFIG_ITEM_VIBRATION_STRENGTH = "vibration_strength";
private final String CONFIG_ITEM_TIMEZONE_OFFSET = "timezone_offset";
public final String CONFIG_ITEM_BUTTONS = "buttons";
private int lastButtonIndex = -1;
Logger logger = LoggerFactory.getLogger(getClass());
public FossilWatchAdapter(QHybridSupport deviceSupport) {
super(deviceSupport);
}
@Override
public void initialize() {
playPairingAnimation();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
queueWrite(new RequestMtuRequest(512), false);
}
// queueWrite(new FileCloseRequest((short) 0xFFFF));
// queueWrite(new ConfigurationGetRequest(this), false);
syncConfiguration();
syncNotificationSettings();
syncButtonSettings();
/* queueWrite(new ButtonConfigurationGetRequest(this) {
@Override
public void onConfigurationsGet(ConfigPayload[] configs) {
super.onConfigurationsGet(configs);
JSONArray buttons = new JSONArray();
for (ConfigPayload payload : configs) buttons.put(String.valueOf(payload));
String json = buttons.toString();
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_BUTTONS, json));
}
}); */
queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED), false);
}
private void syncButtonSettings(){
String buttonConfig = getDeviceSpecificPreferences().getString(CONFIG_ITEM_BUTTONS, null);
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_BUTTONS, buttonConfig));
overwriteButtons(buttonConfig);
}
private SharedPreferences getDeviceSpecificPreferences(){
return GBApplication.getDeviceSpecificSharedPrefs(
getDeviceSupport().getDevice().getAddress()
);
}
private void syncConfiguration(){
SharedPreferences preferences = getDeviceSpecificPreferences();
int stepGoal = preferences.getInt(CONFIG_ITEM_STEP_GOAL, 1000000);
byte vibrationStrength = (byte) preferences.getInt(CONFIG_ITEM_VIBRATION_STRENGTH, 100);
int timezoneOffset = preferences.getInt(CONFIG_ITEM_TIMEZONE_OFFSET, 0);
GBDevice device = getDeviceSupport().getDevice();
device.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(stepGoal)));
device.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(vibrationStrength)));
device.addDeviceInfo(new GenericItem(ITEM_TIMEZONE_OFFSET, String.valueOf(timezoneOffset)));
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[]{
new ConfigurationPutRequest.DailyStepGoalConfigItem(stepGoal),
new ConfigurationPutRequest.VibrationStrengthConfigItem(vibrationStrength),
new ConfigurationPutRequest.TimezoneOffsetConfigItem((short) timezoneOffset)
}, this));
}
public int getMTU() {
if (this.MTU < 0) throw new RuntimeException("MTU not configured");
return this.MTU;
}
@Override
public void playPairingAnimation() {
queueWrite(new AnimationRequest(), false);
}
@Override
public void playNotification(NotificationConfiguration config) {
if (config.getPackageName() == null) {
log("package name in notification not set");
return;
}
queueWrite(new PlayNotificationRequest(config.getPackageName(), this), false);
}
@Override
public void setTime() {
long millis = System.currentTimeMillis();
TimeZone zone = new GregorianCalendar().getTimeZone();
queueWrite(
new ConfigurationPutRequest(
new ConfigurationPutRequest.TimeConfigItem(
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
(short) (millis % 1000),
(short) ((zone.getRawOffset() + (zone.inDaylightTime(new Date()) ? 1 : 0)) / 60000)
),
this), false
);
}
@Override
public void overwriteButtons(String jsonConfigString) {
try {
if(jsonConfigString == null) return;
getDeviceSpecificPreferences()
.edit()
.putString(CONFIG_ITEM_BUTTONS, jsonConfigString)
.apply();
JSONArray buttonConfigJson = new JSONArray(jsonConfigString);
// JSONArray buttonConfigJson = new JSONArray(getDeviceSupport().getDevice().getDeviceInfo(ITEM_BUTTONS).getDetails());
ConfigPayload[] payloads = new ConfigPayload[buttonConfigJson.length()];
for (int i = 0; i < buttonConfigJson.length(); i++) {
try {
payloads[i] = ConfigPayload.valueOf(buttonConfigJson.getString(i));
} catch (IllegalArgumentException e) {
payloads[i] = ConfigPayload.FORWARD_TO_PHONE;
}
}
ConfigFileBuilder builder = new ConfigFileBuilder(payloads);
FilePutRequest fileUploadRequets = new FilePutRequest((short) 0x0600, builder.build(true), this) {
@Override
public void onFilePut(boolean success) {
if (success)
GB.toast("successfully overwritten button settings", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error overwriting button settings", Toast.LENGTH_SHORT, GB.INFO);
}
};
queueWrite(fileUploadRequets);
} catch (JSONException e) {
GB.log("error", GB.ERROR, e);
}
}
@Override
public void setActivityHand(double progress) {
queueWrite(new ConfigurationPutRequest(
new ConfigurationPutRequest.CurrentStepCountConfigItem(Math.min(999999, (int) (1000000 * progress))),
this
), false);
}
@Override
public void setHands(short hour, short minute) {
queueWrite(new MoveHandsRequest(false, minute, hour, (short) -1), false);
}
public void vibrate(nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest.VibrationType vibration) {
// queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest(vibration, -1, -1));
}
@Override
public void vibrateFindMyDevicePattern() {
}
@Override
public void requestHandsControl() {
queueWrite(new RequestHandControlRequest(), false);
}
@Override
public void releaseHandsControl() {
queueWrite(new ReleaseHandsControlRequest(), false);
}
@Override
public void setStepGoal(int stepGoal) {
getDeviceSpecificPreferences()
.edit()
.putInt(CONFIG_ITEM_STEP_GOAL, stepGoal)
.apply();
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.DailyStepGoalConfigItem(stepGoal), this) {
@Override
public void onFilePut(boolean success) {
if (success)
GB.toast("successfully updated step goal", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error updating step goal", Toast.LENGTH_SHORT, GB.INFO);
}
}, false);
}
@Override
public void setVibrationStrength(short strength) {
getDeviceSpecificPreferences()
.edit()
.putInt(CONFIG_ITEM_VIBRATION_STRENGTH, (byte) strength)
.apply();
ConfigurationPutRequest.ConfigItem vibrationItem = new ConfigurationPutRequest.VibrationStrengthConfigItem((byte) strength);
queueWrite(
new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[]{vibrationItem}, this) {
@Override
public void onFilePut(boolean success) {
if (success)
GB.toast("successfully updated vibration strength", Toast.LENGTH_SHORT, GB.INFO);
else
GB.toast("error updating vibration strength", Toast.LENGTH_SHORT, GB.INFO);
}
}, false
);
// queueWrite(new FileVerifyRequest((short) 0x0800));
}
@Override
public void syncNotificationSettings() {
log("syncing notification settings...");
try {
PackageConfigHelper helper = new PackageConfigHelper(getContext());
final ArrayList<NotificationConfiguration> configurations = helper.getNotificationConfigurations();
if (configurations.size() == 1) configurations.add(configurations.get(0));
queueWrite(new NotificationFilterPutRequest(configurations, FossilWatchAdapter.this) {
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
if (!success) {
GB.toast("error writing notification settings", Toast.LENGTH_SHORT, GB.ERROR);
getDeviceSupport().getDevice().setState(GBDevice.State.NOT_CONNECTED);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
getDeviceSupport().getDevice().setState(GBDevice.State.INITIALIZED);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
}, false);
} catch (GBException e) {
GB.log("error", GB.ERROR, e);
}
}
@Override
public void onTestNewFunction() {
queueWrite(new FilePutRequest(
(short) 0x0600,
new byte[]{
(byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x01, (byte) 0x24, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x30, (byte) 0x52, (byte) 0xFF, (byte) 0x26, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x09, (byte) 0x04, (byte) 0x01, (byte) 0x03, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x09, (byte) 0x04, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x24, (byte) 0x00, (byte) 0x00, (byte) 0x24, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x1F, (byte) 0xBE, (byte) 0xB4, (byte) 0x1B
},
this)
);
}
@Override
public void setTimezoneOffsetMinutes(short offset) {
getDeviceSpecificPreferences()
.edit()
.putInt(CONFIG_ITEM_TIMEZONE_OFFSET, offset)
.apply();
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.TimezoneOffsetConfigItem(offset), this){
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
if(success) GB.toast("successfully updated timezone", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error updating timezone", Toast.LENGTH_SHORT, GB.ERROR);
}
});
}
@Override
public boolean supportsFindDevice() {
return false;
}
@Override
public boolean supportsExtendedVibration() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
return false;
case "DN.1.0":
return true;
}
throw new UnsupportedOperationException("model " + modelNumber + " not supported");
}
@Override
public boolean supportsActivityHand() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
return false;
case "DN.1.0":
return false;
}
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
}
@Override
public void onFetchActivityData() {
// queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[0], this));
setVibrationStrength((byte) 50);
// queueWrite(new FileCloseRequest((short) 0x0800));
// queueWrite(new ConfigurationGetRequest(this));
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
// throw new RuntimeException("noope");
ArrayList<nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm> activeAlarms = new ArrayList<>();
for (Alarm alarm : alarms){
if(!alarm.getEnabled()) continue;
if(alarm.getRepetition() == 0){
activeAlarms.add(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm(
(byte) alarm.getMinute(),
(byte) alarm.getHour(),
false
));
continue;
}
int repitition = alarm.getRepetition();
repitition = (repitition << 1) | ((repitition >> 6) & 1);
activeAlarms.add(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm(
(byte) alarm.getMinute(),
(byte) alarm.getHour(),
(byte) repitition
));
}
queueWrite(new AlarmsSetRequest(activeAlarms.toArray(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm[0]), this){
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
if(success) GB.toast("successfully set alarms", Toast.LENGTH_SHORT, GB.INFO);
else GB.toast("error setting alarms", Toast.LENGTH_SHORT, GB.INFO);
}
});
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
switch (characteristic.getUuid().toString()) {
case "3dda0006-957f-7d4a-34a6-74696673696d": {
handleBackgroundCharacteristic(characteristic);
break;
}
case "3dda0002-957f-7d4a-34a6-74696673696d":
case "3dda0004-957f-7d4a-34a6-74696673696d":
case "3dda0003-957f-7d4a-34a6-74696673696d": {
if (fossilRequest != null) {
boolean requestFinished;
try {
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
byte requestType = (byte) (characteristic.getValue()[0] & 0x0F);
if (requestType != 0x0A && requestType != fossilRequest.getType()) {
// throw new RuntimeException("Answer type " + requestType + " does not match current request " + fossilRequest.getType());
}
}
fossilRequest.handleResponse(characteristic);
requestFinished = fossilRequest.isFinished();
} catch (RuntimeException e) {
GB.log("error", GB.ERROR, e);
getDeviceSupport().notifiyException(e);
GB.toast(fossilRequest.getName() + " failed", Toast.LENGTH_SHORT, GB.ERROR);
requestFinished = true;
}
if (requestFinished) {
log(fossilRequest.getName() + " finished");
fossilRequest = null;
} else {
return true;
}
}
queueNextRequest();
}
}
return true;
}
private void handleBackgroundCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
switch (value[1]) {
case 2: {
byte syncId = value[2];
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(QHybridSupport.ITEM_LAST_HEARTBEAT, DateFormat.getTimeInstance().format(new Date())));
break;
}
case 8: {
if (value.length != 12) {
throw new RuntimeException("wrong button message");
}
int index = value[2] & 0xFF;
int button = value[9] >> 4 & 0xFF;
if (index != this.lastButtonIndex) {
lastButtonIndex = index;
log("Button press on button " + button);
Intent i = new Intent(QHYBRID_EVENT_BUTTON_PRESS);
i.putExtra("BUTTON", button);
getContext().sendBroadcast(i);
}
break;
}
case 5: {
if (value.length != 4) {
throw new RuntimeException("wrong button message");
}
int action = value[3];
String actionString = "SINGLE";
if(action == 3) actionString = "DOUBLE";
else if(action == 4) actionString = "LONG";
// lastButtonIndex = index;
log(actionString + " button press");
Intent i = new Intent(QHYBRID_EVENT_MULTI_BUTTON_PRESS);
i.putExtra("ACTION", actionString);
getContext().sendBroadcast(i);
break;
}
}
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
super.onMtuChanged(gatt, mtu, status);
log("MTU changed: " + mtu);
this.MTU = mtu;
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_MTU, String.valueOf(mtu)));
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
((RequestMtuRequest) fossilRequest).setFinished(true);
queueNextRequest();
}
public void queueWrite(RequestMtuRequest request, boolean priorise) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
new TransactionBuilder("requestMtu")
.requestMtu(512)
.queue(getDeviceSupport().getQueue());
this.fossilRequest = request;
}
}
private void log(String message) {
logger.debug(message);
}
public void queueWrite(SetDeviceStateRequest request, boolean priorise) {
if (fossilRequest != null && !fossilRequest.isFinished()) {
log("queing request: " + request.getName());
if (priorise) {
requestQueue.add(0, request);
} else {
requestQueue.add(request);
}
return;
}
log("setting device state: " + request.getDeviceState());
getDeviceSupport().getDevice().setState(request.getDeviceState());
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
queueNextRequest();
}
public void queueWrite(FossilRequest request, boolean priorise) {
if (fossilRequest != null && !fossilRequest.isFinished()) {
log("queing request: " + request.getName());
if (priorise) {
requestQueue.add(0, request);
} else {
requestQueue.add(request);
}
return;
}
log("executing request: " + request.getName());
this.fossilRequest = request;
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
}
public void queueWrite(Request request, boolean priorise) {
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
queueNextRequest();
}
void queueWrite(Request request) {
if (request instanceof SetDeviceStateRequest)
queueWrite((SetDeviceStateRequest) request, false);
else if (request instanceof RequestMtuRequest)
queueWrite((RequestMtuRequest) request, false);
else if (request instanceof FossilRequest) queueWrite((FossilRequest) request, false);
else queueWrite(request, false);
}
private void queueNextRequest() {
try {
Request request = requestQueue.remove(0);
queueWrite(request);
} catch (IndexOutOfBoundsException e) {
log("requestsQueue empty");
}
}
}

View File

@ -0,0 +1,483 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.misfit;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.TimeZone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ActivityPointGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.AnimationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.BatteryLevelRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.EraseFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetCountdownSettingsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetCurrentStepCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetStepGoalRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetVibrationStrengthRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GoalTrackingGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ListFilesRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.OTAEnterRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.OTAEraseRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ReleaseHandsControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.RequestHandControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetCurrentStepCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetStepGoalRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetTimeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetVibrationStrengthRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.UploadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.VibrateRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_ACTIVITY_POINT;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_GOAL;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_VIBRATION_STRENGTH;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_FILE_UPLOADED;
public class MisfitWatchAdapter extends WatchAdapter {
private int lastButtonIndex = -1;
private final SparseArray<Request> responseFilters = new SparseArray<>();
private UploadFileRequest uploadFileRequest;
private Request fileRequest = null;
private Queue<Request> requestQueue = new ArrayDeque<>();
Logger logger = LoggerFactory.getLogger(getClass());
public MisfitWatchAdapter(QHybridSupport deviceSupport) {
super(deviceSupport);
fillResponseList();
}
@Override
public void initialize() {
requestQueue.add(new GetStepGoalRequest());
requestQueue.add(new GetVibrationStrengthRequest());
requestQueue.add(new ActivityPointGetRequest());
requestQueue.add(prepareSetTimeRequest());
requestQueue.add(new AnimationRequest());
requestQueue.add(new SetCurrentStepCountRequest((int) (999999 * getDeviceSupport().calculateNotificationProgress())));
queueWrite(new GetCurrentStepCountRequest());
getDeviceSupport().getDevice().setState(GBDevice.State.INITIALIZED);
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
}
private SetTimeRequest prepareSetTimeRequest() {
long millis = System.currentTimeMillis();
TimeZone zone = new GregorianCalendar().getTimeZone();
return new SetTimeRequest(
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
(short) (millis % 1000),
(short) ((zone.getRawOffset() + zone.getDSTSavings()) / 60000));
}
@Override
public void playPairingAnimation() {
queueWrite(new AnimationRequest());
}
@Override
public void playNotification(NotificationConfiguration config) {
queueWrite(new PlayNotificationRequest(
config.getVibration(),
config.getHour(),
config.getMin(),
config.getSubEye()
));
}
@Override
public void setTime() {
queueWrite(prepareSetTimeRequest());
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
GBDevice gbDevice = getDeviceSupport().getDevice();
switch (characteristic.getUuid().toString()) {
case "3dda0004-957f-7d4a-34a6-74696673696d":
case "3dda0003-957f-7d4a-34a6-74696673696d": {
return handleFileDownloadCharacteristic(characteristic);
}
case "3dda0007-957f-7d4a-34a6-74696673696d": {
return handleFileUploadCharacteristic(characteristic);
}
case "3dda0002-957f-7d4a-34a6-74696673696d": {
return handleBasicCharacteristic(characteristic);
}
case "3dda0006-957f-7d4a-34a6-74696673696d": {
return handleButtonCharacteristic(characteristic);
}
case "00002a19-0000-1000-8000-00805f9b34fb": {
short level = characteristic.getValue()[0];
gbDevice.setBatteryLevel(level);
gbDevice.setBatteryThresholdPercent((short) 2);
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = gbDevice.getBatteryLevel();
batteryInfo.state = BatteryState.BATTERY_NORMAL;
getDeviceSupport().handleGBDeviceEvent(batteryInfo);
break;
}
default: {
log("unknown shit on " + characteristic.getUuid().toString() + ": " + arrayToString(characteristic.getValue()));
try {
File charLog = new File("/sdcard/qFiles/charLog.txt");
if (!charLog.exists()) {
charLog.createNewFile();
}
FileOutputStream fos = new FileOutputStream(charLog, true);
fos.write((new Date().toString() + ": " + characteristic.getUuid().toString() + ": " + arrayToString(characteristic.getValue())).getBytes());
} catch (IOException e) {
GB.log("error", GB.ERROR, e);
}
break;
}
}
return getDeviceSupport().onCharacteristicChanged(gatt, characteristic);
}
private void fillResponseList() {
Class<? extends Request>[] classes = new Class[]{
BatteryLevelRequest.class,
GetStepGoalRequest.class,
GetVibrationStrengthRequest.class,
GetCurrentStepCountRequest.class,
OTAEnterRequest.class,
GoalTrackingGetRequest.class,
ActivityPointGetRequest.class,
GetCountdownSettingsRequest.class
};
for (Class<? extends Request> c : classes) {
try {
c.getSuperclass().getDeclaredMethod("handleResponse", BluetoothGattCharacteristic.class);
Request object = c.newInstance();
byte[] sequence = object.getStartSequence();
if (sequence.length > 1) {
responseFilters.put((int) object.getStartSequence()[1], object);
log("response filter " + object.getStartSequence()[1] + ": " + c.getSimpleName());
}
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException e) {
log("skipping class " + c.getName());
}
}
}
private boolean handleBasicCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] values = characteristic.getValue();
Request request = resolveAnswer(characteristic);
GBDevice gbDevice = getDeviceSupport().getDevice();
if (request == null) {
StringBuilder valueString = new StringBuilder(String.valueOf(values[0]));
for (int i = 1; i < characteristic.getValue().length; i++) {
valueString.append(", ").append(values[i]);
}
log("unable to resolve " + characteristic.getUuid().toString() + ": " + valueString);
return true;
}
log("response: " + request.getClass().getSimpleName());
request.handleResponse(characteristic);
if (request instanceof GetStepGoalRequest) {
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(((GetStepGoalRequest) request).stepGoal)));
} else if (request instanceof GetVibrationStrengthRequest) {
int strength = ((GetVibrationStrengthRequest) request).strength;
gbDevice.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(strength)));
} else if (request instanceof GetCurrentStepCountRequest) {
int steps = ((GetCurrentStepCountRequest) request).steps;
logger.debug("get current steps: " + steps);
try {
File f = new File("/sdcard/qFiles/");
if (!f.exists()) f.mkdir();
File file = new File("/sdcard/qFiles/steps");
if (!file.exists()) {
file.createNewFile();
}
logger.debug("Writing file " + file.getPath());
FileOutputStream fos = new FileOutputStream(file, true);
fos.write((System.currentTimeMillis() + ": " + steps + "\n").getBytes());
fos.close();
logger.debug("file written.");
} catch (Exception e) {
GB.log("error", GB.ERROR, e);
}
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_COUNT, String.valueOf(((GetCurrentStepCountRequest) request).steps)));
} else if (request instanceof OTAEnterRequest) {
if (((OTAEnterRequest) request).success) {
fileRequest = new OTAEraseRequest(1024 << 16);
queueWrite(fileRequest);
}
} else if (request instanceof ActivityPointGetRequest) {
gbDevice.addDeviceInfo(new GenericItem(ITEM_ACTIVITY_POINT, String.valueOf(((ActivityPointGetRequest) request).activityPoint)));
}
try {
queueWrite(requestQueue.remove());
} catch (NoSuchElementException e) {
}
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(DeviceManager.ACTION_DEVICES_CHANGED));
return true;
}
private Request resolveAnswer(BluetoothGattCharacteristic characteristic) {
byte[] values = characteristic.getValue();
if (values[0] != 3) return null;
return responseFilters.get(values[1]);
}
private boolean handleFileDownloadCharacteristic(BluetoothGattCharacteristic characteristic) {
Request request;
request = fileRequest;
request.handleResponse(characteristic);
if (request instanceof ListFilesRequest) {
if (((ListFilesRequest) request).completed) {
logger.debug("File count: " + ((ListFilesRequest) request).fileCount + " size: " + ((ListFilesRequest) request).size);
if (((ListFilesRequest) request).fileCount == 0) return true;
// queueWrite(new DownloadFileRequest((short) (256 + ((ListFilesRequest) request).fileCount)));
}
} else if (request instanceof DownloadFileRequest) {
if (((FileRequest) request).completed) {
logger.debug("file " + ((DownloadFileRequest) request).fileHandle + " completed: " + ((DownloadFileRequest) request).size);
// backupFile((DownloadFileRequest) request);
}
} else if (request instanceof EraseFileRequest) {
if (((EraseFileRequest) request).fileHandle > 257) {
queueWrite(new DownloadFileRequest((short) (((EraseFileRequest) request).fileHandle - 1)));
}
}
return true;
}
private boolean handleFileUploadCharacteristic(BluetoothGattCharacteristic characteristic) {
if (uploadFileRequest == null) {
logger.debug("no uploadFileRequest to handle response");
return true;
}
uploadFileRequest.handleResponse(characteristic);
switch (uploadFileRequest.state) {
case ERROR:
Intent fileIntent = new Intent(QHYBRID_EVENT_FILE_UPLOADED);
fileIntent.putExtra("EXTRA_ERROR", true);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(fileIntent);
uploadFileRequest = null;
break;
case UPLOAD:
for (byte[] packet : this.uploadFileRequest.packets) {
new TransactionBuilder("File upload").write(characteristic, packet).queue(getDeviceSupport().getQueue());
}
break;
case UPLOADED:
fileIntent = new Intent(QHYBRID_EVENT_FILE_UPLOADED);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(fileIntent);
uploadFileRequest = null;
break;
}
return true;
}
private boolean handleButtonCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (value.length != 11) {
logger.debug("wrong button message");
return true;
}
int index = value[6] & 0xFF;
int button = value[8] >> 4 & 0xFF;
if (index != this.lastButtonIndex) {
lastButtonIndex = index;
logger.debug("Button press on button " + button);
Intent i = new Intent(QHYBRID_EVENT_BUTTON_PRESS);
i.putExtra("BUTTON", button);
//ByteBuffer buffer = ByteBuffer.allocate(16);
//buffer.put(new byte[]{0x01, 0x00, 0x08});
//buffer.put(value, 2, 8);
//buffer.put(new byte[]{(byte)0xFF, 0x05, 0x00, 0x01, 0x00});
//FilePutRequest request = new FilePutRequest((short)0, buffer.array());
//for(byte[] packet : request.packets){
// new TransactionBuilder("File upload").write(getCharacteristic(UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d")), packet).queue(getQueue());
//}
getContext().sendBroadcast(i);
}
return true;
}
private void log(String message){
logger.debug(message);
}
public void setActivityHand(double progress) {
queueWrite(new SetCurrentStepCountRequest(Math.min((int) (1000000 * progress), 999999)));
}
public void setHands(short hour, short minute) {
queueWrite(new MoveHandsRequest(false, minute, hour, (short) -1));
}
public void vibrate(PlayNotificationRequest.VibrationType vibration) {
queueWrite(new PlayNotificationRequest(vibration, -1, -1));
}
@Override
public void vibrateFindMyDevicePattern() {
queueWrite(new VibrateRequest(false, (short) 4, (short) 1));
}
@Override
public void requestHandsControl() {
queueWrite(new RequestHandControlRequest());
}
@Override
public void releaseHandsControl() {
queueWrite(new ReleaseHandsControlRequest());
}
@Override
public void setStepGoal(int stepGoal) {
queueWrite(new SetStepGoalRequest(stepGoal));
}
@Override
public void setVibrationStrength(short strength) {
queueWrite(new SetVibrationStrengthRequest(strength));
}
@Override
public void syncNotificationSettings() {
}
@Override
public void onTestNewFunction() {
}
@Override
public void setTimezoneOffsetMinutes(short offset) {
GB.toast("old firmware does't support timezones", Toast.LENGTH_LONG, GB.ERROR);
}
@Override
public boolean supportsFindDevice() {
return supportsExtendedVibration();
}
@Override
public boolean supportsExtendedVibration() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
return false;
case "DN.1.0":
return true;
}
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
}
@Override
public boolean supportsActivityHand() {
String modelNumber = getDeviceSupport().getDevice().getModel();
switch (modelNumber) {
case "HW.0.0":
return true;
case "HL.0.0":
return false;
case "DN.1.0":
return false;
}
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
}
@Override
public void onFetchActivityData() {
requestQueue.add(new BatteryLevelRequest());
requestQueue.add(new GetCurrentStepCountRequest());
// requestQueue.add(new ListFilesRequest());
queueWrite(new ActivityPointGetRequest());
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
GB.toast("alarms not supported with this firmware", Toast.LENGTH_LONG, GB.ERROR);
return;
}
@Override
public void overwriteButtons(String jsonConfigString) {
uploadFileRequest = new UploadFileRequest((short) 0x0800, new byte[]{
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00,
(byte) 0x30, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x2E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
(byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x8B, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01,
(byte) 0x08, (byte) 0x01, (byte) 0x14, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xBF, (byte) 0xD5, (byte) 0x54, (byte) 0xD1,
(byte) 0x00
});
queueWrite(uploadFileRequest);
}
private void queueWrite(Request request) {
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
// if (request instanceof FileRequest) this.fileRequest = request;
if (!request.expectsResponse()) {
try {
queueWrite(requestQueue.remove());
} catch (NoSuchElementException e) {
}
}
}
}

View File

@ -0,0 +1,82 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.zip.CRC32;
public class ConfigFileBuilder {
private ConfigPayload[] configs;
public ConfigFileBuilder(ConfigPayload[] configs) {
this.configs = configs;
}
public byte[] build(boolean appendChecksum) {
int payloadSize = 0;
for (ConfigPayload payload : this.configs) {
payloadSize += payload.getData().length;
}
int headerSize = 0;
for (ConfigPayload payload : this.configs) {
headerSize += payload.getHeader().length + 3; // button + version + null;
}
ByteBuffer buffer = ByteBuffer.allocate(
3 // version bytes
+ 1 // header count byte
+ headerSize
+ 1 // payload count byte
+ payloadSize
+ 1 // customization count byte
+ (appendChecksum ? 4 : 0) // checksum
);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x00}); // version
buffer.put((byte) this.configs.length);
int buttonIndex = 0x00;
for (ConfigPayload payload : configs) {
buffer.put((byte) (buttonIndex += 0x10));
buffer.put((byte) 0x01);
buffer.put(payload.getHeader());
buffer.put((byte) 0x00);
}
ArrayList<ConfigPayload> distinctPayloads = new ArrayList<>(3);
// distinctPayloads.add(configs[0].getData());
compareLoop:
for (int payloadIndex = 0; payloadIndex < configs.length; payloadIndex++) {
for (int compareTo = 0; compareTo < distinctPayloads.size(); compareTo++) {
if (configs[payloadIndex].equals(distinctPayloads.get(compareTo))) {
continue compareLoop;
}
}
distinctPayloads.add(configs[payloadIndex]);
}
buffer.put((byte) distinctPayloads.size());
for (ConfigPayload payload : distinctPayloads) {
buffer.put(payload.getData());
}
buffer.put((byte) 0x00);
ByteBuffer buffer2 = ByteBuffer.allocate(buffer.position() + (appendChecksum ? 4 : 0));
buffer2.order(ByteOrder.LITTLE_ENDIAN);
buffer2.put(buffer.array(), 0, buffer.position());
if (!appendChecksum) return buffer2.array();
CRC32 crc = new CRC32();
crc.update(buffer.array(), 0, buffer.position());
buffer2.putInt((int) crc.getValue());
return buffer2.array();
}
}

View File

@ -0,0 +1,89 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
public enum ConfigPayload {
FORWARD_TO_PHONE(
"forward to phone",
new byte[]{(byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x2E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x8B, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x08, (byte) 0x01, (byte) 0x14, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xBF, (byte) 0xD5, (byte) 0x54, (byte) 0xD1,}
),
FORWARD_TO_PHONE_MULTI(
"forward to phone (multifunction)",
new byte[]{(byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x00},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x63, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x44, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xCD, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB6, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x04, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB5, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x7B, (byte) 0x56, (byte) 0x4E, (byte) 0x97}
),
STOPWATCH(
"stopwatch",
new byte[]{(byte) 0x02, (byte) 0x01, (byte) 0x20, (byte) 0x01},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x07, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x08, (byte) 0x00, (byte) 0x92, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x0F, (byte) 0xC0, (byte) 0x5F, (byte) 0x2A}
),
DATE(
"show date",
new byte[]{(byte) 0x01, (byte) 0x01, (byte) 0x14, (byte) 0x00},
new byte[]{(byte) 0x01 , (byte) 0x00 , (byte) 0x01 , (byte) 0x01 , (byte) 0x14 , (byte) 0x2D , (byte) 0x00 , (byte) 0x00 , (byte) 0x00 , (byte) 0x01 , (byte) 0x00 , (byte) 0x06 , (byte) 0x00 , (byte) 0x02 , (byte) 0x00 , (byte) 0x00 , (byte) 0x07 , (byte) 0x00 , (byte) 0x01 , (byte) 0x01 , (byte) 0x16 , (byte) 0x00 , (byte) 0x89 , (byte) 0x05 , (byte) 0x01 , (byte) 0x07 , (byte) 0xB0 , (byte) 0x00 , (byte) 0x00 , (byte) 0xB0 , (byte) 0x00 , (byte) 0x00 , (byte) 0xB0 , (byte) 0x00 , (byte) 0x00 , (byte) 0x08 , (byte) 0x01 , (byte) 0x50 , (byte) 0x00 , (byte) 0x01 , (byte) 0x00 , (byte) 0xD0 , (byte) 0x89 , (byte) 0xDE , (byte) 0x6E}
),
LAST_NOTIFICATION(
"show last notification",
new byte[]{(byte) 0x01, (byte) 0x01, (byte) 0x18, (byte) 0x00},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x18, (byte) 0x2F, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x16, (byte) 0x00, (byte) 0x89, (byte) 0x05, (byte) 0x01, (byte) 0x07, (byte) 0xB0, (byte) 0x02, (byte) 0x00, (byte) 0xB0, (byte) 0x02, (byte) 0x00, (byte) 0xB0, (byte) 0x02, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x6B, (byte) 0x9D, (byte) 0x55, (byte) 0x3A}
),
SECOND_TIMEZONE(
"show second timezone",
new byte[]{0x01, (byte) 0x01, (byte) 0x16, (byte) 0x00},
new byte[]{0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x16, (byte) 0x2F, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x02, (byte) 0x02, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x16, (byte) 0x00, (byte) 0x89, (byte) 0x05, (byte) 0x01, (byte) 0x07, (byte) 0xB0, (byte) 0x01, (byte) 0x00, (byte) 0xB0, (byte) 0x01, (byte) 0x00, (byte) 0xB0, (byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x3D, (byte) 0x07, (byte) 0x28, (byte) 0x01}
)
/* PLAY_PAUSE(
"play/pause music",
new byte[]{(byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x00},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x63, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x44, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xCD, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB6, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x04, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB5, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x7B, (byte) 0x56, (byte) 0x4E, (byte) 0x97}
),
VOLUME_UP(
"music volume up",
new byte[]{(byte) 0x01, (byte) 0x04, (byte) 0x12, (byte) 0x00},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x12, (byte) 0x5E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x48, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xE9, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0B, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xE9, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x0A, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xC6, (byte) 0xB2, (byte) 0xCB, (byte) 0xAC}
),
VOLUME_DOWN(
"music volume down",
new byte[]{(byte) 0x01, (byte) 0x05, (byte) 0x12, (byte) 0x00},
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x12, (byte) 0x5E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x48, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xEA, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0B, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xEA, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x0A, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xFA, (byte) 0x18, (byte) 0x49, (byte) 0x03}
) */
;
private byte[] header, data;
static public ConfigPayload fromId(short id) throws RuntimeException{
for(ConfigPayload payload : ConfigPayload.values()){
ByteBuffer buffer = ByteBuffer.wrap(payload.header);
buffer.order(ByteOrder.LITTLE_ENDIAN);
if(id == buffer.getShort(1)) return payload;
}
throw new RuntimeException("app " + id + " not found");
}
public byte[] getHeader() {
return header;
}
public byte[] getData() {
return data;
}
public String getDescription() {
return description;
}
public boolean equals(ConfigPayload p1, ConfigPayload p2){
return Arrays.equals(p1.getData(), p2.getData());
}
private String description;
ConfigPayload(String description, byte[] header, byte[] data) {
this.description = description;
this.header = header;
this.data = data;
}
}

View File

@ -0,0 +1,65 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
public abstract class Request {
protected byte[] data;
private Logger logger = (Logger) LoggerFactory.getLogger(getName());
//protected ByteBuffer buffer;
public Request(){
this.data = getStartSequence();
}
public ByteBuffer createBuffer(){
return createBuffer(getPayloadLength());
}
public ByteBuffer createBuffer(int length){
ByteBuffer buffer = ByteBuffer.allocate(length);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(getStartSequence());
return buffer;
}
public byte[] getRequestData(){
return data;
}
public UUID getRequestUUID(){
return UUID.fromString("3dda0002-957f-7d4a-34a6-74696673696d");
}
public int getPayloadLength(){ return getStartSequence().length; }
public abstract byte[] getStartSequence();
public void handleResponse(BluetoothGattCharacteristic characteristic){};
public String getName(){
Class thisClass = getClass();
while(thisClass.isAnonymousClass()) thisClass = thisClass.getSuperclass();
return thisClass.getSimpleName();
}
protected void log(String message){
logger.debug(message);
}
public boolean isBasicRequest(){
return this.getRequestUUID().toString().equals("3dda0002-957f-7d4a-34a6-74696673696d");
}
public boolean expectsResponse(){
return this.data[0] == 1;
}
}

View File

@ -0,0 +1,18 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public abstract class FossilRequest extends Request {
public abstract boolean isFinished();
public byte getType(){
return getStartSequence()[0];
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil;
import android.os.Build;
import androidx.annotation.RequiresApi;
public class RequestMtuRequest extends FossilRequest {
private int mtu;
private boolean finished = false;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public RequestMtuRequest(int mtu) {
this.mtu = mtu;
}
public int getMtu() {
return mtu;
}
@Override
public boolean isFinished() {
return finished;
}
public void setFinished(boolean finished) {
this.finished = finished;
}
@Override
public byte[] getStartSequence() {
return new byte[0];
}
}

View File

@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class SetDeviceStateRequest extends FossilRequest {
private GBDevice.State deviceState;
public SetDeviceStateRequest(GBDevice.State deviceState) {
this.deviceState = deviceState;
}
public GBDevice.State getDeviceState() {
return deviceState;
}
@Override
public boolean isFinished() {
return true;
}
@Override
public byte[] getStartSequence() {
return new byte[0];
}
}

View File

@ -0,0 +1,87 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
import androidx.annotation.NonNull;
public class Alarm {
public final int WEEKDAY_SUNDAY = 0;
public final int WEEKDAY_MONDAY = 1;
public final int WEEKDAY_TUESDAY = 2;
public final int WEEKDAY_THURSDAY = 3;
public final int WEEKDAY_WEDNESDAY = 4;
public final int WEEKDAY_FRIDAY = 5;
public final int WEEKDAY_SATURDAY = 6;
private byte days = 0;
private byte minute, hour;
private boolean repeat;
public Alarm(byte minute, byte hour){
this.minute = minute;
this.hour = hour;
this.repeat = false;
}
public Alarm(byte minute, byte hour, boolean repeat){
this.minute = minute;
this.hour = hour;
this.repeat = repeat;
}
public Alarm(byte minute, byte hour, byte days){
this.minute = minute;
this.hour = hour;
this.repeat = true;
this.days = days;
}
public void setDayEnabled(int day, boolean enabled){
if(enabled) this.days |= 1 << day;
else this.days &= ~(1 << day);
}
public byte[] getData(){
byte first = (byte) 0xFF;
if(repeat){
first = (byte) (0x80 | this.days);
}
byte second = (byte) this.minute;
if(repeat) second |= 0x80;
byte third = this.hour;
return new byte[]{first, second, third};
}
static public Alarm fromBytes(byte[] bytes){
if(bytes.length != 3) throw new RuntimeException("alarm bytes length must be 3");
byte days = bytes[0];
byte minutes = (byte)(bytes[1] & 0b01111111);
boolean repeat = (bytes[1] & 0x80) == 0x80;
if(repeat) {
return new Alarm(minutes, bytes[2], days);
}
return new Alarm(minutes, bytes[2]);
}
@NonNull
@Override
public String toString() {
String description = this.hour + ":" + this.minute + " ";
if(repeat){
String[] dayNames = new String[]{"sunday", "monday", "tuesday", "thursday", "wednesday", "friday", "saturday"};
for(int i = WEEKDAY_SUNDAY; i <= WEEKDAY_SATURDAY; i++){
if((days & 1 << i) != 0){
description += dayNames[i] + " ";
}
}
}else{
description += "not repeating";
}
return description;
}
}

View File

@ -0,0 +1,46 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
public class AlarmsGetRequest extends FileLookupAndGetRequest {
public AlarmsGetRequest(FossilWatchAdapter adapter) {
super((byte) 0x0A, adapter);
}
@Override
public void handleFileData(byte[] fileData) {
ByteBuffer buffer = ByteBuffer.wrap(fileData);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(0);
if(handle != (short) 0x0A00) throw new RuntimeException("wrong alarm handle");
int length = buffer.getInt(8) / 3;
Alarm[] alarms = new Alarm[length];
for (int i = 0; i < length; i++){
buffer.position(12 + i * 3);
byte[] alarmBytes = new byte[]{
buffer.get(),
buffer.get(),
buffer.get()
};
alarms[i] = Alarm.fromBytes(alarmBytes);
}
this.handleAlarms(alarms);
}
public void handleAlarms(Alarm[] alarms){
Alarm[] alarms2 = new Alarm[alarms.length];
for(int i = 0; i < alarms.length; i++){
alarms2[i] = Alarm.fromBytes(alarms[i].getData());
}
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
public class AlarmsSetRequest extends FilePutRequest {
public AlarmsSetRequest(Alarm[] alarms, FossilWatchAdapter adapter) {
super((short) 0x0A00, createFileFromAlarms(alarms), adapter);
}
static byte[] createFileFromAlarms(Alarm[] alarms){
ByteBuffer buffer = ByteBuffer.allocate(alarms.length * 3);
for(Alarm alarm : alarms) buffer.put(alarm.getData());
return buffer.array();
}
}

View File

@ -0,0 +1,50 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.button;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
public class ButtonConfigurationGetRequest extends FileGetRequest {
public ButtonConfigurationGetRequest(FossilWatchAdapter adapter) {
super((short) 0x0600, adapter);
}
@Override
public void handleFileData(byte[] fileData) {
log("fileData");
ByteBuffer buffer = ByteBuffer.wrap(fileData);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short fileHandle = buffer.getShort(0);
// TODO check file handle
// if(fileData != )
byte count = buffer.get(15);
ConfigPayload[] configs = new ConfigPayload[count];
buffer.position(16);
for(int i = 0; i < count; i++){
int buttonIndex = buffer.get() >> 4;
int entryCount = buffer.get();
buffer.get();
short appId = buffer.getShort();
buffer.position(buffer.position() + entryCount * 5 - 3);
try {
configs[buttonIndex - 1] = ConfigPayload.fromId(appId);
}catch (RuntimeException e){
configs[buttonIndex - 1] = null;
}
}
this.onConfigurationsGet(configs);
}
public void onConfigurationsGet(ConfigPayload[] configs){}
}

View File

@ -0,0 +1,44 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
public class ConfigurationGetRequest extends FileGetRequest {
public ConfigurationGetRequest(FossilWatchAdapter adapter) {
super((byte) 8, adapter);
}
@Override
public void handleFileData(byte[] fileData) {
byte[] data = new byte[fileData.length - 12 - 4];
System.arraycopy(fileData, 12, data, 0, data.length);
log("config file: " + getAdapter().arrayToString(fileData));
log("config file: " + getAdapter().arrayToString(data));
GBDevice device = getAdapter().getDeviceSupport().getDevice();
ConfigurationPutRequest.ConfigItem[] items = ConfigurationPutRequest.parsePayload(data);
for(ConfigurationPutRequest.ConfigItem item : items){
if(item instanceof ConfigurationPutRequest.VibrationStrengthConfigItem){
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_VIBRATION_STRENGTH, String.valueOf(((ConfigurationPutRequest.VibrationStrengthConfigItem) item).getValue())));
}else if(item instanceof ConfigurationPutRequest.DailyStepGoalConfigItem){
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_GOAL, String.valueOf(((ConfigurationPutRequest.DailyStepGoalConfigItem) item).getValue())));
}else if(item instanceof ConfigurationPutRequest.CurrentStepCountConfigItem){
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_COUNT, String.valueOf(((ConfigurationPutRequest.CurrentStepCountConfigItem) item).getValue())));
}else if(item instanceof ConfigurationPutRequest.TimezoneOffsetConfigItem) {
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_TIMEZONE_OFFSET, String.valueOf(((ConfigurationPutRequest.TimezoneOffsetConfigItem) item).getValue())));
}
}
device.sendDeviceUpdateIntent(getAdapter().getContext());
handleConfigurationLoaded();
}
public void handleConfigurationLoaded(){}
}

View File

@ -0,0 +1,263 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration;
import android.graphics.Bitmap;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileCloseAndPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ConfigurationPutRequest extends FilePutRequest {
private static HashMap<Short, Class<? extends ConfigItem>> itemsById = new HashMap<>();
static {
itemsById.put((short)3, DailyStepGoalConfigItem.class);
itemsById.put((short)10, VibrationStrengthConfigItem.class);
itemsById.put((short)2, CurrentStepCountConfigItem.class);
itemsById.put((short)3, DailyStepGoalConfigItem.class);
itemsById.put((short)12, TimeConfigItem.class);
}
static ConfigItem[] parsePayload(byte[] data) {
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN);
ArrayList<ConfigItem> configItems = new ArrayList<>();
while(buffer.hasRemaining()){
short id = buffer.getShort();
byte length = buffer.get();
byte[] payload = new byte[length];
for(int i = 0; i < length; i++){
payload[i] = buffer.get();
}
Class<? extends ConfigItem> configClass = itemsById.get(id);
if(configClass == null){
continue;
}
ConfigItem item = null;
try {
item = configClass.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
GB.log("error", GB.ERROR, e);
continue;
}
item.parseData(payload);
configItems.add(item);
}
return configItems.toArray(new ConfigItem[0]);
}
public ConfigurationPutRequest(ConfigItem item, FossilWatchAdapter adapter) {
super((short) 0x0800, createFileContent(new ConfigItem[]{item}), adapter);
}
public ConfigurationPutRequest(ConfigItem[] items, FossilWatchAdapter adapter) {
super((short) 0x0800, createFileContent(items), adapter);
}
private static byte[] createFileContent(ConfigItem[] items) {
int overallSize = 0;
for(ConfigItem item : items){
overallSize += item.getItemSize() + 3;
}
ByteBuffer buffer = ByteBuffer.allocate(overallSize);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for(ConfigItem item : items){
buffer.putShort(item.getId());
buffer.put((byte) item.getItemSize());
buffer.put(item.getContent());
}
return buffer.array();
}
public static abstract class ConfigItem {
public abstract int getItemSize();
public abstract short getId();
public abstract byte[] getContent();
public abstract void parseData(byte[] data);
}
static public class GenericConfigItem<T> extends ConfigItem {
private T value;
private short configId;
public GenericConfigItem(short configId, T value) {
this.value = value;
this.configId = configId;
}
public T getValue(){
return value;
}
@Override
public int getItemSize() {
switch (value.getClass().getName()) {
case "java.lang.Byte":
return 1;
case "java.lang.Short":
return 2;
case "java.lang.Integer":
return 4;
case "java.lang.Long":
return 8;
}
throw new UnsupportedOperationException("config type " + value.getClass().getName() + " not supported");
}
@Override
public short getId() {
return this.configId;
}
@Override
public byte[] getContent() {
ByteBuffer buffer = ByteBuffer.allocate(getItemSize());
buffer.order(ByteOrder.LITTLE_ENDIAN);
switch (value.getClass().getName()) {
case "java.lang.Byte": {
buffer.put((Byte) this.value);
break;
}
case "java.lang.Integer": {
buffer.putInt((Integer) this.value);
break;
}
case "java.lang.Long": {
buffer.putLong((Long) this.value);
break;
}
case "java.lang.Short": {
buffer.putShort((Short) this.value);
break;
}
}
return buffer.array();
}
@Override
public void parseData(byte[] data) {
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN);
switch (data.length){
case 1:{
this.value = (T) (Byte) buffer.get();
break;
}
case 2:{
this.value = (T) (Short) buffer.getShort();
break;
}
case 4:{
this.value = (T) (Integer) buffer.getInt();
break;
}
case 8:{
this.value = (T) (Long) buffer.getLong();
break;
}
}
}
}
static public class DailyStepGoalConfigItem extends GenericConfigItem<Integer> {
public DailyStepGoalConfigItem(){
this(-1);
}
public DailyStepGoalConfigItem(int value) {
super((short) 3, value);
}
}
static public class TimezoneOffsetConfigItem extends GenericConfigItem<Short> {
public TimezoneOffsetConfigItem(Short value) {
super((short) 17, value);
}
}
static public class VibrationStrengthConfigItem extends GenericConfigItem<Byte> {
public VibrationStrengthConfigItem(){
this((byte) -1);
}
public VibrationStrengthConfigItem(Byte value) {
super((short) 10, value);
}
}
static public class CurrentStepCountConfigItem extends GenericConfigItem<Integer> {
public CurrentStepCountConfigItem(){
this(-1);
}
public CurrentStepCountConfigItem(Integer value) {
super((short) 2, value);
}
}
static public class TimeConfigItem extends ConfigItem {
private int epochSeconds;
private short millis, offsetMinutes;
public TimeConfigItem(){
this(-1, (short) -1, (short) -1);
}
public TimeConfigItem(int epochSeconds, short millis, short offsetMinutes) {
this.epochSeconds = epochSeconds;
this.millis = millis;
this.offsetMinutes = offsetMinutes;
}
@Override
public int getItemSize() {
return 8;
}
@Override
public short getId() {
return (short) 12;
}
@Override
public byte[] getContent() {
ByteBuffer buffer = ByteBuffer.allocate(getItemSize());
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(this.epochSeconds);
buffer.putShort(millis);
buffer.putShort(offsetMinutes);
return buffer.array();
}
@Override
public void parseData(byte[] data) {
if(data.length != 8) throw new RuntimeException("wrong data");
ByteBuffer buffer = ByteBuffer.wrap(data);
buffer.order(ByteOrder.LITTLE_ENDIAN);
this.epochSeconds = buffer.getInt();
this.millis = buffer.getShort();
this.offsetMinutes = buffer.getShort();
}
}
}

View File

@ -0,0 +1,37 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.connection;
import android.bluetooth.BluetoothGattCharacteristic;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public class SetConnectionParametersRequest extends FossilRequest {
private boolean finished = false;
@Override
public boolean isFinished() {
return finished;
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0002-957f-7d4a-34a6-74696673696d");
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
this.finished = true;
}
@Override
public byte[] getStartSequence() {
return new byte[]{0x02, 0x09, 0x0C, 0x00, 0x0C, 0x00, 0x2D, 0x00, 0x58, 0x02};
}
@Override
public boolean isBasicRequest() {
return false;
}
}

View File

@ -0,0 +1,37 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.widget.Toast;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FileCloseAndPutRequest extends FileCloseRequest {
FossilWatchAdapter adapter;
byte[] data;
public FileCloseAndPutRequest(short fileHandle, byte[] data, FossilWatchAdapter adapter) {
super(fileHandle);
this.adapter = adapter;
this.data = data;
}
@Override
public void onPrepare() {
super.onPrepare();
adapter.queueWrite(new FilePutRequest(getHandle(), this.data, adapter) {
@Override
public void onFilePut(boolean success) {
super.onFilePut(success);
FileCloseAndPutRequest.this.onFilePut(success);
}
}, false);
}
public void onFilePut(boolean success){
}
}

View File

@ -0,0 +1,79 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public class FileCloseRequest extends FossilRequest {
private boolean isFinished = false;
private short handle;
public FileCloseRequest(short fileHandle) {
this.handle = fileHandle;
ByteBuffer buffer = this.createBuffer();
buffer.putShort(fileHandle);
this.data = buffer.array();
}
public short getHandle() {
return handle;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
if(!characteristic.getUuid().toString().equals(this.getRequestUUID().toString())){
throw new RuntimeException("wrong response UUID");
}
byte[] value = characteristic.getValue();
byte type = (byte)(value[0] & 0x0F);
if(type != 9) throw new RuntimeException("wrong response type");
if(value.length != 4) throw new RuntimeException("wrong response length");
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
if(this.handle != buffer.getShort(1)) throw new RuntimeException("wrong response handle");
byte status = buffer.get(3);
if(status != 0) throw new RuntimeException("wrong response status");
this.isFinished = true;
this.onPrepare();
}
public void onPrepare(){}
@Override
public byte[] getStartSequence() {
return new byte[]{9};
}
@Override
public int getPayloadLength() {
return 3;
}
@Override
public boolean isFinished(){
return this.isFinished;
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -0,0 +1,59 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public class FileDeleteRequest extends FossilRequest {
private boolean finished = false;
private short handle;
public FileDeleteRequest(short handle) {
this.handle = handle;
ByteBuffer buffer = createBuffer();
buffer.putShort(handle);
this.data = buffer.array();
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
if(!characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d"))
throw new RuntimeException("wrong response UUID");
byte[] value = characteristic.getValue();
if(value.length != 4) throw new RuntimeException("wrong response length");
if(value[0] != (byte) 0x8B) throw new RuntimeException("wrong response start");
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
if(buffer.getShort(1) != this.handle) throw new RuntimeException("wrong response handle");
if(buffer.get(3) != 0) throw new RuntimeException("wrong response status: " + buffer.get(3));
this.finished = true;
}
@Override
public boolean isFinished() {
return finished;
}
@Override
public byte[] getStartSequence() {
return new byte[]{(byte) 0x0B};
}
@Override
public int getPayloadLength() {
return 3;
}
}

View File

@ -0,0 +1,116 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.UUID;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public abstract class FileGetRequest extends FossilRequest {
private short handle;
private FossilWatchAdapter adapter;
private ByteBuffer fileBuffer;
private byte[] fileData;
private boolean finished = false;
public FileGetRequest(short handle, FossilWatchAdapter adapter) {
this.handle = handle;
this.adapter = adapter;
this.data =
createBuffer()
.putShort(handle)
.putInt(0)
.putInt(0xFFFFFFFF)
.array();
}
public FossilWatchAdapter getAdapter() {
return adapter;
}
@Override
public boolean isFinished(){
return finished;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
byte first = value[0];
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
if((first & 0x0F) == 1){
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
int size = buffer.getInt(4);
byte status = buffer.get(3);
if(status != 0){
throw new RuntimeException("FileGet error: " + status);
}
if(this.handle != handle){
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
}
log("file size: " + size);
fileBuffer = ByteBuffer.allocate(size);
}else if((first & 0x0F) == 8){
this.finished = true;
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
if(this.handle != handle){
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
}
CRC32 crc = new CRC32();
crc.update(this.fileData);
int crcExpected = buffer.getInt(8);
if((int) crc.getValue() != crcExpected){
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
}
this.handleFileData(this.fileData);
}
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
fileBuffer.put(value, 1, value.length - 1);
if((first & 0x80) == 0x80){
this.fileData = fileBuffer.array();
}
}
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
@Override
public byte[] getStartSequence() {
return new byte[]{1};
}
@Override
public int getPayloadLength() {
return 11;
}
abstract public void handleFileData(byte[] fileData);
}

View File

@ -0,0 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
public abstract class FileLookupAndGetRequest extends FileLookupRequest {
public FileLookupAndGetRequest(byte fileType, FossilWatchAdapter adapter) {
super(fileType, adapter);
}
@Override
public void handleFileLookup(short fileHandle){
getAdapter().queueWrite(new FileGetRequest(getHandle(), getAdapter()) {
@Override
public void handleFileData(byte[] fileData) {
FileLookupAndGetRequest.this.handleFileData(fileData);
}
}, true);
}
abstract public void handleFileData(byte[] fileData);
}

View File

@ -0,0 +1,123 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.UUID;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public class FileLookupRequest extends FossilRequest {
private short handle = -1;
private byte fileType;
private FossilWatchAdapter adapter;
private ByteBuffer fileBuffer;
private byte[] fileData;
protected boolean finished = false;
public FileLookupRequest(byte fileType, FossilWatchAdapter adapter) {
this.fileType = fileType;
this.adapter = adapter;
this.data =
createBuffer()
.put(fileType)
.array();
}
protected FossilWatchAdapter getAdapter() {
return adapter;
}
public short getHandle() {
if(!finished){
throw new UnsupportedOperationException("File lookup not finished");
}
return handle;
}
@Override
public boolean isFinished(){
return finished;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
byte first = value[0];
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
if((first & 0x0F) == 2){
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
int size = buffer.getInt(4);
byte status = buffer.get(3);
if(status != 0){
throw new RuntimeException("file lookup error: " + status);
}
if(this.handle != handle){
// throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
}
log("file size: " + size);
fileBuffer = ByteBuffer.allocate(size);
}else if((first & 0x0F) == 8){
this.finished = true;
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
CRC32 crc = new CRC32();
crc.update(this.fileData);
int crcExpected = buffer.getInt(8);
if((int) crc.getValue() != crcExpected){
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
}
ByteBuffer dataBuffer = ByteBuffer.wrap(fileData);
dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
this.handle = dataBuffer.getShort(0);
this.handleFileLookup(this.handle);
}
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
fileBuffer.put(value, 1, value.length - 1);
if((first & 0x80) == 0x80){
this.fileData = fileBuffer.array();
}
}
}
public void handleFileLookup(short fileHandle){}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, (byte) 0xFF};
}
@Override
public int getPayloadLength() {
return 3;
}
}

View File

@ -0,0 +1,231 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.bluetooth.BluetoothGattCharacteristic;
import android.widget.Toast;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.UUID;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.CRC32C;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FilePutRequest extends FossilRequest {
public enum UploadState {INITIALIZED, UPLOADING, CLOSING, UPLOADED}
public UploadState state;
public ArrayList<byte[]> packets = new ArrayList<>();
private short handle;
private FossilWatchAdapter adapter;
byte[] file;
int fullCRC;
public FilePutRequest(short handle, byte[] file, FossilWatchAdapter adapter) {
this.handle = handle;
this.adapter = adapter;
int fileLength = file.length + 16;
ByteBuffer buffer = this.createBuffer();
buffer.putShort(1, handle);
buffer.putInt(3, 0);
buffer.putInt(7, fileLength);
buffer.putInt(11, fileLength);
this.data = buffer.array();
this.file = file;
state = UploadState.INITIALIZED;
}
public short getHandle() {
return handle;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
int responseType = value[0] & 0x0F;
log("response: " + responseType);
switch (responseType) {
case 3: {
if (value.length != 5 || (value[0] & 0x0F) != 3) {
throw new RuntimeException("wrong answer header");
}
state = UploadState.UPLOADING;
TransactionBuilder transactionBuilder = new TransactionBuilder("file upload");
BluetoothGattCharacteristic uploadCharacteristic = adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0004-957f-7d4a-34a6-74696673696d"));
this.prepareFilePackets(this.file);
for (byte[] packet : packets) {
transactionBuilder.write(uploadCharacteristic, packet);
}
transactionBuilder.queue(adapter.getDeviceSupport().getQueue());
break;
}
case 8: {
if (value.length == 4) return;
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
int crc = buffer.getInt(8);
byte status = value[3];
if (status != 0) {
throw new RuntimeException("upload status: " + status);
}
if (handle != this.handle) {
throw new RuntimeException("wrong response handle");
}
if (crc != this.fullCRC) {
throw new RuntimeException("file upload exception: wrong crc");
}
ByteBuffer buffer2 = ByteBuffer.allocate(3);
buffer2.order(ByteOrder.LITTLE_ENDIAN);
buffer2.put((byte) 4);
buffer2.putShort(this.handle);
new TransactionBuilder("file close")
.write(
adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")),
buffer2.array()
)
.queue(adapter.getDeviceSupport().getQueue());
this.state = UploadState.CLOSING;
break;
}
case 4: {
if (value.length == 9) return;
if (value.length != 4 || (value[0] & 0x0F) != 4) {
throw new RuntimeException("wrong file closing header");
}
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short handle = buffer.getShort(1);
if (handle != this.handle) {
onFilePut(false);
throw new RuntimeException("wrong file closing handle");
}
byte status = buffer.get(3);
if (status != 0) {
onFilePut(false);
throw new RuntimeException("wrong closing status: " + status);
}
this.state = UploadState.UPLOADED;
onFilePut(true);
log("uploaded file");
break;
}
case 9: {
this.onFilePut(false);
throw new RuntimeException("file put timeout");
/*timeout = true;
ByteBuffer buffer2 = ByteBuffer.allocate(3);
buffer2.order(ByteOrder.LITTLE_ENDIAN);
buffer2.put((byte) 4);
buffer2.putShort(this.handle);
new TransactionBuilder("file close")
.write(
adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")),
buffer2.array()
)
.queue(adapter.getDeviceSupport().getQueue());
this.state = UploadState.CLOSING;
break;*/
}
}
}
}
@Override
public boolean isFinished() {
return this.state == UploadState.UPLOADED;
}
private void prepareFilePackets(byte[] file) {
int maxPacketSize = adapter.getMTU() - 4;
ByteBuffer buffer = ByteBuffer.allocate(file.length + 12 + 4);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort(handle);
buffer.put((byte) 2);
buffer.put((byte) 0);
buffer.putInt(0);
buffer.putInt(file.length);
buffer.put(file);
CRC32C crc = new CRC32C();
crc.update(file);
buffer.putInt((int) crc.getValue());
byte[] data = buffer.array();
CRC32 fullCRC = new CRC32();
fullCRC.update(data);
this.fullCRC = (int) fullCRC.getValue();
int packetCount = (int) Math.ceil(data.length / (float) maxPacketSize);
for (int i = 0; i < packetCount; i++) {
int currentPacketLength = Math.min(maxPacketSize, data.length - i * maxPacketSize);
byte[] packet = new byte[currentPacketLength + 1];
packet[0] = (byte) i;
System.arraycopy(data, i * maxPacketSize, packet, 1, currentPacketLength);
packets.add(packet);
}
}
public void onFilePut(boolean success) {
}
@Override
public byte[] getStartSequence() {
return new byte[]{0x03};
}
@Override
public int getPayloadLength() {
return 15;
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -0,0 +1,75 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public class FileVerifyRequest extends FossilRequest {
private boolean isFinished = false;
private short handle;
public FileVerifyRequest(short fileHandle) {
this.handle = fileHandle;
ByteBuffer buffer = this.createBuffer();
buffer.putShort(fileHandle);
this.data = buffer.array();
}
public short getHandle() {
return handle;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
if(!characteristic.getUuid().toString().equals(this.getRequestUUID().toString())){
throw new RuntimeException("wrong response UUID");
}
byte[] value = characteristic.getValue();
byte type = (byte)(value[0] & 0x0F);
if(type == 0x0A) return;
if(type != 4) throw new RuntimeException("wrong response type");
if(value.length != 4) throw new RuntimeException("wrong response length");
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
if(this.handle != buffer.getShort(1)) throw new RuntimeException("wrong response handle");
byte status = buffer.get(3);
if(status != 0) throw new RuntimeException("wrong response status");
this.isFinished = true;
this.onPrepare();
}
public void onPrepare(){}
@Override
public byte[] getStartSequence() {
return new byte[]{4};
}
@Override
public int getPayloadLength() {
return 3;
}
@Override
public boolean isFinished(){
return this.isFinished;
}
}

View File

@ -0,0 +1,160 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.microapp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public interface MicroAppCommand {
byte[] getData();
}
class StartCriticalCommand implements MicroAppCommand{
@Override
public byte[] getData() {
return new byte[]{(byte) 0x03, (byte) 0x00};
}
}
class CloseCommand implements MicroAppCommand{
@Override
public byte[] getData() {
return new byte[]{(byte) 0x01, (byte) 0x00};
}
}
class DelayCommand implements MicroAppCommand{
private double delayInSeconds;
public DelayCommand(double delayInSeconds) {
this.delayInSeconds = delayInSeconds;
}
@Override
public byte[] getData() {
return ByteBuffer.wrap(new byte[]{0x08, 0x01, 0x00, 0x00})
.order(ByteOrder.LITTLE_ENDIAN)
.putShort(2, (short)(delayInSeconds * 10f))
.array();
}
}
enum VibrationType{
NORMAL((byte) 0x04);
private byte value;
VibrationType(byte value){
this.value = value;
}
public byte getValue(){ return this.value; }
}
class VibrateCommand implements MicroAppCommand{
private VibrationType vibrationType;
public VibrateCommand(VibrationType vibrationType) {
this.vibrationType = vibrationType;
}
@Override
public byte[] getData() {
return ByteBuffer.wrap(new byte[]{(byte) 0x93, 0x00, 0x00})
.put(2, vibrationType.getValue())
.array();
}
}
enum MovingDirection{
CLOCKWISE((byte) 0x00),
COUNTER_CLOCKWISE((byte) 0x01),
SHORTEST((byte) 0x02),
;
private byte value;
MovingDirection(byte value){ this.value = value; }
public byte getValue() {
return value;
}
}
enum MovingSpeed{
MAX((byte) 0x00),
HALF((byte) 0x01),
QUARTER((byte) 0x02),
EIGHTH((byte) 0x03),
SIXTEENTH((byte) 0x04),
;
private byte value;
MovingSpeed(byte value){ this.value = value; }
public byte getValue() {
return value;
}
}
class StreamCommand implements MicroAppCommand{
private byte type;
public StreamCommand(byte type) {
this.type = type;
}
@Override
public byte[] getData() {
return new byte[]{(byte) 0x8B, (byte) 0x00, this.type};
}
}
class RepeatStartCommand implements MicroAppCommand{
private byte count;
public RepeatStartCommand(byte count) {
this.count = count;
}
@Override
public byte[] getData() {
return new byte[]{(byte) 0x86, (byte) 0x00, this.count};
}
}
class RepeatStopCommand implements MicroAppCommand{
@Override
public byte[] getData() {
return new byte[]{(byte) 0x07, (byte) 0x00};
}
}
class AnimationCommand implements MicroAppCommand{
private short hour, minute;
private MovingDirection direction;
private MovingSpeed speed;
private byte absoluteMovementFlag;
public AnimationCommand(short hour, short minute) {
this.hour = hour;
this.minute = minute;
this.speed = MovingSpeed.MAX;
this.direction = MovingDirection.SHORTEST;
this.absoluteMovementFlag = 1;
}
@Override
public byte[] getData() {
return ByteBuffer.allocate(10)
.order(ByteOrder.LITTLE_ENDIAN)
.put((byte) 0x09)
.put((byte) 0x04)
.put((byte) 0x01)
.put((byte) 0x03)
.put((byte) ((direction.getValue() << 6) | (byte)(absoluteMovementFlag << 5) | this.speed.getValue()))
.putShort(this.hour)
.put((byte) ((direction.getValue() << 6) | (byte)(absoluteMovementFlag << 5) | this.speed.getValue()))
.putShort(this.minute)
.array();
}
}

View File

@ -0,0 +1,59 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.microapp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
public class PlayCrazyShitRequest extends FilePutRequest {
public PlayCrazyShitRequest(byte[] appData, FossilWatchAdapter adapter) {
super((short) 0x0600, createPayload(appData), adapter);
}
private static byte[] createPayload(byte[] appData) {
List<MicroAppCommand> commands = new ArrayList<>();
commands.add(new StartCriticalCommand());
// commands.add(new RepeatStartCommand((byte) 10));
commands.add(new VibrateCommand(VibrationType.NORMAL));
commands.add(new DelayCommand(1));
// commands.add(new RepeatStopCommand());
// commands.add(new StreamCommand((byte) 0b11111111));
// commands.add(new AnimationCommand((short) 300, (short) 60));
// commands.add(new DelayCommand(2));
commands.add(new CloseCommand());
int length = 0;
for (MicroAppCommand command : commands) length += command.getData().length;
ByteBuffer buffer = ByteBuffer.allocate(
3 /* magic bytes */
+ 8 /* button header copy */
+ 1 /* 0xFF */
+ 2 /* payload length */
+ length
+ 4 /* crc */
);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put((byte) 0x01);
buffer.put((byte) 0x00);
buffer.put((byte) 0x08);
buffer.put(appData, 3, 8);
buffer.put((byte) 0xFF);
buffer.putShort((short)(length + 3));
for(MicroAppCommand command : commands) buffer.put(command.getData());
CRC32 crc = new CRC32();
crc.update(buffer.array(), 0, buffer.position());
buffer.putInt((int) crc.getValue());
return buffer.array();
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.CRC32C;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
public class NotificationFilterGetRequest extends FileGetRequest {
public NotificationFilterGetRequest(FossilWatchAdapter adapter) {
super((short) 0x0C00, adapter);
}
@Override
public void handleFileData(byte[] fileData) {
log("handleFileData");
ByteBuffer buffer = ByteBuffer.wrap(fileData);
buffer.order(ByteOrder.LITTLE_ENDIAN);
byte[] data = new byte[fileData.length - 12 - 4];
System.arraycopy(fileData, 12, data, 0, data.length);
CRC32C crc32c = new CRC32C();
crc32c.update(data);
if((int) crc32c.getValue() != buffer.getInt(fileData.length - 4)){
throw new RuntimeException("CRC invalid");
}
}
}

View File

@ -0,0 +1,89 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileCloseAndPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
public class NotificationFilterPutRequest extends FilePutRequest {
public NotificationFilterPutRequest(NotificationConfiguration[] configs, FossilWatchAdapter adapter) {
super((short) 0x0C00, createFile(configs), adapter);
}
public NotificationFilterPutRequest(ArrayList<NotificationConfiguration> configs, FossilWatchAdapter adapter) {
super((short) 0x0C00, createFile(configs.toArray(new NotificationConfiguration[0])), adapter);
}
private static byte[] createFile(NotificationConfiguration[] configs){
ByteBuffer buffer = ByteBuffer.allocate(configs.length * 27);
buffer.order(ByteOrder.LITTLE_ENDIAN);
for(NotificationConfiguration config : configs){
buffer.putShort((short) 25); //packet length
CRC32 crc = new CRC32();
crc.update(config.getPackageName().getBytes());
buffer.put(PacketID.PACKAGE_NAME_CRC.id);
buffer.put((byte) 4);
buffer.putInt((int) crc.getValue());
buffer.put(PacketID.GROUP_ID.id);
buffer.put((byte) 1);
buffer.put((byte) 2);
buffer.put(PacketID.PRIORITY.id);
buffer.put((byte) 1);
buffer.put((byte) 0xFF);
buffer.put(PacketID.MOVEMENT.id);
buffer.put((byte) 8);
buffer.putShort(config.getHour())
.putShort(config.getMin())
.putShort(config.getSubEye())
.putShort((short) 5000);
buffer.put(PacketID.VIBRATION.id);
buffer.put((byte) 1);
buffer.put(config.getVibration().getValue());
}
return buffer.array();
}
enum PacketID{
PACKAGE_NAME((byte) 1),
SENDER_NAME((byte) 2),
PACKAGE_NAME_CRC((byte) 4),
GROUP_ID((byte) 128),
APP_DISPLAY_NAME((byte) 129),
PRIORITY((byte) 0xC1),
MOVEMENT((byte) 0xC2),
VIBRATION((byte) 0xC3);
byte id;
PacketID(byte id){
this.id = id;
}
}
enum VibrationType{
SINGLE_SHORT((byte) 5),
DOUBLE_SHORT((byte) 6),
TRIPLE_SHORT((byte) 7),
SINGLE_LONG((byte) 8),
SILENT((byte) 9);
byte id;
VibrationType(byte id){
this.id = id;
}
}
}

View File

@ -0,0 +1,93 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
public class PlayNotificationRequest extends FilePutRequest {
public PlayNotificationRequest(String packageName, FossilWatchAdapter adapter) {
// super((short) 0x0900, createFile("org.telegram.messenger", "org.telegram.messenger", "org.telegram.messenger"), adapter);
super((short) 0x0900, createFile(packageName), adapter);
}
private static byte[] createFile(String packageName){
CRC32 crc = new CRC32();
crc.update(packageName.getBytes());
return createFile(packageName, packageName, packageName, (int)crc.getValue());
}
private static byte[] createFile(String title, String sender, String message, int packageCrc) {
// return new byte[]{(byte) 0x57, (byte) 0x00, (byte) 0x0A, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x17, (byte) 0x17, (byte) 0x17, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x49, (byte) 0x7B, (byte) 0x3B, (byte) 0x62, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00};
// gwb.k(var6, "ByteBuffer.allocate(10)");
byte lengthBufferLength = (byte) 10;
byte typeId = 3;
byte flags = getFlags();
byte uidLength = (byte) 4;
byte appBundleCRCLength = (byte) 4;
String nullTerminatedTitle = terminateNull(title);
Charset charsetUTF8 = Charset.forName("UTF-8");
byte[] titleBytes = nullTerminatedTitle.getBytes(charsetUTF8);
// gwb.k(var13, "(this as java.lang.String).getBytes(charset)");
String nullTerminatedSender = terminateNull(sender);
byte[] senderBytes = nullTerminatedSender.getBytes(charsetUTF8);
// gwb.k(var15, "(this as java.lang.String).getBytes(charset)");
String nullTerminatedMessage = terminateNull(message);
byte[] messageBytes = nullTerminatedMessage.getBytes(charsetUTF8);
// gwb.k(var17, "(this as java.lang.String).getBytes(charset)");
short mainBufferLength = (short) (lengthBufferLength + uidLength + appBundleCRCLength + titleBytes.length + senderBytes.length + messageBytes.length);
ByteBuffer lengthBuffer = ByteBuffer.allocate(lengthBufferLength);
lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
lengthBuffer.putShort(mainBufferLength);
lengthBuffer.put(lengthBufferLength);
lengthBuffer.put(typeId);
lengthBuffer.put(flags);
lengthBuffer.put(uidLength);
lengthBuffer.put(appBundleCRCLength);
lengthBuffer.put((byte) titleBytes.length);
lengthBuffer.put((byte) senderBytes.length);
lengthBuffer.put((byte) messageBytes.length);
ByteBuffer mainBuffer = ByteBuffer.allocate(mainBufferLength);
// gwb.k(var11, "ByteBuffer.allocate(totalLen.toInt())");
mainBuffer.order(ByteOrder.LITTLE_ENDIAN);
mainBuffer.put(lengthBuffer.array());
lengthBuffer = ByteBuffer.allocate(mainBufferLength - lengthBufferLength);
// gwb.k(var6, "ByteBuffer.allocate(totalLen - headerLen)");
lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
lengthBuffer.putInt(0);
lengthBuffer.putInt(packageCrc);
lengthBuffer.put(titleBytes);
lengthBuffer.put(senderBytes);
lengthBuffer.put(messageBytes);
mainBuffer.put(lengthBuffer.array());
return mainBuffer.array();
}
private static byte getFlags(){
return (byte) 2;
}
public static String terminateNull(String input){
if(input.length() == 0){
return new String(new byte[]{(byte) 0});
}
char lastChar = input.charAt(input.length() - 1);
if(lastChar == 0) return input;
byte[] newArray = new byte[input.length() + 1];
System.arraycopy(input.getBytes(), 0, newArray, 0, input.length());
newArray[newArray.length - 1] = 0;
return new String(newArray);
}
}

View File

@ -0,0 +1,30 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class ActivityPointGetRequest extends Request {
public int activityPoint;
@Override
public byte[] getStartSequence() {
return new byte[]{1, 6};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
byte[] value = characteristic.getValue();
if (value.length != 6) return;
ByteBuffer wrap = ByteBuffer.wrap(value);
wrap.order(ByteOrder.LITTLE_ENDIAN);
activityPoint = wrap.getInt(2) >> 8;
}
}

View File

@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class AnimationRequest extends Request {
@Override
public byte[] getStartSequence() {
return new byte[]{(byte)2, (byte) -15, (byte)5};
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class BatteryLevelRequest extends Request {
public short level = -1;
@Override
public byte[] getStartSequence() {
return new byte[]{1, 8};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
byte[] value = characteristic.getValue();
if (value.length >= 3) {
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
level = buffer.get(2);
}
}
}

View File

@ -0,0 +1,98 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.CRC32;
public class DownloadFileRequest extends FileRequest {
ByteBuffer buffer = null;
public byte[] file = null;
public int fileHandle;
public int size;
public long timeStamp;
public DownloadFileRequest(short handle){
init(handle, 0, 65535);
}
public DownloadFileRequest(short handle, int offset, int length) {
init(handle, offset, length);
}
private void init(short handle, int offset, int length) {
ByteBuffer buffer = createBuffer();
buffer.putShort(handle);
buffer.putInt(offset);
buffer.putInt(length);
this.data = buffer.array();
this.fileHandle = handle;
this.timeStamp = System.currentTimeMillis();
}
@Override
public byte[] getStartSequence() {
return new byte[]{1};
}
@Override
public int getPayloadLength() {
return 11;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
byte[] data = characteristic.getValue();
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
if(buffer == null){
buffer = ByteBuffer.allocate(4096);
ByteBuffer buffer1 = ByteBuffer.wrap(data);
buffer1.order(ByteOrder.LITTLE_ENDIAN);
this.status = buffer1.get(3);
short realHandle = buffer1.getShort(1);
if(status != 0){
log("wrong status: " + status);
}else if(realHandle != fileHandle){
log("wrong handle: " + realHandle);
completed = true;
}else{
log("handle: " + realHandle);
}
}else{
completed = true;
}
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
buffer.put(data, 1, data.length - 1);
if((data[0] & -128) != 0){
ByteBuffer buffer1 = ByteBuffer.allocate(buffer.position());
buffer1.put(buffer.array(), 0, buffer.position());
buffer1.order(ByteOrder.LITTLE_ENDIAN);
file = buffer1.array();
CRC32 crc = new CRC32();
crc.update(file, 0, file.length - 4);
this.size = file.length;
log("file content: " + bytesToString(file));
if(crc.getValue() != cutBits(buffer1.getInt(size - 4))){
log("checksum invalid expected: " + buffer1.getInt(size - 4) + " actual: " + crc.getValue());
}
}
}
}
long cutBits(int value) {
return value & 0b11111111111111111111111111111111L;
}
private String bytesToString(byte[] bytes){
String s = "";
String chars = "0123456789ABCDEF";
for(byte b : bytes){
s += chars.charAt((b >> 4) & 0xF);
s += chars.charAt((b >> 0) & 0xF);
}
return s;
}
}

View File

@ -0,0 +1,43 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class EraseFileRequest extends FileRequest{
public short fileHandle, deletedHandle;
public EraseFileRequest(short handle) {
fileHandle = handle;
ByteBuffer buffer = createBuffer();
buffer.putShort(handle);
this.data = buffer.array();
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
if(!characteristic.getUuid().toString().equals(getRequestUUID().toString())){
log("wrong descriptor");
return;
}
ByteBuffer buffer = ByteBuffer.wrap(characteristic.getValue());
buffer.order(ByteOrder.LITTLE_ENDIAN);
deletedHandle = buffer.getShort(1);
status = buffer.get(3);
log("file " + deletedHandle + " erased: " + status);
}
@Override
public int getPayloadLength() {
return 3;
}
@Override
public byte[] getStartSequence() {
return new byte[]{3};
}
}

View File

@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class EventStreamRequest extends Request {
public EventStreamRequest(short handle) {
super();
ByteBuffer buffer = createBuffer();
buffer.putShort(1, handle);
this.data = buffer.array();
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0006-957f-7d4a-34a6-74696673696d");
}
@Override
public byte[] getStartSequence() {
return new byte[]{1};
}
@Override
public int getPayloadLength() {
return 3;
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class FileRequest extends Request {
public boolean completed = false;
public int status;
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
@Override
public byte[] getStartSequence() {
return null;
}
}

View File

@ -0,0 +1,39 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GetCountdownSettingsRequest extends Request {
@Override
public byte[] getStartSequence() {
return new byte[]{1, 19, 1};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (value.length != 14) {
return;
}
ByteBuffer buffer = ByteBuffer.wrap(value);
long startTime = j(buffer.getInt(3));
long endTime = j(buffer.getInt(7));
byte progress = buffer.get(13);
short offset = buffer.getShort(11);
log("progress: " + progress);
}
public static long j(final int n) {
if (n < 0) {
return 4294967296L + n;
}
return n;
}
}

View File

@ -0,0 +1,30 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GetCurrentStepCountRequest extends Request {
public int steps = -1;
@Override
public byte[] getStartSequence() {
return new byte[]{1, 17};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
byte[] value = characteristic.getValue();
if (value.length >= 6) {
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
steps = buffer.getInt(2);
}
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GetStepGoalRequest extends Request {
public int stepGoal = -1;
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
super.handleResponse(characteristic);
byte[] value = characteristic.getValue();
if (value.length < 6) {
return;
} else {
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
stepGoal = buffer.getInt(2);
}
}
@Override
public byte[] getStartSequence() {
return new byte[]{1, 16};
}
}

View File

@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GetTripleTapEnabledRequest extends Request {
@Override
public byte[] getStartSequence() {
return new byte[]{1, 7, 3};
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GetVibrationStrengthRequest extends Request {
public int strength = -1;
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (value.length < 4) {
return;
} else {
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
strength = (int) buffer.get(3);
}
}
@Override
public byte[] getStartSequence() {
return new byte[]{1, 15, 8};
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GoalTrackingGetRequest extends Request {
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if(value.length != 5) return;
ByteBuffer buffer = ByteBuffer.wrap(value);
buffer.order(ByteOrder.LITTLE_ENDIAN);
short id = buffer.get(3);
boolean state = buffer.get(4) == 1;
}
@Override
public byte[] getStartSequence() {
return new byte[]{01, 20, 01};
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class GoalTrackingSetRequest extends Request {
public GoalTrackingSetRequest(int id, boolean state) {
}
@Override
public byte[] getStartSequence() {
return new byte[]{02, 20, 01};
}
@Override
public int getPayloadLength() {
return 5;
}
}

View File

@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class ListFilesRequest extends FileRequest{
public int fileCount = -1;
public int size = 0;
private ByteBuffer buffer = null;
private int length = 0;
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
String uuid = characteristic.getUuid().toString();
byte[] value = characteristic.getValue();
if(uuid.equals("3dda0004-957f-7d4a-34a6-74696673696d")){
buffer.put(value, 1, value.length - 1);
length += value.length - 1;
if((value[0] & -128) != 0){
ByteBuffer buffer2 = ByteBuffer.wrap(buffer.array(), 0, length);
buffer2.order(ByteOrder.LITTLE_ENDIAN);
fileCount = buffer2.get(0);
size = buffer2.getInt(1);
}
}else if(uuid.equals("3dda0003-957f-7d4a-34a6-74696673696d")){
if(buffer == null){
buffer = ByteBuffer.allocate(128);
}else{
completed = true;
}
}
}
@Override
public byte[] getStartSequence() {
return new byte[]{(byte)5};
}
}

View File

@ -0,0 +1,49 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class MoveHandsRequest extends Request {
public MoveHandsRequest(boolean moveRelative, short degreesMin, short degreesHour, short degreesSub){
init(moveRelative, degreesMin, degreesHour, degreesSub);
}
private void init(boolean moveRelative, short degreesMin, short degreesHour, short degreesSub) {
int count = 0;
if(degreesHour != -1) count++;
if(degreesMin != -1) count++;
if(degreesSub != -1) count++;
ByteBuffer buffer = createBuffer(count * 5 + 5);
buffer.put(moveRelative ? 1 : (byte)2);
buffer.put((byte)count);
if(degreesHour > -1){
buffer.put((byte)1);
buffer.putShort(degreesHour);
buffer.put((byte)3);
buffer.put((byte)1);
}
if(degreesMin > -1){
buffer.put((byte)2);
buffer.putShort(degreesMin);
buffer.put((byte)3);
buffer.put((byte)1);
}
if(degreesSub > -1){
buffer.put((byte)3);
buffer.putShort(degreesSub);
buffer.put((byte)3);
buffer.put((byte)1);
}
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 21, 3};
}
}

View File

@ -0,0 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class OTAEnterRequest extends Request {
public boolean success = false;
@Override
public byte[] getStartSequence() {
return new byte[]{2, -15, 8};
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] result = characteristic.getValue();
success = result[2] == 9;
}
}

View File

@ -0,0 +1,45 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class OTAEraseRequest extends Request {
public OTAEraseRequest(int pageOffset) {
ByteBuffer buffer = createBuffer();
buffer.putShort((short) 23131);
buffer.putInt(pageOffset);
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{18};
}
@Override
public int getPayloadLength() {
return 7;
}
public UUID getRequestUUID(){
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] bytes = characteristic.getValue();
final ByteBuffer wrap = ByteBuffer.wrap(bytes);
wrap.order(ByteOrder.LITTLE_ENDIAN);
short fileHandle = wrap.getShort(1);
byte status = wrap.get(3);
int sizeWritten = wrap.getInt(4);
}
}

View File

@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class OTAResetRequest extends Request {
@Override
public byte[] getStartSequence() {
return new byte[]{2, -15, 10};
}
}

View File

@ -0,0 +1,70 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import java.security.InvalidParameterException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class PlayNotificationRequest extends Request {
public enum VibrationType{
SINGLE_SHORT(3),
DOUBLE_SHORT(2),
TRIPLE_SHORT(1),
SINGLE_NORMAL(5),
DOUBLE_NORMAL(6),
TRIPLE_NORMAL(7),
SINGLE_LONG(8),
NO_VIBE(9);
private byte value;
VibrationType(int value) {
this.value = (byte)value;
}
public static VibrationType fromValue(byte value){
for(VibrationType type : values()){
if(type.getValue() == value) return type;
}
throw new InvalidParameterException("vibration Type not supported");
}
public byte getValue() {
return value;
}
}
public PlayNotificationRequest(VibrationType vibrationType, int degreesHour, int degreesMins, int degreesActivityHand){
int length = 0;
if(degreesHour > -1) length++;
if(degreesMins > -1) length++;
if(degreesActivityHand > -1) length++;
ByteBuffer buffer = createBuffer(length * 2 + 10);
buffer.put(vibrationType.getValue());
buffer.put((byte)5);
buffer.put((byte)(length * 2 + 2));
buffer.putShort((short)0);
if(degreesHour > -1){
buffer.putShort((short) ((degreesHour % 360) | (1 << 12)));
}
if(degreesMins > -1){
buffer.putShort((short)((degreesMins % 360) | (2 << 12)));
}
if(degreesActivityHand > -1) {
buffer.putShort((short)((degreesActivityHand % 360) | (3 << 12)));
}
this.data = buffer.array();
}
public PlayNotificationRequest(VibrationType vibrationType, int degreesHour, int degreesMins){
this(vibrationType, degreesHour, degreesMins, -1);
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 7, 15, 10, 1};
}
}

View File

@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class PutSettingsFileRequest extends Request {
@Override
public byte[] getStartSequence() {
return new byte[0];
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class ReleaseHandsControlRequest extends Request {
public ReleaseHandsControlRequest(){
super();
init((short)0);
}
private void init(short delayBeforeRelease) {
ByteBuffer buffer = createBuffer();
buffer.putShort(3, delayBeforeRelease);
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 21, 2};
}
@Override
public int getPayloadLength() {
return 5;
}
}

View File

@ -0,0 +1,36 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class RequestHandControlRequest extends Request {
public RequestHandControlRequest(byte priority, boolean moveCompleteNotify, boolean controlLostNOtify){
super();
init(priority, moveCompleteNotify, controlLostNOtify);
}
public RequestHandControlRequest(){
super();
init((byte)1, false, false);
}
private void init(byte priority, boolean moveCompleteNotify, boolean controlLostNOtify) {
ByteBuffer buffer = createBuffer();
buffer.put(priority);
buffer.put(moveCompleteNotify ? (byte)1 : 0);
buffer.put(controlLostNOtify ? (byte)1 : 0);
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 21, 1};
}
@Override
public int getPayloadLength() {
return 6;
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetCountdownSettings extends Request {
public SetCountdownSettings(int startTime, int endTime, short offset) {
ByteBuffer buffer = createBuffer();
buffer.putInt(startTime);
buffer.putInt(endTime);
buffer.putShort(offset);
// buff
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 19, 1};
}
@Override
public int getPayloadLength() {
return 13;
}
}

View File

@ -0,0 +1,24 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetCurrentStepCountRequest extends Request {
public SetCurrentStepCountRequest(int steps){
super();
ByteBuffer buffer = createBuffer();
buffer.putInt(steps);
this.data = buffer.array();
}
@Override
public int getPayloadLength() {
return 6;
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 17};
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetCurrentTimeServiceRequest extends Request {
public SetCurrentTimeServiceRequest(int timeStampSecs, short millis, short offsetInMins){
super();
init(timeStampSecs, millis, offsetInMins);
}
private void init(int timeStampSecs, short millis, short offsetInMins){
ByteBuffer buffer = createBuffer();
buffer.putInt(timeStampSecs);
buffer.putShort(millis);
buffer.putShort(offsetInMins);
this.data = buffer.array();
}
@Override
public int getPayloadLength() {
return 11;
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 18, 2};
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetStepGoalRequest extends Request {
public SetStepGoalRequest(int goal){
super();
init(goal);
}
private void init(int goal) {
ByteBuffer buffer = createBuffer();
buffer.putInt(goal);
this.data = buffer.array();
}
@Override
public int getPayloadLength() {
return 6;
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 16};
}
}

View File

@ -0,0 +1,26 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetTimeRequest extends Request {
public SetTimeRequest(int epochSeconds, short millis, short minutesOffset) {
ByteBuffer buffer = createBuffer();
buffer.putInt(epochSeconds);
buffer.putShort(millis);
buffer.putShort(minutesOffset);
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 4};
}
@Override
public int getPayloadLength() {
return 10;
}
}

View File

@ -0,0 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetTripleTapEnabledRequest extends Request {
@Override
public byte[] getStartSequence() {
return new byte[]{2, 7, 3, 1};
}
}

View File

@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SetVibrationStrengthRequest extends Request {
public SetVibrationStrengthRequest(short strength){
super();
init(strength);
}
private void init(int strength){
ByteBuffer buffer = createBuffer();
buffer.put((byte)strength);
this.data = buffer.array();
}
@Override
public int getPayloadLength() {
return 4;
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 15, 8};
}
}

View File

@ -0,0 +1,38 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class SettingsFilePutRequest extends Request {
public int fileLength;
public byte[] file;
public SettingsFilePutRequest(byte[] file){
this.fileLength = file.length;
this.file = file;
ByteBuffer buffer = this.createBuffer();
buffer.putShort(1, (short)0x0800);
buffer.putInt(3, 0);
buffer.putInt(7, fileLength - 10);
buffer.putInt(11, fileLength - 10);
this.data = buffer.array();
}
@Override
public int getPayloadLength() {
return 15;
}
@Override
public byte[] getStartSequence() {
return new byte[]{17};
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -0,0 +1,97 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import android.bluetooth.BluetoothGattCharacteristic;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.UUID;
import java.util.zip.CRC32;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class UploadFileRequest extends Request {
public enum UploadState{INITIALIZED, UPLOAD, UPLOADED, ERROR}
public UploadState state;
public ArrayList<byte[]> packets = new ArrayList<>();
public UploadFileRequest(short handle, byte[] file) {
int fileLength = file.length + 4;
ByteBuffer buffer = this.createBuffer();
buffer.putShort(1, handle);
buffer.putInt(3, 0);
buffer.putInt(7, fileLength);
buffer.putInt(11, fileLength);
this.data = buffer.array();
prepareFilePackets(file);
state = UploadState.INITIALIZED;
}
@Override
public void handleResponse(BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
if (value.length == 4) {
if (value[1] != 0) {
state = UploadState.ERROR;
return;
}
state = UploadState.UPLOAD;
}else if(value.length == 9){
if(value[1] != 0){
state = UploadState.ERROR;
return;
}
state = UploadState.UPLOADED;
}
}
private void prepareFilePackets(byte[] file) {
ByteBuffer buffer = ByteBuffer.allocate(file.length + 4);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(file);
CRC32 crc = new CRC32();
crc.update(file);
buffer.putInt((int) crc.getValue());
byte[] fileFull = buffer.array();
for (int i = 0, sequence = 0; i < fileFull.length + 4; i += 18, sequence++) {
byte[] packet;
if (i + 18 >= fileFull.length) {
packet = new byte[fileFull.length - i + 2];
System.arraycopy(fileFull, i, packet, 2, fileFull.length - i);
} else {
packet = new byte[20];
System.arraycopy(fileFull, i, packet, 2, 18);
}
packet[0] = 0x12;
packet[1] = (byte) sequence;
packets.add(packet);
}
packets.get(0)[1] |= 0x40;
if (packets.size() > 1) {
packets.get(packets.size() - 1)[1] |= 0x80;
}
}
@Override
public byte[] getStartSequence() {
return new byte[]{17};
}
@Override
public int getPayloadLength() {
return 15;
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -0,0 +1,28 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
public class VibrateRequest extends Request {
public VibrateRequest(boolean longVibration, short repeats, short millisBetween){
ByteBuffer buffer = createBuffer();
buffer.put(longVibration ? (byte)1 : 0);
buffer.put((byte) repeats);
buffer.putShort(millisBetween);
this.data = buffer.array();
}
@Override
public int getPayloadLength() {
return 7;
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 15, 5};
}
}

View File

@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Co
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator;
@ -226,6 +227,7 @@ public class DeviceHelper {
result.add(new EXRIZUK8Coordinator());
result.add(new TeclastH30Coordinator());
result.add(new XWatchCoordinator());
result.add(new QHybridCoordinator());
result.add(new ZeTimeCoordinator());
result.add(new ID115Coordinator());
result.add(new Watch9DeviceCoordinator());

View File

@ -53,4 +53,13 @@ public class LimitedQueue {
}
return null;
}
synchronized public Object lookupByValue(Object value){
for (Pair entry : list) {
if (value.equals(entry.second)) {
return entry.first;
}
}
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Some files were not shown because too many files have changed in this diff Show More