1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-02-18 05:17:08 +01:00

Merge branch 'master' of github.com:gfwilliams/Gadgetbridge

This commit is contained in:
Gordon Williams 2019-12-09 10:13:02 +00:00
commit fb70da856a
119 changed files with 10125 additions and 83 deletions

View File

@ -1,5 +1,5 @@
**IF YOU WANT TO EDIT THE WIKI**, do so on [codeberg.org](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki)
The wiki on github.com is a read-only mirror, as is the git repo itself. Issues and PRs will move to codeberg summer 2019, if you want your issue/PR comments migrated properly, please create a codeberg acount before we will migrate.
The wiki on github.com is a read-only mirror, as is the git repo itself. Issues and PRs will move to codeberg on 2019-12-10 if everything works out as planned, if you want your issue/PR comments migrated properly, please create a codeberg acount before we will migrate.
Gadgetbridge
============
@ -37,6 +37,7 @@ vendor's servers.
* Amazfit GTS (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT ONCE) [Wiki] (https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-GTS)
* BFH-16
* Casio GB-6900B
* Fossil Q Hybrid
* HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* ID115
* Lenovo Watch 9
@ -79,6 +80,7 @@ Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/ma
* Jean-François Greffier (Mi Scale 2)
* Johannes Schmitt (BFH-16)
* Lukas Schwichtenberg (Makibes HR3)
* Daniel Dakhno (Fossil Q Hybrid)
## Contribute

View File

@ -70,7 +70,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "com.google.android.material:material:1.0.0"
@ -85,12 +85,18 @@ 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'
}
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

@ -1,5 +1,6 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa, boun, Carsten Pfeiffer,
Daniele Gobbetti, JohnnySun, jonnsoft, Lem Dulfo, Taavi Eomäe, Uwe Hermann
/* Copyright (C) 2015-2019 Andreas Shimokawa, boun, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, JohnnySun, jonnsoft, Lem Dulfo, Taavi Eomäe,
Uwe Hermann
This file is part of Gadgetbridge.
@ -80,7 +81,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 boolean disableNewBLEScanning = false;
private final Handler handler = new Handler();
@ -267,6 +268,11 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
disableNewBLEScanning = GBApplication.getPrefs().getBoolean("disable_new_ble_scanning", false);
if (disableNewBLEScanning) {
LOG.info("new BLE scanning disabled via settings, using old method");
}
setContentView(R.layout.activity_discovery);
startButton = findViewById(R.id.discovery_start);
startButton.setOnClickListener(new View.OnClickListener() {

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Felix Konstantin Maurer, José Rebelo, Martin, Normano64,
Pavel Elagin, Sebastian Kranz, vanous
Daniel Dakhno, Daniele Gobbetti, Felix Konstantin Maurer, José Rebelo,
Martin, Normano64, Pavel Elagin, Sebastian Kranz, vanous
This file is part of Gadgetbridge.
@ -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,657 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,131 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,158 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,206 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,185 @@
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,109 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,51 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,294 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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

@ -1,7 +1,7 @@
/* Copyright (C) 2015-2019 abettenburg, Andreas Shimokawa, AndrewBedscastle,
Carsten Pfeiffer, Daniele Gobbetti, Frank Slezak, Hasan Ammar, José Rebelo,
Julien Pivotto, Kevin Richter, Matthieu Baerts, Normano64, Steffen Liebergeld,
Taavi Eomäe, veecue, Zhong Jianxin
Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti, Frank Slezak, Hasan Ammar,
José Rebelo, Julien Pivotto, Kevin Richter, Matthieu Baerts, Normano64,
Steffen Liebergeld, Taavi Eomäe, veecue, Zhong Jianxin
This file is part of Gadgetbridge.
@ -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

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2019 Alberto, Andreas Böhler, Andreas Shimokawa,
Carsten Pfeiffer, criogenic, dakhnod, Daniele Gobbetti, Frank Slezak,
Carsten Pfeiffer, criogenic, Daniel Dakhno, Daniele Gobbetti, Frank Slezak,
ivanovlev, José Rebelo, Julien Pivotto, Kasha, Roi Greenberg, Sebastian
Kranz, Steffen Liebergeld

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, dakhnod,
Daniele Gobbetti, Frank Slezak, ivanovlev, JohnnySun, José Rebelo, Julien
Pivotto, Kasha, Sebastian Kranz, Steffen Liebergeld
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, Frank Slezak, ivanovlev, JohnnySun, José Rebelo,
Julien Pivotto, Kasha, Sebastian Kranz, Steffen Liebergeld
This file is part of Gadgetbridge.

View File

@ -1,7 +1,8 @@
/* Copyright (C) 2015-2019 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Cre3per, Daniele Gobbetti, Jean-François Greffier, João Paulo
Barraca, José Rebelo, Kranz, ladbsoft, Manuel Ruß, maxirnilian, protomors,
Quallenauge, Sami Alaoui, Sebastian Kranz, Sophanimus, tiparega, Vadim Kaushan
Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Jean-François Greffier,
João Paulo Barraca, José Rebelo, Kranz, ladbsoft, Manuel Ruß, maxirnilian,
protomors, Quallenauge, Sami Alaoui, Sebastian Kranz, Sophanimus, tiparega,
Vadim Kaushan
This file is part of Gadgetbridge.
@ -51,6 +52,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

@ -1,8 +1,8 @@
/* Copyright (C) 2015-2019 Andreas Böhler, Andreas Shimokawa, Avamander,
Carsten Pfeiffer, dakhnod, Daniele Gobbetti, Daniel Hauck, Dikay900, Frank
Slezak, ivanovlev, João Paulo Barraca, José Rebelo, Julien Pivotto, Kasha,
keeshii, Martin, Matthieu Baerts, Nephiel, Sebastian Kranz, Sergey Trofimov,
Steffen Liebergeld, Taavi Eomäe, Uwe Hermann
Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti, Daniel Hauck, Dikay900,
Frank Slezak, ivanovlev, João Paulo Barraca, José Rebelo, Julien Pivotto,
Kasha, keeshii, Martin, Matthieu Baerts, Nephiel, Sebastian Kranz, Sergey
Trofimov, Steffen Liebergeld, Taavi Eomäe, Uwe Hermann
This file is part of Gadgetbridge.

View File

@ -1,8 +1,8 @@
/* Copyright (C) 2015-2019 0nse, Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Cre3per, criogenic, Daniele Gobbetti, Jean-François Greffier,
João Paulo Barraca, José Rebelo, Kranz, ladbsoft, Manuel Ruß, maxirnilian,
protomors, Quallenauge, Sami Alaoui, Sergey Trofimov, Sophanimus, tiparega,
Vadim Kaushan
Pfeiffer, Cre3per, criogenic, Daniel Dakhno, Daniele Gobbetti, Jean-François
Greffier, João Paulo Barraca, José Rebelo, Kranz, ladbsoft, Manuel Ruß,
maxirnilian, protomors, Quallenauge, Sami Alaoui, Sergey Trofimov, Sophanimus,
tiparega, Vadim Kaushan
This file is part of Gadgetbridge.
@ -53,6 +53,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;
@ -184,7 +185,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

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Julien Pivotto, Kasha, Sebastian Kranz, Steffen
Liebergeld
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti, José Rebelo, Julien Pivotto, Kasha, Sebastian
Kranz, Steffen Liebergeld
This file is part of Gadgetbridge.

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2019 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Daniele Gobbetti, JohnnySun, José Rebelo
Pfeiffer, Daniel Dakhno, Daniele Gobbetti, JohnnySun, José Rebelo
This file is part of Gadgetbridge.
@ -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

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Uwe Hermann
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Uwe Hermann
This file is part of Gadgetbridge.
@ -209,4 +210,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

@ -1,4 +1,4 @@
/* Copyright (C) 2015-2019 Carsten Pfeiffer
/* Copyright (C) 2015-2019 Carsten Pfeiffer, Daniel Dakhno
This file is part of Gadgetbridge.
@ -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

@ -1,5 +1,6 @@
/* Copyright (C) 2015-2019 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Cre3per, Daniele Gobbetti, Sergey Trofimov, Uwe Hermann
Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Sergey Trofimov,
Uwe Hermann
This file is part of Gadgetbridge.
@ -532,6 +533,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

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -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,47 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,196 @@
/* Copyright (C) 2015-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer, Daniel
Dakhno, José Rebelo, Julien Pivotto, Sebastian Kranz, Steffen Liebergeld
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,577 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,97 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,33 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,611 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,499 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,98 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,105 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,81 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,34 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,49 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,41 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,103 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,62 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,35 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,66 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,60 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,279 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,53 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,53 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,95 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,75 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,132 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,37 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,139 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,243 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
public class FilePutRequest extends FossilRequest {
public enum UploadState {INITIALIZED, UPLOADING, CLOSING, UPLOADED}
public UploadState state;
private ArrayList<byte[]> packets = new ArrayList<>();
private short handle;
private FossilWatchAdapter adapter;
private byte[] file;
private 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,0,data.length);
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,91 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,176 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,75 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,48 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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.adapter.fossil.FossilWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
import nodomain.freeyourgadget.gadgetbridge.util.CRC32C;
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,0,data.length);
if((int) crc32c.getValue() != buffer.getInt(fileData.length - 4)){
throw new RuntimeException("CRC invalid");
}
}
}

View File

@ -0,0 +1,105 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,109 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,46 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,26 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,45 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,114 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,59 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,47 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,35 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,55 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,46 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,45 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,26 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,45 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,44 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,35 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,58 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,65 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,36 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,61 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,26 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,86 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,26 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,45 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,52 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,44 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,40 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,45 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,44 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,42 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,26 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,45 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,54 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,113 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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,44 @@
/* Copyright (C) 2019 Daniel Dakhno
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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

@ -0,0 +1,633 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Some portions of this file Copyright (c) 2004-2006 Intel Corportation
* and licensed under the BSD license.
*/
package nodomain.freeyourgadget.gadgetbridge.util;
import java.util.zip.Checksum;
/**
* A pure-java implementation of the CRC32 checksum that uses
* the CRC32-C polynomial, the same polynomial used by iSCSI
* and implemented on many Intel chipsets supporting SSE4.2.
*/
public class CRC32C implements Checksum {
/** the current CRC value, bit-flipped */
private int crc;
/** Create a new PureJavaCrc32 object. */
public CRC32C() {
reset();
}
@Override
public long getValue() {
long ret = crc;
return (~ret) & 0xffffffffL;
}
@Override
public void reset() {
crc = 0xffffffff;
}
@Override
public void update(byte[] b, int off, int len) {
int localCrc = crc;
while(len > 7) {
final int c0 =(b[off+0] ^ localCrc) & 0xff;
final int c1 =(b[off+1] ^ (localCrc >>>= 8)) & 0xff;
final int c2 =(b[off+2] ^ (localCrc >>>= 8)) & 0xff;
final int c3 =(b[off+3] ^ (localCrc >>>= 8)) & 0xff;
localCrc = (T[T8_7_start + c0] ^ T[T8_6_start + c1])
^ (T[T8_5_start + c2] ^ T[T8_4_start + c3]);
final int c4 = b[off+4] & 0xff;
final int c5 = b[off+5] & 0xff;
final int c6 = b[off+6] & 0xff;
final int c7 = b[off+7] & 0xff;
localCrc ^= (T[T8_3_start + c4] ^ T[T8_2_start + c5])
^ (T[T8_1_start + c6] ^ T[T8_0_start + c7]);
off += 8;
len -= 8;
}
/* loop unroll - duff's device style */
switch(len) {
case 7: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
case 6: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
case 5: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
case 4: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
case 3: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
case 2: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
case 1: localCrc = (localCrc >>> 8) ^ T[T8_0_start + ((localCrc ^ b[off++]) & 0xff)];
default:
/* nothing */
}
// Publish crc out to object
crc = localCrc;
}
@Override
final public void update(int b) {
crc = (crc >>> 8) ^ T[T8_0_start + ((crc ^ b) & 0xff)];
}
// CRC polynomial tables generated by:
// java -cp build/test/classes/:build/classes/ \
// org.apache.hadoop.util.TestPureJavaCrc32\$Table 82F63B78
private static final int T8_0_start = 0*256;
private static final int T8_1_start = 1*256;
private static final int T8_2_start = 2*256;
private static final int T8_3_start = 3*256;
private static final int T8_4_start = 4*256;
private static final int T8_5_start = 5*256;
private static final int T8_6_start = 6*256;
private static final int T8_7_start = 7*256;
private static final int[] T = new int[] {
/* T8_0 */
0x00000000, 0xF26B8303, 0xE13B70F7, 0x1350F3F4,
0xC79A971F, 0x35F1141C, 0x26A1E7E8, 0xD4CA64EB,
0x8AD958CF, 0x78B2DBCC, 0x6BE22838, 0x9989AB3B,
0x4D43CFD0, 0xBF284CD3, 0xAC78BF27, 0x5E133C24,
0x105EC76F, 0xE235446C, 0xF165B798, 0x030E349B,
0xD7C45070, 0x25AFD373, 0x36FF2087, 0xC494A384,
0x9A879FA0, 0x68EC1CA3, 0x7BBCEF57, 0x89D76C54,
0x5D1D08BF, 0xAF768BBC, 0xBC267848, 0x4E4DFB4B,
0x20BD8EDE, 0xD2D60DDD, 0xC186FE29, 0x33ED7D2A,
0xE72719C1, 0x154C9AC2, 0x061C6936, 0xF477EA35,
0xAA64D611, 0x580F5512, 0x4B5FA6E6, 0xB93425E5,
0x6DFE410E, 0x9F95C20D, 0x8CC531F9, 0x7EAEB2FA,
0x30E349B1, 0xC288CAB2, 0xD1D83946, 0x23B3BA45,
0xF779DEAE, 0x05125DAD, 0x1642AE59, 0xE4292D5A,
0xBA3A117E, 0x4851927D, 0x5B016189, 0xA96AE28A,
0x7DA08661, 0x8FCB0562, 0x9C9BF696, 0x6EF07595,
0x417B1DBC, 0xB3109EBF, 0xA0406D4B, 0x522BEE48,
0x86E18AA3, 0x748A09A0, 0x67DAFA54, 0x95B17957,
0xCBA24573, 0x39C9C670, 0x2A993584, 0xD8F2B687,
0x0C38D26C, 0xFE53516F, 0xED03A29B, 0x1F682198,
0x5125DAD3, 0xA34E59D0, 0xB01EAA24, 0x42752927,
0x96BF4DCC, 0x64D4CECF, 0x77843D3B, 0x85EFBE38,
0xDBFC821C, 0x2997011F, 0x3AC7F2EB, 0xC8AC71E8,
0x1C661503, 0xEE0D9600, 0xFD5D65F4, 0x0F36E6F7,
0x61C69362, 0x93AD1061, 0x80FDE395, 0x72966096,
0xA65C047D, 0x5437877E, 0x4767748A, 0xB50CF789,
0xEB1FCBAD, 0x197448AE, 0x0A24BB5A, 0xF84F3859,
0x2C855CB2, 0xDEEEDFB1, 0xCDBE2C45, 0x3FD5AF46,
0x7198540D, 0x83F3D70E, 0x90A324FA, 0x62C8A7F9,
0xB602C312, 0x44694011, 0x5739B3E5, 0xA55230E6,
0xFB410CC2, 0x092A8FC1, 0x1A7A7C35, 0xE811FF36,
0x3CDB9BDD, 0xCEB018DE, 0xDDE0EB2A, 0x2F8B6829,
0x82F63B78, 0x709DB87B, 0x63CD4B8F, 0x91A6C88C,
0x456CAC67, 0xB7072F64, 0xA457DC90, 0x563C5F93,
0x082F63B7, 0xFA44E0B4, 0xE9141340, 0x1B7F9043,
0xCFB5F4A8, 0x3DDE77AB, 0x2E8E845F, 0xDCE5075C,
0x92A8FC17, 0x60C37F14, 0x73938CE0, 0x81F80FE3,
0x55326B08, 0xA759E80B, 0xB4091BFF, 0x466298FC,
0x1871A4D8, 0xEA1A27DB, 0xF94AD42F, 0x0B21572C,
0xDFEB33C7, 0x2D80B0C4, 0x3ED04330, 0xCCBBC033,
0xA24BB5A6, 0x502036A5, 0x4370C551, 0xB11B4652,
0x65D122B9, 0x97BAA1BA, 0x84EA524E, 0x7681D14D,
0x2892ED69, 0xDAF96E6A, 0xC9A99D9E, 0x3BC21E9D,
0xEF087A76, 0x1D63F975, 0x0E330A81, 0xFC588982,
0xB21572C9, 0x407EF1CA, 0x532E023E, 0xA145813D,
0x758FE5D6, 0x87E466D5, 0x94B49521, 0x66DF1622,
0x38CC2A06, 0xCAA7A905, 0xD9F75AF1, 0x2B9CD9F2,
0xFF56BD19, 0x0D3D3E1A, 0x1E6DCDEE, 0xEC064EED,
0xC38D26C4, 0x31E6A5C7, 0x22B65633, 0xD0DDD530,
0x0417B1DB, 0xF67C32D8, 0xE52CC12C, 0x1747422F,
0x49547E0B, 0xBB3FFD08, 0xA86F0EFC, 0x5A048DFF,
0x8ECEE914, 0x7CA56A17, 0x6FF599E3, 0x9D9E1AE0,
0xD3D3E1AB, 0x21B862A8, 0x32E8915C, 0xC083125F,
0x144976B4, 0xE622F5B7, 0xF5720643, 0x07198540,
0x590AB964, 0xAB613A67, 0xB831C993, 0x4A5A4A90,
0x9E902E7B, 0x6CFBAD78, 0x7FAB5E8C, 0x8DC0DD8F,
0xE330A81A, 0x115B2B19, 0x020BD8ED, 0xF0605BEE,
0x24AA3F05, 0xD6C1BC06, 0xC5914FF2, 0x37FACCF1,
0x69E9F0D5, 0x9B8273D6, 0x88D28022, 0x7AB90321,
0xAE7367CA, 0x5C18E4C9, 0x4F48173D, 0xBD23943E,
0xF36E6F75, 0x0105EC76, 0x12551F82, 0xE03E9C81,
0x34F4F86A, 0xC69F7B69, 0xD5CF889D, 0x27A40B9E,
0x79B737BA, 0x8BDCB4B9, 0x988C474D, 0x6AE7C44E,
0xBE2DA0A5, 0x4C4623A6, 0x5F16D052, 0xAD7D5351,
/* T8_1 */
0x00000000, 0x13A29877, 0x274530EE, 0x34E7A899,
0x4E8A61DC, 0x5D28F9AB, 0x69CF5132, 0x7A6DC945,
0x9D14C3B8, 0x8EB65BCF, 0xBA51F356, 0xA9F36B21,
0xD39EA264, 0xC03C3A13, 0xF4DB928A, 0xE7790AFD,
0x3FC5F181, 0x2C6769F6, 0x1880C16F, 0x0B225918,
0x714F905D, 0x62ED082A, 0x560AA0B3, 0x45A838C4,
0xA2D13239, 0xB173AA4E, 0x859402D7, 0x96369AA0,
0xEC5B53E5, 0xFFF9CB92, 0xCB1E630B, 0xD8BCFB7C,
0x7F8BE302, 0x6C297B75, 0x58CED3EC, 0x4B6C4B9B,
0x310182DE, 0x22A31AA9, 0x1644B230, 0x05E62A47,
0xE29F20BA, 0xF13DB8CD, 0xC5DA1054, 0xD6788823,
0xAC154166, 0xBFB7D911, 0x8B507188, 0x98F2E9FF,
0x404E1283, 0x53EC8AF4, 0x670B226D, 0x74A9BA1A,
0x0EC4735F, 0x1D66EB28, 0x298143B1, 0x3A23DBC6,
0xDD5AD13B, 0xCEF8494C, 0xFA1FE1D5, 0xE9BD79A2,
0x93D0B0E7, 0x80722890, 0xB4958009, 0xA737187E,
0xFF17C604, 0xECB55E73, 0xD852F6EA, 0xCBF06E9D,
0xB19DA7D8, 0xA23F3FAF, 0x96D89736, 0x857A0F41,
0x620305BC, 0x71A19DCB, 0x45463552, 0x56E4AD25,
0x2C896460, 0x3F2BFC17, 0x0BCC548E, 0x186ECCF9,
0xC0D23785, 0xD370AFF2, 0xE797076B, 0xF4359F1C,
0x8E585659, 0x9DFACE2E, 0xA91D66B7, 0xBABFFEC0,
0x5DC6F43D, 0x4E646C4A, 0x7A83C4D3, 0x69215CA4,
0x134C95E1, 0x00EE0D96, 0x3409A50F, 0x27AB3D78,
0x809C2506, 0x933EBD71, 0xA7D915E8, 0xB47B8D9F,
0xCE1644DA, 0xDDB4DCAD, 0xE9537434, 0xFAF1EC43,
0x1D88E6BE, 0x0E2A7EC9, 0x3ACDD650, 0x296F4E27,
0x53028762, 0x40A01F15, 0x7447B78C, 0x67E52FFB,
0xBF59D487, 0xACFB4CF0, 0x981CE469, 0x8BBE7C1E,
0xF1D3B55B, 0xE2712D2C, 0xD69685B5, 0xC5341DC2,
0x224D173F, 0x31EF8F48, 0x050827D1, 0x16AABFA6,
0x6CC776E3, 0x7F65EE94, 0x4B82460D, 0x5820DE7A,
0xFBC3FAF9, 0xE861628E, 0xDC86CA17, 0xCF245260,
0xB5499B25, 0xA6EB0352, 0x920CABCB, 0x81AE33BC,
0x66D73941, 0x7575A136, 0x419209AF, 0x523091D8,
0x285D589D, 0x3BFFC0EA, 0x0F186873, 0x1CBAF004,
0xC4060B78, 0xD7A4930F, 0xE3433B96, 0xF0E1A3E1,
0x8A8C6AA4, 0x992EF2D3, 0xADC95A4A, 0xBE6BC23D,
0x5912C8C0, 0x4AB050B7, 0x7E57F82E, 0x6DF56059,
0x1798A91C, 0x043A316B, 0x30DD99F2, 0x237F0185,
0x844819FB, 0x97EA818C, 0xA30D2915, 0xB0AFB162,
0xCAC27827, 0xD960E050, 0xED8748C9, 0xFE25D0BE,
0x195CDA43, 0x0AFE4234, 0x3E19EAAD, 0x2DBB72DA,
0x57D6BB9F, 0x447423E8, 0x70938B71, 0x63311306,
0xBB8DE87A, 0xA82F700D, 0x9CC8D894, 0x8F6A40E3,
0xF50789A6, 0xE6A511D1, 0xD242B948, 0xC1E0213F,
0x26992BC2, 0x353BB3B5, 0x01DC1B2C, 0x127E835B,
0x68134A1E, 0x7BB1D269, 0x4F567AF0, 0x5CF4E287,
0x04D43CFD, 0x1776A48A, 0x23910C13, 0x30339464,
0x4A5E5D21, 0x59FCC556, 0x6D1B6DCF, 0x7EB9F5B8,
0x99C0FF45, 0x8A626732, 0xBE85CFAB, 0xAD2757DC,
0xD74A9E99, 0xC4E806EE, 0xF00FAE77, 0xE3AD3600,
0x3B11CD7C, 0x28B3550B, 0x1C54FD92, 0x0FF665E5,
0x759BACA0, 0x663934D7, 0x52DE9C4E, 0x417C0439,
0xA6050EC4, 0xB5A796B3, 0x81403E2A, 0x92E2A65D,
0xE88F6F18, 0xFB2DF76F, 0xCFCA5FF6, 0xDC68C781,
0x7B5FDFFF, 0x68FD4788, 0x5C1AEF11, 0x4FB87766,
0x35D5BE23, 0x26772654, 0x12908ECD, 0x013216BA,
0xE64B1C47, 0xF5E98430, 0xC10E2CA9, 0xD2ACB4DE,
0xA8C17D9B, 0xBB63E5EC, 0x8F844D75, 0x9C26D502,
0x449A2E7E, 0x5738B609, 0x63DF1E90, 0x707D86E7,
0x0A104FA2, 0x19B2D7D5, 0x2D557F4C, 0x3EF7E73B,
0xD98EEDC6, 0xCA2C75B1, 0xFECBDD28, 0xED69455F,
0x97048C1A, 0x84A6146D, 0xB041BCF4, 0xA3E32483,
/* T8_2 */
0x00000000, 0xA541927E, 0x4F6F520D, 0xEA2EC073,
0x9EDEA41A, 0x3B9F3664, 0xD1B1F617, 0x74F06469,
0x38513EC5, 0x9D10ACBB, 0x773E6CC8, 0xD27FFEB6,
0xA68F9ADF, 0x03CE08A1, 0xE9E0C8D2, 0x4CA15AAC,
0x70A27D8A, 0xD5E3EFF4, 0x3FCD2F87, 0x9A8CBDF9,
0xEE7CD990, 0x4B3D4BEE, 0xA1138B9D, 0x045219E3,
0x48F3434F, 0xEDB2D131, 0x079C1142, 0xA2DD833C,
0xD62DE755, 0x736C752B, 0x9942B558, 0x3C032726,
0xE144FB14, 0x4405696A, 0xAE2BA919, 0x0B6A3B67,
0x7F9A5F0E, 0xDADBCD70, 0x30F50D03, 0x95B49F7D,
0xD915C5D1, 0x7C5457AF, 0x967A97DC, 0x333B05A2,
0x47CB61CB, 0xE28AF3B5, 0x08A433C6, 0xADE5A1B8,
0x91E6869E, 0x34A714E0, 0xDE89D493, 0x7BC846ED,
0x0F382284, 0xAA79B0FA, 0x40577089, 0xE516E2F7,
0xA9B7B85B, 0x0CF62A25, 0xE6D8EA56, 0x43997828,
0x37691C41, 0x92288E3F, 0x78064E4C, 0xDD47DC32,
0xC76580D9, 0x622412A7, 0x880AD2D4, 0x2D4B40AA,
0x59BB24C3, 0xFCFAB6BD, 0x16D476CE, 0xB395E4B0,
0xFF34BE1C, 0x5A752C62, 0xB05BEC11, 0x151A7E6F,
0x61EA1A06, 0xC4AB8878, 0x2E85480B, 0x8BC4DA75,
0xB7C7FD53, 0x12866F2D, 0xF8A8AF5E, 0x5DE93D20,
0x29195949, 0x8C58CB37, 0x66760B44, 0xC337993A,
0x8F96C396, 0x2AD751E8, 0xC0F9919B, 0x65B803E5,
0x1148678C, 0xB409F5F2, 0x5E273581, 0xFB66A7FF,
0x26217BCD, 0x8360E9B3, 0x694E29C0, 0xCC0FBBBE,
0xB8FFDFD7, 0x1DBE4DA9, 0xF7908DDA, 0x52D11FA4,
0x1E704508, 0xBB31D776, 0x511F1705, 0xF45E857B,
0x80AEE112, 0x25EF736C, 0xCFC1B31F, 0x6A802161,
0x56830647, 0xF3C29439, 0x19EC544A, 0xBCADC634,
0xC85DA25D, 0x6D1C3023, 0x8732F050, 0x2273622E,
0x6ED23882, 0xCB93AAFC, 0x21BD6A8F, 0x84FCF8F1,
0xF00C9C98, 0x554D0EE6, 0xBF63CE95, 0x1A225CEB,
0x8B277743, 0x2E66E53D, 0xC448254E, 0x6109B730,
0x15F9D359, 0xB0B84127, 0x5A968154, 0xFFD7132A,
0xB3764986, 0x1637DBF8, 0xFC191B8B, 0x595889F5,
0x2DA8ED9C, 0x88E97FE2, 0x62C7BF91, 0xC7862DEF,
0xFB850AC9, 0x5EC498B7, 0xB4EA58C4, 0x11ABCABA,
0x655BAED3, 0xC01A3CAD, 0x2A34FCDE, 0x8F756EA0,
0xC3D4340C, 0x6695A672, 0x8CBB6601, 0x29FAF47F,
0x5D0A9016, 0xF84B0268, 0x1265C21B, 0xB7245065,
0x6A638C57, 0xCF221E29, 0x250CDE5A, 0x804D4C24,
0xF4BD284D, 0x51FCBA33, 0xBBD27A40, 0x1E93E83E,
0x5232B292, 0xF77320EC, 0x1D5DE09F, 0xB81C72E1,
0xCCEC1688, 0x69AD84F6, 0x83834485, 0x26C2D6FB,
0x1AC1F1DD, 0xBF8063A3, 0x55AEA3D0, 0xF0EF31AE,
0x841F55C7, 0x215EC7B9, 0xCB7007CA, 0x6E3195B4,
0x2290CF18, 0x87D15D66, 0x6DFF9D15, 0xC8BE0F6B,
0xBC4E6B02, 0x190FF97C, 0xF321390F, 0x5660AB71,
0x4C42F79A, 0xE90365E4, 0x032DA597, 0xA66C37E9,
0xD29C5380, 0x77DDC1FE, 0x9DF3018D, 0x38B293F3,
0x7413C95F, 0xD1525B21, 0x3B7C9B52, 0x9E3D092C,
0xEACD6D45, 0x4F8CFF3B, 0xA5A23F48, 0x00E3AD36,
0x3CE08A10, 0x99A1186E, 0x738FD81D, 0xD6CE4A63,
0xA23E2E0A, 0x077FBC74, 0xED517C07, 0x4810EE79,
0x04B1B4D5, 0xA1F026AB, 0x4BDEE6D8, 0xEE9F74A6,
0x9A6F10CF, 0x3F2E82B1, 0xD50042C2, 0x7041D0BC,
0xAD060C8E, 0x08479EF0, 0xE2695E83, 0x4728CCFD,
0x33D8A894, 0x96993AEA, 0x7CB7FA99, 0xD9F668E7,
0x9557324B, 0x3016A035, 0xDA386046, 0x7F79F238,
0x0B899651, 0xAEC8042F, 0x44E6C45C, 0xE1A75622,
0xDDA47104, 0x78E5E37A, 0x92CB2309, 0x378AB177,
0x437AD51E, 0xE63B4760, 0x0C158713, 0xA954156D,
0xE5F54FC1, 0x40B4DDBF, 0xAA9A1DCC, 0x0FDB8FB2,
0x7B2BEBDB, 0xDE6A79A5, 0x3444B9D6, 0x91052BA8,
/* T8_3 */
0x00000000, 0xDD45AAB8, 0xBF672381, 0x62228939,
0x7B2231F3, 0xA6679B4B, 0xC4451272, 0x1900B8CA,
0xF64463E6, 0x2B01C95E, 0x49234067, 0x9466EADF,
0x8D665215, 0x5023F8AD, 0x32017194, 0xEF44DB2C,
0xE964B13D, 0x34211B85, 0x560392BC, 0x8B463804,
0x924680CE, 0x4F032A76, 0x2D21A34F, 0xF06409F7,
0x1F20D2DB, 0xC2657863, 0xA047F15A, 0x7D025BE2,
0x6402E328, 0xB9474990, 0xDB65C0A9, 0x06206A11,
0xD725148B, 0x0A60BE33, 0x6842370A, 0xB5079DB2,
0xAC072578, 0x71428FC0, 0x136006F9, 0xCE25AC41,
0x2161776D, 0xFC24DDD5, 0x9E0654EC, 0x4343FE54,
0x5A43469E, 0x8706EC26, 0xE524651F, 0x3861CFA7,
0x3E41A5B6, 0xE3040F0E, 0x81268637, 0x5C632C8F,
0x45639445, 0x98263EFD, 0xFA04B7C4, 0x27411D7C,
0xC805C650, 0x15406CE8, 0x7762E5D1, 0xAA274F69,
0xB327F7A3, 0x6E625D1B, 0x0C40D422, 0xD1057E9A,
0xABA65FE7, 0x76E3F55F, 0x14C17C66, 0xC984D6DE,
0xD0846E14, 0x0DC1C4AC, 0x6FE34D95, 0xB2A6E72D,
0x5DE23C01, 0x80A796B9, 0xE2851F80, 0x3FC0B538,
0x26C00DF2, 0xFB85A74A, 0x99A72E73, 0x44E284CB,
0x42C2EEDA, 0x9F874462, 0xFDA5CD5B, 0x20E067E3,
0x39E0DF29, 0xE4A57591, 0x8687FCA8, 0x5BC25610,
0xB4868D3C, 0x69C32784, 0x0BE1AEBD, 0xD6A40405,
0xCFA4BCCF, 0x12E11677, 0x70C39F4E, 0xAD8635F6,
0x7C834B6C, 0xA1C6E1D4, 0xC3E468ED, 0x1EA1C255,
0x07A17A9F, 0xDAE4D027, 0xB8C6591E, 0x6583F3A6,
0x8AC7288A, 0x57828232, 0x35A00B0B, 0xE8E5A1B3,
0xF1E51979, 0x2CA0B3C1, 0x4E823AF8, 0x93C79040,
0x95E7FA51, 0x48A250E9, 0x2A80D9D0, 0xF7C57368,
0xEEC5CBA2, 0x3380611A, 0x51A2E823, 0x8CE7429B,
0x63A399B7, 0xBEE6330F, 0xDCC4BA36, 0x0181108E,
0x1881A844, 0xC5C402FC, 0xA7E68BC5, 0x7AA3217D,
0x52A0C93F, 0x8FE56387, 0xEDC7EABE, 0x30824006,
0x2982F8CC, 0xF4C75274, 0x96E5DB4D, 0x4BA071F5,
0xA4E4AAD9, 0x79A10061, 0x1B838958, 0xC6C623E0,
0xDFC69B2A, 0x02833192, 0x60A1B8AB, 0xBDE41213,
0xBBC47802, 0x6681D2BA, 0x04A35B83, 0xD9E6F13B,
0xC0E649F1, 0x1DA3E349, 0x7F816A70, 0xA2C4C0C8,
0x4D801BE4, 0x90C5B15C, 0xF2E73865, 0x2FA292DD,
0x36A22A17, 0xEBE780AF, 0x89C50996, 0x5480A32E,
0x8585DDB4, 0x58C0770C, 0x3AE2FE35, 0xE7A7548D,
0xFEA7EC47, 0x23E246FF, 0x41C0CFC6, 0x9C85657E,
0x73C1BE52, 0xAE8414EA, 0xCCA69DD3, 0x11E3376B,
0x08E38FA1, 0xD5A62519, 0xB784AC20, 0x6AC10698,
0x6CE16C89, 0xB1A4C631, 0xD3864F08, 0x0EC3E5B0,
0x17C35D7A, 0xCA86F7C2, 0xA8A47EFB, 0x75E1D443,
0x9AA50F6F, 0x47E0A5D7, 0x25C22CEE, 0xF8878656,
0xE1873E9C, 0x3CC29424, 0x5EE01D1D, 0x83A5B7A5,
0xF90696D8, 0x24433C60, 0x4661B559, 0x9B241FE1,
0x8224A72B, 0x5F610D93, 0x3D4384AA, 0xE0062E12,
0x0F42F53E, 0xD2075F86, 0xB025D6BF, 0x6D607C07,
0x7460C4CD, 0xA9256E75, 0xCB07E74C, 0x16424DF4,
0x106227E5, 0xCD278D5D, 0xAF050464, 0x7240AEDC,
0x6B401616, 0xB605BCAE, 0xD4273597, 0x09629F2F,
0xE6264403, 0x3B63EEBB, 0x59416782, 0x8404CD3A,
0x9D0475F0, 0x4041DF48, 0x22635671, 0xFF26FCC9,
0x2E238253, 0xF36628EB, 0x9144A1D2, 0x4C010B6A,
0x5501B3A0, 0x88441918, 0xEA669021, 0x37233A99,
0xD867E1B5, 0x05224B0D, 0x6700C234, 0xBA45688C,
0xA345D046, 0x7E007AFE, 0x1C22F3C7, 0xC167597F,
0xC747336E, 0x1A0299D6, 0x782010EF, 0xA565BA57,
0xBC65029D, 0x6120A825, 0x0302211C, 0xDE478BA4,
0x31035088, 0xEC46FA30, 0x8E647309, 0x5321D9B1,
0x4A21617B, 0x9764CBC3, 0xF54642FA, 0x2803E842,
/* T8_4 */
0x00000000, 0x38116FAC, 0x7022DF58, 0x4833B0F4,
0xE045BEB0, 0xD854D11C, 0x906761E8, 0xA8760E44,
0xC5670B91, 0xFD76643D, 0xB545D4C9, 0x8D54BB65,
0x2522B521, 0x1D33DA8D, 0x55006A79, 0x6D1105D5,
0x8F2261D3, 0xB7330E7F, 0xFF00BE8B, 0xC711D127,
0x6F67DF63, 0x5776B0CF, 0x1F45003B, 0x27546F97,
0x4A456A42, 0x725405EE, 0x3A67B51A, 0x0276DAB6,
0xAA00D4F2, 0x9211BB5E, 0xDA220BAA, 0xE2336406,
0x1BA8B557, 0x23B9DAFB, 0x6B8A6A0F, 0x539B05A3,
0xFBED0BE7, 0xC3FC644B, 0x8BCFD4BF, 0xB3DEBB13,
0xDECFBEC6, 0xE6DED16A, 0xAEED619E, 0x96FC0E32,
0x3E8A0076, 0x069B6FDA, 0x4EA8DF2E, 0x76B9B082,
0x948AD484, 0xAC9BBB28, 0xE4A80BDC, 0xDCB96470,
0x74CF6A34, 0x4CDE0598, 0x04EDB56C, 0x3CFCDAC0,
0x51EDDF15, 0x69FCB0B9, 0x21CF004D, 0x19DE6FE1,
0xB1A861A5, 0x89B90E09, 0xC18ABEFD, 0xF99BD151,
0x37516AAE, 0x0F400502, 0x4773B5F6, 0x7F62DA5A,
0xD714D41E, 0xEF05BBB2, 0xA7360B46, 0x9F2764EA,
0xF236613F, 0xCA270E93, 0x8214BE67, 0xBA05D1CB,
0x1273DF8F, 0x2A62B023, 0x625100D7, 0x5A406F7B,
0xB8730B7D, 0x806264D1, 0xC851D425, 0xF040BB89,
0x5836B5CD, 0x6027DA61, 0x28146A95, 0x10050539,
0x7D1400EC, 0x45056F40, 0x0D36DFB4, 0x3527B018,
0x9D51BE5C, 0xA540D1F0, 0xED736104, 0xD5620EA8,
0x2CF9DFF9, 0x14E8B055, 0x5CDB00A1, 0x64CA6F0D,
0xCCBC6149, 0xF4AD0EE5, 0xBC9EBE11, 0x848FD1BD,
0xE99ED468, 0xD18FBBC4, 0x99BC0B30, 0xA1AD649C,
0x09DB6AD8, 0x31CA0574, 0x79F9B580, 0x41E8DA2C,
0xA3DBBE2A, 0x9BCAD186, 0xD3F96172, 0xEBE80EDE,
0x439E009A, 0x7B8F6F36, 0x33BCDFC2, 0x0BADB06E,
0x66BCB5BB, 0x5EADDA17, 0x169E6AE3, 0x2E8F054F,
0x86F90B0B, 0xBEE864A7, 0xF6DBD453, 0xCECABBFF,
0x6EA2D55C, 0x56B3BAF0, 0x1E800A04, 0x269165A8,
0x8EE76BEC, 0xB6F60440, 0xFEC5B4B4, 0xC6D4DB18,
0xABC5DECD, 0x93D4B161, 0xDBE70195, 0xE3F66E39,
0x4B80607D, 0x73910FD1, 0x3BA2BF25, 0x03B3D089,
0xE180B48F, 0xD991DB23, 0x91A26BD7, 0xA9B3047B,
0x01C50A3F, 0x39D46593, 0x71E7D567, 0x49F6BACB,
0x24E7BF1E, 0x1CF6D0B2, 0x54C56046, 0x6CD40FEA,
0xC4A201AE, 0xFCB36E02, 0xB480DEF6, 0x8C91B15A,
0x750A600B, 0x4D1B0FA7, 0x0528BF53, 0x3D39D0FF,
0x954FDEBB, 0xAD5EB117, 0xE56D01E3, 0xDD7C6E4F,
0xB06D6B9A, 0x887C0436, 0xC04FB4C2, 0xF85EDB6E,
0x5028D52A, 0x6839BA86, 0x200A0A72, 0x181B65DE,
0xFA2801D8, 0xC2396E74, 0x8A0ADE80, 0xB21BB12C,
0x1A6DBF68, 0x227CD0C4, 0x6A4F6030, 0x525E0F9C,
0x3F4F0A49, 0x075E65E5, 0x4F6DD511, 0x777CBABD,
0xDF0AB4F9, 0xE71BDB55, 0xAF286BA1, 0x9739040D,
0x59F3BFF2, 0x61E2D05E, 0x29D160AA, 0x11C00F06,
0xB9B60142, 0x81A76EEE, 0xC994DE1A, 0xF185B1B6,
0x9C94B463, 0xA485DBCF, 0xECB66B3B, 0xD4A70497,
0x7CD10AD3, 0x44C0657F, 0x0CF3D58B, 0x34E2BA27,
0xD6D1DE21, 0xEEC0B18D, 0xA6F30179, 0x9EE26ED5,
0x36946091, 0x0E850F3D, 0x46B6BFC9, 0x7EA7D065,
0x13B6D5B0, 0x2BA7BA1C, 0x63940AE8, 0x5B856544,
0xF3F36B00, 0xCBE204AC, 0x83D1B458, 0xBBC0DBF4,
0x425B0AA5, 0x7A4A6509, 0x3279D5FD, 0x0A68BA51,
0xA21EB415, 0x9A0FDBB9, 0xD23C6B4D, 0xEA2D04E1,
0x873C0134, 0xBF2D6E98, 0xF71EDE6C, 0xCF0FB1C0,
0x6779BF84, 0x5F68D028, 0x175B60DC, 0x2F4A0F70,
0xCD796B76, 0xF56804DA, 0xBD5BB42E, 0x854ADB82,
0x2D3CD5C6, 0x152DBA6A, 0x5D1E0A9E, 0x650F6532,
0x081E60E7, 0x300F0F4B, 0x783CBFBF, 0x402DD013,
0xE85BDE57, 0xD04AB1FB, 0x9879010F, 0xA0686EA3,
/* T8_5 */
0x00000000, 0xEF306B19, 0xDB8CA0C3, 0x34BCCBDA,
0xB2F53777, 0x5DC55C6E, 0x697997B4, 0x8649FCAD,
0x6006181F, 0x8F367306, 0xBB8AB8DC, 0x54BAD3C5,
0xD2F32F68, 0x3DC34471, 0x097F8FAB, 0xE64FE4B2,
0xC00C303E, 0x2F3C5B27, 0x1B8090FD, 0xF4B0FBE4,
0x72F90749, 0x9DC96C50, 0xA975A78A, 0x4645CC93,
0xA00A2821, 0x4F3A4338, 0x7B8688E2, 0x94B6E3FB,
0x12FF1F56, 0xFDCF744F, 0xC973BF95, 0x2643D48C,
0x85F4168D, 0x6AC47D94, 0x5E78B64E, 0xB148DD57,
0x370121FA, 0xD8314AE3, 0xEC8D8139, 0x03BDEA20,
0xE5F20E92, 0x0AC2658B, 0x3E7EAE51, 0xD14EC548,
0x570739E5, 0xB83752FC, 0x8C8B9926, 0x63BBF23F,
0x45F826B3, 0xAAC84DAA, 0x9E748670, 0x7144ED69,
0xF70D11C4, 0x183D7ADD, 0x2C81B107, 0xC3B1DA1E,
0x25FE3EAC, 0xCACE55B5, 0xFE729E6F, 0x1142F576,
0x970B09DB, 0x783B62C2, 0x4C87A918, 0xA3B7C201,
0x0E045BEB, 0xE13430F2, 0xD588FB28, 0x3AB89031,
0xBCF16C9C, 0x53C10785, 0x677DCC5F, 0x884DA746,
0x6E0243F4, 0x813228ED, 0xB58EE337, 0x5ABE882E,
0xDCF77483, 0x33C71F9A, 0x077BD440, 0xE84BBF59,
0xCE086BD5, 0x213800CC, 0x1584CB16, 0xFAB4A00F,
0x7CFD5CA2, 0x93CD37BB, 0xA771FC61, 0x48419778,
0xAE0E73CA, 0x413E18D3, 0x7582D309, 0x9AB2B810,
0x1CFB44BD, 0xF3CB2FA4, 0xC777E47E, 0x28478F67,
0x8BF04D66, 0x64C0267F, 0x507CEDA5, 0xBF4C86BC,
0x39057A11, 0xD6351108, 0xE289DAD2, 0x0DB9B1CB,
0xEBF65579, 0x04C63E60, 0x307AF5BA, 0xDF4A9EA3,
0x5903620E, 0xB6330917, 0x828FC2CD, 0x6DBFA9D4,
0x4BFC7D58, 0xA4CC1641, 0x9070DD9B, 0x7F40B682,
0xF9094A2F, 0x16392136, 0x2285EAEC, 0xCDB581F5,
0x2BFA6547, 0xC4CA0E5E, 0xF076C584, 0x1F46AE9D,
0x990F5230, 0x763F3929, 0x4283F2F3, 0xADB399EA,
0x1C08B7D6, 0xF338DCCF, 0xC7841715, 0x28B47C0C,
0xAEFD80A1, 0x41CDEBB8, 0x75712062, 0x9A414B7B,
0x7C0EAFC9, 0x933EC4D0, 0xA7820F0A, 0x48B26413,
0xCEFB98BE, 0x21CBF3A7, 0x1577387D, 0xFA475364,
0xDC0487E8, 0x3334ECF1, 0x0788272B, 0xE8B84C32,
0x6EF1B09F, 0x81C1DB86, 0xB57D105C, 0x5A4D7B45,
0xBC029FF7, 0x5332F4EE, 0x678E3F34, 0x88BE542D,
0x0EF7A880, 0xE1C7C399, 0xD57B0843, 0x3A4B635A,
0x99FCA15B, 0x76CCCA42, 0x42700198, 0xAD406A81,
0x2B09962C, 0xC439FD35, 0xF08536EF, 0x1FB55DF6,
0xF9FAB944, 0x16CAD25D, 0x22761987, 0xCD46729E,
0x4B0F8E33, 0xA43FE52A, 0x90832EF0, 0x7FB345E9,
0x59F09165, 0xB6C0FA7C, 0x827C31A6, 0x6D4C5ABF,
0xEB05A612, 0x0435CD0B, 0x308906D1, 0xDFB96DC8,
0x39F6897A, 0xD6C6E263, 0xE27A29B9, 0x0D4A42A0,
0x8B03BE0D, 0x6433D514, 0x508F1ECE, 0xBFBF75D7,
0x120CEC3D, 0xFD3C8724, 0xC9804CFE, 0x26B027E7,
0xA0F9DB4A, 0x4FC9B053, 0x7B757B89, 0x94451090,
0x720AF422, 0x9D3A9F3B, 0xA98654E1, 0x46B63FF8,
0xC0FFC355, 0x2FCFA84C, 0x1B736396, 0xF443088F,
0xD200DC03, 0x3D30B71A, 0x098C7CC0, 0xE6BC17D9,
0x60F5EB74, 0x8FC5806D, 0xBB794BB7, 0x544920AE,
0xB206C41C, 0x5D36AF05, 0x698A64DF, 0x86BA0FC6,
0x00F3F36B, 0xEFC39872, 0xDB7F53A8, 0x344F38B1,
0x97F8FAB0, 0x78C891A9, 0x4C745A73, 0xA344316A,
0x250DCDC7, 0xCA3DA6DE, 0xFE816D04, 0x11B1061D,
0xF7FEE2AF, 0x18CE89B6, 0x2C72426C, 0xC3422975,
0x450BD5D8, 0xAA3BBEC1, 0x9E87751B, 0x71B71E02,
0x57F4CA8E, 0xB8C4A197, 0x8C786A4D, 0x63480154,
0xE501FDF9, 0x0A3196E0, 0x3E8D5D3A, 0xD1BD3623,
0x37F2D291, 0xD8C2B988, 0xEC7E7252, 0x034E194B,
0x8507E5E6, 0x6A378EFF, 0x5E8B4525, 0xB1BB2E3C,
/* T8_6 */
0x00000000, 0x68032CC8, 0xD0065990, 0xB8057558,
0xA5E0C5D1, 0xCDE3E919, 0x75E69C41, 0x1DE5B089,
0x4E2DFD53, 0x262ED19B, 0x9E2BA4C3, 0xF628880B,
0xEBCD3882, 0x83CE144A, 0x3BCB6112, 0x53C84DDA,
0x9C5BFAA6, 0xF458D66E, 0x4C5DA336, 0x245E8FFE,
0x39BB3F77, 0x51B813BF, 0xE9BD66E7, 0x81BE4A2F,
0xD27607F5, 0xBA752B3D, 0x02705E65, 0x6A7372AD,
0x7796C224, 0x1F95EEEC, 0xA7909BB4, 0xCF93B77C,
0x3D5B83BD, 0x5558AF75, 0xED5DDA2D, 0x855EF6E5,
0x98BB466C, 0xF0B86AA4, 0x48BD1FFC, 0x20BE3334,
0x73767EEE, 0x1B755226, 0xA370277E, 0xCB730BB6,
0xD696BB3F, 0xBE9597F7, 0x0690E2AF, 0x6E93CE67,
0xA100791B, 0xC90355D3, 0x7106208B, 0x19050C43,
0x04E0BCCA, 0x6CE39002, 0xD4E6E55A, 0xBCE5C992,
0xEF2D8448, 0x872EA880, 0x3F2BDDD8, 0x5728F110,
0x4ACD4199, 0x22CE6D51, 0x9ACB1809, 0xF2C834C1,
0x7AB7077A, 0x12B42BB2, 0xAAB15EEA, 0xC2B27222,
0xDF57C2AB, 0xB754EE63, 0x0F519B3B, 0x6752B7F3,
0x349AFA29, 0x5C99D6E1, 0xE49CA3B9, 0x8C9F8F71,
0x917A3FF8, 0xF9791330, 0x417C6668, 0x297F4AA0,
0xE6ECFDDC, 0x8EEFD114, 0x36EAA44C, 0x5EE98884,
0x430C380D, 0x2B0F14C5, 0x930A619D, 0xFB094D55,
0xA8C1008F, 0xC0C22C47, 0x78C7591F, 0x10C475D7,
0x0D21C55E, 0x6522E996, 0xDD279CCE, 0xB524B006,
0x47EC84C7, 0x2FEFA80F, 0x97EADD57, 0xFFE9F19F,
0xE20C4116, 0x8A0F6DDE, 0x320A1886, 0x5A09344E,
0x09C17994, 0x61C2555C, 0xD9C72004, 0xB1C40CCC,
0xAC21BC45, 0xC422908D, 0x7C27E5D5, 0x1424C91D,
0xDBB77E61, 0xB3B452A9, 0x0BB127F1, 0x63B20B39,
0x7E57BBB0, 0x16549778, 0xAE51E220, 0xC652CEE8,
0x959A8332, 0xFD99AFFA, 0x459CDAA2, 0x2D9FF66A,
0x307A46E3, 0x58796A2B, 0xE07C1F73, 0x887F33BB,
0xF56E0EF4, 0x9D6D223C, 0x25685764, 0x4D6B7BAC,
0x508ECB25, 0x388DE7ED, 0x808892B5, 0xE88BBE7D,
0xBB43F3A7, 0xD340DF6F, 0x6B45AA37, 0x034686FF,
0x1EA33676, 0x76A01ABE, 0xCEA56FE6, 0xA6A6432E,
0x6935F452, 0x0136D89A, 0xB933ADC2, 0xD130810A,
0xCCD53183, 0xA4D61D4B, 0x1CD36813, 0x74D044DB,
0x27180901, 0x4F1B25C9, 0xF71E5091, 0x9F1D7C59,
0x82F8CCD0, 0xEAFBE018, 0x52FE9540, 0x3AFDB988,
0xC8358D49, 0xA036A181, 0x1833D4D9, 0x7030F811,
0x6DD54898, 0x05D66450, 0xBDD31108, 0xD5D03DC0,
0x8618701A, 0xEE1B5CD2, 0x561E298A, 0x3E1D0542,
0x23F8B5CB, 0x4BFB9903, 0xF3FEEC5B, 0x9BFDC093,
0x546E77EF, 0x3C6D5B27, 0x84682E7F, 0xEC6B02B7,
0xF18EB23E, 0x998D9EF6, 0x2188EBAE, 0x498BC766,
0x1A438ABC, 0x7240A674, 0xCA45D32C, 0xA246FFE4,
0xBFA34F6D, 0xD7A063A5, 0x6FA516FD, 0x07A63A35,
0x8FD9098E, 0xE7DA2546, 0x5FDF501E, 0x37DC7CD6,
0x2A39CC5F, 0x423AE097, 0xFA3F95CF, 0x923CB907,
0xC1F4F4DD, 0xA9F7D815, 0x11F2AD4D, 0x79F18185,
0x6414310C, 0x0C171DC4, 0xB412689C, 0xDC114454,
0x1382F328, 0x7B81DFE0, 0xC384AAB8, 0xAB878670,
0xB66236F9, 0xDE611A31, 0x66646F69, 0x0E6743A1,
0x5DAF0E7B, 0x35AC22B3, 0x8DA957EB, 0xE5AA7B23,
0xF84FCBAA, 0x904CE762, 0x2849923A, 0x404ABEF2,
0xB2828A33, 0xDA81A6FB, 0x6284D3A3, 0x0A87FF6B,
0x17624FE2, 0x7F61632A, 0xC7641672, 0xAF673ABA,
0xFCAF7760, 0x94AC5BA8, 0x2CA92EF0, 0x44AA0238,
0x594FB2B1, 0x314C9E79, 0x8949EB21, 0xE14AC7E9,
0x2ED97095, 0x46DA5C5D, 0xFEDF2905, 0x96DC05CD,
0x8B39B544, 0xE33A998C, 0x5B3FECD4, 0x333CC01C,
0x60F48DC6, 0x08F7A10E, 0xB0F2D456, 0xD8F1F89E,
0xC5144817, 0xAD1764DF, 0x15121187, 0x7D113D4F,
/* T8_7 */
0x00000000, 0x493C7D27, 0x9278FA4E, 0xDB448769,
0x211D826D, 0x6821FF4A, 0xB3657823, 0xFA590504,
0x423B04DA, 0x0B0779FD, 0xD043FE94, 0x997F83B3,
0x632686B7, 0x2A1AFB90, 0xF15E7CF9, 0xB86201DE,
0x847609B4, 0xCD4A7493, 0x160EF3FA, 0x5F328EDD,
0xA56B8BD9, 0xEC57F6FE, 0x37137197, 0x7E2F0CB0,
0xC64D0D6E, 0x8F717049, 0x5435F720, 0x1D098A07,
0xE7508F03, 0xAE6CF224, 0x7528754D, 0x3C14086A,
0x0D006599, 0x443C18BE, 0x9F789FD7, 0xD644E2F0,
0x2C1DE7F4, 0x65219AD3, 0xBE651DBA, 0xF759609D,
0x4F3B6143, 0x06071C64, 0xDD439B0D, 0x947FE62A,
0x6E26E32E, 0x271A9E09, 0xFC5E1960, 0xB5626447,
0x89766C2D, 0xC04A110A, 0x1B0E9663, 0x5232EB44,
0xA86BEE40, 0xE1579367, 0x3A13140E, 0x732F6929,
0xCB4D68F7, 0x827115D0, 0x593592B9, 0x1009EF9E,
0xEA50EA9A, 0xA36C97BD, 0x782810D4, 0x31146DF3,
0x1A00CB32, 0x533CB615, 0x8878317C, 0xC1444C5B,
0x3B1D495F, 0x72213478, 0xA965B311, 0xE059CE36,
0x583BCFE8, 0x1107B2CF, 0xCA4335A6, 0x837F4881,
0x79264D85, 0x301A30A2, 0xEB5EB7CB, 0xA262CAEC,
0x9E76C286, 0xD74ABFA1, 0x0C0E38C8, 0x453245EF,
0xBF6B40EB, 0xF6573DCC, 0x2D13BAA5, 0x642FC782,
0xDC4DC65C, 0x9571BB7B, 0x4E353C12, 0x07094135,
0xFD504431, 0xB46C3916, 0x6F28BE7F, 0x2614C358,
0x1700AEAB, 0x5E3CD38C, 0x857854E5, 0xCC4429C2,
0x361D2CC6, 0x7F2151E1, 0xA465D688, 0xED59ABAF,
0x553BAA71, 0x1C07D756, 0xC743503F, 0x8E7F2D18,
0x7426281C, 0x3D1A553B, 0xE65ED252, 0xAF62AF75,
0x9376A71F, 0xDA4ADA38, 0x010E5D51, 0x48322076,
0xB26B2572, 0xFB575855, 0x2013DF3C, 0x692FA21B,
0xD14DA3C5, 0x9871DEE2, 0x4335598B, 0x0A0924AC,
0xF05021A8, 0xB96C5C8F, 0x6228DBE6, 0x2B14A6C1,
0x34019664, 0x7D3DEB43, 0xA6796C2A, 0xEF45110D,
0x151C1409, 0x5C20692E, 0x8764EE47, 0xCE589360,
0x763A92BE, 0x3F06EF99, 0xE44268F0, 0xAD7E15D7,
0x572710D3, 0x1E1B6DF4, 0xC55FEA9D, 0x8C6397BA,
0xB0779FD0, 0xF94BE2F7, 0x220F659E, 0x6B3318B9,
0x916A1DBD, 0xD856609A, 0x0312E7F3, 0x4A2E9AD4,
0xF24C9B0A, 0xBB70E62D, 0x60346144, 0x29081C63,
0xD3511967, 0x9A6D6440, 0x4129E329, 0x08159E0E,
0x3901F3FD, 0x703D8EDA, 0xAB7909B3, 0xE2457494,
0x181C7190, 0x51200CB7, 0x8A648BDE, 0xC358F6F9,
0x7B3AF727, 0x32068A00, 0xE9420D69, 0xA07E704E,
0x5A27754A, 0x131B086D, 0xC85F8F04, 0x8163F223,
0xBD77FA49, 0xF44B876E, 0x2F0F0007, 0x66337D20,
0x9C6A7824, 0xD5560503, 0x0E12826A, 0x472EFF4D,
0xFF4CFE93, 0xB67083B4, 0x6D3404DD, 0x240879FA,
0xDE517CFE, 0x976D01D9, 0x4C2986B0, 0x0515FB97,
0x2E015D56, 0x673D2071, 0xBC79A718, 0xF545DA3F,
0x0F1CDF3B, 0x4620A21C, 0x9D642575, 0xD4585852,
0x6C3A598C, 0x250624AB, 0xFE42A3C2, 0xB77EDEE5,
0x4D27DBE1, 0x041BA6C6, 0xDF5F21AF, 0x96635C88,
0xAA7754E2, 0xE34B29C5, 0x380FAEAC, 0x7133D38B,
0x8B6AD68F, 0xC256ABA8, 0x19122CC1, 0x502E51E6,
0xE84C5038, 0xA1702D1F, 0x7A34AA76, 0x3308D751,
0xC951D255, 0x806DAF72, 0x5B29281B, 0x1215553C,
0x230138CF, 0x6A3D45E8, 0xB179C281, 0xF845BFA6,
0x021CBAA2, 0x4B20C785, 0x906440EC, 0xD9583DCB,
0x613A3C15, 0x28064132, 0xF342C65B, 0xBA7EBB7C,
0x4027BE78, 0x091BC35F, 0xD25F4436, 0x9B633911,
0xA777317B, 0xEE4B4C5C, 0x350FCB35, 0x7C33B612,
0x866AB316, 0xCF56CE31, 0x14124958, 0x5D2E347F,
0xE54C35A1, 0xAC704886, 0x7734CFEF, 0x3E08B2C8,
0xC451B7CC, 0x8D6DCAEB, 0x56294D82, 0x1F1530A5
};
}

View File

@ -1,7 +1,7 @@
/* Copyright (C) 2015-2019 0nse, Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Cre3per, Daniele Gobbetti, Jean-François Greffier, João Paulo
Barraca, José Rebelo, Kranz, ladbsoft, Manuel Ruß, maxirnilian, protomors,
Quallenauge, Sami Alaoui, Sophanimus, tiparega, Vadim Kaushan
Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Jean-François Greffier,
João Paulo Barraca, José Rebelo, Kranz, ladbsoft, Manuel Ruß, maxirnilian,
protomors, Quallenauge, Sami Alaoui, Sophanimus, tiparega, Vadim Kaushan
This file is part of Gadgetbridge.
@ -68,6 +68,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;
@ -227,6 +228,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

@ -1,4 +1,5 @@
/* Copyright (C) 2015-2019 Andreas Shimokawa, Daniele Gobbetti, Julien Pivotto
/* Copyright (C) 2015-2019 Andreas Shimokawa, Daniel Dakhno, Daniele Gobbetti,
Julien Pivotto
This file is part of Gadgetbridge.
@ -53,4 +54,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: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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