Merge branch 'dakhnod-fossil-q-hybrid'
@ -54,6 +54,7 @@ vendor's servers.
|
||||
* XWatch (Affordable Chinese Casio-like smartwatches)
|
||||
* Vibratissimo (experimental)
|
||||
* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
|
||||
* Fossil Q Hybrid (not officially supported by Fossil)
|
||||
|
||||
|
||||
## Features
|
||||
|
@ -85,12 +85,21 @@ dependencies {
|
||||
implementation "net.e175.klaus:solarpositioning:0.0.9"
|
||||
// use pristine greendao instead of our custom version, since our custom jitpack-packaged
|
||||
// version contains way too much and our custom patches are in the generator only.
|
||||
|
||||
implementation 'com.twofortyfouram:android-plugin-client-sdk-for-locale:[4.0.3, 5.0['
|
||||
implementation 'com.twofortyfouram:android-plugin-host-sdk-for-locale:[2.0.3,3.0['
|
||||
implementation 'com.twofortyfouram:android-plugin-api-for-locale:[1.0.2,2.0['
|
||||
|
||||
implementation "org.greenrobot:greendao:2.2.1"
|
||||
implementation "org.apache.commons:commons-lang3:3.7"
|
||||
implementation "org.cyanogenmod:platform.sdk:6.0"
|
||||
implementation 'com.jaredrummler:colorpicker:1.0.2'
|
||||
// implementation project(":DaoCore")
|
||||
implementation 'com.github.wax911:android-emojify:0.1.7'
|
||||
|
||||
implementation 'com.android.support:support-v4:28.1.0'
|
||||
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||
}
|
||||
|
||||
preBuild.dependsOn(":GBDaoGenerator:genSources")
|
||||
|
@ -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>
|
||||
|
@ -80,7 +80,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
private ScanCallback newLeScanCallback = null;
|
||||
|
||||
// Disabled for testing, it seems worse for a few people
|
||||
private final boolean disableNewBLEScanning = true;
|
||||
private final boolean disableNewBLEScanning = false;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
@ -394,7 +394,11 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
LOG.warn("Not starting discovery, because already scanning.");
|
||||
return;
|
||||
}
|
||||
startDiscovery(Scanning.SCANNING_BT);
|
||||
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
|
||||
startDiscovery(Scanning.SCANNING_NEW_BTLE);
|
||||
} else {
|
||||
startDiscovery(Scanning.SCANNING_BTLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void startDiscovery(Scanning what) {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,641 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.DeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class ConfigActivity extends AbstractGBActivity {
|
||||
PackageAdapter adapter;
|
||||
ArrayList<NotificationConfiguration> list;
|
||||
PackageConfigHelper helper;
|
||||
|
||||
final int REQUEST_CODE_ADD_APP = 0;
|
||||
|
||||
private boolean hasControl = false;
|
||||
|
||||
SharedPreferences prefs;
|
||||
|
||||
TextView timeOffsetView, timezoneOffsetView;
|
||||
|
||||
GBDevice device;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_qhybrid_settings);
|
||||
|
||||
findViewById(R.id.buttonOverwriteButtons).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
|
||||
}
|
||||
});
|
||||
|
||||
prefs = getSharedPreferences(getPackageName(), MODE_PRIVATE);
|
||||
timeOffsetView = findViewById(R.id.qhybridTimeOffset);
|
||||
timeOffsetView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int timeOffset = prefs.getInt("QHYBRID_TIME_OFFSET", 0);
|
||||
LinearLayout layout2 = new LinearLayout(ConfigActivity.this);
|
||||
layout2.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
final NumberPicker hourPicker = new NumberPicker(ConfigActivity.this);
|
||||
hourPicker.setMinValue(0);
|
||||
hourPicker.setMaxValue(23);
|
||||
hourPicker.setValue(timeOffset / 60);
|
||||
|
||||
final NumberPicker minPicker = new NumberPicker(ConfigActivity.this);
|
||||
minPicker.setMinValue(0);
|
||||
minPicker.setMaxValue(59);
|
||||
minPicker.setValue(timeOffset % 60);
|
||||
|
||||
layout2.addView(hourPicker);
|
||||
TextView tw = new TextView(ConfigActivity.this);
|
||||
tw.setText(":");
|
||||
layout2.addView(tw);
|
||||
layout2.addView(minPicker);
|
||||
|
||||
layout2.setGravity(Gravity.CENTER);
|
||||
|
||||
new AlertDialog.Builder(ConfigActivity.this)
|
||||
.setTitle(getString(R.string.qhybrid_offset_time_by))
|
||||
.setView(layout2)
|
||||
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
prefs.edit().putInt("QHYBRID_TIME_OFFSET", hourPicker.getValue() * 60 + minPicker.getValue()).apply();
|
||||
updateTimeOffset();
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE));
|
||||
GB.toast(getString(R.string.qhybrid_changes_delay_prompt), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("cancel", null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
updateTimeOffset();
|
||||
|
||||
|
||||
timezoneOffsetView = findViewById(R.id.timezoneOffset);
|
||||
timezoneOffsetView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int timeOffset = prefs.getInt("QHYBRID_TIMEZONE_OFFSET", 0);
|
||||
LinearLayout layout2 = new LinearLayout(ConfigActivity.this);
|
||||
layout2.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
final NumberPicker hourPicker = new NumberPicker(ConfigActivity.this);
|
||||
hourPicker.setMinValue(0);
|
||||
hourPicker.setMaxValue(23);
|
||||
hourPicker.setValue(timeOffset / 60);
|
||||
|
||||
final NumberPicker minPicker = new NumberPicker(ConfigActivity.this);
|
||||
minPicker.setMinValue(0);
|
||||
minPicker.setMaxValue(59);
|
||||
minPicker.setValue(timeOffset % 60);
|
||||
|
||||
layout2.addView(hourPicker);
|
||||
TextView tw = new TextView(ConfigActivity.this);
|
||||
tw.setText(":");
|
||||
layout2.addView(tw);
|
||||
layout2.addView(minPicker);
|
||||
|
||||
layout2.setGravity(Gravity.CENTER);
|
||||
|
||||
new AlertDialog.Builder(ConfigActivity.this)
|
||||
.setTitle(getString(R.string.qhybrid_offset_timezone))
|
||||
.setView(layout2)
|
||||
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
prefs.edit().putInt("QHYBRID_TIMEZONE_OFFSET", hourPicker.getValue() * 60 + minPicker.getValue()).apply();
|
||||
updateTimezoneOffset();
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_TIMEZONE));
|
||||
GB.toast(getString(R.string.qhybrid_changes_delay_prompt), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("cancel", null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
updateTimezoneOffset();
|
||||
|
||||
setTitle(R.string.preferences_qhybrid_settings);
|
||||
|
||||
ListView appList = findViewById(R.id.qhybrid_appList);
|
||||
|
||||
try {
|
||||
helper = new PackageConfigHelper(getApplicationContext());
|
||||
list = helper.getNotificationConfigurations();
|
||||
} catch (GBException e) {
|
||||
GB.log("error getting configurations", GB.ERROR, e);
|
||||
GB.toast("error getting configurations", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
// null is added to indicate the plus button added handled in PackageAdapter#getView
|
||||
list.add(null);
|
||||
appList.setAdapter(adapter = new PackageAdapter(this, R.layout.qhybrid_package_settings_item, list));
|
||||
appList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||||
@Override
|
||||
public boolean onItemLongClick(final AdapterView<?> adapterView, View view, final int i, long l) {
|
||||
PopupMenu menu = new PopupMenu(ConfigActivity.this, view);
|
||||
menu.getMenu().add(0, 0, 0, "edit");
|
||||
menu.getMenu().add(0, 1, 1, "delete");
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case 0: {
|
||||
TimePicker picker = new TimePicker(ConfigActivity.this, (NotificationConfiguration) adapterView.getItemAtPosition(i));
|
||||
picker.finishListener = new TimePicker.OnFinishListener() {
|
||||
@Override
|
||||
public void onFinish(boolean success, NotificationConfiguration config) {
|
||||
setControl(false, null);
|
||||
if (success) {
|
||||
try {
|
||||
helper.saveNotificationConfiguration(config);
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
|
||||
} catch (GBException e) {
|
||||
GB.log("error saving configuration", GB.ERROR, e);
|
||||
|
||||
GB.toast("error saving notification", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
}
|
||||
refreshList();
|
||||
}
|
||||
}
|
||||
};
|
||||
picker.handsListener = new TimePicker.OnHandsSetListener() {
|
||||
@Override
|
||||
public void onHandsSet(NotificationConfiguration config) {
|
||||
setHands(config);
|
||||
}
|
||||
};
|
||||
picker.vibrationListener = new TimePicker.OnVibrationSetListener() {
|
||||
@Override
|
||||
public void onVibrationSet(NotificationConfiguration config) {
|
||||
vibrate(config);
|
||||
}
|
||||
};
|
||||
setControl(true, picker.getSettings());
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
try {
|
||||
helper.deleteNotificationConfiguration((NotificationConfiguration) adapterView.getItemAtPosition(i));
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
|
||||
} catch (GBException e) {
|
||||
GB.log("error deleting configuration", GB.ERROR, e);
|
||||
GB.toast("error deleting setting", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
}
|
||||
refreshList();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
appList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
Intent notificationIntent = new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION);
|
||||
notificationIntent.putExtra("CONFIG", (NotificationConfiguration) adapterView.getItemAtPosition(i));
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(notificationIntent);
|
||||
}
|
||||
});
|
||||
SeekBar vibeBar = findViewById(R.id.vibrationStrengthBar);
|
||||
vibeBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
int start;
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
start = seekBar.getProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
int progress;
|
||||
if ((progress = seekBar.getProgress()) == start) return;
|
||||
String[] values = {"25", "50", "100"};
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_VIBRATION_STRENGTH, values[progress]));
|
||||
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
|
||||
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_VIBRATION_STRENGTH);
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
|
||||
device = GBApplication.app().getDeviceManager().getSelectedDevice();
|
||||
if (device == null || device.getType() != DeviceType.FOSSILQHYBRID) {
|
||||
setSettingsError(getString(R.string.watch_not_connected));
|
||||
} else {
|
||||
updateSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimeOffset() {
|
||||
int timeOffset = prefs.getInt("QHYBRID_TIME_OFFSET", 0);
|
||||
DecimalFormat format = new DecimalFormat("00");
|
||||
timeOffsetView.setText(
|
||||
format.format(timeOffset / 60) + ":" +
|
||||
format.format(timeOffset % 60)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void updateTimezoneOffset() {
|
||||
int timeOffset = prefs.getInt("QHYBRID_TIMEZONE_OFFSET", 0);
|
||||
DecimalFormat format = new DecimalFormat("00");
|
||||
timezoneOffsetView.setText(
|
||||
format.format(timeOffset / 60) + ":" +
|
||||
format.format(timeOffset % 60)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private void setSettingsEnabled(boolean enables) {
|
||||
findViewById(R.id.settingsLayout).setAlpha(enables ? 1f : 0.2f);
|
||||
}
|
||||
|
||||
private void updateSettings() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
EditText et = findViewById(R.id.stepGoalEt);
|
||||
et.setOnEditorActionListener(null);
|
||||
final String text = device.getDeviceInfo(QHybridSupport.ITEM_STEP_GOAL).getDetails();
|
||||
et.setText(text);
|
||||
et.setSelection(text.length());
|
||||
et.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
|
||||
if (i == EditorInfo.IME_ACTION_DONE || i == EditorInfo.IME_ACTION_NEXT) {
|
||||
String t = textView.getText().toString();
|
||||
if (!t.isEmpty()) {
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_GOAL, t));
|
||||
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
|
||||
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_STEP_GOAL);
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
|
||||
updateSettings();
|
||||
}
|
||||
((InputMethodManager) getApplicationContext().getSystemService(Activity.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if ("true".equals(device.getDeviceInfo(QHybridSupport.ITEM_EXTENDED_VIBRATION_SUPPORT).getDetails())) {
|
||||
final int strengthProgress = (int) (Math.log(Double.parseDouble(device.getDeviceInfo(QHybridSupport.ITEM_VIBRATION_STRENGTH).getDetails()) / 25) / Math.log(2));
|
||||
|
||||
setSettingsEnabled(true);
|
||||
SeekBar seekBar = findViewById(R.id.vibrationStrengthBar);
|
||||
seekBar.setProgress(strengthProgress);
|
||||
} else {
|
||||
findViewById(R.id.vibrationStrengthBar).setEnabled(false);
|
||||
findViewById(R.id.vibrationStrengthLayout).setAlpha(0.5f);
|
||||
}
|
||||
CheckBox activityHandCheckbox = findViewById(R.id.checkBoxUserActivityHand);
|
||||
if (device.getDeviceInfo(QHybridSupport.ITEM_HAS_ACTIVITY_HAND).getDetails().equals("true")) {
|
||||
if (device.getDeviceInfo(QHybridSupport.ITEM_USE_ACTIVITY_HAND).getDetails().equals("true")) {
|
||||
activityHandCheckbox.setChecked(true);
|
||||
}
|
||||
activityHandCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean checked) {
|
||||
if (!device.getDeviceInfo(QHybridSupport.ITEM_STEP_GOAL).getDetails().equals("1000000")) {
|
||||
new AlertDialog.Builder(ConfigActivity.this)
|
||||
.setMessage(getString(R.string.qhybrid_prompt_million_steps))
|
||||
.setPositiveButton("ok", null)
|
||||
.show();
|
||||
buttonView.setChecked(false);
|
||||
return;
|
||||
}
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_USE_ACTIVITY_HAND, String.valueOf(checked)));
|
||||
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
|
||||
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_USE_ACTIVITY_HAND);
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// activityHandCheckbox.setEnabled(false);
|
||||
activityHandCheckbox.setAlpha(0.2f);
|
||||
activityHandCheckbox.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
GB.toast("nah.", Toast.LENGTH_SHORT, GB.INFO);
|
||||
((CheckBox) v).setChecked(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final String buttonJson = device.getDeviceInfo(FossilWatchAdapter.ITEM_BUTTONS).getDetails();
|
||||
try {
|
||||
JSONArray buttonConfig_;
|
||||
if (buttonJson == null || buttonJson.isEmpty()) {
|
||||
buttonConfig_ = new JSONArray(new String[]{"", "", ""});
|
||||
}else{
|
||||
buttonConfig_ = new JSONArray(buttonJson);
|
||||
}
|
||||
|
||||
final JSONArray buttonConfig = buttonConfig_;
|
||||
|
||||
LinearLayout buttonLayout = findViewById(R.id.buttonConfigLayout);
|
||||
buttonLayout.removeAllViews();
|
||||
findViewById(R.id.buttonOverwriteButtons).setVisibility(View.GONE);
|
||||
final ConfigPayload[] payloads = ConfigPayload.values();
|
||||
final String[] names = new String[payloads.length];
|
||||
for (int i = 0; i < payloads.length; i++)
|
||||
names[i] = payloads[i].getDescription();
|
||||
for (int i = 0; i < buttonConfig.length(); i++) {
|
||||
final int currentIndex = i;
|
||||
String configName = buttonConfig.getString(i);
|
||||
TextView buttonTextView = new TextView(ConfigActivity.this);
|
||||
buttonTextView.setTextColor(Color.WHITE);
|
||||
buttonTextView.setTextSize(20);
|
||||
try {
|
||||
ConfigPayload payload = ConfigPayload.valueOf(configName);
|
||||
buttonTextView.setText("Button " + (i + 1) + ": " + payload.getDescription());
|
||||
} catch (IllegalArgumentException e) {
|
||||
buttonTextView.setText("Button " + (i + 1) + ": Unknown");
|
||||
}
|
||||
|
||||
buttonTextView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(ConfigActivity.this)
|
||||
.setItems(names, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.cancel();
|
||||
ConfigPayload selected = payloads[which];
|
||||
|
||||
try {
|
||||
buttonConfig.put(currentIndex, selected.toString());
|
||||
device.addDeviceInfo(new GenericItem(FossilWatchAdapter.ITEM_BUTTONS, buttonConfig.toString()));
|
||||
updateSettings();
|
||||
Intent buttonIntent = new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS);
|
||||
buttonIntent.putExtra(FossilWatchAdapter.ITEM_BUTTONS, buttonConfig.toString());
|
||||
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(buttonIntent);
|
||||
} catch (JSONException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.setTitle("Button " + (currentIndex + 1))
|
||||
.create();
|
||||
dialog.show();
|
||||
}
|
||||
});
|
||||
|
||||
buttonLayout.addView(buttonTextView);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
GB.log("error parsing button config", GB.ERROR, e);
|
||||
GB.toast("error parsing button config", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setControl(boolean control, NotificationConfiguration config) {
|
||||
if (hasControl == control) return;
|
||||
Intent intent = new Intent(control ? QHybridSupport.QHYBRID_COMMAND_CONTROL : QHybridSupport.QHYBRID_COMMAND_UNCONTROL);
|
||||
intent.putExtra("CONFIG", config);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
this.hasControl = control;
|
||||
}
|
||||
|
||||
private void setHands(NotificationConfiguration config) {
|
||||
sendControl(config, QHybridSupport.QHYBRID_COMMAND_SET);
|
||||
}
|
||||
|
||||
private void vibrate(NotificationConfiguration config) {
|
||||
sendControl(config, QHybridSupport.QHYBRID_COMMAND_VIBRATE);
|
||||
}
|
||||
|
||||
private void sendControl(NotificationConfiguration config, String request) {
|
||||
Intent intent = new Intent(request);
|
||||
intent.putExtra("CONFIG", config);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void refreshList() {
|
||||
list.clear();
|
||||
try {
|
||||
list.addAll(helper.getNotificationConfigurations());
|
||||
} catch (GBException e) {
|
||||
GB.log("error getting configurations", GB.ERROR, e);
|
||||
GB.toast("error getting configurations", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
}
|
||||
// null is added to indicate the plus button added handled in PackageAdapter#getView
|
||||
list.add(null);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
refreshList();
|
||||
registerReceiver(buttonReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS));
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(settingsReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_SETTINGS_UPDATED));
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(fileReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_FILE_UPLOADED));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
unregisterReceiver(buttonReceiver);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(settingsReceiver);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(fileReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
|
||||
private void setSettingsError(final String error) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setSettingsEnabled(false);
|
||||
((TextView) findViewById(R.id.settingsErrorText)).setVisibility(View.VISIBLE);
|
||||
((TextView) findViewById(R.id.settingsErrorText)).setText(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class PackageAdapter extends ArrayAdapter<NotificationConfiguration> {
|
||||
PackageManager manager;
|
||||
|
||||
PackageAdapter(@NonNull Context context, int resource, @NonNull List<NotificationConfiguration> objects) {
|
||||
super(context, resource, objects);
|
||||
manager = context.getPackageManager();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
|
||||
if (!(view instanceof RelativeLayout))
|
||||
view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.qhybrid_package_settings_item, null);
|
||||
NotificationConfiguration settings = getItem(position);
|
||||
|
||||
if (settings == null) {
|
||||
Button addButton = new Button(ConfigActivity.this);
|
||||
addButton.setText("+");
|
||||
addButton.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
addButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
startActivityForResult(new Intent(ConfigActivity.this, QHybridAppChoserActivity.class), REQUEST_CODE_ADD_APP);
|
||||
}
|
||||
});
|
||||
return addButton;
|
||||
}
|
||||
|
||||
try {
|
||||
((ImageView) view.findViewById(R.id.packageIcon)).setImageDrawable(manager.getApplicationIcon(settings.getPackageName()));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
final int width = 100;
|
||||
((TextView) view.findViewById(R.id.packageName)).setText(settings.getAppName());
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888);
|
||||
Canvas c = new Canvas(bitmap);
|
||||
|
||||
Paint black = new Paint();
|
||||
black.setColor(Color.BLACK);
|
||||
black.setStyle(Paint.Style.STROKE);
|
||||
black.setStrokeWidth(5);
|
||||
|
||||
c.drawCircle(width / 2, width / 2, width / 2 - 3, black);
|
||||
|
||||
int center = width / 2;
|
||||
if (settings.getHour() != -1) {
|
||||
c.drawLine(
|
||||
center,
|
||||
center,
|
||||
(float) (center + Math.sin(Math.toRadians(settings.getHour())) * (width / 4)),
|
||||
(float) (center - Math.cos(Math.toRadians(settings.getHour())) * (width / 4)),
|
||||
black
|
||||
);
|
||||
}
|
||||
if (settings.getMin() != -1) {
|
||||
c.drawLine(
|
||||
center,
|
||||
center,
|
||||
(float) (center + Math.sin(Math.toRadians(settings.getMin())) * (width / 3)),
|
||||
(float) (center - Math.cos(Math.toRadians(settings.getMin())) * (width / 3)),
|
||||
black
|
||||
);
|
||||
}
|
||||
|
||||
((ImageView) view.findViewById(R.id.packageClock)).setImageBitmap(bitmap);
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
BroadcastReceiver fileReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
boolean error = intent.getBooleanExtra("EXTRA_ERROR", false);
|
||||
if (error) {
|
||||
GB.toast(getString(R.string.qhybrid_buttons_overwrite_error), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
GB.toast(getString(R.string.qhybrid_buttons_overwrite_success), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
};
|
||||
|
||||
BroadcastReceiver buttonReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
GB.toast("Button " + intent.getIntExtra("BUTTON", -1) + " pressed", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
};
|
||||
|
||||
BroadcastReceiver settingsReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
GB.toast("Setting updated", Toast.LENGTH_SHORT, GB.INFO);
|
||||
updateSettings();
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
|
||||
public class NotificationConfiguration implements Serializable {
|
||||
private short min, hour, subEye = -1;
|
||||
private String packageName, appName;
|
||||
private PlayNotificationRequest.VibrationType vibration;
|
||||
private boolean respectSilentMode;
|
||||
private long id = -1;
|
||||
|
||||
NotificationConfiguration(short min, short hour, String packageName, String appName, boolean respectSilentMode, PlayNotificationRequest.VibrationType vibration) {
|
||||
this.min = min;
|
||||
this.hour = hour;
|
||||
this.packageName = packageName;
|
||||
this.appName = appName;
|
||||
this.respectSilentMode = respectSilentMode;
|
||||
this.vibration = vibration;
|
||||
}
|
||||
|
||||
public NotificationConfiguration(short min, short hour, short subEye, PlayNotificationRequest.VibrationType vibration) {
|
||||
this.min = min;
|
||||
this.hour = hour;
|
||||
this.subEye = subEye;
|
||||
this.vibration = vibration;
|
||||
}
|
||||
|
||||
public NotificationConfiguration(short min, short hour, String packageName, String appName, boolean respectSilentMode, PlayNotificationRequest.VibrationType vibration, long id) {
|
||||
this.min = min;
|
||||
this.hour = hour;
|
||||
this.packageName = packageName;
|
||||
this.appName = appName;
|
||||
this.respectSilentMode = respectSilentMode;
|
||||
this.vibration = vibration;
|
||||
this.id = id;
|
||||
}
|
||||
NotificationConfiguration(String packageName, String appName) {
|
||||
this.min = -1;
|
||||
this.hour = -1;
|
||||
this.packageName = packageName;
|
||||
this.appName = appName;
|
||||
this.respectSilentMode = false;
|
||||
this.vibration = PlayNotificationRequest.VibrationType.SINGLE_NORMAL;
|
||||
this.id = -1;
|
||||
}
|
||||
|
||||
public PlayNotificationRequest.VibrationType getVibration() {
|
||||
return vibration;
|
||||
}
|
||||
|
||||
public void setVibration(PlayNotificationRequest.VibrationType vibration) {
|
||||
this.vibration = vibration;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public boolean getRespectSilentMode() {
|
||||
Log.d("Config", "respect: " + respectSilentMode);
|
||||
return respectSilentMode;
|
||||
}
|
||||
|
||||
public void setRespectSilentMode(boolean respectSilentMode) {
|
||||
this.respectSilentMode = respectSilentMode;
|
||||
}
|
||||
|
||||
public void setMin(short min) {
|
||||
this.min = min;
|
||||
}
|
||||
|
||||
public void setHour(short hour) {
|
||||
this.hour = hour;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public short getMin() {
|
||||
return min;
|
||||
}
|
||||
|
||||
public short getHour() {
|
||||
return hour;
|
||||
}
|
||||
|
||||
public short getSubEye() {
|
||||
return subEye;
|
||||
}
|
||||
|
||||
public void setSubEye(short subEye) {
|
||||
this.subEye = subEye;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return appName;
|
||||
}
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBOpenHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class PackageConfigHelper {
|
||||
public static final String DB_NAME = "qhybridNotifications.db";
|
||||
public static final String DB_ID = "id";
|
||||
public static final String DB_TABLE = "notifications";
|
||||
public static final String DB_PACKAGE = "package";
|
||||
public static final String DB_APPNAME = "appName";
|
||||
public static final String DB_VIBRATION = "vibrationTtype";
|
||||
public static final String DB_MINUTE = "minDegress";
|
||||
public static final String DB_HOUR = "hourDegrees";
|
||||
public static final String DB_RESPECT_SILENT = "respectSilent";
|
||||
|
||||
|
||||
public PackageConfigHelper(Context context) throws GBException {
|
||||
initDB();
|
||||
}
|
||||
|
||||
public void saveNotificationConfiguration(NotificationConfiguration settings) throws GBException {
|
||||
ContentValues values = new ContentValues(6);
|
||||
values.put(DB_PACKAGE, settings.getPackageName());
|
||||
values.put(DB_APPNAME, settings.getAppName());
|
||||
values.put(DB_HOUR, settings.getHour());
|
||||
values.put(DB_MINUTE, settings.getMin());
|
||||
values.put(DB_VIBRATION, settings.getVibration().getValue());
|
||||
values.put(DB_RESPECT_SILENT, settings.getRespectSilentMode());
|
||||
|
||||
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
|
||||
|
||||
if(settings.getId() == -1) {
|
||||
settings.setId(database.insert(DB_TABLE, null, values));
|
||||
}else{
|
||||
database.update(DB_TABLE, values, DB_ID + "=?", new String[]{String.valueOf(settings.getId())});
|
||||
}
|
||||
|
||||
GBApplication.releaseDB();
|
||||
//LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent());
|
||||
}
|
||||
|
||||
public ArrayList<NotificationConfiguration> getNotificationConfigurations() throws GBException {
|
||||
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
|
||||
|
||||
Cursor cursor = database.query(DB_TABLE, new String[]{"*"}, null, null, null, null, null);
|
||||
|
||||
GBApplication.releaseDB();
|
||||
|
||||
int size = cursor.getCount();
|
||||
ArrayList<NotificationConfiguration> list = new ArrayList<>(size);
|
||||
if(size > 0){
|
||||
int appNamePos = cursor.getColumnIndex(DB_APPNAME);
|
||||
int packageNamePos = cursor.getColumnIndex(DB_PACKAGE);
|
||||
int hourPos = cursor.getColumnIndex(DB_HOUR);
|
||||
int minPos = cursor.getColumnIndex(DB_MINUTE);
|
||||
int silentPos = cursor.getColumnIndex(DB_RESPECT_SILENT);
|
||||
int vibrationPos = cursor.getColumnIndex(DB_VIBRATION);
|
||||
int idPos = cursor.getColumnIndex(DB_ID);
|
||||
cursor.moveToFirst();
|
||||
do {
|
||||
list.add(new NotificationConfiguration(
|
||||
(short)cursor.getInt(minPos),
|
||||
(short)cursor.getInt(hourPos),
|
||||
cursor.getString(packageNamePos),
|
||||
cursor.getString(appNamePos),
|
||||
cursor.getInt(silentPos) == 1,
|
||||
PlayNotificationRequest.VibrationType.fromValue((byte)cursor.getInt(vibrationPos)),
|
||||
cursor.getInt(idPos)
|
||||
));
|
||||
Log.d("Settings", "setting #" + cursor.getPosition() + ": " + cursor.getInt(silentPos));
|
||||
}while (cursor.moveToNext());
|
||||
}
|
||||
cursor.close();
|
||||
return list;
|
||||
}
|
||||
|
||||
public NotificationConfiguration getNotificationConfiguration(String appName) throws GBException {
|
||||
if(appName == null) return null;
|
||||
|
||||
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
|
||||
|
||||
Cursor c = database.query(DB_TABLE, new String[]{"*"}, DB_APPNAME + "=?", new String[]{appName}, null, null, null);
|
||||
|
||||
GBApplication.releaseDB();
|
||||
|
||||
if(c.getCount() == 0){
|
||||
c.close();
|
||||
return null;
|
||||
}
|
||||
c.moveToFirst();
|
||||
NotificationConfiguration settings = new NotificationConfiguration(
|
||||
(short)c.getInt(c.getColumnIndex(DB_MINUTE)),
|
||||
(short)c.getInt(c.getColumnIndex(DB_HOUR)),
|
||||
c.getString(c.getColumnIndex(DB_PACKAGE)),
|
||||
c.getString(c.getColumnIndex(DB_APPNAME)),
|
||||
c.getInt(c.getColumnIndex(DB_RESPECT_SILENT)) == 1,
|
||||
PlayNotificationRequest.VibrationType.fromValue((byte)c.getInt(c.getColumnIndex(DB_VIBRATION))),
|
||||
c.getInt(c.getColumnIndex(DB_ID))
|
||||
);
|
||||
c.close();
|
||||
return settings;
|
||||
}
|
||||
|
||||
private void initDB() throws GBException {
|
||||
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS notifications(" +
|
||||
DB_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
|
||||
DB_PACKAGE + " TEXT, " +
|
||||
DB_VIBRATION + " INTEGER, " +
|
||||
DB_MINUTE + " INTEGER DEFAULT -1, " +
|
||||
DB_APPNAME + " TEXT," +
|
||||
DB_RESPECT_SILENT + " INTEGER," +
|
||||
DB_HOUR + " INTEGER DEFAULT -1);");
|
||||
|
||||
GBApplication.releaseDB();
|
||||
}
|
||||
|
||||
public void deleteNotificationConfiguration(NotificationConfiguration packageSettings) throws GBException {
|
||||
Log.d("DB", "deleting id " + packageSettings.getId());
|
||||
if(packageSettings.getId() == -1) return;
|
||||
|
||||
SQLiteDatabase database = GBApplication.acquireDB().getDatabase();
|
||||
|
||||
database.delete(DB_TABLE, DB_ID + "=?", new String[]{String.valueOf(packageSettings.getId())});
|
||||
|
||||
GBApplication.releaseDB();
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
|
||||
public class QHybridAppChoserActivity extends AbstractGBActivity {
|
||||
boolean hasControl = false;
|
||||
|
||||
PackageConfigHelper helper;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_qhybrid_app_choser);
|
||||
|
||||
try {
|
||||
helper = new PackageConfigHelper(getApplicationContext());
|
||||
} catch (GBException e) {
|
||||
GB.log("database error", GB.ERROR, e);
|
||||
GB.toast("error getting database helper", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
final ListView appList = findViewById(R.id.qhybrid_appChooserList);
|
||||
final PackageManager manager = getPackageManager();
|
||||
final List<PackageInfo> packages = manager.getInstalledPackages(0);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final IdentityHashMap<PackageInfo, String> nameMap = new IdentityHashMap(packages.size());
|
||||
for(PackageInfo info : packages){
|
||||
CharSequence label = manager.getApplicationLabel(info.applicationInfo);
|
||||
if(label == null) label = info.packageName;
|
||||
nameMap.put(info, label.toString());
|
||||
}
|
||||
|
||||
Collections.sort(packages, new Comparator<PackageInfo>() {
|
||||
@Override
|
||||
public int compare(PackageInfo packageInfo, PackageInfo t1) {
|
||||
return nameMap.get(packageInfo)
|
||||
.compareToIgnoreCase(
|
||||
nameMap.get(t1)
|
||||
);
|
||||
}
|
||||
});
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
appList.setAdapter(new ConfigArrayAdapter(QHybridAppChoserActivity.this, R.layout.qhybrid_app_view, packages, manager));
|
||||
findViewById(R.id.qhybrid_packageChooserLoading).setVisibility(GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
appList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
showPackageDialog(packages.get(i));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void setControl(boolean control) {
|
||||
if (hasControl == control) return;
|
||||
Intent intent = new Intent(control ? QHybridSupport.QHYBRID_COMMAND_CONTROL : QHybridSupport.QHYBRID_COMMAND_UNCONTROL);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
this.hasControl = control;
|
||||
}
|
||||
|
||||
private void setHands(NotificationConfiguration config){
|
||||
sendControl(config, QHybridSupport.QHYBRID_COMMAND_SET);
|
||||
}
|
||||
|
||||
private void vibrate(NotificationConfiguration config){
|
||||
sendControl(config, QHybridSupport.QHYBRID_COMMAND_VIBRATE);
|
||||
}
|
||||
|
||||
private void sendControl(NotificationConfiguration config, String request){
|
||||
Intent intent = new Intent(request);
|
||||
intent.putExtra("CONFIG", config);
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void showPackageDialog(PackageInfo info) {
|
||||
TimePicker picker = new TimePicker(this, info);
|
||||
|
||||
picker.finishListener = new TimePicker.OnFinishListener() {
|
||||
@Override
|
||||
public void onFinish(boolean success, NotificationConfiguration config) {
|
||||
setControl(false);
|
||||
if(success){
|
||||
try {
|
||||
helper.saveNotificationConfiguration(config);
|
||||
LocalBroadcastManager.getInstance(QHybridAppChoserActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
|
||||
} catch (GBException e) {
|
||||
GB.log("error saving config", GB.ERROR, e);
|
||||
GB.toast("error saving configuration", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
picker.handsListener = new TimePicker.OnHandsSetListener() {
|
||||
@Override
|
||||
public void onHandsSet(NotificationConfiguration config) {
|
||||
setHands(config);
|
||||
}
|
||||
};
|
||||
|
||||
picker.vibrationListener = new TimePicker.OnVibrationSetListener() {
|
||||
@Override
|
||||
public void onVibrationSet(NotificationConfiguration config) {
|
||||
vibrate(config);
|
||||
}
|
||||
};
|
||||
|
||||
setControl(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
setControl(false);
|
||||
finish();
|
||||
}
|
||||
|
||||
class ConfigArrayAdapter extends ArrayAdapter<PackageInfo> {
|
||||
PackageManager manager;
|
||||
|
||||
public ConfigArrayAdapter(@NonNull Context context, int resource, @NonNull List<PackageInfo> objects, PackageManager manager) {
|
||||
super(context, resource, objects);
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
|
||||
if (view == null)
|
||||
view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.qhybrid_app_view, null);
|
||||
|
||||
ApplicationInfo info = getItem(position).applicationInfo;
|
||||
((ImageView) view.findViewById(R.id.qhybrid_appChooserItemIcon)).setImageDrawable(manager.getApplicationIcon(info));
|
||||
((TextView) view.findViewById(R.id.qhybrid_appChooserItemText)).setText(manager.getApplicationLabel(info));
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
|
||||
public class QHybridCoordinator extends AbstractDeviceCoordinator {
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
for(ParcelUuid uuid : candidate.getServiceUuids()){
|
||||
if(uuid.getUuid().toString().equals("3dda0001-957f-7d4a-34a6-74696673696d")){
|
||||
return DeviceType.FOSSILQHYBRID;
|
||||
}
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
return Collections.singletonList(new ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString("3dda0001-957f-7d4a-34a6-74696673696d")).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.FOSSILQHYBRID;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsAlarmConfiguration() {
|
||||
GBDevice connectedDevice = GBApplication.app().getDeviceManager().getSelectedDevice();
|
||||
if(connectedDevice == null || connectedDevice.getType() != DeviceType.FOSSILQHYBRID || connectedDevice.getState() != GBDevice.State.INITIALIZED){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount() {
|
||||
return this.supportsAlarmConfiguration() ? 5 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Fossil";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return ConfigActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
GBDevice connectedDevice = GBApplication.app().getDeviceManager().getSelectedDevice();
|
||||
if(connectedDevice == null || connectedDevice.getType() != DeviceType.FOSSILQHYBRID){
|
||||
return true;
|
||||
}
|
||||
ItemWithDetails vibration = connectedDevice.getDeviceInfo(QHybridSupport.ITEM_EXTENDED_VIBRATION_SUPPORT);
|
||||
if(vibration == null){
|
||||
return true;
|
||||
}
|
||||
return vibration.getDetails().equals("true");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.twofortyfouram.locale.sdk.client.ui.activity.AbstractPluginActivity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
|
||||
public class TaskerPluginActivity extends AbstractPluginActivity {
|
||||
public static final String key_hours = "qhybrid_hours";
|
||||
public static final String key_minute = "qhybrid_minutes";
|
||||
public static final String key_vibration = "qhybrid_vibration";
|
||||
|
||||
RadioGroup group;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_tasker_plugin);
|
||||
|
||||
group = findViewById(R.id.qhybrid_tasker_vibration);
|
||||
for(PlayNotificationRequest.VibrationType type : PlayNotificationRequest.VibrationType.values()){
|
||||
RadioButton button = new RadioButton(this);
|
||||
button.setText(type.name() + " (" + type.name() + ")");
|
||||
button.setId(type.getValue());
|
||||
group.addView(button);
|
||||
}
|
||||
group.check(PlayNotificationRequest.VibrationType.NO_VIBE.getValue());
|
||||
RadioButton custom = new RadioButton(this);
|
||||
custom.setText("variable %vibration");
|
||||
custom.setId(10);
|
||||
group.addView(custom);
|
||||
|
||||
Intent intent = getIntent();
|
||||
if(intent.hasExtra(key_hours)){
|
||||
((TextView) findViewById(R.id.qhybrid_hour_degrees)).setText(intent.getStringExtra(key_hours));
|
||||
}
|
||||
if(intent.hasExtra(key_minute)){
|
||||
((TextView) findViewById(R.id.qhybrid_minute_degrees)).setText(intent.getStringExtra(key_minute));
|
||||
}
|
||||
if(intent.hasExtra(key_vibration)){
|
||||
String vibe = intent.getStringExtra(key_vibration);
|
||||
if(vibe.equals("%vibration")){
|
||||
group.check(10);
|
||||
}else {
|
||||
group.check(Integer.parseInt(vibe));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBundleValid(@NonNull Bundle bundle) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostCreateWithPreviousResult(@NonNull Bundle bundle, @NonNull String s) {
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bundle getResultBundle() {
|
||||
int vibration = group.getCheckedRadioButtonId();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(key_hours, ((EditText) findViewById(R.id.qhybrid_hour_degrees)).getText().toString());
|
||||
bundle.putString(key_minute, ((EditText) findViewById(R.id.qhybrid_minute_degrees)).getText().toString());
|
||||
|
||||
if(vibration == 10){
|
||||
bundle.putString(key_vibration, "%vibration");
|
||||
}else{
|
||||
bundle.putString(key_vibration, String.valueOf(vibration));
|
||||
}
|
||||
TaskerPlugin.Setting.setVariableReplaceKeys(bundle, new String[]{key_hours, key_minute, key_vibration});
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getResultBlurb(@NonNull Bundle bundle) {
|
||||
return "nope";
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
|
||||
public class TaskerPluginReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String min = intent.getStringExtra(TaskerPluginActivity.key_minute);
|
||||
String hour = intent.getStringExtra(TaskerPluginActivity.key_hours);
|
||||
String vibration = intent.getStringExtra(TaskerPluginActivity.key_vibration);
|
||||
|
||||
int minDegrees = (int)Float.parseFloat(min);
|
||||
int hourDegrees = (int)Float.parseFloat(hour);
|
||||
|
||||
NotificationConfiguration config = new NotificationConfiguration(
|
||||
(short)minDegrees,
|
||||
(short)hourDegrees,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
PlayNotificationRequest.VibrationType.fromValue(Byte.parseByte(vibration))
|
||||
);
|
||||
|
||||
Intent send = new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION);
|
||||
send.putExtra("CONFIG", config);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(send);
|
||||
}
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
|
||||
public class TimePicker extends AlertDialog.Builder {
|
||||
ImageView pickerView;
|
||||
Canvas pickerCanvas;
|
||||
Bitmap pickerBitmap;
|
||||
|
||||
NotificationConfiguration settings;
|
||||
|
||||
int height, width, radius;
|
||||
int radius1, radius2, radius3;
|
||||
int controlledHand = 0;
|
||||
int handRadius;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
OnFinishListener finishListener;
|
||||
OnHandsSetListener handsListener;
|
||||
OnVibrationSetListener vibrationListener;
|
||||
|
||||
protected TimePicker(@NonNull Context context, PackageInfo info) {
|
||||
super(context);
|
||||
|
||||
settings = new NotificationConfiguration(info.packageName, context.getApplicationContext().getPackageManager().getApplicationLabel(info.applicationInfo).toString());
|
||||
initGraphics(context);
|
||||
}
|
||||
|
||||
protected TimePicker(Context context, NotificationConfiguration config){
|
||||
super(context);
|
||||
|
||||
settings = config;
|
||||
initGraphics(context);
|
||||
}
|
||||
|
||||
private void initGraphics(Context context){
|
||||
int w = (int) (((WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getWidth() * 0.8);
|
||||
height = w;
|
||||
width = w;
|
||||
radius = (int) (w * 0.06);
|
||||
radius1 = 0;
|
||||
radius2 = (int) (radius * 2.3);
|
||||
radius3 = (int)(radius2 * 2.15);
|
||||
int offset = (int) (w * 0.1);
|
||||
radius1 += offset;
|
||||
radius2 += offset;
|
||||
radius3 += offset;
|
||||
|
||||
pickerView = new ImageView(context);
|
||||
pickerBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
|
||||
pickerCanvas = new Canvas(pickerBitmap);
|
||||
|
||||
drawClock();
|
||||
|
||||
LinearLayout layout = new LinearLayout(context);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.addView(pickerView);
|
||||
|
||||
CheckBox box = new CheckBox(context);
|
||||
box.setText("Respect silent mode");
|
||||
box.setChecked(settings.getRespectSilentMode());
|
||||
box.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
settings.setRespectSilentMode(b);
|
||||
}
|
||||
});
|
||||
layout.addView(box);
|
||||
|
||||
RadioGroup group = new RadioGroup(context);
|
||||
for(PlayNotificationRequest.VibrationType vibe: PlayNotificationRequest.VibrationType.values()){
|
||||
RadioButton button = new RadioButton(context);
|
||||
button.setText(vibe.toString());
|
||||
button.setId(vibe.getValue());
|
||||
group.addView(button);
|
||||
}
|
||||
|
||||
group.check(settings.getVibration().getValue());
|
||||
group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup radioGroup, int i) {
|
||||
settings.setVibration(PlayNotificationRequest.VibrationType.fromValue((byte)i));
|
||||
if(TimePicker.this.vibrationListener != null) TimePicker.this.vibrationListener.onVibrationSet(settings);
|
||||
}
|
||||
});
|
||||
|
||||
ScrollView scrollView = new ScrollView(context);
|
||||
scrollView.addView(group);
|
||||
layout.addView(scrollView);
|
||||
|
||||
setView(layout);
|
||||
|
||||
|
||||
|
||||
setNegativeButton("cancel", null);
|
||||
setPositiveButton("ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
if(finishListener == null) return;
|
||||
finishListener.onFinish(true, settings);
|
||||
}
|
||||
});
|
||||
setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialogInterface) {
|
||||
if(finishListener == null) return;
|
||||
finishListener.onFinish(false, settings);
|
||||
}
|
||||
});
|
||||
setOnDismissListener(new DialogInterface.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss(DialogInterface dialogInterface) {
|
||||
if(finishListener == null) return;
|
||||
finishListener.onFinish(false, settings);
|
||||
}
|
||||
});
|
||||
dialog = show();
|
||||
|
||||
pickerView.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||
handleTouch(dialog, motionEvent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public NotificationConfiguration getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
private void handleTouch(AlertDialog dialog, MotionEvent event) {
|
||||
int centerX = width / 2;
|
||||
int centerY = height / 2;
|
||||
int difX = centerX - (int) event.getX();
|
||||
int difY = (int) event.getY() - centerY;
|
||||
int dist = (int) Math.sqrt(Math.abs(difX) * Math.abs(difX) + Math.abs(difY) * Math.abs(difY));
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
int radiusHalf = radius;
|
||||
if (dist < (radius1 + radiusHalf) && dist > (radius1 - radiusHalf)) {
|
||||
Log.d("Settings", "hit sub");
|
||||
handRadius = (int) (height / 2f - radius1);
|
||||
controlledHand = 3;
|
||||
} else if (dist < (radius2 + radiusHalf) && dist > (radius2 - radiusHalf)) {
|
||||
Log.d("Settings", "hit hour");
|
||||
controlledHand = 1;
|
||||
handRadius = (int) (height / 2f - radius2);
|
||||
} else if (dist < (radius3 + radiusHalf) && dist > (radius3 - radiusHalf)) {
|
||||
Log.d("Settings", "hit minute");
|
||||
controlledHand = 2;
|
||||
handRadius = (int) (height / 2f - radius3);
|
||||
} else {
|
||||
Log.d("Settings", "hit nothing");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (controlledHand == 0) return;
|
||||
double degree = difY == 0 ? (difX < 0 ? 90 : 270) : Math.toDegrees(Math.atan((float) difX / (float) difY));
|
||||
if (difY > 0) degree = 180 + degree;
|
||||
if (degree < 0) degree = 360 + degree;
|
||||
switch (controlledHand) {
|
||||
case 1: {
|
||||
settings.setHour((short) (((int)(degree + 15) / 30) * 30 % 360));
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
settings.setMin((short) (((int)(degree + 15) / 30) * 30 % 360));
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MotionEvent.ACTION_UP: {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setClickable(true);
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setAlpha(1f);
|
||||
if(handsListener != null) handsListener.onHandsSet(settings);
|
||||
break;
|
||||
}
|
||||
}
|
||||
drawClock();
|
||||
}
|
||||
|
||||
|
||||
private void drawClock() {
|
||||
//pickerCanvas.drawColor(Color.WHITE);
|
||||
Paint white = new Paint();
|
||||
white.setColor(Color.WHITE);
|
||||
white.setStyle(Paint.Style.FILL);
|
||||
pickerCanvas.drawCircle(width / 2, width / 2, width / 2, white);
|
||||
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
paint.setColor(Color.BLUE);
|
||||
Paint text = new Paint();
|
||||
text.setStyle(Paint.Style.FILL);
|
||||
text.setTextSize(radius * 1.5f);
|
||||
text.setColor(Color.BLACK);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
int textShiftY = (int) ((text.descent() + text.ascent()) / 2);
|
||||
|
||||
Paint linePaint = new Paint();
|
||||
linePaint.setStrokeWidth(10);
|
||||
linePaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
linePaint.setColor(Color.BLACK);
|
||||
|
||||
if (settings.getMin() != -1) {
|
||||
paint.setAlpha(255);
|
||||
float x = (float) (width / 2f + Math.sin(Math.toRadians(settings.getMin())) * (float) radius3);
|
||||
float y = (float) (height / 2f - Math.cos(Math.toRadians(settings.getMin())) * (float) radius3);
|
||||
linePaint.setAlpha(255);
|
||||
pickerCanvas.drawLine(width / 2, height / 2, x, y, linePaint);
|
||||
pickerCanvas.drawCircle(
|
||||
x,
|
||||
y,
|
||||
radius,
|
||||
paint
|
||||
);
|
||||
pickerCanvas.drawText(String.valueOf(settings.getMin() / 6), x, y - textShiftY, text);
|
||||
}
|
||||
if (settings.getHour() != -1) {
|
||||
paint.setAlpha(255);
|
||||
float x = (float) (width / 2f + Math.sin(Math.toRadians(settings.getHour())) * (float) radius2);
|
||||
float y = (float) (height / 2f - Math.cos(Math.toRadians(settings.getHour())) * (float) radius2);
|
||||
linePaint.setAlpha(255);
|
||||
pickerCanvas.drawLine(width / 2, height / 2, x, y, linePaint);
|
||||
pickerCanvas.drawCircle(
|
||||
x,
|
||||
y,
|
||||
radius,
|
||||
paint
|
||||
);
|
||||
pickerCanvas.drawText(settings.getHour() == 0 ? "12" : String.valueOf(settings.getHour() / 30), x, y - textShiftY, text);
|
||||
}
|
||||
|
||||
Paint paint2 = new Paint();
|
||||
paint2.setColor(Color.BLACK);
|
||||
paint2.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
pickerCanvas.drawCircle(width / 2, height / 2, 5, paint2);
|
||||
pickerView.setImageBitmap(pickerBitmap);
|
||||
}
|
||||
|
||||
interface OnFinishListener{
|
||||
public void onFinish(boolean success, NotificationConfiguration config);
|
||||
}
|
||||
|
||||
interface OnHandsSetListener{
|
||||
public void onHandsSet(NotificationConfiguration config);
|
||||
}
|
||||
|
||||
interface OnVibrationSetListener{
|
||||
public void onVibrationSet(NotificationConfiguration config);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -51,6 +51,7 @@ public enum DeviceType {
|
||||
NO1F1(50, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_no1_f1),
|
||||
TECLASTH30(60, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_teclast_h30),
|
||||
XWATCH(70, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_xwatch),
|
||||
FOSSILQHYBRID(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_qhybrid),
|
||||
ZETIME(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_mykronoz_zetime),
|
||||
ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115),
|
||||
WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9),
|
||||
|
@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaL
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
|
||||
@ -183,7 +184,10 @@ public class DeviceSupportFactory {
|
||||
case XWATCH:
|
||||
deviceSupport = new ServiceDeviceSupport(new XWatchSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case ZETIME:
|
||||
case FOSSILQHYBRID:
|
||||
deviceSupport = new ServiceDeviceSupport(new QHybridSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case ZETIME:
|
||||
deviceSupport = new ServiceDeviceSupport(new ZeTimeDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case ID115:
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -209,4 +209,9 @@ public abstract class AbstractBTLEOperation<T extends AbstractBTLEDeviceSupport>
|
||||
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||
mSupport.onReadRemoteRssi(gatt, rssi, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
mSupport.onMtuChanged(gatt, mtu, status);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
@ -532,6 +532,19 @@ public final class BtLEQueue {
|
||||
checkWaitingCharacteristic(characteristic, status);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
super.onMtuChanged(gatt, mtu, status);
|
||||
|
||||
if(getCallbackToUse() != null){
|
||||
getCallbackToUse().onMtuChanged(gatt, mtu, status);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic,
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -0,0 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.btle.actions;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
|
||||
|
||||
public class RequestMtuAction extends BtLEAction {
|
||||
private int mtu;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public RequestMtuAction(int mtu) {
|
||||
super(null);
|
||||
this.mtu = mtu;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean expectsResult() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean run(BluetoothGatt gatt) {
|
||||
return gatt.requestMtu(this.mtu);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
import java.util.zip.Checksum;
|
||||
|
||||
public final class CRC32C implements Checksum {
|
||||
private static final int[] crcTable = new int[]{0, (int) 4067132163L, (int) 3778769143L, 324072436, (int) 3348797215L, 904991772, 648144872, (int) 3570033899L, (int) 2329499855L, 2024987596, 1809983544, (int) 2575936315L, 1296289744, (int) 3207089363L, (int) 2893594407L, 1578318884, 274646895, (int) 3795141740L, (int) 4049975192L, 51262619, (int) 3619967088L, 632279923, 922689671, (int) 3298075524L, (int) 2592579488L, 1760304291, 2075979607, (int) 2312596564L, 1562183871, (int) 2943781820L, (int) 3156637768L, 1313733451, 549293790, (int) 3537243613L, (int) 3246849577L, 871202090, (int) 3878099393L, 357341890, 102525238, (int) 4101499445L, (int) 2858735121L, 1477399826, 1264559846, (int) 3107202533L, 1845379342, (int) 2677391885L, (int) 2361733625L, 2125378298, 820201905, (int) 3263744690L, (int) 3520608582L, 598981189, (int) 4151959214L, 85089709, 373468761, (int) 3827903834L, (int) 3124367742L, 1213305469, 1526817161, (int) 2842354314L, 2107672161, (int) 2412447074L, (int) 2627466902L, 1861252501, 1098587580, (int) 3004210879L, (int) 2688576843L, 1378610760, (int) 2262928035L, 1955203488, 1742404180, (int) 2511436119L, (int) 3416409459L, 969524848, 714683780, (int) 3639785095L, 205050476, (int) 4266873199L, (int) 3976438427L, 526918040, 1361435347, (int) 2739821008L, (int) 2954799652L, 1114974503, (int) 2529119692L, 1691668175, 2005155131, (int) 2247081528L, (int) 3690758684L, 697762079, 986182379, (int) 3366744552L, 476452099, (int) 3993867776L, (int) 4250756596L, 255256311, 1640403810, (int) 2477592673L, (int) 2164122517L, 1922457750, (int) 2791048317L, 1412925310, 1197962378, (int) 3037525897L, (int) 3944729517L, 427051182, 170179418, (int) 4165941337L, 746937522, (int) 3740196785L, (int) 3451792453L, 1070968646, 1905808397, (int) 2213795598L, (int) 2426610938L, 1657317369, (int) 3053634322L, 1147748369, 1463399397, (int) 2773627110L, (int) 4215344322L, 153784257, 444234805, (int) 3893493558L, 1021025245, (int) 3467647198L, (int) 3722505002L, 797665321, (int) 2197175160L, 1889384571, 1674398607, (int) 2443626636L, 1164749927, (int) 3070701412L, (int) 2757221520L, 1446797203, 137323447, (int) 4198817972L, (int) 3910406976L, 461344835, (int) 3484808360L, 1037989803, 781091935, (int) 3705997148L, (int) 2460548119L, 1623424788, 1939049696, (int) 2180517859L, 1429367560, (int) 2807687179L, (int) 3020495871L, 1180866812, 410100952, (int) 3927582683L, (int) 4182430767L, 186734380, (int) 3756733383L, 763408580, 1053836080, (int) 3434856499L, (int) 2722870694L, 1344288421, 1131464017, (int) 2971354706L, 1708204729, (int) 2545590714L, (int) 2229949006L, 1988219213, 680717673, (int) 3673779818L, (int) 3383336350L, 1002577565, (int) 4010310262L, 493091189, 238226049, (int) 4233660802L, (int) 2987750089L, 1082061258, 1395524158, (int) 2705686845L, 1972364758, (int) 2279892693L, (int) 2494862625L, 1725896226, 952904198, (int) 3399985413L, (int) 3656866545L, 731699698, (int) 4283874585L, 222117402, 510512622, (int) 3959836397L, (int) 3280807620L, 837199303, 582374963, (int) 3504198960L, 68661723, (int) 4135334616L, (int) 3844915500L, 390545967, 1230274059, (int) 3141532936L, (int) 2825850620L, 1510247935, (int) 2395924756L, 2091215383, 1878366691, (int) 2644384480L, (int) 3553878443L, 565732008, 854102364, (int) 3229815391L, 340358836, (int) 3861050807L, (int) 4117890627L, 119113024, 1493875044, (int) 2875275879L, (int) 3090270611L, 1247431312, (int) 2660249211L, 1828433272, 2141937292, (int) 2378227087L, (int) 3811616794L, 291187481, 34330861, (int) 4032846830L, 615137029, (int) 3603020806L, (int) 3314634738L, 939183345, 1776939221, (int) 2609017814L, (int) 2295496738L, 2058945313, (int) 2926798794L, 1545135305, 1330124605, (int) 3173225534L, (int) 4084100981L, 17165430, 307568514, (int) 3762199681L, 888469610, (int) 3332340585L, (int) 3587147933L, 665062302, 2042050490, (int) 2346497209L, (int) 2559330125L, 1793573966, (int) 3190661285L, 1279665062, 1595330642, (int) 2910671697L};
|
||||
private int crc = 0xFFFFFFFF;
|
||||
|
||||
public long getValue() {
|
||||
return ~this.crc;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.crc = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
public void update(int data) {
|
||||
crc = crcTable[(data & 255 ^ crc) & 255] ^ crc >>> 8;
|
||||
}
|
||||
|
||||
public void update(byte[] data, int offset, int length) {
|
||||
for (int index = offset; index < length + offset; index++) {
|
||||
this.crc = crcTable[(this.crc ^ data[index]) & 255] ^ this.crc >>> 8;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(byte[] data){
|
||||
update(data, 0, data.length);
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
|
||||
class QHybridBaseSupport extends AbstractBTLEDeviceSupport {
|
||||
QHybridBaseSupport(Logger logger) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(UUID uuid, boolean start) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(UUID uuid) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppReorder(UUID[] uuids) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(int flags) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int integer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableHeartRateSleepSupport(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetHeartRateMeasurementInterval(int seconds) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(byte type, long id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,561 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoMaster;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCallback;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapterFactory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class QHybridSupport extends QHybridBaseSupport {
|
||||
public static final String QHYBRID_COMMAND_CONTROL = "qhybrid_command_control";
|
||||
public static final String QHYBRID_COMMAND_UNCONTROL = "qhybrid_command_uncontrol";
|
||||
public static final String QHYBRID_COMMAND_SET = "qhybrid_command_set";
|
||||
public static final String QHYBRID_COMMAND_VIBRATE = "qhybrid_command_vibrate";
|
||||
public static final String QHYBRID_COMMAND_UPDATE = "qhybrid_command_update";
|
||||
public static final String QHYBRID_COMMAND_UPDATE_TIMEZONE = "qhybrid_command_update_timezone";
|
||||
public static final String QHYBRID_COMMAND_NOTIFICATION = "qhybrid_command_notification";
|
||||
public static final String QHYBRID_COMMAND_UPDATE_SETTINGS = "nodomain.freeyourgadget.gadgetbridge.Q_UPDATE_SETTINGS";
|
||||
public static final String QHYBRID_COMMAND_OVERWRITE_BUTTONS = "nodomain.freeyourgadget.gadgetbridge.Q_OVERWRITE_BUTTONS";
|
||||
|
||||
private static final String QHYBRID_ACTION_SET_ACTIVITY_HAND = "nodomain.freeyourgadget.gadgetbridge.Q_SET_ACTIVITY_HAND";
|
||||
|
||||
public static final String QHYBRID_EVENT_SETTINGS_UPDATED = "nodomain.freeyourgadget.gadgetbridge.Q_SETTINGS_UPDATED";
|
||||
public static final String QHYBRID_EVENT_FILE_UPLOADED = "nodomain.freeyourgadget.gadgetbridge.Q_FILE_UPLOADED";
|
||||
public static final String QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED = "nodomain.freeyourgadget.gadgetbridge.Q_NOTIFICATION_CONFIG_CHANGED";
|
||||
|
||||
public static final String QHYBRID_EVENT_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_BUTTON_PRESSED";
|
||||
public static final String QHYBRID_EVENT_MULTI_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_MULTI_BUTTON_PRESSED";
|
||||
|
||||
public static final String ITEM_STEP_GOAL = "STEP_GOAL";
|
||||
public static final String ITEM_STEP_COUNT = "STEP_COUNT";
|
||||
public static final String ITEM_VIBRATION_STRENGTH = "VIBRATION_STRENGTH";
|
||||
public static final String ITEM_ACTIVITY_POINT = "ACTIVITY_POINT";
|
||||
public static final String ITEM_EXTENDED_VIBRATION_SUPPORT = "EXTENDED_VIBRATION";
|
||||
public static final String ITEM_HAS_ACTIVITY_HAND = "HAS_ACTIVITY_HAND";
|
||||
public static final String ITEM_USE_ACTIVITY_HAND = "USE_ACTIVITY_HAND";
|
||||
public static final String ITEM_LAST_HEARTBEAT = "LAST_HEARTBEAT";
|
||||
public static final String ITEM_TIMEZONE_OFFSET = "STEPTIMEZONE_OFFSET_COUNT";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(QHybridSupport.class);
|
||||
|
||||
private PackageConfigHelper helper;
|
||||
|
||||
private volatile boolean searchDevice = false;
|
||||
|
||||
private long timeOffset;
|
||||
|
||||
private boolean useActivityHand;
|
||||
|
||||
private WatchAdapter watchAdapter;
|
||||
|
||||
public QHybridSupport() {
|
||||
super(logger);
|
||||
addSupportedService(UUID.fromString("3dda0001-957f-7d4a-34a6-74696673696d"));
|
||||
addSupportedService(UUID.fromString("0000180a-0000-1000-8000-00805f9b34fb"));
|
||||
addSupportedService(UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"));
|
||||
addSupportedService(UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"));
|
||||
IntentFilter commandFilter = new IntentFilter(QHYBRID_COMMAND_CONTROL);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_UNCONTROL);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_SET);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_VIBRATE);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_UPDATE);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_UPDATE_TIMEZONE);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_NOTIFICATION);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_UPDATE_SETTINGS);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_OVERWRITE_BUTTONS);
|
||||
commandFilter.addAction(QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED);
|
||||
BroadcastReceiver commandReceiver = new BroadcastReceiver() {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Bundle extras = intent.getExtras();
|
||||
NotificationConfiguration config = extras == null ? null : (NotificationConfiguration) intent.getExtras().get("CONFIG");
|
||||
switch (intent.getAction()) {
|
||||
case QHYBRID_COMMAND_CONTROL: {
|
||||
log("sending control request");
|
||||
watchAdapter.requestHandsControl();
|
||||
if (config != null) {
|
||||
watchAdapter.setHands(config.getHour(), config.getMin());
|
||||
} else {
|
||||
watchAdapter.setHands((short) 0, (short) 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_UNCONTROL: {
|
||||
watchAdapter.releaseHandsControl();
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_SET: {
|
||||
watchAdapter.setHands(config.getHour(), config.getMin());
|
||||
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_VIBRATE: {
|
||||
watchAdapter.vibrate(config.getVibration());
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_NOTIFICATION: {
|
||||
watchAdapter.playNotification(config);
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_UPDATE: {
|
||||
loadTimeOffset();
|
||||
onSetTime();
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_UPDATE_TIMEZONE:{
|
||||
loadTimezoneOffset();
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_UPDATE_SETTINGS: {
|
||||
String newSetting = intent.getStringExtra("EXTRA_SETTING");
|
||||
switch (newSetting) {
|
||||
case ITEM_VIBRATION_STRENGTH: {
|
||||
watchAdapter.setVibrationStrength(Short.parseShort(gbDevice.getDeviceInfo(ITEM_VIBRATION_STRENGTH).getDetails()));
|
||||
break;
|
||||
}
|
||||
case ITEM_STEP_GOAL: {
|
||||
watchAdapter.setStepGoal(Integer.parseInt(gbDevice.getDeviceInfo(ITEM_STEP_GOAL).getDetails()));
|
||||
break;
|
||||
}
|
||||
case ITEM_USE_ACTIVITY_HAND: {
|
||||
QHybridSupport.this.useActivityHand = gbDevice.getDeviceInfo(ITEM_USE_ACTIVITY_HAND).getDetails().equals("true");
|
||||
GBApplication.getPrefs().getPreferences().edit().putBoolean("QHYBRID_USE_ACTIVITY_HAND", useActivityHand).apply();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(QHYBRID_EVENT_SETTINGS_UPDATED));
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_OVERWRITE_BUTTONS: {
|
||||
String buttonConfig = intent.getStringExtra(FossilWatchAdapter.ITEM_BUTTONS);
|
||||
watchAdapter.overwriteButtons(buttonConfig);
|
||||
break;
|
||||
}
|
||||
case QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED: {
|
||||
watchAdapter.syncNotificationSettings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
LocalBroadcastManager.getInstance(getContext()).registerReceiver(commandReceiver, commandFilter);
|
||||
|
||||
try {
|
||||
helper = new PackageConfigHelper(GBApplication.getContext());
|
||||
} catch (GBException e) {
|
||||
GB.log("error getting database", GB.ERROR, e);
|
||||
GB.toast("error getting database", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
try {
|
||||
throw e;
|
||||
} catch (GBException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
IntentFilter globalFilter = new IntentFilter();
|
||||
globalFilter.addAction(QHYBRID_ACTION_SET_ACTIVITY_HAND);
|
||||
BroadcastReceiver globalCommandReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
switch (intent.getAction()) {
|
||||
case QHYBRID_ACTION_SET_ACTIVITY_HAND: {
|
||||
try {
|
||||
String extra = String.valueOf(intent.getExtras().get("EXTRA_PROGRESS"));
|
||||
float progress = Float.parseFloat(extra);
|
||||
watchAdapter.setActivityHand(progress);
|
||||
|
||||
watchAdapter.playNotification(new NotificationConfiguration(
|
||||
(short) -1,
|
||||
(short) -1,
|
||||
(short) (progress * 180),
|
||||
PlayNotificationRequest.VibrationType.NO_VIBE
|
||||
));
|
||||
} catch (Exception e) {
|
||||
GB.log("wrong number format", GB.ERROR, e);
|
||||
logger.debug("trash extra should be number 0.0-1.0");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
GBApplication.getContext().registerReceiver(globalCommandReceiver, globalFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
super.onSetAlarms(alarms);
|
||||
if(this.watchAdapter == null){
|
||||
GB.toast("watch not connected", Toast.LENGTH_LONG, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
this.watchAdapter.onSetAlarms(alarms);
|
||||
}
|
||||
|
||||
private void loadTimeOffset() {
|
||||
timeOffset = getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIME_OFFSET", 0);
|
||||
}
|
||||
|
||||
private void loadTimezoneOffset(){
|
||||
short offset = (short) getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIMEZONE_OFFSET", 0);
|
||||
|
||||
this.watchAdapter.setTimezoneOffsetMinutes(offset);
|
||||
}
|
||||
|
||||
public long getTimeOffset(){
|
||||
return this.timeOffset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
|
||||
this.useActivityHand = GBApplication.getPrefs().getBoolean("QHYBRID_USE_ACTIVITY_HAND", false);
|
||||
getDevice().addDeviceInfo(new GenericItem(ITEM_USE_ACTIVITY_HAND, String.valueOf(this.useActivityHand)));
|
||||
|
||||
getDevice().setNotificationIconConnected(R.drawable.ic_notification_qhybrid);
|
||||
getDevice().setNotificationIconDisconnected(R.drawable.ic_notification_disconnected_qhybrid);
|
||||
|
||||
for (int i = 2; i <= 7; i++)
|
||||
builder.notify(getCharacteristic(UUID.fromString("3dda000" + i + "-957f-7d4a-34a6-74696673696d")), true);
|
||||
|
||||
builder
|
||||
.read(getCharacteristic(UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")))
|
||||
.read(getCharacteristic(UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")))
|
||||
.read(getCharacteristic(UUID.fromString("00002a24-0000-1000-8000-00805f9b34fb")))
|
||||
;
|
||||
|
||||
loadTimeOffset();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) {
|
||||
this.watchAdapter.onFetchActivityData();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
log("notif from " + notificationSpec.sourceAppId + " " + notificationSpec.sender + " " + notificationSpec.phoneNumber);
|
||||
//new Exception().printStackTrace();
|
||||
String packageName = notificationSpec.sourceName;
|
||||
|
||||
NotificationConfiguration config = null;
|
||||
try {
|
||||
config = helper.getNotificationConfiguration(packageName);
|
||||
} catch (GBException e) {
|
||||
GB.log("error getting notification configuration", GB.ERROR, e);
|
||||
GB.toast("error getting notification configuration", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
}
|
||||
if (config == null) return;
|
||||
|
||||
log("handling notification");
|
||||
|
||||
if (config.getRespectSilentMode()) {
|
||||
int mode = ((AudioManager) getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE)).getRingerMode();
|
||||
if (mode == AudioManager.RINGER_MODE_SILENT) return;
|
||||
}
|
||||
|
||||
boolean enforceActivityHandNotification = config.getHour() == -1 && config.getMin() == -1;
|
||||
|
||||
playNotification(config);
|
||||
|
||||
showNotificationsByAllActive(enforceActivityHandNotification);
|
||||
}
|
||||
|
||||
private void log(String message){
|
||||
logger.debug(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
super.onDeleteNotification(id);
|
||||
|
||||
showNotificationsByAllActive(true);
|
||||
}
|
||||
|
||||
private void showNotificationsByAllActive(boolean enforceByNotification) {
|
||||
if (!this.useActivityHand) return;
|
||||
double progress = calculateNotificationProgress();
|
||||
showNotificationCountOnActivityHand(progress);
|
||||
|
||||
if (enforceByNotification) {
|
||||
watchAdapter.playNotification(new NotificationConfiguration(
|
||||
(short) -1,
|
||||
(short) -1,
|
||||
(short) (progress * 180),
|
||||
PlayNotificationRequest.VibrationType.NO_VIBE
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public double calculateNotificationProgress() {
|
||||
HashMap<NotificationConfiguration, Boolean> configs = new HashMap<>(0);
|
||||
try {
|
||||
for (NotificationConfiguration config : helper.getNotificationConfigurations()) {
|
||||
configs.put(config, false);
|
||||
}
|
||||
} catch (GBException e) {
|
||||
GB.log("error getting notification configuration", GB.ERROR, e);
|
||||
|
||||
GB.toast("error getting notification configs", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
}
|
||||
|
||||
double notificationProgress = 0;
|
||||
|
||||
for (String notificationPackage : NotificationListener.notificationStack) {
|
||||
for (NotificationConfiguration notificationConfiguration : configs.keySet()) {
|
||||
if (configs.get(notificationConfiguration)) continue;
|
||||
if (notificationConfiguration.getPackageName().equals(notificationPackage)) {
|
||||
notificationProgress += 0.25;
|
||||
configs.put(notificationConfiguration, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notificationProgress;
|
||||
}
|
||||
|
||||
//TODO toggle "Notifications when screen on" options on this check
|
||||
private void showNotificationCountOnActivityHand(double progress) {
|
||||
if (useActivityHand) {
|
||||
watchAdapter.setActivityHand(progress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
super.onMtuChanged(gatt, mtu, status);
|
||||
if(watchAdapter == null) return;
|
||||
watchAdapter.onMtuChanged(gatt, mtu, status);
|
||||
}
|
||||
|
||||
private void playNotification(NotificationConfiguration config) {
|
||||
if (config.getMin() == -1 && config.getHour() == -1 && config.getVibration() == PlayNotificationRequest.VibrationType.NO_VIBE)
|
||||
return;
|
||||
watchAdapter.playNotification(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
watchAdapter.setTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
try {
|
||||
if (watchAdapter.supportsExtendedVibration()) {
|
||||
GB.toast("Device does not support brr brr", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
} catch (UnsupportedOperationException e) {
|
||||
notifiyException(e);
|
||||
GB.toast("Please contact dakhnod@gmail.com\n", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
|
||||
if (start && searchDevice) return;
|
||||
|
||||
searchDevice = start;
|
||||
|
||||
if (start) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
int i = 0;
|
||||
while (searchDevice) {
|
||||
QHybridSupport.this.watchAdapter.vibrateFindMyDevicePattern();
|
||||
try {
|
||||
Thread.sleep(2500);
|
||||
} catch (InterruptedException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
watchAdapter.onTestNewFunction();
|
||||
}
|
||||
|
||||
private void backupFile(DownloadFileRequest request) {
|
||||
try {
|
||||
File f = new File("/sdcard/qFiles/");
|
||||
if (!f.exists()) f.mkdir();
|
||||
|
||||
File file = new File("/sdcard/qFiles/" + request.timeStamp);
|
||||
if (file.exists()) {
|
||||
throw new Exception("file " + file.getPath() + " exists");
|
||||
}
|
||||
logger.debug("Writing file " + file.getPath());
|
||||
file.createNewFile();
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(request.file);
|
||||
fos.close();
|
||||
logger.debug("file written.");
|
||||
|
||||
FileOutputStream fos2 = new FileOutputStream("/sdcard/qFiles/steps", true);
|
||||
fos2.write(("file " + request.timeStamp + " cut\n\n").getBytes());
|
||||
fos2.close();
|
||||
|
||||
//TODO file stuff
|
||||
// queueWrite(new EraseFileRequest((short) request.fileHandle));
|
||||
} catch (Exception e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
if (request.fileHandle > 257) {
|
||||
// queueWrite(new DownloadFileRequest((short) (request.fileHandle - 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleGBDeviceEvent(GBDeviceEventBatteryInfo deviceEvent){
|
||||
super.handleGBDeviceEvent(deviceEvent);
|
||||
}
|
||||
|
||||
public void notifiyException(Exception e){
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
e.printStackTrace(pw);
|
||||
String sStackTrace = sw.toString();
|
||||
|
||||
Notification.Builder notificationBuilder = null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationBuilder = new Notification.Builder(getContext(), GB.NOTIFICATION_CHANNEL_ID);
|
||||
} else {
|
||||
notificationBuilder = new Notification.Builder(getContext());
|
||||
}
|
||||
notificationBuilder
|
||||
.setContentTitle("Q Error")
|
||||
.setSmallIcon(R.drawable.ic_notification_qhybrid)
|
||||
.setContentText(sStackTrace)
|
||||
.setStyle(new Notification.BigTextStyle())
|
||||
.build();
|
||||
|
||||
Intent emailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts(
|
||||
"mailto","dakhnod@gmail.com", null));
|
||||
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Exception Report");
|
||||
emailIntent.putExtra(Intent.EXTRA_TEXT, "Here's a crash from your stupid app: \n\n" + sStackTrace);
|
||||
|
||||
PendingIntent intent = PendingIntent.getActivity(getContext(), 0, emailIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
|
||||
notificationBuilder.addAction(new Notification.Action(0, "report", intent));
|
||||
}else{
|
||||
notificationBuilder.addAction(0, "report", intent);
|
||||
}
|
||||
|
||||
((NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE)).notify((int) System.currentTimeMillis(), notificationBuilder.build());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
switch (characteristic.getUuid().toString()) {
|
||||
case "00002a26-0000-1000-8000-00805f9b34fb": {
|
||||
String firmwareVersion = characteristic.getStringValue(0);
|
||||
|
||||
gbDevice.setFirmwareVersion(firmwareVersion);
|
||||
this.watchAdapter = new WatchAdapterFactory().createWatchAdapter(firmwareVersion, this);
|
||||
this.watchAdapter.initialize();
|
||||
showNotificationsByAllActive(false);
|
||||
break;
|
||||
}
|
||||
case "00002a24-0000-1000-8000-00805f9b34fb": {
|
||||
String modelNumber = characteristic.getStringValue(0);
|
||||
gbDevice.setModel(modelNumber);
|
||||
gbDevice.setName(watchAdapter.getModelName());
|
||||
try {
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_EXTENDED_VIBRATION_SUPPORT, String.valueOf(watchAdapter.supportsExtendedVibration())));
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_HAS_ACTIVITY_HAND, String.valueOf(watchAdapter.supportsActivityHand())));
|
||||
} catch (UnsupportedOperationException e) {
|
||||
notifiyException(e);
|
||||
GB.toast("Please contact dakhnod@gmail.com\n", Toast.LENGTH_SHORT, GB.INFO);
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_EXTENDED_VIBRATION_SUPPORT, "false"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "00002a19-0000-1000-8000-00805f9b34fb": {
|
||||
short level = characteristic.getValue()[0];
|
||||
gbDevice.setBatteryLevel(level);
|
||||
|
||||
gbDevice.setBatteryThresholdPercent((short) 2);
|
||||
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.level = gbDevice.getBatteryLevel();
|
||||
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
handleGBDeviceEvent(batteryInfo);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic
|
||||
characteristic) {
|
||||
if(watchAdapter == null) return super.onCharacteristicChanged(gatt, characteristic);
|
||||
return watchAdapter.onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
|
||||
public abstract class WatchAdapter {
|
||||
private QHybridSupport deviceSupport;
|
||||
|
||||
public WatchAdapter(QHybridSupport deviceSupport){
|
||||
this.deviceSupport = deviceSupport;
|
||||
}
|
||||
|
||||
public QHybridSupport getDeviceSupport(){
|
||||
return this.deviceSupport;
|
||||
}
|
||||
|
||||
public Context getContext(){
|
||||
return getDeviceSupport().getContext();
|
||||
}
|
||||
|
||||
public abstract void initialize();
|
||||
|
||||
public abstract void playPairingAnimation();
|
||||
public abstract void playNotification(NotificationConfiguration config);
|
||||
public abstract void setTime();
|
||||
public abstract void overwriteButtons(String buttonConfigJson);
|
||||
public abstract void setActivityHand(double progress);
|
||||
public abstract void setHands(short hour, short minute);
|
||||
public abstract void vibrate(PlayNotificationRequest.VibrationType vibration);
|
||||
public abstract void vibrateFindMyDevicePattern();
|
||||
public abstract void requestHandsControl();
|
||||
public abstract void releaseHandsControl();
|
||||
public abstract void setStepGoal(int stepGoal);
|
||||
public abstract void setVibrationStrength(short strength);
|
||||
public abstract void syncNotificationSettings();
|
||||
public abstract void onTestNewFunction();
|
||||
public abstract void setTimezoneOffsetMinutes(short offset);
|
||||
|
||||
public abstract boolean supportsFindDevice();
|
||||
public abstract boolean supportsExtendedVibration();
|
||||
public abstract boolean supportsActivityHand();
|
||||
|
||||
public String getModelName() {
|
||||
String modelNumber = getDeviceSupport().getDevice().getModel();
|
||||
switch (modelNumber) {
|
||||
case "HW.0.0":
|
||||
return "Q Commuter";
|
||||
case "HL.0.0":
|
||||
return "Q Activist";
|
||||
case "DN.1.0":
|
||||
return "Hybrid HR Collider";
|
||||
}
|
||||
return "unknwon Q";
|
||||
}
|
||||
|
||||
public abstract void onFetchActivityData();
|
||||
|
||||
public abstract void onSetAlarms(ArrayList<? extends Alarm> alarms);
|
||||
|
||||
public abstract boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic);
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status){};
|
||||
|
||||
|
||||
public String arrayToString(byte[] bytes) {
|
||||
if (bytes.length == 0) return "";
|
||||
StringBuilder s = new StringBuilder();
|
||||
final String chars = "0123456789ABCDEF";
|
||||
for (byte b : bytes) {
|
||||
s.append(chars.charAt((b >> 4) & 0xF)).append(chars.charAt(b & 0xF)).append(" ");
|
||||
}
|
||||
return s.substring(0, s.length() - 1) + "\n";
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.misfit.MisfitWatchAdapter;
|
||||
|
||||
public final class WatchAdapterFactory {
|
||||
public final WatchAdapter createWatchAdapter(String firmwareVersion, QHybridSupport deviceSupport){
|
||||
char major = firmwareVersion.charAt(6);
|
||||
switch (major){
|
||||
case '1': return new MisfitWatchAdapter(deviceSupport);
|
||||
case '2': return new FossilWatchAdapter(deviceSupport);
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Firmware " + firmwareVersion + " not supported");
|
||||
}
|
||||
}
|
@ -0,0 +1,595 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigFileBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.AlarmsSetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.AnimationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ReleaseHandsControlRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.RequestHandControlRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_GOAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_TIMEZONE_OFFSET;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_VIBRATION_STRENGTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_MULTI_BUTTON_PRESS;
|
||||
|
||||
public class FossilWatchAdapter extends WatchAdapter {
|
||||
private ArrayList<Request> requestQueue = new ArrayList<>();
|
||||
|
||||
private FossilRequest fossilRequest;
|
||||
|
||||
private int MTU = 23;
|
||||
|
||||
private final String ITEM_MTU = "MTU";
|
||||
static public final String ITEM_BUTTONS = "BUTTONS";
|
||||
|
||||
private final String CONFIG_ITEM_STEP_GOAL = "step_goal";
|
||||
private final String CONFIG_ITEM_VIBRATION_STRENGTH = "vibration_strength";
|
||||
private final String CONFIG_ITEM_TIMEZONE_OFFSET = "timezone_offset";
|
||||
public final String CONFIG_ITEM_BUTTONS = "buttons";
|
||||
|
||||
private int lastButtonIndex = -1;
|
||||
|
||||
Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
public FossilWatchAdapter(QHybridSupport deviceSupport) {
|
||||
super(deviceSupport);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
playPairingAnimation();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
queueWrite(new RequestMtuRequest(512), false);
|
||||
}
|
||||
// queueWrite(new FileCloseRequest((short) 0xFFFF));
|
||||
// queueWrite(new ConfigurationGetRequest(this), false);
|
||||
|
||||
syncConfiguration();
|
||||
|
||||
syncNotificationSettings();
|
||||
|
||||
syncButtonSettings();
|
||||
|
||||
/* queueWrite(new ButtonConfigurationGetRequest(this) {
|
||||
@Override
|
||||
public void onConfigurationsGet(ConfigPayload[] configs) {
|
||||
super.onConfigurationsGet(configs);
|
||||
|
||||
JSONArray buttons = new JSONArray();
|
||||
for (ConfigPayload payload : configs) buttons.put(String.valueOf(payload));
|
||||
String json = buttons.toString();
|
||||
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_BUTTONS, json));
|
||||
}
|
||||
}); */
|
||||
|
||||
queueWrite(new SetDeviceStateRequest(GBDevice.State.INITIALIZED), false);
|
||||
}
|
||||
|
||||
private void syncButtonSettings(){
|
||||
String buttonConfig = getDeviceSpecificPreferences().getString(CONFIG_ITEM_BUTTONS, null);
|
||||
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_BUTTONS, buttonConfig));
|
||||
overwriteButtons(buttonConfig);
|
||||
}
|
||||
|
||||
private SharedPreferences getDeviceSpecificPreferences(){
|
||||
return GBApplication.getDeviceSpecificSharedPrefs(
|
||||
getDeviceSupport().getDevice().getAddress()
|
||||
);
|
||||
}
|
||||
|
||||
private void syncConfiguration(){
|
||||
SharedPreferences preferences = getDeviceSpecificPreferences();
|
||||
|
||||
int stepGoal = preferences.getInt(CONFIG_ITEM_STEP_GOAL, 1000000);
|
||||
byte vibrationStrength = (byte) preferences.getInt(CONFIG_ITEM_VIBRATION_STRENGTH, 100);
|
||||
int timezoneOffset = preferences.getInt(CONFIG_ITEM_TIMEZONE_OFFSET, 0);
|
||||
|
||||
GBDevice device = getDeviceSupport().getDevice();
|
||||
|
||||
device.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(stepGoal)));
|
||||
device.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(vibrationStrength)));
|
||||
device.addDeviceInfo(new GenericItem(ITEM_TIMEZONE_OFFSET, String.valueOf(timezoneOffset)));
|
||||
|
||||
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[]{
|
||||
new ConfigurationPutRequest.DailyStepGoalConfigItem(stepGoal),
|
||||
new ConfigurationPutRequest.VibrationStrengthConfigItem(vibrationStrength),
|
||||
new ConfigurationPutRequest.TimezoneOffsetConfigItem((short) timezoneOffset)
|
||||
}, this));
|
||||
}
|
||||
|
||||
public int getMTU() {
|
||||
if (this.MTU < 0) throw new RuntimeException("MTU not configured");
|
||||
|
||||
return this.MTU;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playPairingAnimation() {
|
||||
queueWrite(new AnimationRequest(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playNotification(NotificationConfiguration config) {
|
||||
if (config.getPackageName() == null) {
|
||||
log("package name in notification not set");
|
||||
return;
|
||||
}
|
||||
queueWrite(new PlayNotificationRequest(config.getPackageName(), this), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTime() {
|
||||
long millis = System.currentTimeMillis();
|
||||
TimeZone zone = new GregorianCalendar().getTimeZone();
|
||||
|
||||
queueWrite(
|
||||
new ConfigurationPutRequest(
|
||||
new ConfigurationPutRequest.TimeConfigItem(
|
||||
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
|
||||
(short) (millis % 1000),
|
||||
(short) ((zone.getRawOffset() + (zone.inDaylightTime(new Date()) ? 1 : 0)) / 60000)
|
||||
),
|
||||
this), false
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwriteButtons(String jsonConfigString) {
|
||||
try {
|
||||
if(jsonConfigString == null) return;
|
||||
getDeviceSpecificPreferences()
|
||||
.edit()
|
||||
.putString(CONFIG_ITEM_BUTTONS, jsonConfigString)
|
||||
.apply();
|
||||
JSONArray buttonConfigJson = new JSONArray(jsonConfigString);
|
||||
// JSONArray buttonConfigJson = new JSONArray(getDeviceSupport().getDevice().getDeviceInfo(ITEM_BUTTONS).getDetails());
|
||||
|
||||
ConfigPayload[] payloads = new ConfigPayload[buttonConfigJson.length()];
|
||||
|
||||
for (int i = 0; i < buttonConfigJson.length(); i++) {
|
||||
try {
|
||||
payloads[i] = ConfigPayload.valueOf(buttonConfigJson.getString(i));
|
||||
} catch (IllegalArgumentException e) {
|
||||
payloads[i] = ConfigPayload.FORWARD_TO_PHONE;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigFileBuilder builder = new ConfigFileBuilder(payloads);
|
||||
|
||||
FilePutRequest fileUploadRequets = new FilePutRequest((short) 0x0600, builder.build(true), this) {
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
if (success)
|
||||
GB.toast("successfully overwritten button settings", Toast.LENGTH_SHORT, GB.INFO);
|
||||
else GB.toast("error overwriting button settings", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
};
|
||||
queueWrite(fileUploadRequets);
|
||||
} catch (JSONException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActivityHand(double progress) {
|
||||
queueWrite(new ConfigurationPutRequest(
|
||||
new ConfigurationPutRequest.CurrentStepCountConfigItem(Math.min(999999, (int) (1000000 * progress))),
|
||||
this
|
||||
), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHands(short hour, short minute) {
|
||||
queueWrite(new MoveHandsRequest(false, minute, hour, (short) -1), false);
|
||||
}
|
||||
|
||||
|
||||
public void vibrate(nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest.VibrationType vibration) {
|
||||
// queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest(vibration, -1, -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void vibrateFindMyDevicePattern() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void requestHandsControl() {
|
||||
queueWrite(new RequestHandControlRequest(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseHandsControl() {
|
||||
queueWrite(new ReleaseHandsControlRequest(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStepGoal(int stepGoal) {
|
||||
getDeviceSpecificPreferences()
|
||||
.edit()
|
||||
.putInt(CONFIG_ITEM_STEP_GOAL, stepGoal)
|
||||
.apply();
|
||||
|
||||
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.DailyStepGoalConfigItem(stepGoal), this) {
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
if (success)
|
||||
GB.toast("successfully updated step goal", Toast.LENGTH_SHORT, GB.INFO);
|
||||
else GB.toast("error updating step goal", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVibrationStrength(short strength) {
|
||||
getDeviceSpecificPreferences()
|
||||
.edit()
|
||||
.putInt(CONFIG_ITEM_VIBRATION_STRENGTH, (byte) strength)
|
||||
.apply();
|
||||
|
||||
ConfigurationPutRequest.ConfigItem vibrationItem = new ConfigurationPutRequest.VibrationStrengthConfigItem((byte) strength);
|
||||
|
||||
|
||||
queueWrite(
|
||||
new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[]{vibrationItem}, this) {
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
if (success)
|
||||
GB.toast("successfully updated vibration strength", Toast.LENGTH_SHORT, GB.INFO);
|
||||
else
|
||||
GB.toast("error updating vibration strength", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
}, false
|
||||
);
|
||||
// queueWrite(new FileVerifyRequest((short) 0x0800));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncNotificationSettings() {
|
||||
log("syncing notification settings...");
|
||||
try {
|
||||
PackageConfigHelper helper = new PackageConfigHelper(getContext());
|
||||
final ArrayList<NotificationConfiguration> configurations = helper.getNotificationConfigurations();
|
||||
if (configurations.size() == 1) configurations.add(configurations.get(0));
|
||||
queueWrite(new NotificationFilterPutRequest(configurations, FossilWatchAdapter.this) {
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
super.onFilePut(success);
|
||||
|
||||
if (!success) {
|
||||
GB.toast("error writing notification settings", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
|
||||
getDeviceSupport().getDevice().setState(GBDevice.State.NOT_CONNECTED);
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
getDeviceSupport().getDevice().setState(GBDevice.State.INITIALIZED);
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
}, false);
|
||||
} catch (GBException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
queueWrite(new FilePutRequest(
|
||||
(short) 0x0600,
|
||||
new byte[]{
|
||||
(byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x01, (byte) 0x24, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x30, (byte) 0x52, (byte) 0xFF, (byte) 0x26, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x09, (byte) 0x04, (byte) 0x01, (byte) 0x03, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x09, (byte) 0x04, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x24, (byte) 0x00, (byte) 0x00, (byte) 0x24, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x1F, (byte) 0xBE, (byte) 0xB4, (byte) 0x1B
|
||||
},
|
||||
this)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimezoneOffsetMinutes(short offset) {
|
||||
getDeviceSpecificPreferences()
|
||||
.edit()
|
||||
.putInt(CONFIG_ITEM_TIMEZONE_OFFSET, offset)
|
||||
.apply();
|
||||
|
||||
queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.TimezoneOffsetConfigItem(offset), this){
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
super.onFilePut(success);
|
||||
|
||||
if(success) GB.toast("successfully updated timezone", Toast.LENGTH_SHORT, GB.INFO);
|
||||
else GB.toast("error updating timezone", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsExtendedVibration() {
|
||||
String modelNumber = getDeviceSupport().getDevice().getModel();
|
||||
switch (modelNumber) {
|
||||
case "HW.0.0":
|
||||
return true;
|
||||
case "HL.0.0":
|
||||
return false;
|
||||
case "DN.1.0":
|
||||
return true;
|
||||
}
|
||||
throw new UnsupportedOperationException("model " + modelNumber + " not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityHand() {
|
||||
String modelNumber = getDeviceSupport().getDevice().getModel();
|
||||
switch (modelNumber) {
|
||||
case "HW.0.0":
|
||||
return true;
|
||||
case "HL.0.0":
|
||||
return false;
|
||||
case "DN.1.0":
|
||||
return false;
|
||||
}
|
||||
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchActivityData() {
|
||||
|
||||
// queueWrite(new ConfigurationPutRequest(new ConfigurationPutRequest.ConfigItem[0], this));
|
||||
setVibrationStrength((byte) 50);
|
||||
// queueWrite(new FileCloseRequest((short) 0x0800));
|
||||
// queueWrite(new ConfigurationGetRequest(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
// throw new RuntimeException("noope");
|
||||
ArrayList<nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm> activeAlarms = new ArrayList<>();
|
||||
for (Alarm alarm : alarms){
|
||||
if(!alarm.getEnabled()) continue;
|
||||
if(alarm.getRepetition() == 0){
|
||||
activeAlarms.add(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm(
|
||||
(byte) alarm.getMinute(),
|
||||
(byte) alarm.getHour(),
|
||||
false
|
||||
));
|
||||
continue;
|
||||
}
|
||||
int repitition = alarm.getRepetition();
|
||||
repitition = (repitition << 1) | ((repitition >> 6) & 1);
|
||||
activeAlarms.add(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm(
|
||||
(byte) alarm.getMinute(),
|
||||
(byte) alarm.getHour(),
|
||||
(byte) repitition
|
||||
));
|
||||
}
|
||||
queueWrite(new AlarmsSetRequest(activeAlarms.toArray(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm.Alarm[0]), this){
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
super.onFilePut(success);
|
||||
if(success) GB.toast("successfully set alarms", Toast.LENGTH_SHORT, GB.INFO);
|
||||
else GB.toast("error setting alarms", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
switch (characteristic.getUuid().toString()) {
|
||||
case "3dda0006-957f-7d4a-34a6-74696673696d": {
|
||||
handleBackgroundCharacteristic(characteristic);
|
||||
break;
|
||||
}
|
||||
case "3dda0002-957f-7d4a-34a6-74696673696d":
|
||||
case "3dda0004-957f-7d4a-34a6-74696673696d":
|
||||
case "3dda0003-957f-7d4a-34a6-74696673696d": {
|
||||
if (fossilRequest != null) {
|
||||
boolean requestFinished;
|
||||
try {
|
||||
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
|
||||
byte requestType = (byte) (characteristic.getValue()[0] & 0x0F);
|
||||
|
||||
if (requestType != 0x0A && requestType != fossilRequest.getType()) {
|
||||
// throw new RuntimeException("Answer type " + requestType + " does not match current request " + fossilRequest.getType());
|
||||
}
|
||||
}
|
||||
|
||||
fossilRequest.handleResponse(characteristic);
|
||||
requestFinished = fossilRequest.isFinished();
|
||||
} catch (RuntimeException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
getDeviceSupport().notifiyException(e);
|
||||
GB.toast(fossilRequest.getName() + " failed", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
requestFinished = true;
|
||||
}
|
||||
|
||||
if (requestFinished) {
|
||||
log(fossilRequest.getName() + " finished");
|
||||
fossilRequest = null;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
queueNextRequest();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleBackgroundCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
switch (value[1]) {
|
||||
case 2: {
|
||||
byte syncId = value[2];
|
||||
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(QHybridSupport.ITEM_LAST_HEARTBEAT, DateFormat.getTimeInstance().format(new Date())));
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
if (value.length != 12) {
|
||||
throw new RuntimeException("wrong button message");
|
||||
}
|
||||
int index = value[2] & 0xFF;
|
||||
int button = value[9] >> 4 & 0xFF;
|
||||
|
||||
if (index != this.lastButtonIndex) {
|
||||
lastButtonIndex = index;
|
||||
log("Button press on button " + button);
|
||||
|
||||
Intent i = new Intent(QHYBRID_EVENT_BUTTON_PRESS);
|
||||
i.putExtra("BUTTON", button);
|
||||
getContext().sendBroadcast(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 5: {
|
||||
if (value.length != 4) {
|
||||
throw new RuntimeException("wrong button message");
|
||||
}
|
||||
int action = value[3];
|
||||
|
||||
String actionString = "SINGLE";
|
||||
if(action == 3) actionString = "DOUBLE";
|
||||
else if(action == 4) actionString = "LONG";
|
||||
|
||||
// lastButtonIndex = index;
|
||||
log(actionString + " button press");
|
||||
|
||||
Intent i = new Intent(QHYBRID_EVENT_MULTI_BUTTON_PRESS);
|
||||
i.putExtra("ACTION", actionString);
|
||||
getContext().sendBroadcast(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
super.onMtuChanged(gatt, mtu, status);
|
||||
|
||||
log("MTU changed: " + mtu);
|
||||
|
||||
this.MTU = mtu;
|
||||
|
||||
getDeviceSupport().getDevice().addDeviceInfo(new GenericItem(ITEM_MTU, String.valueOf(mtu)));
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
|
||||
((RequestMtuRequest) fossilRequest).setFinished(true);
|
||||
queueNextRequest();
|
||||
}
|
||||
|
||||
public void queueWrite(RequestMtuRequest request, boolean priorise) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
new TransactionBuilder("requestMtu")
|
||||
.requestMtu(512)
|
||||
.queue(getDeviceSupport().getQueue());
|
||||
|
||||
this.fossilRequest = request;
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String message) {
|
||||
logger.debug(message);
|
||||
}
|
||||
|
||||
public void queueWrite(SetDeviceStateRequest request, boolean priorise) {
|
||||
if (fossilRequest != null && !fossilRequest.isFinished()) {
|
||||
log("queing request: " + request.getName());
|
||||
if (priorise) {
|
||||
requestQueue.add(0, request);
|
||||
} else {
|
||||
requestQueue.add(request);
|
||||
}
|
||||
return;
|
||||
}
|
||||
log("setting device state: " + request.getDeviceState());
|
||||
getDeviceSupport().getDevice().setState(request.getDeviceState());
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
queueNextRequest();
|
||||
}
|
||||
|
||||
public void queueWrite(FossilRequest request, boolean priorise) {
|
||||
if (fossilRequest != null && !fossilRequest.isFinished()) {
|
||||
log("queing request: " + request.getName());
|
||||
if (priorise) {
|
||||
requestQueue.add(0, request);
|
||||
} else {
|
||||
requestQueue.add(request);
|
||||
}
|
||||
return;
|
||||
}
|
||||
log("executing request: " + request.getName());
|
||||
this.fossilRequest = request;
|
||||
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
|
||||
}
|
||||
|
||||
public void queueWrite(Request request, boolean priorise) {
|
||||
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
|
||||
|
||||
queueNextRequest();
|
||||
}
|
||||
|
||||
void queueWrite(Request request) {
|
||||
if (request instanceof SetDeviceStateRequest)
|
||||
queueWrite((SetDeviceStateRequest) request, false);
|
||||
else if (request instanceof RequestMtuRequest)
|
||||
queueWrite((RequestMtuRequest) request, false);
|
||||
else if (request instanceof FossilRequest) queueWrite((FossilRequest) request, false);
|
||||
else queueWrite(request, false);
|
||||
}
|
||||
|
||||
private void queueNextRequest() {
|
||||
try {
|
||||
Request request = requestQueue.remove(0);
|
||||
queueWrite(request);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
log("requestsQueue empty");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,483 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Queue;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.WatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ActivityPointGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.AnimationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.BatteryLevelRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.DownloadFileRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.EraseFileRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.FileRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetCountdownSettingsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetCurrentStepCountRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetStepGoalRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GetVibrationStrengthRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.GoalTrackingGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ListFilesRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.MoveHandsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.OTAEnterRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.OTAEraseRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.ReleaseHandsControlRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.RequestHandControlRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetCurrentStepCountRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetStepGoalRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetTimeRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetVibrationStrengthRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.UploadFileRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.VibrateRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_ACTIVITY_POINT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_COUNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_STEP_GOAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.ITEM_VIBRATION_STRENGTH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport.QHYBRID_EVENT_FILE_UPLOADED;
|
||||
|
||||
public class MisfitWatchAdapter extends WatchAdapter {
|
||||
private int lastButtonIndex = -1;
|
||||
private final SparseArray<Request> responseFilters = new SparseArray<>();
|
||||
|
||||
private UploadFileRequest uploadFileRequest;
|
||||
private Request fileRequest = null;
|
||||
|
||||
private Queue<Request> requestQueue = new ArrayDeque<>();
|
||||
|
||||
Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
public MisfitWatchAdapter(QHybridSupport deviceSupport) {
|
||||
super(deviceSupport);
|
||||
|
||||
fillResponseList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
requestQueue.add(new GetStepGoalRequest());
|
||||
requestQueue.add(new GetVibrationStrengthRequest());
|
||||
requestQueue.add(new ActivityPointGetRequest());
|
||||
requestQueue.add(prepareSetTimeRequest());
|
||||
requestQueue.add(new AnimationRequest());
|
||||
requestQueue.add(new SetCurrentStepCountRequest((int) (999999 * getDeviceSupport().calculateNotificationProgress())));
|
||||
|
||||
queueWrite(new GetCurrentStepCountRequest());
|
||||
|
||||
getDeviceSupport().getDevice().setState(GBDevice.State.INITIALIZED);
|
||||
getDeviceSupport().getDevice().sendDeviceUpdateIntent(getContext());
|
||||
}
|
||||
|
||||
|
||||
private SetTimeRequest prepareSetTimeRequest() {
|
||||
long millis = System.currentTimeMillis();
|
||||
TimeZone zone = new GregorianCalendar().getTimeZone();
|
||||
return new SetTimeRequest(
|
||||
(int) (millis / 1000 + getDeviceSupport().getTimeOffset() * 60),
|
||||
(short) (millis % 1000),
|
||||
(short) ((zone.getRawOffset() + zone.getDSTSavings()) / 60000));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void playPairingAnimation() {
|
||||
queueWrite(new AnimationRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playNotification(NotificationConfiguration config) {
|
||||
queueWrite(new PlayNotificationRequest(
|
||||
config.getVibration(),
|
||||
config.getHour(),
|
||||
config.getMin(),
|
||||
config.getSubEye()
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTime() {
|
||||
queueWrite(prepareSetTimeRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
GBDevice gbDevice = getDeviceSupport().getDevice();
|
||||
switch (characteristic.getUuid().toString()) {
|
||||
case "3dda0004-957f-7d4a-34a6-74696673696d":
|
||||
case "3dda0003-957f-7d4a-34a6-74696673696d": {
|
||||
return handleFileDownloadCharacteristic(characteristic);
|
||||
}
|
||||
case "3dda0007-957f-7d4a-34a6-74696673696d": {
|
||||
return handleFileUploadCharacteristic(characteristic);
|
||||
}
|
||||
case "3dda0002-957f-7d4a-34a6-74696673696d": {
|
||||
return handleBasicCharacteristic(characteristic);
|
||||
}
|
||||
case "3dda0006-957f-7d4a-34a6-74696673696d": {
|
||||
return handleButtonCharacteristic(characteristic);
|
||||
}
|
||||
case "00002a19-0000-1000-8000-00805f9b34fb": {
|
||||
short level = characteristic.getValue()[0];
|
||||
gbDevice.setBatteryLevel(level);
|
||||
|
||||
gbDevice.setBatteryThresholdPercent((short) 2);
|
||||
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.level = gbDevice.getBatteryLevel();
|
||||
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
getDeviceSupport().handleGBDeviceEvent(batteryInfo);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
log("unknown shit on " + characteristic.getUuid().toString() + ": " + arrayToString(characteristic.getValue()));
|
||||
try {
|
||||
File charLog = new File("/sdcard/qFiles/charLog.txt");
|
||||
if (!charLog.exists()) {
|
||||
charLog.createNewFile();
|
||||
}
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(charLog, true);
|
||||
fos.write((new Date().toString() + ": " + characteristic.getUuid().toString() + ": " + arrayToString(characteristic.getValue())).getBytes());
|
||||
} catch (IOException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getDeviceSupport().onCharacteristicChanged(gatt, characteristic);
|
||||
}
|
||||
|
||||
private void fillResponseList() {
|
||||
Class<? extends Request>[] classes = new Class[]{
|
||||
BatteryLevelRequest.class,
|
||||
GetStepGoalRequest.class,
|
||||
GetVibrationStrengthRequest.class,
|
||||
GetCurrentStepCountRequest.class,
|
||||
OTAEnterRequest.class,
|
||||
GoalTrackingGetRequest.class,
|
||||
ActivityPointGetRequest.class,
|
||||
GetCountdownSettingsRequest.class
|
||||
};
|
||||
for (Class<? extends Request> c : classes) {
|
||||
try {
|
||||
c.getSuperclass().getDeclaredMethod("handleResponse", BluetoothGattCharacteristic.class);
|
||||
Request object = c.newInstance();
|
||||
byte[] sequence = object.getStartSequence();
|
||||
if (sequence.length > 1) {
|
||||
responseFilters.put((int) object.getStartSequence()[1], object);
|
||||
log("response filter " + object.getStartSequence()[1] + ": " + c.getSimpleName());
|
||||
}
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException e) {
|
||||
log("skipping class " + c.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleBasicCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] values = characteristic.getValue();
|
||||
Request request = resolveAnswer(characteristic);
|
||||
GBDevice gbDevice = getDeviceSupport().getDevice();
|
||||
|
||||
if (request == null) {
|
||||
StringBuilder valueString = new StringBuilder(String.valueOf(values[0]));
|
||||
for (int i = 1; i < characteristic.getValue().length; i++) {
|
||||
valueString.append(", ").append(values[i]);
|
||||
}
|
||||
log("unable to resolve " + characteristic.getUuid().toString() + ": " + valueString);
|
||||
return true;
|
||||
}
|
||||
log("response: " + request.getClass().getSimpleName());
|
||||
request.handleResponse(characteristic);
|
||||
|
||||
if (request instanceof GetStepGoalRequest) {
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(((GetStepGoalRequest) request).stepGoal)));
|
||||
} else if (request instanceof GetVibrationStrengthRequest) {
|
||||
int strength = ((GetVibrationStrengthRequest) request).strength;
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(strength)));
|
||||
} else if (request instanceof GetCurrentStepCountRequest) {
|
||||
int steps = ((GetCurrentStepCountRequest) request).steps;
|
||||
logger.debug("get current steps: " + steps);
|
||||
try {
|
||||
File f = new File("/sdcard/qFiles/");
|
||||
if (!f.exists()) f.mkdir();
|
||||
|
||||
File file = new File("/sdcard/qFiles/steps");
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
}
|
||||
logger.debug("Writing file " + file.getPath());
|
||||
FileOutputStream fos = new FileOutputStream(file, true);
|
||||
fos.write((System.currentTimeMillis() + ": " + steps + "\n").getBytes());
|
||||
fos.close();
|
||||
logger.debug("file written.");
|
||||
} catch (Exception e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
}
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_COUNT, String.valueOf(((GetCurrentStepCountRequest) request).steps)));
|
||||
} else if (request instanceof OTAEnterRequest) {
|
||||
if (((OTAEnterRequest) request).success) {
|
||||
fileRequest = new OTAEraseRequest(1024 << 16);
|
||||
queueWrite(fileRequest);
|
||||
}
|
||||
} else if (request instanceof ActivityPointGetRequest) {
|
||||
gbDevice.addDeviceInfo(new GenericItem(ITEM_ACTIVITY_POINT, String.valueOf(((ActivityPointGetRequest) request).activityPoint)));
|
||||
}
|
||||
try {
|
||||
queueWrite(requestQueue.remove());
|
||||
} catch (NoSuchElementException e) {
|
||||
}
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(DeviceManager.ACTION_DEVICES_CHANGED));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private Request resolveAnswer(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] values = characteristic.getValue();
|
||||
if (values[0] != 3) return null;
|
||||
return responseFilters.get(values[1]);
|
||||
}
|
||||
|
||||
private boolean handleFileDownloadCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
Request request;
|
||||
request = fileRequest;
|
||||
request.handleResponse(characteristic);
|
||||
if (request instanceof ListFilesRequest) {
|
||||
if (((ListFilesRequest) request).completed) {
|
||||
logger.debug("File count: " + ((ListFilesRequest) request).fileCount + " size: " + ((ListFilesRequest) request).size);
|
||||
if (((ListFilesRequest) request).fileCount == 0) return true;
|
||||
// queueWrite(new DownloadFileRequest((short) (256 + ((ListFilesRequest) request).fileCount)));
|
||||
}
|
||||
} else if (request instanceof DownloadFileRequest) {
|
||||
if (((FileRequest) request).completed) {
|
||||
logger.debug("file " + ((DownloadFileRequest) request).fileHandle + " completed: " + ((DownloadFileRequest) request).size);
|
||||
// backupFile((DownloadFileRequest) request);
|
||||
}
|
||||
} else if (request instanceof EraseFileRequest) {
|
||||
if (((EraseFileRequest) request).fileHandle > 257) {
|
||||
queueWrite(new DownloadFileRequest((short) (((EraseFileRequest) request).fileHandle - 1)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private boolean handleFileUploadCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
if (uploadFileRequest == null) {
|
||||
logger.debug("no uploadFileRequest to handle response");
|
||||
return true;
|
||||
}
|
||||
|
||||
uploadFileRequest.handleResponse(characteristic);
|
||||
|
||||
switch (uploadFileRequest.state) {
|
||||
case ERROR:
|
||||
Intent fileIntent = new Intent(QHYBRID_EVENT_FILE_UPLOADED);
|
||||
fileIntent.putExtra("EXTRA_ERROR", true);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(fileIntent);
|
||||
uploadFileRequest = null;
|
||||
break;
|
||||
case UPLOAD:
|
||||
for (byte[] packet : this.uploadFileRequest.packets) {
|
||||
new TransactionBuilder("File upload").write(characteristic, packet).queue(getDeviceSupport().getQueue());
|
||||
}
|
||||
break;
|
||||
case UPLOADED:
|
||||
fileIntent = new Intent(QHYBRID_EVENT_FILE_UPLOADED);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(fileIntent);
|
||||
uploadFileRequest = null;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleButtonCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length != 11) {
|
||||
logger.debug("wrong button message");
|
||||
return true;
|
||||
}
|
||||
int index = value[6] & 0xFF;
|
||||
int button = value[8] >> 4 & 0xFF;
|
||||
|
||||
if (index != this.lastButtonIndex) {
|
||||
lastButtonIndex = index;
|
||||
logger.debug("Button press on button " + button);
|
||||
|
||||
Intent i = new Intent(QHYBRID_EVENT_BUTTON_PRESS);
|
||||
i.putExtra("BUTTON", button);
|
||||
|
||||
//ByteBuffer buffer = ByteBuffer.allocate(16);
|
||||
//buffer.put(new byte[]{0x01, 0x00, 0x08});
|
||||
//buffer.put(value, 2, 8);
|
||||
//buffer.put(new byte[]{(byte)0xFF, 0x05, 0x00, 0x01, 0x00});
|
||||
|
||||
//FilePutRequest request = new FilePutRequest((short)0, buffer.array());
|
||||
//for(byte[] packet : request.packets){
|
||||
// new TransactionBuilder("File upload").write(getCharacteristic(UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d")), packet).queue(getQueue());
|
||||
//}
|
||||
|
||||
getContext().sendBroadcast(i);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void log(String message){
|
||||
logger.debug(message);
|
||||
}
|
||||
|
||||
public void setActivityHand(double progress) {
|
||||
queueWrite(new SetCurrentStepCountRequest(Math.min((int) (1000000 * progress), 999999)));
|
||||
}
|
||||
|
||||
|
||||
public void setHands(short hour, short minute) {
|
||||
queueWrite(new MoveHandsRequest(false, minute, hour, (short) -1));
|
||||
}
|
||||
|
||||
public void vibrate(PlayNotificationRequest.VibrationType vibration) {
|
||||
queueWrite(new PlayNotificationRequest(vibration, -1, -1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void vibrateFindMyDevicePattern() {
|
||||
queueWrite(new VibrateRequest(false, (short) 4, (short) 1));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void requestHandsControl() {
|
||||
queueWrite(new RequestHandControlRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseHandsControl() {
|
||||
queueWrite(new ReleaseHandsControlRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStepGoal(int stepGoal) {
|
||||
queueWrite(new SetStepGoalRequest(stepGoal));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVibrationStrength(short strength) {
|
||||
queueWrite(new SetVibrationStrengthRequest(strength));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncNotificationSettings() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimezoneOffsetMinutes(short offset) {
|
||||
GB.toast("old firmware does't support timezones", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return supportsExtendedVibration();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsExtendedVibration() {
|
||||
String modelNumber = getDeviceSupport().getDevice().getModel();
|
||||
switch (modelNumber) {
|
||||
case "HW.0.0":
|
||||
return true;
|
||||
case "HL.0.0":
|
||||
return false;
|
||||
case "DN.1.0":
|
||||
return true;
|
||||
}
|
||||
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityHand() {
|
||||
String modelNumber = getDeviceSupport().getDevice().getModel();
|
||||
switch (modelNumber) {
|
||||
case "HW.0.0":
|
||||
return true;
|
||||
case "HL.0.0":
|
||||
return false;
|
||||
case "DN.1.0":
|
||||
return false;
|
||||
}
|
||||
throw new UnsupportedOperationException("Model " + modelNumber + " not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchActivityData() {
|
||||
requestQueue.add(new BatteryLevelRequest());
|
||||
requestQueue.add(new GetCurrentStepCountRequest());
|
||||
// requestQueue.add(new ListFilesRequest());
|
||||
queueWrite(new ActivityPointGetRequest());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
GB.toast("alarms not supported with this firmware", Toast.LENGTH_LONG, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwriteButtons(String jsonConfigString) {
|
||||
uploadFileRequest = new UploadFileRequest((short) 0x0800, new byte[]{
|
||||
(byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00,
|
||||
(byte) 0x30, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x2E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01,
|
||||
(byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x8B, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01,
|
||||
(byte) 0x08, (byte) 0x01, (byte) 0x14, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xBF, (byte) 0xD5, (byte) 0x54, (byte) 0xD1,
|
||||
(byte) 0x00
|
||||
});
|
||||
queueWrite(uploadFileRequest);
|
||||
}
|
||||
|
||||
private void queueWrite(Request request) {
|
||||
new TransactionBuilder(request.getClass().getSimpleName()).write(getDeviceSupport().getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getDeviceSupport().getQueue());
|
||||
// if (request instanceof FileRequest) this.fileRequest = request;
|
||||
|
||||
if (!request.expectsResponse()) {
|
||||
try {
|
||||
queueWrite(requestQueue.remove());
|
||||
} catch (NoSuchElementException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
public class ConfigFileBuilder {
|
||||
private ConfigPayload[] configs;
|
||||
|
||||
public ConfigFileBuilder(ConfigPayload[] configs) {
|
||||
this.configs = configs;
|
||||
}
|
||||
|
||||
public byte[] build(boolean appendChecksum) {
|
||||
int payloadSize = 0;
|
||||
for (ConfigPayload payload : this.configs) {
|
||||
payloadSize += payload.getData().length;
|
||||
}
|
||||
|
||||
int headerSize = 0;
|
||||
for (ConfigPayload payload : this.configs) {
|
||||
headerSize += payload.getHeader().length + 3; // button + version + null;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(
|
||||
3 // version bytes
|
||||
+ 1 // header count byte
|
||||
+ headerSize
|
||||
+ 1 // payload count byte
|
||||
+ payloadSize
|
||||
+ 1 // customization count byte
|
||||
+ (appendChecksum ? 4 : 0) // checksum
|
||||
);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buffer.put(new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x00}); // version
|
||||
buffer.put((byte) this.configs.length);
|
||||
int buttonIndex = 0x00;
|
||||
for (ConfigPayload payload : configs) {
|
||||
buffer.put((byte) (buttonIndex += 0x10));
|
||||
buffer.put((byte) 0x01);
|
||||
buffer.put(payload.getHeader());
|
||||
buffer.put((byte) 0x00);
|
||||
}
|
||||
|
||||
ArrayList<ConfigPayload> distinctPayloads = new ArrayList<>(3);
|
||||
|
||||
// distinctPayloads.add(configs[0].getData());
|
||||
|
||||
compareLoop:
|
||||
for (int payloadIndex = 0; payloadIndex < configs.length; payloadIndex++) {
|
||||
for (int compareTo = 0; compareTo < distinctPayloads.size(); compareTo++) {
|
||||
if (configs[payloadIndex].equals(distinctPayloads.get(compareTo))) {
|
||||
continue compareLoop;
|
||||
}
|
||||
}
|
||||
distinctPayloads.add(configs[payloadIndex]);
|
||||
}
|
||||
|
||||
buffer.put((byte) distinctPayloads.size());
|
||||
for (ConfigPayload payload : distinctPayloads) {
|
||||
buffer.put(payload.getData());
|
||||
}
|
||||
|
||||
buffer.put((byte) 0x00);
|
||||
|
||||
ByteBuffer buffer2 = ByteBuffer.allocate(buffer.position() + (appendChecksum ? 4 : 0));
|
||||
buffer2.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer2.put(buffer.array(), 0, buffer.position());
|
||||
|
||||
if (!appendChecksum) return buffer2.array();
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(buffer.array(), 0, buffer.position());
|
||||
|
||||
buffer2.putInt((int) crc.getValue());
|
||||
|
||||
return buffer2.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum ConfigPayload {
|
||||
FORWARD_TO_PHONE(
|
||||
"forward to phone",
|
||||
new byte[]{(byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x0C, (byte) 0x2E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x0F, (byte) 0x00, (byte) 0x8B, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x08, (byte) 0x01, (byte) 0x14, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xBF, (byte) 0xD5, (byte) 0x54, (byte) 0xD1,}
|
||||
),
|
||||
FORWARD_TO_PHONE_MULTI(
|
||||
"forward to phone (multifunction)",
|
||||
new byte[]{(byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x63, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x44, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xCD, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB6, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x04, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB5, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x7B, (byte) 0x56, (byte) 0x4E, (byte) 0x97}
|
||||
),
|
||||
STOPWATCH(
|
||||
"stopwatch",
|
||||
new byte[]{(byte) 0x02, (byte) 0x01, (byte) 0x20, (byte) 0x01},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x07, (byte) 0x00, (byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x08, (byte) 0x00, (byte) 0x92, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x0F, (byte) 0xC0, (byte) 0x5F, (byte) 0x2A}
|
||||
),
|
||||
DATE(
|
||||
"show date",
|
||||
new byte[]{(byte) 0x01, (byte) 0x01, (byte) 0x14, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01 , (byte) 0x00 , (byte) 0x01 , (byte) 0x01 , (byte) 0x14 , (byte) 0x2D , (byte) 0x00 , (byte) 0x00 , (byte) 0x00 , (byte) 0x01 , (byte) 0x00 , (byte) 0x06 , (byte) 0x00 , (byte) 0x02 , (byte) 0x00 , (byte) 0x00 , (byte) 0x07 , (byte) 0x00 , (byte) 0x01 , (byte) 0x01 , (byte) 0x16 , (byte) 0x00 , (byte) 0x89 , (byte) 0x05 , (byte) 0x01 , (byte) 0x07 , (byte) 0xB0 , (byte) 0x00 , (byte) 0x00 , (byte) 0xB0 , (byte) 0x00 , (byte) 0x00 , (byte) 0xB0 , (byte) 0x00 , (byte) 0x00 , (byte) 0x08 , (byte) 0x01 , (byte) 0x50 , (byte) 0x00 , (byte) 0x01 , (byte) 0x00 , (byte) 0xD0 , (byte) 0x89 , (byte) 0xDE , (byte) 0x6E}
|
||||
),
|
||||
LAST_NOTIFICATION(
|
||||
"show last notification",
|
||||
new byte[]{(byte) 0x01, (byte) 0x01, (byte) 0x18, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x18, (byte) 0x2F, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x16, (byte) 0x00, (byte) 0x89, (byte) 0x05, (byte) 0x01, (byte) 0x07, (byte) 0xB0, (byte) 0x02, (byte) 0x00, (byte) 0xB0, (byte) 0x02, (byte) 0x00, (byte) 0xB0, (byte) 0x02, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x6B, (byte) 0x9D, (byte) 0x55, (byte) 0x3A}
|
||||
),
|
||||
SECOND_TIMEZONE(
|
||||
"show second timezone",
|
||||
new byte[]{0x01, (byte) 0x01, (byte) 0x16, (byte) 0x00},
|
||||
new byte[]{0x01, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x16, (byte) 0x2F, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x07, (byte) 0x02, (byte) 0x02, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x16, (byte) 0x00, (byte) 0x89, (byte) 0x05, (byte) 0x01, (byte) 0x07, (byte) 0xB0, (byte) 0x01, (byte) 0x00, (byte) 0xB0, (byte) 0x01, (byte) 0x00, (byte) 0xB0, (byte) 0x01, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x50, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x3D, (byte) 0x07, (byte) 0x28, (byte) 0x01}
|
||||
)
|
||||
/* PLAY_PAUSE(
|
||||
"play/pause music",
|
||||
new byte[]{(byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x06, (byte) 0x12, (byte) 0x63, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x44, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xCD, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB6, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x04, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xB5, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x7B, (byte) 0x56, (byte) 0x4E, (byte) 0x97}
|
||||
),
|
||||
VOLUME_UP(
|
||||
"music volume up",
|
||||
new byte[]{(byte) 0x01, (byte) 0x04, (byte) 0x12, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x12, (byte) 0x5E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x48, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xE9, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0B, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xE9, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x0A, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xC6, (byte) 0xB2, (byte) 0xCB, (byte) 0xAC}
|
||||
),
|
||||
VOLUME_DOWN(
|
||||
"music volume down",
|
||||
new byte[]{(byte) 0x01, (byte) 0x05, (byte) 0x12, (byte) 0x00},
|
||||
new byte[]{(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x12, (byte) 0x5E, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x05, (byte) 0x01, (byte) 0x1D, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0xF6, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x42, (byte) 0x02, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x43, (byte) 0x03, (byte) 0x00, (byte) 0x85, (byte) 0x01, (byte) 0x48, (byte) 0x04, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x1E, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x0D, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xEA, (byte) 0x00, (byte) 0x01, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x03, (byte) 0x0B, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0xEA, (byte) 0x00, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x01, (byte) 0x04, (byte) 0x0A, (byte) 0x00, (byte) 0x8C, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0xFE, (byte) 0x08, (byte) 0x00, (byte) 0x93, (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0xFA, (byte) 0x18, (byte) 0x49, (byte) 0x03}
|
||||
) */
|
||||
;
|
||||
private byte[] header, data;
|
||||
|
||||
static public ConfigPayload fromId(short id) throws RuntimeException{
|
||||
for(ConfigPayload payload : ConfigPayload.values()){
|
||||
ByteBuffer buffer = ByteBuffer.wrap(payload.header);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
if(id == buffer.getShort(1)) return payload;
|
||||
}
|
||||
|
||||
throw new RuntimeException("app " + id + " not found");
|
||||
}
|
||||
|
||||
public byte[] getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public boolean equals(ConfigPayload p1, ConfigPayload p2){
|
||||
return Arrays.equals(p1.getData(), p2.getData());
|
||||
}
|
||||
|
||||
private String description;
|
||||
|
||||
ConfigPayload(String description, byte[] header, byte[] data) {
|
||||
this.description = description;
|
||||
this.header = header;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class Request {
|
||||
protected byte[] data;
|
||||
private Logger logger = (Logger) LoggerFactory.getLogger(getName());
|
||||
//protected ByteBuffer buffer;
|
||||
|
||||
public Request(){
|
||||
this.data = getStartSequence();
|
||||
}
|
||||
|
||||
|
||||
public ByteBuffer createBuffer(){
|
||||
return createBuffer(getPayloadLength());
|
||||
}
|
||||
|
||||
public ByteBuffer createBuffer(int length){
|
||||
ByteBuffer buffer = ByteBuffer.allocate(length);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.put(getStartSequence());
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public byte[] getRequestData(){
|
||||
return data;
|
||||
}
|
||||
|
||||
public UUID getRequestUUID(){
|
||||
return UUID.fromString("3dda0002-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
public int getPayloadLength(){ return getStartSequence().length; }
|
||||
|
||||
public abstract byte[] getStartSequence();
|
||||
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic){};
|
||||
|
||||
public String getName(){
|
||||
Class thisClass = getClass();
|
||||
while(thisClass.isAnonymousClass()) thisClass = thisClass.getSuperclass();
|
||||
return thisClass.getSimpleName();
|
||||
}
|
||||
|
||||
protected void log(String message){
|
||||
logger.debug(message);
|
||||
}
|
||||
|
||||
public boolean isBasicRequest(){
|
||||
return this.getRequestUUID().toString().equals("3dda0002-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
public boolean expectsResponse(){
|
||||
return this.data[0] == 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public abstract class FossilRequest extends Request {
|
||||
public abstract boolean isFinished();
|
||||
|
||||
public byte getType(){
|
||||
return getStartSequence()[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class RequestMtuRequest extends FossilRequest {
|
||||
private int mtu;
|
||||
private boolean finished = false;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public RequestMtuRequest(int mtu) {
|
||||
this.mtu = mtu;
|
||||
}
|
||||
|
||||
public int getMtu() {
|
||||
return mtu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
public void setFinished(boolean finished) {
|
||||
this.finished = finished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class SetDeviceStateRequest extends FossilRequest {
|
||||
private GBDevice.State deviceState;
|
||||
|
||||
public SetDeviceStateRequest(GBDevice.State deviceState) {
|
||||
this.deviceState = deviceState;
|
||||
}
|
||||
|
||||
public GBDevice.State getDeviceState() {
|
||||
return deviceState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class Alarm {
|
||||
public final int WEEKDAY_SUNDAY = 0;
|
||||
public final int WEEKDAY_MONDAY = 1;
|
||||
public final int WEEKDAY_TUESDAY = 2;
|
||||
public final int WEEKDAY_THURSDAY = 3;
|
||||
public final int WEEKDAY_WEDNESDAY = 4;
|
||||
public final int WEEKDAY_FRIDAY = 5;
|
||||
public final int WEEKDAY_SATURDAY = 6;
|
||||
private byte days = 0;
|
||||
private byte minute, hour;
|
||||
private boolean repeat;
|
||||
|
||||
public Alarm(byte minute, byte hour){
|
||||
this.minute = minute;
|
||||
this.hour = hour;
|
||||
this.repeat = false;
|
||||
}
|
||||
|
||||
public Alarm(byte minute, byte hour, boolean repeat){
|
||||
this.minute = minute;
|
||||
this.hour = hour;
|
||||
this.repeat = repeat;
|
||||
}
|
||||
|
||||
public Alarm(byte minute, byte hour, byte days){
|
||||
this.minute = minute;
|
||||
this.hour = hour;
|
||||
this.repeat = true;
|
||||
this.days = days;
|
||||
}
|
||||
|
||||
public void setDayEnabled(int day, boolean enabled){
|
||||
if(enabled) this.days |= 1 << day;
|
||||
else this.days &= ~(1 << day);
|
||||
}
|
||||
|
||||
public byte[] getData(){
|
||||
byte first = (byte) 0xFF;
|
||||
if(repeat){
|
||||
first = (byte) (0x80 | this.days);
|
||||
}
|
||||
|
||||
byte second = (byte) this.minute;
|
||||
|
||||
if(repeat) second |= 0x80;
|
||||
|
||||
byte third = this.hour;
|
||||
|
||||
return new byte[]{first, second, third};
|
||||
}
|
||||
|
||||
static public Alarm fromBytes(byte[] bytes){
|
||||
if(bytes.length != 3) throw new RuntimeException("alarm bytes length must be 3");
|
||||
|
||||
byte days = bytes[0];
|
||||
|
||||
byte minutes = (byte)(bytes[1] & 0b01111111);
|
||||
boolean repeat = (bytes[1] & 0x80) == 0x80;
|
||||
|
||||
if(repeat) {
|
||||
return new Alarm(minutes, bytes[2], days);
|
||||
}
|
||||
return new Alarm(minutes, bytes[2]);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
String description = this.hour + ":" + this.minute + " ";
|
||||
if(repeat){
|
||||
String[] dayNames = new String[]{"sunday", "monday", "tuesday", "thursday", "wednesday", "friday", "saturday"};
|
||||
for(int i = WEEKDAY_SUNDAY; i <= WEEKDAY_SATURDAY; i++){
|
||||
if((days & 1 << i) != 0){
|
||||
description += dayNames[i] + " ";
|
||||
}
|
||||
}
|
||||
}else{
|
||||
description += "not repeating";
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
|
||||
|
||||
public class AlarmsGetRequest extends FileLookupAndGetRequest {
|
||||
public AlarmsGetRequest(FossilWatchAdapter adapter) {
|
||||
super((byte) 0x0A, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(fileData);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short handle = buffer.getShort(0);
|
||||
if(handle != (short) 0x0A00) throw new RuntimeException("wrong alarm handle");
|
||||
|
||||
int length = buffer.getInt(8) / 3;
|
||||
Alarm[] alarms = new Alarm[length];
|
||||
|
||||
for (int i = 0; i < length; i++){
|
||||
buffer.position(12 + i * 3);
|
||||
byte[] alarmBytes = new byte[]{
|
||||
buffer.get(),
|
||||
buffer.get(),
|
||||
buffer.get()
|
||||
};
|
||||
alarms[i] = Alarm.fromBytes(alarmBytes);
|
||||
}
|
||||
|
||||
this.handleAlarms(alarms);
|
||||
}
|
||||
|
||||
public void handleAlarms(Alarm[] alarms){
|
||||
Alarm[] alarms2 = new Alarm[alarms.length];
|
||||
|
||||
for(int i = 0; i < alarms.length; i++){
|
||||
alarms2[i] = Alarm.fromBytes(alarms[i].getData());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.alarm;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
|
||||
|
||||
public class AlarmsSetRequest extends FilePutRequest {
|
||||
public AlarmsSetRequest(Alarm[] alarms, FossilWatchAdapter adapter) {
|
||||
super((short) 0x0A00, createFileFromAlarms(alarms), adapter);
|
||||
}
|
||||
|
||||
static byte[] createFileFromAlarms(Alarm[] alarms){
|
||||
ByteBuffer buffer = ByteBuffer.allocate(alarms.length * 3);
|
||||
for(Alarm alarm : alarms) buffer.put(alarm.getData());
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.button;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
|
||||
|
||||
public class ButtonConfigurationGetRequest extends FileGetRequest {
|
||||
public ButtonConfigurationGetRequest(FossilWatchAdapter adapter) {
|
||||
super((short) 0x0600, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
log("fileData");
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(fileData);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short fileHandle = buffer.getShort(0);
|
||||
// TODO check file handle
|
||||
// if(fileData != )
|
||||
|
||||
byte count = buffer.get(15);
|
||||
|
||||
ConfigPayload[] configs = new ConfigPayload[count];
|
||||
|
||||
buffer.position(16);
|
||||
for(int i = 0; i < count; i++){
|
||||
int buttonIndex = buffer.get() >> 4;
|
||||
int entryCount = buffer.get();
|
||||
buffer.get();
|
||||
short appId = buffer.getShort();
|
||||
|
||||
buffer.position(buffer.position() + entryCount * 5 - 3);
|
||||
|
||||
try {
|
||||
configs[buttonIndex - 1] = ConfigPayload.fromId(appId);
|
||||
}catch (RuntimeException e){
|
||||
configs[buttonIndex - 1] = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.onConfigurationsGet(configs);
|
||||
}
|
||||
|
||||
public void onConfigurationsGet(ConfigPayload[] configs){}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupAndGetRequest;
|
||||
|
||||
public class ConfigurationGetRequest extends FileGetRequest {
|
||||
public ConfigurationGetRequest(FossilWatchAdapter adapter) {
|
||||
super((byte) 8, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
byte[] data = new byte[fileData.length - 12 - 4];
|
||||
|
||||
System.arraycopy(fileData, 12, data, 0, data.length);
|
||||
log("config file: " + getAdapter().arrayToString(fileData));
|
||||
log("config file: " + getAdapter().arrayToString(data));
|
||||
|
||||
GBDevice device = getAdapter().getDeviceSupport().getDevice();
|
||||
|
||||
ConfigurationPutRequest.ConfigItem[] items = ConfigurationPutRequest.parsePayload(data);
|
||||
for(ConfigurationPutRequest.ConfigItem item : items){
|
||||
if(item instanceof ConfigurationPutRequest.VibrationStrengthConfigItem){
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_VIBRATION_STRENGTH, String.valueOf(((ConfigurationPutRequest.VibrationStrengthConfigItem) item).getValue())));
|
||||
}else if(item instanceof ConfigurationPutRequest.DailyStepGoalConfigItem){
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_GOAL, String.valueOf(((ConfigurationPutRequest.DailyStepGoalConfigItem) item).getValue())));
|
||||
}else if(item instanceof ConfigurationPutRequest.CurrentStepCountConfigItem){
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_COUNT, String.valueOf(((ConfigurationPutRequest.CurrentStepCountConfigItem) item).getValue())));
|
||||
}else if(item instanceof ConfigurationPutRequest.TimezoneOffsetConfigItem) {
|
||||
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_TIMEZONE_OFFSET, String.valueOf(((ConfigurationPutRequest.TimezoneOffsetConfigItem) item).getValue())));
|
||||
}
|
||||
}
|
||||
|
||||
device.sendDeviceUpdateIntent(getAdapter().getContext());
|
||||
|
||||
handleConfigurationLoaded();
|
||||
}
|
||||
|
||||
public void handleConfigurationLoaded(){}
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileCloseAndPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class ConfigurationPutRequest extends FilePutRequest {
|
||||
private static HashMap<Short, Class<? extends ConfigItem>> itemsById = new HashMap<>();
|
||||
|
||||
static {
|
||||
itemsById.put((short)3, DailyStepGoalConfigItem.class);
|
||||
itemsById.put((short)10, VibrationStrengthConfigItem.class);
|
||||
itemsById.put((short)2, CurrentStepCountConfigItem.class);
|
||||
itemsById.put((short)3, DailyStepGoalConfigItem.class);
|
||||
itemsById.put((short)12, TimeConfigItem.class);
|
||||
}
|
||||
|
||||
static ConfigItem[] parsePayload(byte[] data) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ArrayList<ConfigItem> configItems = new ArrayList<>();
|
||||
|
||||
while(buffer.hasRemaining()){
|
||||
short id = buffer.getShort();
|
||||
byte length = buffer.get();
|
||||
byte[] payload = new byte[length];
|
||||
|
||||
for(int i = 0; i < length; i++){
|
||||
payload[i] = buffer.get();
|
||||
}
|
||||
|
||||
Class<? extends ConfigItem> configClass = itemsById.get(id);
|
||||
if(configClass == null){
|
||||
continue;
|
||||
}
|
||||
ConfigItem item = null;
|
||||
try {
|
||||
item = configClass.newInstance();
|
||||
} catch (IllegalAccessException | InstantiationException e) {
|
||||
GB.log("error", GB.ERROR, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
item.parseData(payload);
|
||||
|
||||
configItems.add(item);
|
||||
}
|
||||
|
||||
return configItems.toArray(new ConfigItem[0]);
|
||||
}
|
||||
|
||||
public ConfigurationPutRequest(ConfigItem item, FossilWatchAdapter adapter) {
|
||||
super((short) 0x0800, createFileContent(new ConfigItem[]{item}), adapter);
|
||||
}
|
||||
|
||||
public ConfigurationPutRequest(ConfigItem[] items, FossilWatchAdapter adapter) {
|
||||
super((short) 0x0800, createFileContent(items), adapter);
|
||||
}
|
||||
|
||||
private static byte[] createFileContent(ConfigItem[] items) {
|
||||
int overallSize = 0;
|
||||
for(ConfigItem item : items){
|
||||
overallSize += item.getItemSize() + 3;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(overallSize);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
for(ConfigItem item : items){
|
||||
buffer.putShort(item.getId());
|
||||
buffer.put((byte) item.getItemSize());
|
||||
buffer.put(item.getContent());
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public static abstract class ConfigItem {
|
||||
public abstract int getItemSize();
|
||||
|
||||
public abstract short getId();
|
||||
|
||||
public abstract byte[] getContent();
|
||||
|
||||
public abstract void parseData(byte[] data);
|
||||
}
|
||||
|
||||
static public class GenericConfigItem<T> extends ConfigItem {
|
||||
private T value;
|
||||
private short configId;
|
||||
|
||||
public GenericConfigItem(short configId, T value) {
|
||||
this.value = value;
|
||||
this.configId = configId;
|
||||
}
|
||||
|
||||
public T getValue(){
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemSize() {
|
||||
switch (value.getClass().getName()) {
|
||||
case "java.lang.Byte":
|
||||
return 1;
|
||||
case "java.lang.Short":
|
||||
return 2;
|
||||
case "java.lang.Integer":
|
||||
return 4;
|
||||
case "java.lang.Long":
|
||||
return 8;
|
||||
}
|
||||
throw new UnsupportedOperationException("config type " + value.getClass().getName() + " not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getId() {
|
||||
return this.configId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(getItemSize());
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
switch (value.getClass().getName()) {
|
||||
case "java.lang.Byte": {
|
||||
buffer.put((Byte) this.value);
|
||||
break;
|
||||
}
|
||||
case "java.lang.Integer": {
|
||||
buffer.putInt((Integer) this.value);
|
||||
break;
|
||||
}
|
||||
case "java.lang.Long": {
|
||||
buffer.putLong((Long) this.value);
|
||||
break;
|
||||
}
|
||||
case "java.lang.Short": {
|
||||
buffer.putShort((Short) this.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(byte[] data) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
switch (data.length){
|
||||
case 1:{
|
||||
this.value = (T) (Byte) buffer.get();
|
||||
break;
|
||||
}
|
||||
case 2:{
|
||||
this.value = (T) (Short) buffer.getShort();
|
||||
break;
|
||||
}
|
||||
case 4:{
|
||||
this.value = (T) (Integer) buffer.getInt();
|
||||
break;
|
||||
}
|
||||
case 8:{
|
||||
this.value = (T) (Long) buffer.getLong();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static public class DailyStepGoalConfigItem extends GenericConfigItem<Integer> {
|
||||
public DailyStepGoalConfigItem(){
|
||||
this(-1);
|
||||
}
|
||||
|
||||
public DailyStepGoalConfigItem(int value) {
|
||||
super((short) 3, value);
|
||||
}
|
||||
}
|
||||
|
||||
static public class TimezoneOffsetConfigItem extends GenericConfigItem<Short> {
|
||||
public TimezoneOffsetConfigItem(Short value) {
|
||||
super((short) 17, value);
|
||||
}
|
||||
}
|
||||
|
||||
static public class VibrationStrengthConfigItem extends GenericConfigItem<Byte> {
|
||||
public VibrationStrengthConfigItem(){
|
||||
this((byte) -1);
|
||||
}
|
||||
|
||||
public VibrationStrengthConfigItem(Byte value) {
|
||||
super((short) 10, value);
|
||||
}
|
||||
}
|
||||
|
||||
static public class CurrentStepCountConfigItem extends GenericConfigItem<Integer> {
|
||||
public CurrentStepCountConfigItem(){
|
||||
this(-1);
|
||||
}
|
||||
|
||||
public CurrentStepCountConfigItem(Integer value) {
|
||||
super((short) 2, value);
|
||||
}
|
||||
}
|
||||
|
||||
static public class TimeConfigItem extends ConfigItem {
|
||||
private int epochSeconds;
|
||||
private short millis, offsetMinutes;
|
||||
|
||||
public TimeConfigItem(){
|
||||
this(-1, (short) -1, (short) -1);
|
||||
}
|
||||
|
||||
public TimeConfigItem(int epochSeconds, short millis, short offsetMinutes) {
|
||||
this.epochSeconds = epochSeconds;
|
||||
this.millis = millis;
|
||||
this.offsetMinutes = offsetMinutes;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getItemSize() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getId() {
|
||||
return (short) 12;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(getItemSize());
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.putInt(this.epochSeconds);
|
||||
buffer.putShort(millis);
|
||||
buffer.putShort(offsetMinutes);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(byte[] data) {
|
||||
if(data.length != 8) throw new RuntimeException("wrong data");
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
this.epochSeconds = buffer.getInt();
|
||||
this.millis = buffer.getShort();
|
||||
this.offsetMinutes = buffer.getShort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.connection;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
|
||||
public class SetConnectionParametersRequest extends FossilRequest {
|
||||
private boolean finished = false;
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0002-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
this.finished = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{0x02, 0x09, 0x0C, 0x00, 0x0C, 0x00, 0x2D, 0x00, 0x58, 0x02};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBasicRequest() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class FileCloseAndPutRequest extends FileCloseRequest {
|
||||
FossilWatchAdapter adapter;
|
||||
byte[] data;
|
||||
|
||||
public FileCloseAndPutRequest(short fileHandle, byte[] data, FossilWatchAdapter adapter) {
|
||||
super(fileHandle);
|
||||
this.adapter = adapter;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepare() {
|
||||
super.onPrepare();
|
||||
adapter.queueWrite(new FilePutRequest(getHandle(), this.data, adapter) {
|
||||
@Override
|
||||
public void onFilePut(boolean success) {
|
||||
super.onFilePut(success);
|
||||
FileCloseAndPutRequest.this.onFilePut(success);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
public void onFilePut(boolean success){
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
|
||||
public class FileCloseRequest extends FossilRequest {
|
||||
private boolean isFinished = false;
|
||||
private short handle;
|
||||
|
||||
public FileCloseRequest(short fileHandle) {
|
||||
this.handle = fileHandle;
|
||||
ByteBuffer buffer = this.createBuffer();
|
||||
buffer.putShort(fileHandle);
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
public short getHandle() {
|
||||
return handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
|
||||
if(!characteristic.getUuid().toString().equals(this.getRequestUUID().toString())){
|
||||
throw new RuntimeException("wrong response UUID");
|
||||
}
|
||||
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
byte type = (byte)(value[0] & 0x0F);
|
||||
|
||||
if(type != 9) throw new RuntimeException("wrong response type");
|
||||
|
||||
if(value.length != 4) throw new RuntimeException("wrong response length");
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
if(this.handle != buffer.getShort(1)) throw new RuntimeException("wrong response handle");
|
||||
|
||||
byte status = buffer.get(3);
|
||||
|
||||
if(status != 0) throw new RuntimeException("wrong response status");
|
||||
|
||||
this.isFinished = true;
|
||||
|
||||
this.onPrepare();
|
||||
}
|
||||
|
||||
public void onPrepare(){}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{9};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished(){
|
||||
return this.isFinished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
|
||||
public class FileDeleteRequest extends FossilRequest {
|
||||
private boolean finished = false;
|
||||
private short handle;
|
||||
|
||||
public FileDeleteRequest(short handle) {
|
||||
this.handle = handle;
|
||||
|
||||
ByteBuffer buffer = createBuffer();
|
||||
|
||||
buffer.putShort(handle);
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
if(!characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d"))
|
||||
throw new RuntimeException("wrong response UUID");
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
if(value.length != 4) throw new RuntimeException("wrong response length");
|
||||
|
||||
if(value[0] != (byte) 0x8B) throw new RuntimeException("wrong response start");
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
if(buffer.getShort(1) != this.handle) throw new RuntimeException("wrong response handle");
|
||||
|
||||
if(buffer.get(3) != 0) throw new RuntimeException("wrong response status: " + buffer.get(3));
|
||||
|
||||
this.finished = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{(byte) 0x0B};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
|
||||
public abstract class FileGetRequest extends FossilRequest {
|
||||
private short handle;
|
||||
private FossilWatchAdapter adapter;
|
||||
|
||||
private ByteBuffer fileBuffer;
|
||||
|
||||
private byte[] fileData;
|
||||
|
||||
private boolean finished = false;
|
||||
|
||||
public FileGetRequest(short handle, FossilWatchAdapter adapter) {
|
||||
this.handle = handle;
|
||||
this.adapter = adapter;
|
||||
|
||||
this.data =
|
||||
createBuffer()
|
||||
.putShort(handle)
|
||||
.putInt(0)
|
||||
.putInt(0xFFFFFFFF)
|
||||
.array();
|
||||
}
|
||||
|
||||
public FossilWatchAdapter getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished(){
|
||||
return finished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
byte first = value[0];
|
||||
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
|
||||
if((first & 0x0F) == 1){
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short handle = buffer.getShort(1);
|
||||
int size = buffer.getInt(4);
|
||||
|
||||
byte status = buffer.get(3);
|
||||
|
||||
if(status != 0){
|
||||
throw new RuntimeException("FileGet error: " + status);
|
||||
}
|
||||
|
||||
if(this.handle != handle){
|
||||
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
log("file size: " + size);
|
||||
fileBuffer = ByteBuffer.allocate(size);
|
||||
}else if((first & 0x0F) == 8){
|
||||
this.finished = true;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short handle = buffer.getShort(1);
|
||||
if(this.handle != handle){
|
||||
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(this.fileData);
|
||||
|
||||
int crcExpected = buffer.getInt(8);
|
||||
|
||||
if((int) crc.getValue() != crcExpected){
|
||||
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
|
||||
this.handleFileData(this.fileData);
|
||||
}
|
||||
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
|
||||
fileBuffer.put(value, 1, value.length - 1);
|
||||
if((first & 0x80) == 0x80){
|
||||
this.fileData = fileBuffer.array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 11;
|
||||
}
|
||||
|
||||
abstract public void handleFileData(byte[] fileData);
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
|
||||
public abstract class FileLookupAndGetRequest extends FileLookupRequest {
|
||||
public FileLookupAndGetRequest(byte fileType, FossilWatchAdapter adapter) {
|
||||
super(fileType, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileLookup(short fileHandle){
|
||||
getAdapter().queueWrite(new FileGetRequest(getHandle(), getAdapter()) {
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
FileLookupAndGetRequest.this.handleFileData(fileData);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
abstract public void handleFileData(byte[] fileData);
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
|
||||
public class FileLookupRequest extends FossilRequest {
|
||||
private short handle = -1;
|
||||
private byte fileType;
|
||||
|
||||
private FossilWatchAdapter adapter;
|
||||
|
||||
private ByteBuffer fileBuffer;
|
||||
|
||||
private byte[] fileData;
|
||||
|
||||
protected boolean finished = false;
|
||||
|
||||
public FileLookupRequest(byte fileType, FossilWatchAdapter adapter) {
|
||||
this.fileType = fileType;
|
||||
this.adapter = adapter;
|
||||
|
||||
this.data =
|
||||
createBuffer()
|
||||
.put(fileType)
|
||||
.array();
|
||||
}
|
||||
|
||||
protected FossilWatchAdapter getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
public short getHandle() {
|
||||
if(!finished){
|
||||
throw new UnsupportedOperationException("File lookup not finished");
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished(){
|
||||
return finished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
byte first = value[0];
|
||||
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
|
||||
if((first & 0x0F) == 2){
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short handle = buffer.getShort(1);
|
||||
int size = buffer.getInt(4);
|
||||
|
||||
byte status = buffer.get(3);
|
||||
|
||||
if(status != 0){
|
||||
throw new RuntimeException("file lookup error: " + status);
|
||||
}
|
||||
|
||||
if(this.handle != handle){
|
||||
// throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
log("file size: " + size);
|
||||
fileBuffer = ByteBuffer.allocate(size);
|
||||
}else if((first & 0x0F) == 8){
|
||||
this.finished = true;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(this.fileData);
|
||||
|
||||
int crcExpected = buffer.getInt(8);
|
||||
|
||||
if((int) crc.getValue() != crcExpected){
|
||||
throw new RuntimeException("handle: " + handle + " expected: " + this.handle);
|
||||
}
|
||||
|
||||
ByteBuffer dataBuffer = ByteBuffer.wrap(fileData);
|
||||
dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
this.handle = dataBuffer.getShort(0);
|
||||
|
||||
this.handleFileLookup(this.handle);
|
||||
}
|
||||
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
|
||||
fileBuffer.put(value, 1, value.length - 1);
|
||||
if((first & 0x80) == 0x80){
|
||||
this.fileData = fileBuffer.array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleFileLookup(short fileHandle){}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, (byte) 0xFF};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.CRC32C;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class FilePutRequest extends FossilRequest {
|
||||
public enum UploadState {INITIALIZED, UPLOADING, CLOSING, UPLOADED}
|
||||
|
||||
public UploadState state;
|
||||
|
||||
public ArrayList<byte[]> packets = new ArrayList<>();
|
||||
|
||||
private short handle;
|
||||
|
||||
private FossilWatchAdapter adapter;
|
||||
|
||||
byte[] file;
|
||||
|
||||
int fullCRC;
|
||||
|
||||
public FilePutRequest(short handle, byte[] file, FossilWatchAdapter adapter) {
|
||||
this.handle = handle;
|
||||
this.adapter = adapter;
|
||||
|
||||
int fileLength = file.length + 16;
|
||||
ByteBuffer buffer = this.createBuffer();
|
||||
buffer.putShort(1, handle);
|
||||
buffer.putInt(3, 0);
|
||||
buffer.putInt(7, fileLength);
|
||||
buffer.putInt(11, fileLength);
|
||||
|
||||
this.data = buffer.array();
|
||||
|
||||
this.file = file;
|
||||
|
||||
state = UploadState.INITIALIZED;
|
||||
}
|
||||
|
||||
public short getHandle() {
|
||||
return handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if (characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")) {
|
||||
int responseType = value[0] & 0x0F;
|
||||
log("response: " + responseType);
|
||||
switch (responseType) {
|
||||
case 3: {
|
||||
if (value.length != 5 || (value[0] & 0x0F) != 3) {
|
||||
throw new RuntimeException("wrong answer header");
|
||||
}
|
||||
state = UploadState.UPLOADING;
|
||||
|
||||
TransactionBuilder transactionBuilder = new TransactionBuilder("file upload");
|
||||
BluetoothGattCharacteristic uploadCharacteristic = adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0004-957f-7d4a-34a6-74696673696d"));
|
||||
|
||||
this.prepareFilePackets(this.file);
|
||||
|
||||
for (byte[] packet : packets) {
|
||||
transactionBuilder.write(uploadCharacteristic, packet);
|
||||
}
|
||||
|
||||
transactionBuilder.queue(adapter.getDeviceSupport().getQueue());
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
if (value.length == 4) return;
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
short handle = buffer.getShort(1);
|
||||
int crc = buffer.getInt(8);
|
||||
byte status = value[3];
|
||||
|
||||
if (status != 0) {
|
||||
throw new RuntimeException("upload status: " + status);
|
||||
}
|
||||
|
||||
if (handle != this.handle) {
|
||||
throw new RuntimeException("wrong response handle");
|
||||
}
|
||||
|
||||
if (crc != this.fullCRC) {
|
||||
throw new RuntimeException("file upload exception: wrong crc");
|
||||
}
|
||||
|
||||
|
||||
ByteBuffer buffer2 = ByteBuffer.allocate(3);
|
||||
buffer2.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer2.put((byte) 4);
|
||||
buffer2.putShort(this.handle);
|
||||
|
||||
new TransactionBuilder("file close")
|
||||
.write(
|
||||
adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")),
|
||||
buffer2.array()
|
||||
)
|
||||
.queue(adapter.getDeviceSupport().getQueue());
|
||||
|
||||
this.state = UploadState.CLOSING;
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
if (value.length == 9) return;
|
||||
if (value.length != 4 || (value[0] & 0x0F) != 4) {
|
||||
throw new RuntimeException("wrong file closing header");
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short handle = buffer.getShort(1);
|
||||
|
||||
if (handle != this.handle) {
|
||||
onFilePut(false);
|
||||
throw new RuntimeException("wrong file closing handle");
|
||||
}
|
||||
|
||||
byte status = buffer.get(3);
|
||||
|
||||
if (status != 0) {
|
||||
onFilePut(false);
|
||||
throw new RuntimeException("wrong closing status: " + status);
|
||||
}
|
||||
|
||||
this.state = UploadState.UPLOADED;
|
||||
|
||||
onFilePut(true);
|
||||
|
||||
log("uploaded file");
|
||||
|
||||
break;
|
||||
}
|
||||
case 9: {
|
||||
this.onFilePut(false);
|
||||
throw new RuntimeException("file put timeout");
|
||||
/*timeout = true;
|
||||
ByteBuffer buffer2 = ByteBuffer.allocate(3);
|
||||
buffer2.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer2.put((byte) 4);
|
||||
buffer2.putShort(this.handle);
|
||||
|
||||
new TransactionBuilder("file close")
|
||||
.write(
|
||||
adapter.getDeviceSupport().getCharacteristic(UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d")),
|
||||
buffer2.array()
|
||||
)
|
||||
.queue(adapter.getDeviceSupport().getQueue());
|
||||
|
||||
this.state = UploadState.CLOSING;
|
||||
break;*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return this.state == UploadState.UPLOADED;
|
||||
}
|
||||
|
||||
private void prepareFilePackets(byte[] file) {
|
||||
int maxPacketSize = adapter.getMTU() - 4;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(file.length + 12 + 4);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buffer.putShort(handle);
|
||||
buffer.put((byte) 2);
|
||||
buffer.put((byte) 0);
|
||||
buffer.putInt(0);
|
||||
buffer.putInt(file.length);
|
||||
|
||||
buffer.put(file);
|
||||
|
||||
CRC32C crc = new CRC32C();
|
||||
|
||||
crc.update(file);
|
||||
buffer.putInt((int) crc.getValue());
|
||||
|
||||
byte[] data = buffer.array();
|
||||
|
||||
CRC32 fullCRC = new CRC32();
|
||||
|
||||
fullCRC.update(data);
|
||||
this.fullCRC = (int) fullCRC.getValue();
|
||||
|
||||
int packetCount = (int) Math.ceil(data.length / (float) maxPacketSize);
|
||||
|
||||
for (int i = 0; i < packetCount; i++) {
|
||||
int currentPacketLength = Math.min(maxPacketSize, data.length - i * maxPacketSize);
|
||||
byte[] packet = new byte[currentPacketLength + 1];
|
||||
packet[0] = (byte) i;
|
||||
System.arraycopy(data, i * maxPacketSize, packet, 1, currentPacketLength);
|
||||
|
||||
packets.add(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void onFilePut(boolean success) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{0x03};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 15;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
|
||||
|
||||
public class FileVerifyRequest extends FossilRequest {
|
||||
private boolean isFinished = false;
|
||||
private short handle;
|
||||
|
||||
public FileVerifyRequest(short fileHandle) {
|
||||
this.handle = fileHandle;
|
||||
ByteBuffer buffer = this.createBuffer();
|
||||
buffer.putShort(fileHandle);
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
public short getHandle() {
|
||||
return handle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
|
||||
if(!characteristic.getUuid().toString().equals(this.getRequestUUID().toString())){
|
||||
throw new RuntimeException("wrong response UUID");
|
||||
}
|
||||
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
byte type = (byte)(value[0] & 0x0F);
|
||||
|
||||
if(type == 0x0A) return;
|
||||
|
||||
if(type != 4) throw new RuntimeException("wrong response type");
|
||||
|
||||
if(value.length != 4) throw new RuntimeException("wrong response length");
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
if(this.handle != buffer.getShort(1)) throw new RuntimeException("wrong response handle");
|
||||
|
||||
byte status = buffer.get(3);
|
||||
|
||||
if(status != 0) throw new RuntimeException("wrong response status");
|
||||
|
||||
this.isFinished = true;
|
||||
|
||||
this.onPrepare();
|
||||
}
|
||||
|
||||
public void onPrepare(){}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{4};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished(){
|
||||
return this.isFinished;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.microapp;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public interface MicroAppCommand {
|
||||
byte[] getData();
|
||||
}
|
||||
|
||||
class StartCriticalCommand implements MicroAppCommand{
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return new byte[]{(byte) 0x03, (byte) 0x00};
|
||||
}
|
||||
}
|
||||
|
||||
class CloseCommand implements MicroAppCommand{
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return new byte[]{(byte) 0x01, (byte) 0x00};
|
||||
}
|
||||
}
|
||||
|
||||
class DelayCommand implements MicroAppCommand{
|
||||
private double delayInSeconds;
|
||||
|
||||
public DelayCommand(double delayInSeconds) {
|
||||
this.delayInSeconds = delayInSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return ByteBuffer.wrap(new byte[]{0x08, 0x01, 0x00, 0x00})
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
.putShort(2, (short)(delayInSeconds * 10f))
|
||||
.array();
|
||||
}
|
||||
}
|
||||
|
||||
enum VibrationType{
|
||||
NORMAL((byte) 0x04);
|
||||
|
||||
private byte value;
|
||||
|
||||
VibrationType(byte value){
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public byte getValue(){ return this.value; }
|
||||
}
|
||||
|
||||
class VibrateCommand implements MicroAppCommand{
|
||||
private VibrationType vibrationType;
|
||||
|
||||
public VibrateCommand(VibrationType vibrationType) {
|
||||
this.vibrationType = vibrationType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return ByteBuffer.wrap(new byte[]{(byte) 0x93, 0x00, 0x00})
|
||||
.put(2, vibrationType.getValue())
|
||||
.array();
|
||||
}
|
||||
}
|
||||
|
||||
enum MovingDirection{
|
||||
CLOCKWISE((byte) 0x00),
|
||||
COUNTER_CLOCKWISE((byte) 0x01),
|
||||
SHORTEST((byte) 0x02),
|
||||
;
|
||||
private byte value;
|
||||
|
||||
MovingDirection(byte value){ this.value = value; }
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
enum MovingSpeed{
|
||||
MAX((byte) 0x00),
|
||||
HALF((byte) 0x01),
|
||||
QUARTER((byte) 0x02),
|
||||
EIGHTH((byte) 0x03),
|
||||
SIXTEENTH((byte) 0x04),
|
||||
;
|
||||
private byte value;
|
||||
|
||||
MovingSpeed(byte value){ this.value = value; }
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
class StreamCommand implements MicroAppCommand{
|
||||
private byte type;
|
||||
|
||||
public StreamCommand(byte type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return new byte[]{(byte) 0x8B, (byte) 0x00, this.type};
|
||||
}
|
||||
}
|
||||
|
||||
class RepeatStartCommand implements MicroAppCommand{
|
||||
private byte count;
|
||||
|
||||
public RepeatStartCommand(byte count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return new byte[]{(byte) 0x86, (byte) 0x00, this.count};
|
||||
}
|
||||
}
|
||||
|
||||
class RepeatStopCommand implements MicroAppCommand{
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return new byte[]{(byte) 0x07, (byte) 0x00};
|
||||
}
|
||||
}
|
||||
|
||||
class AnimationCommand implements MicroAppCommand{
|
||||
private short hour, minute;
|
||||
private MovingDirection direction;
|
||||
private MovingSpeed speed;
|
||||
private byte absoluteMovementFlag;
|
||||
|
||||
public AnimationCommand(short hour, short minute) {
|
||||
this.hour = hour;
|
||||
this.minute = minute;
|
||||
this.speed = MovingSpeed.MAX;
|
||||
this.direction = MovingDirection.SHORTEST;
|
||||
this.absoluteMovementFlag = 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getData() {
|
||||
return ByteBuffer.allocate(10)
|
||||
.order(ByteOrder.LITTLE_ENDIAN)
|
||||
.put((byte) 0x09)
|
||||
.put((byte) 0x04)
|
||||
.put((byte) 0x01)
|
||||
.put((byte) 0x03)
|
||||
.put((byte) ((direction.getValue() << 6) | (byte)(absoluteMovementFlag << 5) | this.speed.getValue()))
|
||||
.putShort(this.hour)
|
||||
.put((byte) ((direction.getValue() << 6) | (byte)(absoluteMovementFlag << 5) | this.speed.getValue()))
|
||||
.putShort(this.minute)
|
||||
.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.microapp;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
|
||||
|
||||
public class PlayCrazyShitRequest extends FilePutRequest {
|
||||
public PlayCrazyShitRequest(byte[] appData, FossilWatchAdapter adapter) {
|
||||
super((short) 0x0600, createPayload(appData), adapter);
|
||||
}
|
||||
|
||||
private static byte[] createPayload(byte[] appData) {
|
||||
List<MicroAppCommand> commands = new ArrayList<>();
|
||||
|
||||
commands.add(new StartCriticalCommand());
|
||||
// commands.add(new RepeatStartCommand((byte) 10));
|
||||
commands.add(new VibrateCommand(VibrationType.NORMAL));
|
||||
commands.add(new DelayCommand(1));
|
||||
// commands.add(new RepeatStopCommand());
|
||||
// commands.add(new StreamCommand((byte) 0b11111111));
|
||||
// commands.add(new AnimationCommand((short) 300, (short) 60));
|
||||
// commands.add(new DelayCommand(2));
|
||||
commands.add(new CloseCommand());
|
||||
|
||||
int length = 0;
|
||||
|
||||
for (MicroAppCommand command : commands) length += command.getData().length;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(
|
||||
3 /* magic bytes */
|
||||
+ 8 /* button header copy */
|
||||
+ 1 /* 0xFF */
|
||||
+ 2 /* payload length */
|
||||
+ length
|
||||
+ 4 /* crc */
|
||||
);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.put((byte) 0x01);
|
||||
buffer.put((byte) 0x00);
|
||||
buffer.put((byte) 0x08);
|
||||
buffer.put(appData, 3, 8);
|
||||
buffer.put((byte) 0xFF);
|
||||
buffer.putShort((short)(length + 3));
|
||||
|
||||
for(MicroAppCommand command : commands) buffer.put(command.getData());
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(buffer.array(), 0, buffer.position());
|
||||
|
||||
buffer.putInt((int) crc.getValue());
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.CRC32C;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest;
|
||||
|
||||
public class NotificationFilterGetRequest extends FileGetRequest {
|
||||
public NotificationFilterGetRequest(FossilWatchAdapter adapter) {
|
||||
super((short) 0x0C00, adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFileData(byte[] fileData) {
|
||||
log("handleFileData");
|
||||
ByteBuffer buffer = ByteBuffer.wrap(fileData);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
byte[] data = new byte[fileData.length - 12 - 4];
|
||||
|
||||
System.arraycopy(fileData, 12, data, 0, data.length);
|
||||
|
||||
CRC32C crc32c = new CRC32C();
|
||||
crc32c.update(data);
|
||||
|
||||
if((int) crc32c.getValue() != buffer.getInt(fileData.length - 4)){
|
||||
throw new RuntimeException("CRC invalid");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileCloseAndPutRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
|
||||
|
||||
public class NotificationFilterPutRequest extends FilePutRequest {
|
||||
public NotificationFilterPutRequest(NotificationConfiguration[] configs, FossilWatchAdapter adapter) {
|
||||
super((short) 0x0C00, createFile(configs), adapter);
|
||||
}
|
||||
|
||||
|
||||
public NotificationFilterPutRequest(ArrayList<NotificationConfiguration> configs, FossilWatchAdapter adapter) {
|
||||
super((short) 0x0C00, createFile(configs.toArray(new NotificationConfiguration[0])), adapter);
|
||||
}
|
||||
|
||||
private static byte[] createFile(NotificationConfiguration[] configs){
|
||||
ByteBuffer buffer = ByteBuffer.allocate(configs.length * 27);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
for(NotificationConfiguration config : configs){
|
||||
buffer.putShort((short) 25); //packet length
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(config.getPackageName().getBytes());
|
||||
|
||||
buffer.put(PacketID.PACKAGE_NAME_CRC.id);
|
||||
buffer.put((byte) 4);
|
||||
buffer.putInt((int) crc.getValue());
|
||||
|
||||
buffer.put(PacketID.GROUP_ID.id);
|
||||
buffer.put((byte) 1);
|
||||
buffer.put((byte) 2);
|
||||
|
||||
buffer.put(PacketID.PRIORITY.id);
|
||||
buffer.put((byte) 1);
|
||||
buffer.put((byte) 0xFF);
|
||||
|
||||
buffer.put(PacketID.MOVEMENT.id);
|
||||
buffer.put((byte) 8);
|
||||
buffer.putShort(config.getHour())
|
||||
.putShort(config.getMin())
|
||||
.putShort(config.getSubEye())
|
||||
.putShort((short) 5000);
|
||||
|
||||
buffer.put(PacketID.VIBRATION.id);
|
||||
buffer.put((byte) 1);
|
||||
buffer.put(config.getVibration().getValue());
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
enum PacketID{
|
||||
PACKAGE_NAME((byte) 1),
|
||||
SENDER_NAME((byte) 2),
|
||||
PACKAGE_NAME_CRC((byte) 4),
|
||||
GROUP_ID((byte) 128),
|
||||
APP_DISPLAY_NAME((byte) 129),
|
||||
PRIORITY((byte) 0xC1),
|
||||
MOVEMENT((byte) 0xC2),
|
||||
VIBRATION((byte) 0xC3);
|
||||
|
||||
byte id;
|
||||
|
||||
PacketID(byte id){
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
enum VibrationType{
|
||||
SINGLE_SHORT((byte) 5),
|
||||
DOUBLE_SHORT((byte) 6),
|
||||
TRIPLE_SHORT((byte) 7),
|
||||
SINGLE_LONG((byte) 8),
|
||||
SILENT((byte) 9);
|
||||
|
||||
byte id;
|
||||
VibrationType(byte id){
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
|
||||
|
||||
public class PlayNotificationRequest extends FilePutRequest {
|
||||
|
||||
public PlayNotificationRequest(String packageName, FossilWatchAdapter adapter) {
|
||||
// super((short) 0x0900, createFile("org.telegram.messenger", "org.telegram.messenger", "org.telegram.messenger"), adapter);
|
||||
super((short) 0x0900, createFile(packageName), adapter);
|
||||
}
|
||||
|
||||
private static byte[] createFile(String packageName){
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(packageName.getBytes());
|
||||
return createFile(packageName, packageName, packageName, (int)crc.getValue());
|
||||
}
|
||||
|
||||
private static byte[] createFile(String title, String sender, String message, int packageCrc) {
|
||||
// return new byte[]{(byte) 0x57, (byte) 0x00, (byte) 0x0A, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x17, (byte) 0x17, (byte) 0x17, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x49, (byte) 0x7B, (byte) 0x3B, (byte) 0x62, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00, (byte) 0x6F, (byte) 0x72, (byte) 0x67, (byte) 0x2E, (byte) 0x74, (byte) 0x65, (byte) 0x6C, (byte) 0x65, (byte) 0x67, (byte) 0x72, (byte) 0x61, (byte) 0x6D, (byte) 0x2E, (byte) 0x6D, (byte) 0x65, (byte) 0x73, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x67, (byte) 0x65, (byte) 0x72, (byte) 0x00};
|
||||
// gwb.k(var6, "ByteBuffer.allocate(10)");
|
||||
byte lengthBufferLength = (byte) 10;
|
||||
byte typeId = 3;
|
||||
byte flags = getFlags();
|
||||
byte uidLength = (byte) 4;
|
||||
byte appBundleCRCLength = (byte) 4;
|
||||
String nullTerminatedTitle = terminateNull(title);
|
||||
|
||||
Charset charsetUTF8 = Charset.forName("UTF-8");
|
||||
byte[] titleBytes = nullTerminatedTitle.getBytes(charsetUTF8);
|
||||
// gwb.k(var13, "(this as java.lang.String).getBytes(charset)");
|
||||
String nullTerminatedSender = terminateNull(sender);
|
||||
byte[] senderBytes = nullTerminatedSender.getBytes(charsetUTF8);
|
||||
// gwb.k(var15, "(this as java.lang.String).getBytes(charset)");
|
||||
String nullTerminatedMessage = terminateNull(message);
|
||||
byte[] messageBytes = nullTerminatedMessage.getBytes(charsetUTF8);
|
||||
// gwb.k(var17, "(this as java.lang.String).getBytes(charset)");
|
||||
|
||||
short mainBufferLength = (short) (lengthBufferLength + uidLength + appBundleCRCLength + titleBytes.length + senderBytes.length + messageBytes.length);
|
||||
|
||||
ByteBuffer lengthBuffer = ByteBuffer.allocate(lengthBufferLength);
|
||||
lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
lengthBuffer.putShort(mainBufferLength);
|
||||
lengthBuffer.put(lengthBufferLength);
|
||||
lengthBuffer.put(typeId);
|
||||
lengthBuffer.put(flags);
|
||||
lengthBuffer.put(uidLength);
|
||||
lengthBuffer.put(appBundleCRCLength);
|
||||
lengthBuffer.put((byte) titleBytes.length);
|
||||
lengthBuffer.put((byte) senderBytes.length);
|
||||
lengthBuffer.put((byte) messageBytes.length);
|
||||
|
||||
ByteBuffer mainBuffer = ByteBuffer.allocate(mainBufferLength);
|
||||
// gwb.k(var11, "ByteBuffer.allocate(totalLen.toInt())");
|
||||
mainBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
mainBuffer.put(lengthBuffer.array());
|
||||
|
||||
lengthBuffer = ByteBuffer.allocate(mainBufferLength - lengthBufferLength);
|
||||
// gwb.k(var6, "ByteBuffer.allocate(totalLen - headerLen)");
|
||||
lengthBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
lengthBuffer.putInt(0);
|
||||
lengthBuffer.putInt(packageCrc);
|
||||
lengthBuffer.put(titleBytes);
|
||||
lengthBuffer.put(senderBytes);
|
||||
lengthBuffer.put(messageBytes);
|
||||
mainBuffer.put(lengthBuffer.array());
|
||||
return mainBuffer.array();
|
||||
}
|
||||
|
||||
private static byte getFlags(){
|
||||
return (byte) 2;
|
||||
}
|
||||
|
||||
public static String terminateNull(String input){
|
||||
if(input.length() == 0){
|
||||
return new String(new byte[]{(byte) 0});
|
||||
}
|
||||
char lastChar = input.charAt(input.length() - 1);
|
||||
if(lastChar == 0) return input;
|
||||
|
||||
byte[] newArray = new byte[input.length() + 1];
|
||||
System.arraycopy(input.getBytes(), 0, newArray, 0, input.length());
|
||||
|
||||
newArray[newArray.length - 1] = 0;
|
||||
|
||||
return new String(newArray);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class ActivityPointGetRequest extends Request {
|
||||
public int activityPoint;
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 6};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
if (value.length != 6) return;
|
||||
|
||||
ByteBuffer wrap = ByteBuffer.wrap(value);
|
||||
wrap.order(ByteOrder.LITTLE_ENDIAN);
|
||||
activityPoint = wrap.getInt(2) >> 8;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class AnimationRequest extends Request {
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{(byte)2, (byte) -15, (byte)5};
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class BatteryLevelRequest extends Request {
|
||||
public short level = -1;
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 8};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
if (value.length >= 3) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
level = buffer.get(2);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
public class DownloadFileRequest extends FileRequest {
|
||||
ByteBuffer buffer = null;
|
||||
public byte[] file = null;
|
||||
public int fileHandle;
|
||||
public int size;
|
||||
public long timeStamp;
|
||||
|
||||
public DownloadFileRequest(short handle){
|
||||
init(handle, 0, 65535);
|
||||
}
|
||||
|
||||
public DownloadFileRequest(short handle, int offset, int length) {
|
||||
init(handle, offset, length);
|
||||
}
|
||||
|
||||
private void init(short handle, int offset, int length) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putShort(handle);
|
||||
buffer.putInt(offset);
|
||||
buffer.putInt(length);
|
||||
this.data = buffer.array();
|
||||
this.fileHandle = handle;
|
||||
this.timeStamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 11;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
byte[] data = characteristic.getValue();
|
||||
if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){
|
||||
if(buffer == null){
|
||||
buffer = ByteBuffer.allocate(4096);
|
||||
ByteBuffer buffer1 = ByteBuffer.wrap(data);
|
||||
buffer1.order(ByteOrder.LITTLE_ENDIAN);
|
||||
this.status = buffer1.get(3);
|
||||
short realHandle = buffer1.getShort(1);
|
||||
if(status != 0){
|
||||
log("wrong status: " + status);
|
||||
}else if(realHandle != fileHandle){
|
||||
log("wrong handle: " + realHandle);
|
||||
completed = true;
|
||||
}else{
|
||||
log("handle: " + realHandle);
|
||||
}
|
||||
}else{
|
||||
completed = true;
|
||||
}
|
||||
}else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){
|
||||
buffer.put(data, 1, data.length - 1);
|
||||
if((data[0] & -128) != 0){
|
||||
ByteBuffer buffer1 = ByteBuffer.allocate(buffer.position());
|
||||
buffer1.put(buffer.array(), 0, buffer.position());
|
||||
buffer1.order(ByteOrder.LITTLE_ENDIAN);
|
||||
file = buffer1.array();
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(file, 0, file.length - 4);
|
||||
this.size = file.length;
|
||||
log("file content: " + bytesToString(file));
|
||||
if(crc.getValue() != cutBits(buffer1.getInt(size - 4))){
|
||||
log("checksum invalid expected: " + buffer1.getInt(size - 4) + " actual: " + crc.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long cutBits(int value) {
|
||||
return value & 0b11111111111111111111111111111111L;
|
||||
|
||||
}
|
||||
|
||||
private String bytesToString(byte[] bytes){
|
||||
String s = "";
|
||||
String chars = "0123456789ABCDEF";
|
||||
for(byte b : bytes){
|
||||
s += chars.charAt((b >> 4) & 0xF);
|
||||
s += chars.charAt((b >> 0) & 0xF);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class EraseFileRequest extends FileRequest{
|
||||
public short fileHandle, deletedHandle;
|
||||
|
||||
public EraseFileRequest(short handle) {
|
||||
fileHandle = handle;
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putShort(handle);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
if(!characteristic.getUuid().toString().equals(getRequestUUID().toString())){
|
||||
log("wrong descriptor");
|
||||
return;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.wrap(characteristic.getValue());
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
deletedHandle = buffer.getShort(1);
|
||||
status = buffer.get(3);
|
||||
|
||||
log("file " + deletedHandle + " erased: " + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{3};
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class EventStreamRequest extends Request {
|
||||
public EventStreamRequest(short handle) {
|
||||
super();
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putShort(1, handle);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0006-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 3;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class FileRequest extends Request {
|
||||
public boolean completed = false;
|
||||
public int status;
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.util.Log;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GetCountdownSettingsRequest extends Request {
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 19, 1};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length != 14) {
|
||||
return;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
long startTime = j(buffer.getInt(3));
|
||||
long endTime = j(buffer.getInt(7));
|
||||
byte progress = buffer.get(13);
|
||||
short offset = buffer.getShort(11);
|
||||
|
||||
log("progress: " + progress);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static long j(final int n) {
|
||||
if (n < 0) {
|
||||
return 4294967296L + n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GetCurrentStepCountRequest extends Request {
|
||||
public int steps = -1;
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 17};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
if (value.length >= 6) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
steps = buffer.getInt(2);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GetStepGoalRequest extends Request {
|
||||
public int stepGoal = -1;
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
super.handleResponse(characteristic);
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length < 6) {
|
||||
return;
|
||||
} else {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
stepGoal = buffer.getInt(2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 16};
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GetTripleTapEnabledRequest extends Request {
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 7, 3};
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GetVibrationStrengthRequest extends Request {
|
||||
public int strength = -1;
|
||||
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length < 4) {
|
||||
return;
|
||||
} else {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
strength = (int) buffer.get(3);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{1, 15, 8};
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GoalTrackingGetRequest extends Request {
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if(value.length != 5) return;
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(value);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
short id = buffer.get(3);
|
||||
|
||||
boolean state = buffer.get(4) == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{01, 20, 01};
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class GoalTrackingSetRequest extends Request {
|
||||
public GoalTrackingSetRequest(int id, boolean state) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{02, 20, 01};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 5;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class ListFilesRequest extends FileRequest{
|
||||
public int fileCount = -1;
|
||||
public int size = 0;
|
||||
private ByteBuffer buffer = null;
|
||||
private int length = 0;
|
||||
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
String uuid = characteristic.getUuid().toString();
|
||||
byte[] value = characteristic.getValue();
|
||||
|
||||
if(uuid.equals("3dda0004-957f-7d4a-34a6-74696673696d")){
|
||||
buffer.put(value, 1, value.length - 1);
|
||||
length += value.length - 1;
|
||||
if((value[0] & -128) != 0){
|
||||
ByteBuffer buffer2 = ByteBuffer.wrap(buffer.array(), 0, length);
|
||||
buffer2.order(ByteOrder.LITTLE_ENDIAN);
|
||||
fileCount = buffer2.get(0);
|
||||
size = buffer2.getInt(1);
|
||||
}
|
||||
}else if(uuid.equals("3dda0003-957f-7d4a-34a6-74696673696d")){
|
||||
if(buffer == null){
|
||||
buffer = ByteBuffer.allocate(128);
|
||||
}else{
|
||||
completed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{(byte)5};
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class MoveHandsRequest extends Request {
|
||||
public MoveHandsRequest(boolean moveRelative, short degreesMin, short degreesHour, short degreesSub){
|
||||
init(moveRelative, degreesMin, degreesHour, degreesSub);
|
||||
}
|
||||
|
||||
private void init(boolean moveRelative, short degreesMin, short degreesHour, short degreesSub) {
|
||||
int count = 0;
|
||||
if(degreesHour != -1) count++;
|
||||
if(degreesMin != -1) count++;
|
||||
if(degreesSub != -1) count++;
|
||||
|
||||
ByteBuffer buffer = createBuffer(count * 5 + 5);
|
||||
buffer.put(moveRelative ? 1 : (byte)2);
|
||||
buffer.put((byte)count);
|
||||
|
||||
if(degreesHour > -1){
|
||||
buffer.put((byte)1);
|
||||
buffer.putShort(degreesHour);
|
||||
buffer.put((byte)3);
|
||||
buffer.put((byte)1);
|
||||
}
|
||||
if(degreesMin > -1){
|
||||
buffer.put((byte)2);
|
||||
buffer.putShort(degreesMin);
|
||||
buffer.put((byte)3);
|
||||
buffer.put((byte)1);
|
||||
}
|
||||
if(degreesSub > -1){
|
||||
buffer.put((byte)3);
|
||||
buffer.putShort(degreesSub);
|
||||
buffer.put((byte)3);
|
||||
buffer.put((byte)1);
|
||||
}
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 21, 3};
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class OTAEnterRequest extends Request {
|
||||
public boolean success = false;
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, -15, 8};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] result = characteristic.getValue();
|
||||
success = result[2] == 9;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class OTAEraseRequest extends Request {
|
||||
|
||||
public OTAEraseRequest(int pageOffset) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putShort((short) 23131);
|
||||
buffer.putInt(pageOffset);
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{18};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 7;
|
||||
}
|
||||
|
||||
|
||||
public UUID getRequestUUID(){
|
||||
return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] bytes = characteristic.getValue();
|
||||
final ByteBuffer wrap = ByteBuffer.wrap(bytes);
|
||||
wrap.order(ByteOrder.LITTLE_ENDIAN);
|
||||
short fileHandle = wrap.getShort(1);
|
||||
byte status = wrap.get(3);
|
||||
int sizeWritten = wrap.getInt(4);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class OTAResetRequest extends Request {
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, -15, 10};
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidParameterException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class PlayNotificationRequest extends Request {
|
||||
|
||||
public enum VibrationType{
|
||||
SINGLE_SHORT(3),
|
||||
DOUBLE_SHORT(2),
|
||||
TRIPLE_SHORT(1),
|
||||
SINGLE_NORMAL(5),
|
||||
DOUBLE_NORMAL(6),
|
||||
TRIPLE_NORMAL(7),
|
||||
SINGLE_LONG(8),
|
||||
NO_VIBE(9);
|
||||
|
||||
private byte value;
|
||||
|
||||
VibrationType(int value) {
|
||||
this.value = (byte)value;
|
||||
}
|
||||
|
||||
public static VibrationType fromValue(byte value){
|
||||
for(VibrationType type : values()){
|
||||
if(type.getValue() == value) return type;
|
||||
}
|
||||
throw new InvalidParameterException("vibration Type not supported");
|
||||
}
|
||||
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public PlayNotificationRequest(VibrationType vibrationType, int degreesHour, int degreesMins, int degreesActivityHand){
|
||||
int length = 0;
|
||||
if(degreesHour > -1) length++;
|
||||
if(degreesMins > -1) length++;
|
||||
if(degreesActivityHand > -1) length++;
|
||||
ByteBuffer buffer = createBuffer(length * 2 + 10);
|
||||
buffer.put(vibrationType.getValue());
|
||||
buffer.put((byte)5);
|
||||
buffer.put((byte)(length * 2 + 2));
|
||||
buffer.putShort((short)0);
|
||||
if(degreesHour > -1){
|
||||
buffer.putShort((short) ((degreesHour % 360) | (1 << 12)));
|
||||
}
|
||||
if(degreesMins > -1){
|
||||
buffer.putShort((short)((degreesMins % 360) | (2 << 12)));
|
||||
}
|
||||
if(degreesActivityHand > -1) {
|
||||
buffer.putShort((short)((degreesActivityHand % 360) | (3 << 12)));
|
||||
}
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public PlayNotificationRequest(VibrationType vibrationType, int degreesHour, int degreesMins){
|
||||
this(vibrationType, degreesHour, degreesMins, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 7, 15, 10, 1};
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class PutSettingsFileRequest extends Request {
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class ReleaseHandsControlRequest extends Request {
|
||||
public ReleaseHandsControlRequest(){
|
||||
super();
|
||||
init((short)0);
|
||||
}
|
||||
|
||||
private void init(short delayBeforeRelease) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putShort(3, delayBeforeRelease);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 21, 2};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 5;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class RequestHandControlRequest extends Request {
|
||||
public RequestHandControlRequest(byte priority, boolean moveCompleteNotify, boolean controlLostNOtify){
|
||||
super();
|
||||
init(priority, moveCompleteNotify, controlLostNOtify);
|
||||
}
|
||||
|
||||
public RequestHandControlRequest(){
|
||||
super();
|
||||
init((byte)1, false, false);
|
||||
}
|
||||
|
||||
private void init(byte priority, boolean moveCompleteNotify, boolean controlLostNOtify) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.put(priority);
|
||||
buffer.put(moveCompleteNotify ? (byte)1 : 0);
|
||||
buffer.put(controlLostNOtify ? (byte)1 : 0);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 21, 1};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 6;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetCountdownSettings extends Request {
|
||||
public SetCountdownSettings(int startTime, int endTime, short offset) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
|
||||
buffer.putInt(startTime);
|
||||
buffer.putInt(endTime);
|
||||
buffer.putShort(offset);
|
||||
// buff
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 19, 1};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 13;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetCurrentStepCountRequest extends Request {
|
||||
public SetCurrentStepCountRequest(int steps){
|
||||
super();
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putInt(steps);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 17};
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetCurrentTimeServiceRequest extends Request {
|
||||
public SetCurrentTimeServiceRequest(int timeStampSecs, short millis, short offsetInMins){
|
||||
super();
|
||||
init(timeStampSecs, millis, offsetInMins);
|
||||
}
|
||||
private void init(int timeStampSecs, short millis, short offsetInMins){
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putInt(timeStampSecs);
|
||||
buffer.putShort(millis);
|
||||
buffer.putShort(offsetInMins);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 11;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 18, 2};
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetStepGoalRequest extends Request {
|
||||
public SetStepGoalRequest(int goal){
|
||||
super();
|
||||
init(goal);
|
||||
}
|
||||
|
||||
private void init(int goal) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putInt(goal);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 16};
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetTimeRequest extends Request {
|
||||
public SetTimeRequest(int epochSeconds, short millis, short minutesOffset) {
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.putInt(epochSeconds);
|
||||
buffer.putShort(millis);
|
||||
buffer.putShort(minutesOffset);
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 4};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 10;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetTripleTapEnabledRequest extends Request {
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 7, 3, 1};
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SetVibrationStrengthRequest extends Request {
|
||||
public SetVibrationStrengthRequest(short strength){
|
||||
super();
|
||||
init(strength);
|
||||
}
|
||||
|
||||
private void init(int strength){
|
||||
ByteBuffer buffer = createBuffer();
|
||||
buffer.put((byte)strength);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 15, 8};
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class SettingsFilePutRequest extends Request {
|
||||
public int fileLength;
|
||||
public byte[] file;
|
||||
|
||||
public SettingsFilePutRequest(byte[] file){
|
||||
this.fileLength = file.length;
|
||||
this.file = file;
|
||||
ByteBuffer buffer = this.createBuffer();
|
||||
buffer.putShort(1, (short)0x0800);
|
||||
buffer.putInt(3, 0);
|
||||
buffer.putInt(7, fileLength - 10);
|
||||
buffer.putInt(11, fileLength - 10);
|
||||
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 15;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{17};
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class UploadFileRequest extends Request {
|
||||
public enum UploadState{INITIALIZED, UPLOAD, UPLOADED, ERROR}
|
||||
|
||||
public UploadState state;
|
||||
|
||||
public ArrayList<byte[]> packets = new ArrayList<>();
|
||||
|
||||
public UploadFileRequest(short handle, byte[] file) {
|
||||
int fileLength = file.length + 4;
|
||||
ByteBuffer buffer = this.createBuffer();
|
||||
buffer.putShort(1, handle);
|
||||
buffer.putInt(3, 0);
|
||||
buffer.putInt(7, fileLength);
|
||||
buffer.putInt(11, fileLength);
|
||||
|
||||
this.data = buffer.array();
|
||||
|
||||
prepareFilePackets(file);
|
||||
|
||||
state = UploadState.INITIALIZED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(BluetoothGattCharacteristic characteristic) {
|
||||
byte[] value = characteristic.getValue();
|
||||
if (value.length == 4) {
|
||||
if (value[1] != 0) {
|
||||
state = UploadState.ERROR;
|
||||
return;
|
||||
}
|
||||
state = UploadState.UPLOAD;
|
||||
}else if(value.length == 9){
|
||||
if(value[1] != 0){
|
||||
state = UploadState.ERROR;
|
||||
return;
|
||||
}
|
||||
state = UploadState.UPLOADED;
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareFilePackets(byte[] file) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(file.length + 4);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buffer.put(file);
|
||||
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(file);
|
||||
buffer.putInt((int) crc.getValue());
|
||||
|
||||
byte[] fileFull = buffer.array();
|
||||
for (int i = 0, sequence = 0; i < fileFull.length + 4; i += 18, sequence++) {
|
||||
byte[] packet;
|
||||
if (i + 18 >= fileFull.length) {
|
||||
packet = new byte[fileFull.length - i + 2];
|
||||
System.arraycopy(fileFull, i, packet, 2, fileFull.length - i);
|
||||
} else {
|
||||
packet = new byte[20];
|
||||
System.arraycopy(fileFull, i, packet, 2, 18);
|
||||
}
|
||||
packet[0] = 0x12;
|
||||
packet[1] = (byte) sequence;
|
||||
packets.add(packet);
|
||||
}
|
||||
packets.get(0)[1] |= 0x40;
|
||||
if (packets.size() > 1) {
|
||||
packets.get(packets.size() - 1)[1] |= 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{17};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 15;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getRequestUUID() {
|
||||
return UUID.fromString("3dda0007-957f-7d4a-34a6-74696673696d");
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
|
||||
|
||||
public class VibrateRequest extends Request {
|
||||
|
||||
public VibrateRequest(boolean longVibration, short repeats, short millisBetween){
|
||||
ByteBuffer buffer = createBuffer();
|
||||
|
||||
buffer.put(longVibration ? (byte)1 : 0);
|
||||
buffer.put((byte) repeats);
|
||||
buffer.putShort(millisBetween);
|
||||
this.data = buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPayloadLength() {
|
||||
return 7;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getStartSequence() {
|
||||
return new byte[]{2, 15, 5};
|
||||
}
|
||||
}
|
@ -67,6 +67,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Co
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator;
|
||||
@ -226,6 +227,7 @@ public class DeviceHelper {
|
||||
result.add(new EXRIZUK8Coordinator());
|
||||
result.add(new TeclastH30Coordinator());
|
||||
result.add(new XWatchCoordinator());
|
||||
result.add(new QHybridCoordinator());
|
||||
result.add(new ZeTimeCoordinator());
|
||||
result.add(new ID115Coordinator());
|
||||
result.add(new Watch9DeviceCoordinator());
|
||||
|
@ -53,4 +53,13 @@ public class LimitedQueue {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized public Object lookupByValue(Object value){
|
||||
for (Pair entry : list) {
|
||||
if (value.equals(entry.second)) {
|
||||
return entry.first;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 14 KiB |
BIN
app/src/main/res/drawable-hdpi/ic_notification_qhybrid.png
Normal file
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 8.0 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_notification_qhybrid.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 22 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_notification_qhybrid.png
Normal file
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 44 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_notification_qhybrid.png
Normal file
After Width: | Height: | Size: 43 KiB |