Fossil Q hybrid and Skagen support added

Opening this branch for the second time inorder to purge certain files form the repo
This commit is contained in:
root 2019-07-14 19:45:40 +02:00
parent 487c4b7498
commit 09f6cada87
55 changed files with 4431 additions and 18 deletions

View File

@ -4,6 +4,8 @@ The wiki on github.com is a read-only mirror, as is the git repo itself. Issues
Gadgetbridge
============
This implementation for the Fossil Q hybrid is not officially supported by Fossil nor does Fossil provide any warranty concerning the functionality of this code.
Gadgetbridge is an Android (4.4+) application which will allow you to use your
Pebble, Mi Band, Amazfit Bip and HPlus device (and more) without the vendor's closed source application
and without the need to create an account and transmit any of your data to the
@ -48,6 +50,8 @@ vendor's servers.
* XWatch (Affordable Chinese Casio-like smartwatches)
* Vibratissimo (experimental)
* ZeTime (WIP) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
* Fossil Q Hybrid
* Skagen Connected
## Features

View File

@ -66,7 +66,6 @@ dependencies {
testImplementation "org.robolectric:robolectric:4.2.1"
testImplementation "com.google.code.gson:gson:2.8.5"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "androidx.appcompat:appcompat:1.0.2"
implementation "androidx.preference:preference:1.1.0-alpha05"
implementation "androidx.cardview:cardview:1.0.0"
@ -85,12 +84,21 @@ dependencies {
implementation "net.e175.klaus:solarpositioning:0.0.9"
// use pristine greendao instead of our custom version, since our custom jitpack-packaged
// version contains way too much and our custom patches are in the generator only.
implementation 'com.twofortyfouram:android-plugin-client-sdk-for-locale:[4.0.3, 5.0['
implementation 'com.twofortyfouram:android-plugin-host-sdk-for-locale:[2.0.3,3.0['
implementation 'com.twofortyfouram:android-plugin-api-for-locale:[1.0.2,2.0['
implementation "org.greenrobot:greendao:2.2.1"
implementation "org.apache.commons:commons-lang3:3.7"
implementation "org.cyanogenmod:platform.sdk:6.0"
implementation 'com.jaredrummler:colorpicker:1.0.2'
// implementation project(":DaoCore")
implementation 'com.github.wax911:android-emojify:0.1.7'
implementation 'com.android.support:support-v4:28.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}
preBuild.dependsOn(":GBDaoGenerator:genSources")

View File

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

View File

@ -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.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
@ -105,6 +106,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) {
@ -285,16 +295,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);
@ -424,8 +434,7 @@ public class SettingsActivity extends AbstractSettingsActivity {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
}
}
catch (Exception fdfsdfds) {
} catch (Exception fdfsdfds) {
LOG.warn("fuck");
}
}

View File

@ -0,0 +1,483 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
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.IBinder;
import android.os.Bundle;
import android.util.Log;
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.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 nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
public class ConfigActivity extends AbstractGBActivity implements ServiceConnection, QHybridSupport.OnVibrationStrengthListener {
PackageAdapter adapter;
ArrayList<PackageConfig> list;
PackageConfigHelper helper;
final int REQUEST_CODE_ADD_APP = 0;
private boolean hasControl = false;
QHybridSupport support;
SharedPreferences prefs;
TextView timeOffsetView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qhybrid_settings);
Log.d("Config", "device: " + (getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE) == null));
findViewById(R.id.buttonOverwriteButtons).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
setSettingsEnables(false);
ConfigActivity.this.support.overwriteButtons(new QHybridSupport.OnButtonOverwriteListener() {
@Override
public void OnButtonOverwrite(final boolean success) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setSettingsEnables(true);
if(!success){
Toast.makeText(ConfigActivity.this, "Error overwriting buttons", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(ConfigActivity.this, "successfully overwritten buttons.", Toast.LENGTH_SHORT).show();
}
}
});
}
});
}
});
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("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));
Toast.makeText(ConfigActivity.this, "change might take some seconds...", Toast.LENGTH_SHORT).show();
}
})
.setNegativeButton("cancel", null)
.show();
}
});
updateTimeOffset();
bindService(new Intent(getApplicationContext(), DeviceCommunicationService.class), this, 0);
setTitle(R.string.preferences_qhybrid_settings);
ListView appList = findViewById(R.id.qhybrid_appList);
helper = new PackageConfigHelper(getApplicationContext());
list = helper.getSettings();
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(AdapterView<?> adapterView, View view, final int i, long l) {
PopupMenu menu = new PopupMenu(ConfigActivity.this, view);
menu.getMenu().add("edit");
menu.getMenu().add("delete");
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getTitle().toString()) {
case "edit": {
TimePicker picker = new TimePicker(ConfigActivity.this, list.get(i));
picker.finishListener = new TimePicker.OnFinishListener() {
@Override
public void onFinish(boolean success, PackageConfig config) {
setControl(false, null);
if (success) {
helper.saveConfig(config);
refreshList();
}
}
};
picker.handsListener = new TimePicker.OnHandsSetListener() {
@Override
public void onHandsSet(PackageConfig config) {
setHands(config);
}
};
picker.vibrationListener = new TimePicker.OnVibrationSetListener() {
@Override
public void onVibrationSet(PackageConfig config) {
vibrate(config);
}
};
setControl(true, picker.getSettings());
break;
}
case "delete": {
helper.deleteConfig(list.get(i));
refreshList();
break;
}
}
return false;
}
});
menu.show();
return false;
}
});
appList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if(ConfigActivity.this.support == null){
Toast.makeText(ConfigActivity.this, "connect device to test notification", Toast.LENGTH_SHORT).show();
return;
}
ConfigActivity.this.support.playNotification(list.get(i));
}
});
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;
support.setVibrationStrength((int) Math.pow(2, progress) * 25);
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 setSettingsEnables(boolean enables) {
findViewById(R.id.settingsLayout).setAlpha(enables ? 1f : 0.2f);
findViewById(R.id.vibrationSettingProgressBar).setVisibility(enables ? View.GONE : View.VISIBLE);
}
private void updateSettings() {
runOnUiThread(new Runnable() {
@Override
public void run() {
setSettingsEnables(false);
}
});
this.support.getGoal(new QHybridSupport.OnGoalListener() {
@Override
public void onGoal(final long goal) {
runOnUiThread(new Runnable() {
@Override
public void run() {
EditText et = findViewById(R.id.stepGoalEt);
et.setOnEditorActionListener(null);
final String text = String.valueOf(goal);
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.equals(text)) {
support.setGoal(Integer.parseInt(t));
updateSettings();
}
((InputMethodManager) getApplicationContext().getSystemService(Activity.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
}
return true;
}
});
}
});
}
});
this.support.getVibrationStrength(this);
}
private void setControl(boolean control, PackageConfig 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(PackageConfig config) {
sendControl(config, QHybridSupport.QHYBRID_COMMAND_SET);
}
private void vibrate(PackageConfig config) {
sendControl(config, QHybridSupport.QHYBRID_COMMAND_VIBRATE);
}
private void sendControl(PackageConfig config, String request) {
Intent intent = new Intent(request);
intent.putExtra("CONFIG", config);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
private void refreshList() {
list.clear();
list.addAll(helper.getSettings());
list.add(null);
adapter.notifyDataSetChanged();
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(this);
helper.close();
}
@Override
protected void onResume() {
super.onResume();
refreshList();
registerReceiver(buttonReceiver, new IntentFilter(QHybridSupport.QHYBRID_EVENT_BUTTON_PRESS));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(buttonReceiver);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Log.d("Config", "service connected");
DeviceCommunicationService.CommunicationServiceBinder binder = (DeviceCommunicationService.CommunicationServiceBinder) iBinder;
if (binder == null) {
Log.d("Config", "Service not running");
setSettingsError("Service not running");
return;
}
DeviceSupport support = ((DeviceCommunicationService.CommunicationServiceBinder) iBinder).getDeviceSupport();
if (!(support instanceof QHybridSupport)) {
Log.d("Config", "Watch not connected");
setSettingsError("Watch not connected");
return;
}
this.support = (QHybridSupport) support;
updateSettings();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
}
@Override
public void onVibrationStrength(int strength) {
final int strengthProgress = (int) (Math.log(strength / 25) / Math.log(2));
Log.d("Config", "got strength: " + strength);
runOnUiThread(new Runnable() {
@Override
public void run() {
setSettingsEnables(true);
SeekBar seekBar = findViewById(R.id.vibrationStrengthBar);
seekBar.setProgress(strengthProgress);
}
});
}
private void setSettingsError(final String error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setSettingsEnables(false);
findViewById(R.id.vibrationSettingProgressBar).setVisibility(View.GONE);
((TextView) findViewById(R.id.settingsErrorText)).setVisibility(View.VISIBLE);
((TextView) findViewById(R.id.settingsErrorText)).setText(error);
}
});
}
class PackageAdapter extends ArrayAdapter<PackageConfig> {
PackageManager manager;
PackageAdapter(@NonNull Context context, int resource, @NonNull List<PackageConfig> 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);
PackageConfig 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) {
e.printStackTrace();
}
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 buttonReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(ConfigActivity.this, "Button " + intent.getIntExtra("BUTTON", -1) + " pressed", Toast.LENGTH_SHORT).show();
}
};
class AddPackageConfig extends PackageConfig{
AddPackageConfig() {
super(null, null);
}
}
}

View File

@ -0,0 +1,100 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.util.Log;
import java.io.Serializable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.PlayNotificationRequest;
public class PackageConfig implements Serializable {
private short min, hour;
private String packageName, appName;
private int vibration;
private boolean respectSilentMode;
private long id = -1;
PackageConfig(short min, short hour, String packageName, String appName, boolean respectSilentMode, int vibration) {
this.min = min;
this.hour = hour;
this.packageName = packageName;
this.appName = appName;
this.respectSilentMode = respectSilentMode;
this.vibration = vibration;
}
PackageConfig(short min, short hour, String packageName, String appName, boolean respectSilentMode, int vibration, long id) {
this.min = min;
this.hour = hour;
this.packageName = packageName;
this.appName = appName;
this.respectSilentMode = respectSilentMode;
this.vibration = vibration;
this.id = id;
}
PackageConfig(String packageName, String appName) {
this.min = 0;
this.hour = 0;
this.packageName = packageName;
this.appName = appName;
this.respectSilentMode = false;
this.vibration = PlayNotificationRequest.VibrationType.SINGLE_NORMAL.getValue();
this.id = -1;
}
public int getVibration() {
return vibration;
}
public void setVibration(int 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 String getPackageName() {
return packageName;
}
public String getAppName() {
return appName;
}
}

View File

@ -0,0 +1,123 @@
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.database.DBOpenHelper;
public class PackageConfigHelper extends DBOpenHelper {
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";
SQLiteDatabase database;
public PackageConfigHelper(Context context) {
super(context, DB_NAME, null);
this.database = getWritableDatabase();
initDB();
}
public void saveConfig(PackageConfig settings){
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());
values.put(DB_RESPECT_SILENT, settings.getRespectSilentMode());
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())});
}
//LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent());
}
public ArrayList<PackageConfig> getSettings(){
Cursor cursor = database.query(DB_TABLE, new String[]{"*"}, null, null, null, null, null);
int size = cursor.getCount();
ArrayList<PackageConfig> 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 PackageConfig(
(short)cursor.getInt(minPos),
(short)cursor.getInt(hourPos),
cursor.getString(packageNamePos),
cursor.getString(appNamePos),
cursor.getInt(silentPos) == 1,
cursor.getInt(vibrationPos),
cursor.getInt(idPos)
));
Log.d("Settings", "setting #" + cursor.getPosition() + ": " + cursor.getInt(silentPos));
}while (cursor.moveToNext());
}
cursor.close();
return list;
}
public PackageConfig getSetting(String appName){
if(appName == null) return null;
Cursor c = database.query(DB_TABLE, new String[]{"*"}, DB_APPNAME + "=?", new String[]{appName}, null, null, null);
if(c.getCount() == 0){
c.close();
return null;
}
c.moveToFirst();
PackageConfig settings = new PackageConfig(
(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,
c.getInt(c.getColumnIndex(DB_VIBRATION)),
c.getInt(c.getColumnIndex(DB_ID))
);
c.close();
return settings;
}
private void initDB(){
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);");
}
@Override
public void close(){
super.close();
database.close();
}
public void deleteConfig(PackageConfig packageSettings) {
Log.d("DB", "deleting id " + packageSettings.getId());
if(packageSettings.getId() == -1) return;
this.database.delete(DB_TABLE, DB_ID + "=?", new String[]{String.valueOf(packageSettings.getId())});
}
}

View File

@ -0,0 +1,168 @@
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 java.util.Collections;
import java.util.Comparator;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport;
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);
helper = new PackageConfigHelper(getApplicationContext());
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() {
Collections.sort(packages, new Comparator<PackageInfo>() {
@Override
public int compare(PackageInfo packageInfo, PackageInfo t1) {
return manager.getApplicationLabel(packageInfo.applicationInfo)
.toString()
.compareToIgnoreCase(
manager.getApplicationLabel(t1.applicationInfo)
.toString()
);
}
});
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();
helper.close();
}
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(PackageConfig config){
sendControl(config, QHybridSupport.QHYBRID_COMMAND_SET);
}
private void vibrate(PackageConfig config){
sendControl(config, QHybridSupport.QHYBRID_COMMAND_VIBRATE);
}
private void sendControl(PackageConfig 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, PackageConfig config) {
setControl(false);
if(success){
helper.saveConfig(config);
finish();
}
}
};
picker.handsListener = new TimePicker.OnHandsSetListener() {
@Override
public void onHandsSet(PackageConfig config) {
setHands(config);
}
};
picker.vibrationListener = new TimePicker.OnVibrationSetListener() {
@Override
public void onVibrationSet(PackageConfig config) {
vibrate(config);
}
};
setControl(true);
}
@Override
protected void onPause() {
super.onPause();
setControl(false);
finish();
}
class ConfigArrayAdapter extends ArrayAdapter<PackageInfo> {
PackageManager manager;
public ConfigArrayAdapter(@NonNull Context context, int resource, @NonNull List<PackageInfo> objects, PackageManager manager) {
super(context, resource, objects);
this.manager = manager;
}
@NonNull
@Override
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
if (view == null)
view = ((LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE)).inflate(R.layout.qhybrid_app_view, null);
ApplicationInfo info = getItem(position).applicationInfo;
((ImageView) view.findViewById(R.id.qhybrid_appChooserItemIcon)).setImageDrawable(manager.getApplicationIcon(info));
((TextView) view.findViewById(R.id.qhybrid_appChooserItemText)).setText(manager.getApplicationLabel(info));
return view;
}
}
}

View File

@ -0,0 +1,149 @@
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 nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class QHybridCoordinator extends AbstractDeviceCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
for(ParcelUuid uuid : candidate.getServiceUuids()){
if(uuid.getUuid().toString().equals("00001812-0000-1000-8000-00805f9b34fb")){
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("00001812-0000-1000-8000-00805f9b34fb")).build());
}
@Override
public DeviceType getDeviceType() {
return DeviceType.FOSSILQHYBRID;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
public boolean supportsAlarmConfiguration() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 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 false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
}

View File

@ -0,0 +1,93 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.Intent;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.twofortyfouram.locale.sdk.client.ui.activity.AbstractPluginActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.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.getValue() + ")");
button.setId(type.getValue());
group.addView(button);
}
group.check(PlayNotificationRequest.VibrationType.NO_VIBE.getValue());
RadioButton custom = new RadioButton(this);
custom.setText("variable %vibration");
custom.setId(10);
group.addView(custom);
Intent intent = getIntent();
if(intent.hasExtra(key_hours)){
((TextView) findViewById(R.id.qhybrid_hour_degrees)).setText(intent.getStringExtra(key_hours));
}
if(intent.hasExtra(key_minute)){
((TextView) findViewById(R.id.qhybrid_minute_degrees)).setText(intent.getStringExtra(key_minute));
}
if(intent.hasExtra(key_vibration)){
String vibe = intent.getStringExtra(key_vibration);
if(vibe.equals("%vibration")){
group.check(10);
}else {
group.check(Integer.parseInt(vibe));
}
}
}
@Override
public boolean isBundleValid(@NonNull Bundle bundle) {
return true;
}
@Override
public void onPostCreateWithPreviousResult(@NonNull Bundle bundle, @NonNull String s) {
}
@Nullable
@Override
public Bundle getResultBundle() {
int vibration = group.getCheckedRadioButtonId();
Bundle bundle = new Bundle();
bundle.putString(key_hours, ((EditText) findViewById(R.id.qhybrid_hour_degrees)).getText().toString());
bundle.putString(key_minute, ((EditText) findViewById(R.id.qhybrid_minute_degrees)).getText().toString());
if(vibration == 10){
bundle.putString(key_vibration, "%vibration");
}else{
bundle.putString(key_vibration, String.valueOf(vibration));
}
TaskerPlugin.Setting.setVariableReplaceKeys(bundle, new String[]{key_hours, key_minute, key_vibration});
return bundle;
}
@NonNull
@Override
public String getResultBlurb(@NonNull Bundle bundle) {
return "nope";
}
}

View File

@ -0,0 +1,27 @@
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;
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);
PackageConfig config = new PackageConfig((short)minDegrees, (short)hourDegrees, null, null, false, Integer.parseInt(vibration));
Intent send = new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION);
send.putExtra("CONFIG", config);
LocalBroadcastManager.getInstance(context).sendBroadcast(send);
}
}

View File

@ -0,0 +1,305 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.app.AlertDialog;
import android.content.ContentValues;
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.PlayNotificationRequest;
public class TimePicker extends AlertDialog.Builder {
ImageView pickerView;
Canvas pickerCanvas;
Bitmap pickerBitmap;
PackageConfig 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 PackageConfig(info.packageName, context.getApplicationContext().getPackageManager().getApplicationLabel(info.applicationInfo).toString());
initGraphics(context);
}
protected TimePicker(Context context, PackageConfig 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());
group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
settings.setVibration(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();
if(this.settings.getHour() == -1 && this.settings.getMin() == -1) {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setClickable(false);
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setAlpha(0.4f);
}
pickerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
handleTouch(dialog, motionEvent);
return true;
}
});
}
public PackageConfig 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) {
linePaint.setAlpha(100);
pickerCanvas.drawLine(width / 2, height / 2, width / 2, height / 2f - radius3, linePaint);
paint.setAlpha(255);
paint.setColor(Color.WHITE);
pickerCanvas.drawCircle(width / 2f, height / 2f - radius3, radius, paint);
paint.setAlpha(100);
paint.setColor(Color.BLUE);
pickerCanvas.drawCircle(width / 2f, height / 2f - radius3, radius, paint);
} else {
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(100);
if (settings.getMin() != -1) {
linePaint.setAlpha(100);
pickerCanvas.drawLine(width / 2, height / 2, width / 2, height / 2f - radius2, linePaint);
}
paint.setAlpha(255);
paint.setColor(Color.WHITE);
pickerCanvas.drawCircle(width / 2f, height / 2f - radius2, radius, paint);
paint.setAlpha(100);
paint.setColor(Color.BLUE);
pickerCanvas.drawCircle(width / 2f, height / 2f - radius2, radius, paint);
} else {
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, PackageConfig config);
}
interface OnHandsSetListener{
public void onHandsSet(PackageConfig config);
}
interface OnVibrationSetListener{
public void onVibrationSet(PackageConfig config);
}
}

View File

@ -48,6 +48,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_default, R.drawable.ic_device_default_disabled, R.string.devicetype_qhybrid),
ZETIME(80, R.drawable.ic_device_zetime, R.drawable.ic_device_zetime_disabled, R.string.devicetype_mykronoz_zetime),
ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115),
WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9),

View File

@ -32,6 +32,7 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.widget.Toast;
@ -813,9 +814,16 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
}
public class CommunicationServiceBinder extends Binder{
public DeviceSupport getDeviceSupport(){
if(mDeviceSupport == null) return null;
return ((ServiceDeviceSupport)DeviceCommunicationService.this.mDeviceSupport).getDelegate();
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
return new CommunicationServiceBinder();
}
@Override

View File

@ -46,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport
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;
@ -168,7 +169,10 @@ public class DeviceSupportFactory {
case XWATCH:
deviceSupport = new ServiceDeviceSupport(new XWatchSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ZETIME:
case FOSSILQHYBRID:
deviceSupport = new ServiceDeviceSupport(new QHybridSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ZETIME:
deviceSupport = new ServiceDeviceSupport(new ZeTimeDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case ID115:

View File

@ -63,6 +63,10 @@ public class ServiceDeviceSupport implements DeviceSupport {
this.flags = flags;
}
public DeviceSupport getDelegate() {
return delegate;
}
@Override
public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) {
delegate.setContext(gbDevice, btAdapter, context);

View File

@ -0,0 +1,179 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;
import android.net.Uri;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
public class QHybridBaseSupport extends AbstractBTLEDeviceSupport {
public QHybridBaseSupport(Logger logger) {
super(logger);
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
}
@Override
public void onDeleteNotification(int id) {
}
@Override
public void onSetTime() {
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
}
@Override
public void onSetCallState(CallSpec callSpec) {
}
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
}
@Override
public void onInstallApp(Uri uri) {
}
@Override
public void onAppInfoReq() {
}
@Override
public void onAppStart(UUID uuid, boolean start) {
}
@Override
public void onAppDelete(UUID uuid) {
}
@Override
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
}
@Override
public void onAppReorder(UUID[] uuids) {
}
@Override
public void onFetchRecordedData(int dataTypes) {
}
@Override
public void onReset(int flags) {
}
@Override
public void onHeartRateTest() {
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
}
@Override
public void onFindDevice(boolean start) {
}
@Override
public void onSetConstantVibration(int integer) {
}
@Override
public void onScreenshotReq() {
}
@Override
public void onEnableHeartRateSleepSupport(boolean enable) {
}
@Override
public void onSetHeartRateMeasurementInterval(int seconds) {
}
@Override
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
}
@Override
public void onDeleteCalendarEvent(byte type, long id) {
}
@Override
public void onSendConfiguration(String config) {
}
@Override
public void onReadConfiguration(String config) {
}
@Override
public void onTestNewFunction() {
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
}
}

View File

@ -0,0 +1,599 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid;
import android.app.AlarmManager;
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.wifi.aware.Characteristics;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.UUID;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.PackageConfigHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.AnimationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.BatteryLevelRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.DownloadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.EraseFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.FileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.GetCurrentStepCountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.GetStepGoalRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.GetVibrationStrengthRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.ListFilesRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.MoveHandsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.OTAEnterRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.OTAEraseRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.PlayNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.ReleaseHandsControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.RequestHandControlRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.SetCurrentTimeServiceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.SetStepGoalRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.SetVibrationStrengthRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.SettingsFilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.UploadFileRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.VibrateRequest;
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_NOTIFICATION = "qhybrid_command_notification";
public static final String QHYBRID_EVENT_BUTTON_PRESS = "nodomain.freeyourgadget.gadgetbridge.Q_BUTTON_PRESSED";
private static final String ITEM_STEP_GOAL = "STEP_GOAL";
private static final String ITEM_VIBRATION_STRENGTH = "VIBRATION_STRENGTH";
private static final Logger logger = LoggerFactory.getLogger(QHybridSupport.class);
private PackageConfigHelper helper;
private volatile boolean searchDevice = false;
private int lastButtonIndex = -1;
private final SparseArray<Request> responseFilters = new SparseArray<>();
private OnVibrationStrengthListener vibrationStrengthListener;
private OnGoalListener goalListener;
private OnButtonOverwriteListener buttonOverwriteListener;
private Request fileRequest = null;
private boolean dumpInited = false;
private long timeOffset;
private UploadFileRequest uploadFileRequest;
private PendingIntent dumpIntent;
private PendingIntent stepIntent;
public QHybridSupport() {
super(logger);
addSupportedService(UUID.fromString("3dda0001-957f-7d4a-34a6-74696673696d"));
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_NOTIFICATION);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(commandReceiver, commandFilter);
fillResponseList();
}
private void fillResponseList() {
Class<? extends Request>[] classes = new Class[]{
BatteryLevelRequest.class,
GetStepGoalRequest.class,
GetVibrationStrengthRequest.class,
GetCurrentStepCountRequest.class,
OTAEnterRequest.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.d("Service", "response filter " + object.getStartSequence()[1] + ": " + c.getSimpleName());
}
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException e) {
Log.d("Service", "skipping class " + c.getName());
}
}
}
private void getTimeOffset() {
timeOffset = getContext().getSharedPreferences(getContext().getPackageName(), Context.MODE_PRIVATE).getInt("QHYBRID_TIME_OFFSET", 0);
}
@Override
public void dispose() {
super.dispose();
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(commandReceiver);
getContext().unregisterReceiver(dumpReceiver);
((AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE)).cancel(dumpIntent);
getContext().unregisterReceiver(stepReceiver);
((AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE)).cancel(stepIntent);
dumpInited = false;
}
public void getGoal(OnGoalListener listener) {
this.goalListener = listener;
queueWrite(new GetStepGoalRequest());
}
public void setGoal(int goal) {
queueWrite(new SetStepGoalRequest(goal));
}
public void getVibrationStrength(OnVibrationStrengthListener listener) {
this.vibrationStrengthListener = listener;
queueWrite(new GetVibrationStrengthRequest());
}
public void setVibrationStrength(int strength) {
queueWrite(new SetVibrationStrengthRequest((short) strength));
}
private void queueWrite(Request request) {
new TransactionBuilder(request.getClass().getSimpleName()).write(getCharacteristic(request.getRequestUUID()), request.getRequestData()).queue(getQueue());
if (request instanceof FileRequest) this.fileRequest = request;
}
private final BroadcastReceiver stepReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
queueWrite(new GetCurrentStepCountRequest());
}
};
private final BroadcastReceiver dumpReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("Dump", "dumping...");
downloadActivityFiles();
}
};
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
for (int i = 2; i <= 7; i++)
builder.notify(getCharacteristic(UUID.fromString("3dda000" + i + "-957f-7d4a-34a6-74696673696d")), true);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
helper = new PackageConfigHelper(getContext());
if (!dumpInited) {
getContext().registerReceiver(dumpReceiver, new IntentFilter("dumpReceiver2"));
getContext().registerReceiver(stepReceiver, new IntentFilter("stepDumpReceiver"));
dumpIntent = PendingIntent.getBroadcast(getContext(), 0, new Intent("dumpReceiver2"), PendingIntent.FLAG_UPDATE_CURRENT);
stepIntent = PendingIntent.getBroadcast(getContext(), 0, new Intent("stepDumpReceiver"), PendingIntent.FLAG_UPDATE_CURRENT);
((AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE)).setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000, AlarmManager.INTERVAL_HOUR, dumpIntent);
((AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE)).setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000, AlarmManager.INTERVAL_HOUR / 60, stepIntent);
dumpInited = true;
}
getTimeOffset();
return builder;
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt) {
super.onServicesDiscovered(gatt);
playAnimation();
queueWrite(new BatteryLevelRequest());
logger.debug("onServicesDiscovered");
}
private void playAnimation() {
queueWrite(new AnimationRequest());
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
Log.d("Service", "notif from " + notificationSpec.sourceAppId + " " + notificationSpec.sender + " " + notificationSpec.phoneNumber);
//new Exception().printStackTrace();
String packageName = notificationSpec.sourceName;
PackageConfig config = helper.getSetting(packageName);
if (config == null) return;
Log.d("Service", "handling notification");
int mode = ((AudioManager) getContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE)).getRingerMode();
if (mode == AudioManager.RINGER_MODE_SILENT && config.getRespectSilentMode()) return;
playNotification(config);
}
public void playNotification(PackageConfig config){
queueWrite(new PlayNotificationRequest(config.getVibration(), config.getHour(), config.getMin()));
}
@Override
public void onSetTime() {
long millis = System.currentTimeMillis();
TimeZone zone = new GregorianCalendar().getTimeZone();
SetCurrentTimeServiceRequest request = new SetCurrentTimeServiceRequest(
(int) (millis / 1000 + timeOffset * 60),
(short) (millis % 1000),
(short) ((zone.getRawOffset() + zone.getDSTSavings()) / 60000));
queueWrite(request);
}
@Override
public void onFindDevice(boolean start) {
logger.debug("onFindDevice");
if (start && searchDevice) return;
searchDevice = start;
if (start) {
new Thread(new Runnable() {
@Override
public void run() {
VibrateRequest request = new VibrateRequest(false, (short) 4, (short) 1);
BluetoothGattCharacteristic chara = getCharacteristic(request.getRequestUUID());
int i = 0;
while (searchDevice) {
new TransactionBuilder("findDevice#" + i++).write(chara, request.getRequestData()).queue(getQueue());
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
@Override
public void onTestNewFunction() {
//downloadActivityFiles();
//queueWrite(new GetCurrentStepCountRequest());
// queueWrite(new EventStreamRequest((short)4));
// queueWrite(new OTAEraseRequest(0));
// queueWrite(new OTAResetRequest());
new UploadFileRequest((short)00, new byte[]{0x01, 0x00, 0x08, 0x01, 0x01, 0x0C, 0x00, (byte)0xBD, 0x01, 0x30, 0x71, (byte)0xFF, 0x05, 0x00, 0x01, 0x00});
}
public void overwriteButtons(OnButtonOverwriteListener listener){
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
});
this.buttonOverwriteListener = listener;
queueWrite(uploadFileRequest);
}
private void downloadActivityFiles() {
queueWrite(new ListFilesRequest());
}
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();
queueWrite(new EraseFileRequest((short) request.fileHandle));
} catch (Exception e) {
e.printStackTrace();
if (request.fileHandle > 257) {
queueWrite(new DownloadFileRequest((short) (request.fileHandle - 1)));
}
}
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
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);
}
default:{
Log.d("Service", "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) {
e.printStackTrace();
}
break;
}
}
return super.onCharacteristicChanged(gatt, characteristic);
}
private boolean handleFileUploadCharacteristic(BluetoothGattCharacteristic characteristic) {
uploadFileRequest.handleResponse(characteristic);
switch (uploadFileRequest.state){
case ERROR:
buttonOverwriteListener.OnButtonOverwrite(false);
break;
case UPLOAD:
for(byte[] packet : this.uploadFileRequest.packets){
new TransactionBuilder("File upload").write(characteristic, packet).queue(getQueue());
}
break;
case UPLOADED:
buttonOverwriteListener.OnButtonOverwrite(true);
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});
UploadFileRequest request = new UploadFileRequest((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 boolean handleBasicCharacteristic(BluetoothGattCharacteristic characteristic) {
byte[] values = characteristic.getValue();
Request request;
request = resolveAnswer(characteristic);
StringBuilder valueString = new StringBuilder(String.valueOf(values[0]));
for (int i = 1; i < characteristic.getValue().length; i++) {
valueString.append(", ").append(values[i]);
}
if (request == null) {
Log.d("Service", "unable to resolve " + characteristic.getUuid().toString() + ": " + valueString);
return true;
}
Log.d("Service", "response: " + request.getClass().getSimpleName());
request.handleResponse(characteristic);
if (request instanceof BatteryLevelRequest) {
gbDevice.setBatteryLevel(((BatteryLevelRequest) request).level);
} else if (request instanceof GetStepGoalRequest) {
if (this.goalListener != null) {
this.goalListener.onGoal(((GetStepGoalRequest) request).stepGoal);
this.goalListener = null;
}
gbDevice.addDeviceInfo(new GenericItem(ITEM_STEP_GOAL, String.valueOf(((GetStepGoalRequest) request).stepGoal)));
} else if (request instanceof GetVibrationStrengthRequest) {
if (this.vibrationStrengthListener != null) {
logger.debug("got vibration: " + ((GetVibrationStrengthRequest) request).strength);
this.vibrationStrengthListener.onVibrationStrength(((GetVibrationStrengthRequest) request).strength);
this.vibrationStrengthListener = null;
}
gbDevice.addDeviceInfo(new GenericItem(ITEM_VIBRATION_STRENGTH, String.valueOf(((GetVibrationStrengthRequest) request).strength)));
} else if (fileRequest instanceof ListFilesRequest) {
ListFilesRequest r = (ListFilesRequest) fileRequest;
//if(r.fileCount != -1){
if (r.completed) {
Log.d("Service", "FileCount: " + r.fileCount);
this.fileRequest = null;
}
//}
} 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) {
e.printStackTrace();
}
} else if (request instanceof OTAEnterRequest) {
if (((OTAEnterRequest) request).success) {
fileRequest = new OTAEraseRequest(1024 << 16);
queueWrite(fileRequest);
}
}
return true;
}
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 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";
}
private Request resolveAnswer(BluetoothGattCharacteristic characteristic) {
byte[] values = characteristic.getValue();
if (values[0] != 3) return null;
return responseFilters.get(values[1]);
}
@Override
public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
return super.onCharacteristicWrite(gatt, characteristic, status);
}
private void setHands(short hour, short minute) {
queueWrite(new MoveHandsRequest(false, minute, hour, (short) -1));
}
private void vibrate(int vibration) {
queueWrite(new PlayNotificationRequest(vibration, -1, -1));
}
private final BroadcastReceiver commandReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle extras = intent.getExtras();
PackageConfig config = extras == null ? null : (PackageConfig) intent.getExtras().get("CONFIG");
switch (intent.getAction()) {
case QHYBRID_COMMAND_CONTROL: {
Log.d("Service", "sending control request");
queueWrite(new RequestHandControlRequest());
if (config != null) {
setHands(config.getHour(), config.getMin());
} else {
setHands((short) 0, (short) 0);
}
break;
}
case QHYBRID_COMMAND_UNCONTROL: {
queueWrite(new ReleaseHandsControlRequest());
break;
}
case QHYBRID_COMMAND_SET: {
setHands(config.getHour(), config.getMin());
break;
}
case QHYBRID_COMMAND_VIBRATE: {
vibrate(config.getVibration());
break;
}
case QHYBRID_COMMAND_NOTIFICATION: {
queueWrite(new PlayNotificationRequest(config.getVibration(), config.getHour(), config.getMin()));
break;
}
case QHYBRID_COMMAND_UPDATE: {
getTimeOffset();
onSetTime();
break;
}
}
}
};
public interface OnVibrationStrengthListener {
void onVibrationStrength(int strength);
}
public interface OnGoalListener {
void onGoal(long goal);
}
public interface OnButtonOverwriteListener{
void OnButtonOverwrite(boolean success);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,44 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
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.d(getName(), "wrong descriptor");
return;
}
ByteBuffer buffer = ByteBuffer.wrap(characteristic.getValue());
buffer.order(ByteOrder.LITTLE_ENDIAN);
deletedHandle = buffer.getShort(1);
status = buffer.get(3);
log("file " + deletedHandle + " erased: " + status);
}
@Override
public int getPayloadLength() {
return 3;
}
@Override
public byte[] getStartSequence() {
return new byte[]{3};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,50 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests;
import java.nio.ByteBuffer;
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);
public byte value;
VibrationType(int value) {
this.value = (byte)value;
}
public byte getValue() {
return value;
}
}
public PlayNotificationRequest(int vibrationType, int degreesHour, int degreesMins){
int length = 0;
if(degreesHour > -1) length++;
if(degreesMins > -1) length++;
ByteBuffer buffer = createBuffer(length * 2 + 10);
buffer.put((byte)vibrationType);
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)));
}
this.data = buffer.array();
}
@Override
public byte[] getStartSequence() {
return new byte[]{2, 7, 15, 10, 1};
}
}

View File

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

View File

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

View File

@ -0,0 +1,51 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests;
import android.bluetooth.BluetoothGattCharacteristic;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.UUID;
public abstract class Request {
protected byte[] data;
//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(){
return this.getClass().getSimpleName();
}
protected void log(String message){
Log.d(getName(), message);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,6 +62,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
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;
@ -219,6 +220,7 @@ public class DeviceHelper {
result.add(new EXRIZUK8Coordinator());
result.add(new TeclastH30Coordinator());
result.add(new XWatchCoordinator());
result.add(new QHybridCoordinator());
result.add(new ZeTimeCoordinator());
result.add(new ID115Coordinator());
result.add(new Watch9DeviceCoordinator());

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".devices.qhybrid.QHybridAppChoserActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:paddingTop="40dp"
android:id="@+id/qhybrid_packageChooserLoading"/>
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/qhybrid_appChooserList"/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/qhybridMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="vertical"
android:id="@+id/settingsLayout"
android:focusableInTouchMode="true" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="vibration strength:" />
<SeekBar
android:id="@+id/vibrationStrengthBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="2"
android:min="0" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Goal in steps" />
<EditText
android:id="@+id/stepGoalEt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:text="time shift" />
<TextView
android:id="@+id/qhybridTimeOffset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true" />
</RelativeLayout>
<Button
android:id="@+id/buttonOverwriteButtons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="overwrite buttons" />
</LinearLayout>
<ProgressBar
android:id="@+id/vibrationSettingProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/settingsLayout"
android:layout_alignTop="@id/settingsLayout"
android:layout_centerHorizontal="true"
android:gravity="center" />
<TextView
android:id="@+id/settingsErrorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/settingsLayout"
android:layout_alignBottom="@id/settingsLayout"
android:layout_centerHorizontal="true"
android:visibility="gone"
android:gravity="center" />
<ListView
android:id="@+id/qhybrid_appList"
android:layout_marginTop="50dp"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_below="@id/settingsLayout"
android:layout_alignParentBottom="true"/>
</RelativeLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".devices.qhybrid.TaskerPluginActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hour degrees:" />
<EditText
android:id="@+id/qhybrid_hour_degrees"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="minute degrees:" />
<EditText
android:id="@+id/qhybrid_minute_degrees"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/qhybrid_tasker_vibration"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:layout_alignParentStart="true"
android:id="@+id/qhybrid_appChooserItemIcon"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20dp"
android:id="@+id/qhybrid_appChooserItemText"/>
</RelativeLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="5dp">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:layout_alignParentStart="true"
android:id="@+id/packageIcon"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/packageName"
android:textSize="20dp"
android:layout_centerInParent="true"/>
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="fitXY"
android:id="@+id/packageClock"
android:layout_alignParentEnd="true"/>
</RelativeLayout>

View File

@ -358,8 +358,8 @@
<string name="pref_summary_pebble_health_store_raw">Speichert Daten \"wie sie sind\" und erhöht die Datenbanknutzung, um eine spätere Auswertung zu ermöglichen.</string>
<string name="action_db_management">Datenbankverwaltung</string>
<string name="title_activity_db_management">Datenbankverwaltung</string>
<string name="activity_db_management_import_export_explanation">Die Datenbankoperationen verwenden den folgenden Pfad auf deinem Gerät.
\nDieser Pfad ist für andere Android-Apps und deinem Computer zugänglich.
<string name="activity_db_management_import_export_explanation">Die Datenbankoperationen verwenden den folgenden Pfad auf deinem Gerät.
\nDieser Pfad ist für andere Android-Apps und deinem Computer zugänglich.
\nDu findest die exportierte Datenbank hier (bzw. lege die zu importierende dort ab):</string>
<string name="activity_db_management_merge_old_title">Legacy-Datenbank löschen</string>
<string name="dbmanagementactivvity_cannot_access_export_path">Kann nicht auf den Exportpfad zugreifen. Bitte die Entwickler kontaktieren.</string>
@ -546,6 +546,7 @@
<string name="whitelist_all_for_notifications">Whitelist für alle Benachrichtigungen</string>
<string name="pref_auto_fetch_summary">Abruf erfolgt beim Entsperren des Bildschirms. Funktioniert nur, wenn ein Sperrmechanismus eingestellt ist!</string>
<string name="watch9_pairing_tap_hint">Wenn deine Uhr vibriert, schüttel das Gerät oder drücke die Taste.</string>
<string name="preferences_qhybrid_settings">Q Hybrid Einstellungen</string>
<string name="watch9_calibration_button">Kalibrieren</string>
<string name="title_activity_watch9_pairing">Watch 9 koppeln</string>
<string name="watch9_time_minutes">Minuten:</string>

View File

@ -633,6 +633,7 @@
<string name="devicetype_no1_f1">No.1 F1</string>
<string name="devicetype_teclast_h30">Teclast H30</string>
<string name="devicetype_xwatch">XWatch</string>
<string name="devicetype_qhybrid">Fossil Q Hybrid</string>
<string name="devicetype_mykronoz_zetime">MyKronoz ZeTime</string>
<string name="devicetype_id115">ID115</string>
<string name="devicetype_watch9">Watch 9</string>
@ -655,6 +656,7 @@
<string name="menuitem_compass">Compass</string>
<string name="menuitem_settings">Settings</string>
<string name="menuitem_alipay">Alipay</string>
<string name="preferences_qhybrid_settings">Q Hybrid Settings</string>
<string name="menuitem_music">Music</string>
<string name="menuitem_more">More</string>
<string name="watch9_time_minutes">Minutes:</string>
@ -664,6 +666,7 @@
<string name="watch9_calibration_button">Calibrate</string>
<string name="title_activity_watch9_pairing">Watch 9 pairing</string>
<string name="title_activity_watch9_calibration">Watch 9 calibration</string>
<string name="tasker_notification">Play Q Hybrid notification</string>
<string name="pref_title_contextual_arabic">Contextual Arabic</string>
<string name="pref_summary_contextual_arabic">Enable this to support contextual Arabic</string>
<string name="preferences_rtl_settings">Right To Left Support</string>

View File

@ -267,6 +267,12 @@
<PreferenceCategory
android:title="@string/preferences_category_device_specific_settings">
<Preference
android:icon="@drawable/ic_device_pebble"
android:key="pref_key_qhybrid"
android:title="@string/preferences_qhybrid_settings" />
<Preference
android:icon="@drawable/ic_device_miband"
android:key="pref_key_miband"