From 09f6cada870122d2adc489021be34f88f9c34e6f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 14 Jul 2019 19:45:40 +0200 Subject: [PATCH] Fossil Q hybrid and Skagen support added Opening this branch for the second time inorder to purge certain files form the repo --- README.md | 4 + app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 27 +- .../activities/SettingsActivity.java | 33 +- .../devices/qhybrid/ConfigActivity.java | 483 ++++++++ .../devices/qhybrid/PackageConfig.java | 100 ++ .../devices/qhybrid/PackageConfigHelper.java | 123 ++ .../qhybrid/QHybridAppChoserActivity.java | 168 +++ .../devices/qhybrid/QHybridCoordinator.java | 149 +++ .../devices/qhybrid/TaskerPlugin.java | 1001 +++++++++++++++++ .../devices/qhybrid/TaskerPluginActivity.java | 93 ++ .../devices/qhybrid/TaskerPluginReceiver.java | 27 + .../devices/qhybrid/TimePicker.java | 305 +++++ .../gadgetbridge/model/DeviceType.java | 1 + .../service/DeviceCommunicationService.java | 10 +- .../service/DeviceSupportFactory.java | 6 +- .../service/ServiceDeviceSupport.java | 4 + .../devices/qhybrid/QHybridBaseSupport.java | 179 +++ .../devices/qhybrid/QHybridSupport.java | 599 ++++++++++ .../qhybrid/requests/AnimationRequest.java | 8 + .../qhybrid/requests/BatteryLevelRequest.java | 27 + .../qhybrid/requests/DownloadFileRequest.java | 99 ++ .../qhybrid/requests/EraseFileRequest.java | 44 + .../qhybrid/requests/EventStreamRequest.java | 29 + .../devices/qhybrid/requests/FileRequest.java | 17 + .../requests/GetCurrentStepCountRequest.java | 28 + .../qhybrid/requests/GetStepGoalRequest.java | 27 + .../requests/GetTripleTapEnabledRequest.java | 8 + .../requests/GetVibrationStrengthRequest.java | 27 + .../qhybrid/requests/ListFilesRequest.java | 45 + .../qhybrid/requests/MoveHandsRequest.java | 47 + .../qhybrid/requests/OTAEnterRequest.java | 18 + .../qhybrid/requests/OTAEraseRequest.java | 43 + .../qhybrid/requests/OTAResetRequest.java | 8 + .../requests/PlayNotificationRequest.java | 50 + .../requests/PutSettingsFileRequest.java | 8 + .../requests/ReleaseHandsControlRequest.java | 28 + .../devices/qhybrid/requests/Request.java | 51 + .../requests/RequestHandControlRequest.java | 35 + .../SetCurrentTimeServiceRequest.java | 28 + .../qhybrid/requests/SetStepGoalRequest.java | 27 + .../requests/SetTripleTapEnabledRequest.java | 8 + .../requests/SetVibrationStrengthRequest.java | 28 + .../requests/SettingsFilePutRequest.java | 36 + .../qhybrid/requests/UploadFileRequest.java | 95 ++ .../qhybrid/requests/VibrateRequest.java | 26 + .../gadgetbridge/util/DeviceHelper.java | 2 + .../layout/activity_qhybrid_app_choser.xml | 28 + .../res/layout/activity_qhybrid_settings.xml | 96 ++ .../res/layout/activity_tasker_plugin.xml | 41 + app/src/main/res/layout/qhybrid_app_view.xml | 22 + .../layout/qhybrid_package_settings_item.xml | 29 + app/src/main/res/values-de/strings.xml | 5 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/preferences.xml | 6 + 55 files changed, 4431 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfig.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfigHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridAppChoserActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPlugin.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginReceiver.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TimePicker.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridBaseSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/AnimationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/BatteryLevelRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/DownloadFileRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EraseFileRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EventStreamRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/FileRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetCurrentStepCountRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetStepGoalRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetTripleTapEnabledRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetVibrationStrengthRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ListFilesRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/MoveHandsRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEnterRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEraseRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAResetRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PlayNotificationRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PutSettingsFileRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ReleaseHandsControlRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/Request.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/RequestHandControlRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetCurrentTimeServiceRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetStepGoalRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetTripleTapEnabledRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetVibrationStrengthRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SettingsFilePutRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/UploadFileRequest.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/VibrateRequest.java create mode 100644 app/src/main/res/layout/activity_qhybrid_app_choser.xml create mode 100644 app/src/main/res/layout/activity_qhybrid_settings.xml create mode 100644 app/src/main/res/layout/activity_tasker_plugin.xml create mode 100644 app/src/main/res/layout/qhybrid_app_view.xml create mode 100644 app/src/main/res/layout/qhybrid_package_settings_item.xml diff --git a/README.md b/README.md index 2ba7baf31..17b4978d3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 566b274e7..81cad5681 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6bfd292b9..90a026ab3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + @@ -471,7 +471,32 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index 90cbcbfed..fc7d3a1e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -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"); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java new file mode 100644 index 000000000..c9c26ef19 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/ConfigActivity.java @@ -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 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 { + PackageManager manager; + + PackageAdapter(@NonNull Context context, int resource, @NonNull List 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfig.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfig.java new file mode 100644 index 000000000..f3c2c7e35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfig.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfigHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfigHelper.java new file mode 100644 index 000000000..db93078db --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/PackageConfigHelper.java @@ -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 getSettings(){ + Cursor cursor = database.query(DB_TABLE, new String[]{"*"}, null, null, null, null, null); + int size = cursor.getCount(); + ArrayList 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())}); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridAppChoserActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridAppChoserActivity.java new file mode 100644 index 000000000..6e4160d48 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridAppChoserActivity.java @@ -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 packages = manager.getInstalledPackages(0); + new Thread(new Runnable() { + @Override + public void run() { + Collections.sort(packages, new Comparator() { + @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 { + PackageManager manager; + + public ConfigArrayAdapter(@NonNull Context context, int resource, @NonNull List 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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java new file mode 100644 index 000000000..4f2883944 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java @@ -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 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 getPairingActivity() { + return null; + } + + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider 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 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 { + + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPlugin.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPlugin.java new file mode 100644 index 000000000..1e4ac306e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPlugin.java @@ -0,0 +1,1001 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid; + + +// Constants and functions for Tasker *extensions* to the plugin protocol +// See Also: http://tasker.dinglisch.net/plugins.html + +// Release Notes + +// v1.1 20140202 +// added function variableNameValid() +// fixed some javadoc entries (thanks to David Stone) + +// v1.2 20140211 +// added ACTION_EDIT_EVENT + +// v1.3 20140227 +// added REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX and REQUESTED_TIMEOUT_MS_NEVER +// requestTimeoutMS(): added range check + +// v1.4 20140516 +// support for data pass through in REQUEST_QUERY intent +// some javadoc entries fixed (thanks again David :-)) + +// v1.5 20141120 +// added RESULT_CODE_FAILED_PLUGIN_FIRST +// added Setting.VARNAME_ERROR_MESSAGE + +// v1.6 20150213 +// added Setting.getHintTimeoutMS() +// added Host.addHintTimeoutMS() + +// v1.7 20160619 +// null check for getCallingActivity() in hostSupportsOnFireVariableReplacement( Activity editActivity ) + +// v1.8 20161002 +// added hostSupportsKeyEncoding(), setKeyEncoding() and Host.getKeysWithEncoding() + +import java.net.URISyntaxException; +import java.security.SecureRandom; +import java.util.regex.Pattern; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +public class TaskerPlugin { + + private final static String TAG = "TaskerPlugin"; + + private final static String BASE_KEY = "net.dinglisch.android.tasker"; + + private final static String EXTRAS_PREFIX = BASE_KEY + ".extras."; + + private final static int FIRST_ON_FIRE_VARIABLES_TASKER_VERSION = 80; + + public final static String VARIABLE_PREFIX = "%"; + + // when generating non-repeating integers, look this far back for repeats + // see getPositiveNonRepeatingRandomInteger() + private final static int RANDOM_HISTORY_SIZE = 100; + + /** + * Action that the EditActivity for an event plugin should be launched by + */ + public final static String ACTION_EDIT_EVENT = BASE_KEY + ".ACTION_EDIT_EVENT"; + + private final static String VARIABLE_NAME_START_EXPRESSION = "[\\w&&[^_]]"; + private final static String VARIABLE_NAME_MID_EXPRESSION = "[\\w0-9]+"; + private final static String VARIABLE_NAME_END_EXPRESSION = "[\\w0-9&&[^_]]"; + + public final static String VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION = + VARIABLE_NAME_START_EXPRESSION + VARIABLE_NAME_MID_EXPRESSION + VARIABLE_NAME_END_EXPRESSION + ; + + public final static String VARIABLE_NAME_MATCH_EXPRESSION = + VARIABLE_PREFIX + "+" + + VARIABLE_NAME_MAIN_PART_MATCH_EXPRESSION + ; + + private static Pattern VARIABLE_NAME_MATCH_PATTERN = null; + + /** + * @see #addVariableBundle(Bundle, Bundle) + * @see Host#getVariablesBundle(Bundle) + */ + private final static String EXTRA_VARIABLES_BUNDLE = EXTRAS_PREFIX + "VARIABLES"; + + /** + * Host capabilities, passed to plugin with edit intents + */ + private final static String EXTRA_HOST_CAPABILITIES = EXTRAS_PREFIX + "HOST_CAPABILITIES"; + + /** + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + public final static int EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES = 2; + + /** + * @see Condition#hostSupportsVariableReturn(Bundle) + */ + public final static int EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES = 4; + + /** + * @see Setting#hostSupportsOnFireVariableReplacement(Bundle) + */ + public final static int EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT = 8; + + /** + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + private final static int EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES = 16; + + public final static int EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION = 32; + + public final static int EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH = 64; + + public final static int EXTRA_HOST_CAPABILITY_ENCODING_JSON = 128; + + public final static int EXTRA_HOST_CAPABILITY_ALL = + EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES | + EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES | + EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT | + EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES| + EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION | + EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH | + EXTRA_HOST_CAPABILITY_ENCODING_JSON + ; + + /** + * Possible encodings of text in bundle values + * + * @see #setKeyEncoding(Bundle,String[],Encoding) + */ + public enum Encoding { JSON }; + + private final static String BUNDLE_KEY_ENCODING_JSON_KEYS = BASE_KEY + ".JSON_ENCODED_KEYS"; + + public static boolean hostSupportsKeyEncoding( Bundle extrasFromHost, Encoding encoding ) { + switch ( encoding ) { + case JSON: + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_ENCODING_JSON ); + default: + return false; + } + } + + /** + * + * Miscellaneous operational hints going one way or the other + * @see Setting#hostSupportsVariableReturn(Bundle) + */ + + private final static String EXTRA_HINTS_BUNDLE = EXTRAS_PREFIX + "HINTS"; + + private final static String BUNDLE_KEY_HINT_PREFIX = ".hints."; + + private final static String BUNDLE_KEY_HINT_TIMEOUT_MS = BUNDLE_KEY_HINT_PREFIX + "TIMEOUT"; + + /** + * + * @see #hostSupportsRelevantVariables(Bundle) + * @see #addRelevantVariableList(Intent, String[]) + * @see #getRelevantVariableList(Bundle) + */ + private final static String BUNDLE_KEY_RELEVANT_VARIABLES = BASE_KEY + ".RELEVANT_VARIABLES"; + + public static boolean hostSupportsRelevantVariables( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_RELEVANT_VARIABLES ); + } + + /** + * Specifies to host which variables might be used by the plugin. + * + * Used in EditActivity, before setResult(). + * + * @param intentToHost the intent being returned to the host + * @param variableNames array of relevant variable names + */ + public static void addRelevantVariableList( Intent intentToHost, String [] variableNames ) { + intentToHost.putExtra( BUNDLE_KEY_RELEVANT_VARIABLES, variableNames ); + } + + /** + * Validate a variable name. + * + * The basic requirement for variables from a plugin is that they must be all lower-case. + * + * @param varName name to check + */ + public static boolean variableNameValid( String varName ) { + + boolean validFlag = false; + + if ( varName == null ) + Log.d( TAG, "variableNameValid: null name" ); + else { + if ( VARIABLE_NAME_MATCH_PATTERN == null ) + VARIABLE_NAME_MATCH_PATTERN = Pattern.compile( VARIABLE_NAME_MATCH_EXPRESSION, 0 ); + + if ( VARIABLE_NAME_MATCH_PATTERN.matcher( varName ).matches() ) { + + if ( variableNameIsLocal( varName ) ) + validFlag = true; + else + Log.d( TAG, "variableNameValid: name not local: " + varName ); + } + else + Log.d( TAG, "variableNameValid: invalid name: " + varName ); + } + + return validFlag; + } + + /** + * Allows the plugin/host to indicate to each other a set of variables which they are referencing. + * The host may use this to e.g. show a variable selection list in it's UI. + * The host should use this if it previously indicated to the plugin that it supports relevant vars + * + * @param fromHostIntentExtras usually from getIntent().getExtras() + * @return variableNames an array of relevant variable names + */ + public static String [] getRelevantVariableList( Bundle fromHostIntentExtras ) { + + String [] relevantVars = (String []) getBundleValueSafe( fromHostIntentExtras, BUNDLE_KEY_RELEVANT_VARIABLES, String [].class, "getRelevantVariableList" ); + + if ( relevantVars == null ) + relevantVars = new String [0]; + + return relevantVars; + } + + /** + * Used by: plugin QueryReceiver, FireReceiver + * + * Add a bundle of variable name/value pairs. + * + * Names must be valid Tasker local variable names. + * Values must be String, String [] or ArrayList + * Null values cause deletion of possible already-existing variables + * A null value where the variable does not already exist results in attempted deletion + * of any existing array indices (%arr1, %arr2 etc) + * + * @param resultExtras the result extras from the receiver onReceive (from a call to getResultExtras()) + * @param variables the variables to send + * @see Setting#hostSupportsVariableReturn(Bundle) + * @see #variableNameValid(String) + */ + public static void addVariableBundle( Bundle resultExtras, Bundle variables ) { + resultExtras.putBundle( EXTRA_VARIABLES_BUNDLE, variables ); + } + + /** + * Used by: plugin EditActivity + * + * Specify the encoding for a set of bundle keys. + * + * This is completely optional and currently only necessary if using Setting#setVariableReplaceKeys + * where the corresponding values of some of the keys specified are JSON encoded. + * + * @param resultBundleToHost the bundle being returned to the host + * @param keys the keys being returned to the host which are encoded in some way + * @param encoding the encoding of the values corresponding to the specified keys + * @see #setVariableReplaceKeys(Bundle,String[]) + * @see #hostSupportsKeyEncoding(Bundle, Encoding) + */ + public static void setKeyEncoding( Bundle resultBundleToHost, String [] keys, Encoding encoding ) { + if ( Encoding.JSON.equals( encoding ) ) + addStringArrayToBundleAsString( + keys, resultBundleToHost, BUNDLE_KEY_ENCODING_JSON_KEYS, "setValueEncoding" + ); + else + Log.e( TAG, "unknown encoding: " + encoding ); + } + + // ----------------------------- SETTING PLUGIN ONLY --------------------------------- // + + public static class Setting { + + /** + * Variable name into which a description of any error that occurred can be placed + * for the user to process. + * + * Should *only* be set when the BroadcastReceiver result code indicates a failure. + * + * Note that the user needs to have configured the task to continue after failure of the plugin + * action otherwise they will not be able to make use of the error message. + * + * For use with #addRelevantVariableList(Intent, String[]) and #addVariableBundle(Bundle, Bundle) + * + */ + public final static String VARNAME_ERROR_MESSAGE = VARIABLE_PREFIX + "errmsg"; + + /** + * @see #setVariableReplaceKeys(Bundle, String[]) + */ + private final static String BUNDLE_KEY_VARIABLE_REPLACE_STRINGS = EXTRAS_PREFIX + "VARIABLE_REPLACE_KEYS"; + + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + private final static String EXTRA_REQUESTED_TIMEOUT = EXTRAS_PREFIX + "REQUESTED_TIMEOUT"; + + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + + public final static int REQUESTED_TIMEOUT_MS_NONE = 0; + + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + + public final static int REQUESTED_TIMEOUT_MS_MAX = 3599000; + + /** + * @see #requestTimeoutMS(android.content.Intent, int) + */ + + public final static int REQUESTED_TIMEOUT_MS_NEVER = REQUESTED_TIMEOUT_MS_MAX + 1000; + + /** + * @see #signalFinish(Context, Intent, int, Bundle) + * @see Host#addCompletionIntent(Intent, Intent) + */ + private final static String EXTRA_PLUGIN_COMPLETION_INTENT = EXTRAS_PREFIX + "COMPLETION_INTENT"; + + /** + * @see #signalFinish(Context, Intent, int, Bundle) + * @see Host#getSettingResultCode(Intent) + */ + public final static String EXTRA_RESULT_CODE = EXTRAS_PREFIX + "RESULT_CODE"; + + /** + * @see #signalFinish(Context, Intent, int, Bundle) + * @see Host#getSettingResultCode(Intent) + */ + + public final static int RESULT_CODE_OK = Activity.RESULT_OK; + public final static int RESULT_CODE_OK_MINOR_FAILURES = Activity.RESULT_FIRST_USER; + public final static int RESULT_CODE_FAILED = Activity.RESULT_FIRST_USER + 1; + public final static int RESULT_CODE_PENDING = Activity.RESULT_FIRST_USER + 2; + public final static int RESULT_CODE_UNKNOWN = Activity.RESULT_FIRST_USER + 3; + + /** + * If a plugin wants to define it's own error codes, start numbering them here. + * The code will be placed in an error variable (%err in the case of Tasker) for + * the user to process after the plugin action. + */ + + public final static int RESULT_CODE_FAILED_PLUGIN_FIRST = Activity.RESULT_FIRST_USER + 9; + + /** + * Used by: plugin EditActivity. + * + * Indicates to plugin that host will replace variables in specified bundle keys. + * + * Replacement takes place every time the setting is fired, before the bundle is + * passed to the plugin FireReceiver. + * + * @param extrasFromHost intent extras from the intent received by the edit activity + * @see #setVariableReplaceKeys(Bundle, String[]) + */ + public static boolean hostSupportsOnFireVariableReplacement( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_FIRE_VARIABLE_REPLACEMENT ); + } + + /** + * Used by: plugin EditActivity. + * + * Description as above. + * + * This version also includes backwards compatibility with pre 4.2 Tasker versions. + * At some point this function will be deprecated. + * + * @param editActivity the plugin edit activity, needed to test calling Tasker version + * @see #setVariableReplaceKeys(Bundle, String[]) + */ + + public static boolean hostSupportsOnFireVariableReplacement( Activity editActivity ) { + + boolean supportedFlag = hostSupportsOnFireVariableReplacement( editActivity.getIntent().getExtras() ); + + if ( ! supportedFlag ) { + + ComponentName callingActivity = editActivity.getCallingActivity(); + + if ( callingActivity == null ) + Log.w( TAG, "hostSupportsOnFireVariableReplacement: null callingActivity, defaulting to false" ); + else { + String callerPackage = callingActivity.getPackageName(); + + // Tasker only supporteed this from 1.0.10 + supportedFlag = + ( callerPackage.startsWith( BASE_KEY ) ) && + ( getPackageVersionCode( editActivity.getPackageManager(), callerPackage ) > FIRST_ON_FIRE_VARIABLES_TASKER_VERSION ) + ; + } + } + + return supportedFlag; + } + + public static boolean hostSupportsSynchronousExecution( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_SYNCHRONOUS_EXECUTION ); + } + + /** + * Request the host to wait the specified number of milliseconds before continuing. + * Note that the host may choose to ignore the request. + * + * Maximum value is REQUESTED_TIMEOUT_MS_MAX. + * Also available are REQUESTED_TIMEOUT_MS_NONE (continue immediately without waiting + * for the plugin to finish) and REQUESTED_TIMEOUT_MS_NEVER (wait forever for + * a result). + * + * Used in EditActivity, before setResult(). + * + * @param intentToHost the intent being returned to the host + * @param timeoutMS + */ + public static void requestTimeoutMS( Intent intentToHost, int timeoutMS ) { + if ( timeoutMS < 0 ) + Log.w( TAG, "requestTimeoutMS: ignoring negative timeout (" + timeoutMS + ")" ); + else { + if ( + ( timeoutMS > REQUESTED_TIMEOUT_MS_MAX ) && + ( timeoutMS != REQUESTED_TIMEOUT_MS_NEVER ) + ) { + Log.w( TAG, "requestTimeoutMS: requested timeout " + timeoutMS + " exceeds maximum, setting to max (" + REQUESTED_TIMEOUT_MS_MAX + ")" ); + timeoutMS = REQUESTED_TIMEOUT_MS_MAX; + } + intentToHost.putExtra( EXTRA_REQUESTED_TIMEOUT, timeoutMS ); + } + } + + /** + * Used by: plugin EditActivity + * + * Indicates to host which bundle keys should be replaced. + * + * @param resultBundleToHost the bundle being returned to the host + * @param listOfKeyNames which bundle keys to replace variables in when setting fires + * @see #hostSupportsOnFireVariableReplacement(Bundle) + * @see #setKeyEncoding(Bundle,String[],Encoding) + */ + public static void setVariableReplaceKeys( Bundle resultBundleToHost, String [] listOfKeyNames ) { + addStringArrayToBundleAsString( + listOfKeyNames, resultBundleToHost, BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, + "setVariableReplaceKeys" + ); + } + + /** + * Used by: plugin FireReceiver + * + * Indicates to plugin whether the host will process variables which it passes back + * + * @param extrasFromHost intent extras from the intent received by the FireReceiver + * @see #signalFinish(Context, Intent, int, Bundle) + */ + public static boolean hostSupportsVariableReturn( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_SETTING_RETURN_VARIABLES ); + } + + /** + * Used by: plugin FireReceiver + * + * Tell the host that the plugin has finished execution. + * + * This should only be used if RESULT_CODE_PENDING was returned by FireReceiver.onReceive(). + * + * @param originalFireIntent the intent received from the host (via onReceive()) + * @param resultCode level of success in performing the settings + * @param vars any variables that the plugin wants to set in the host + * @see #hostSupportsSynchronousExecution(Bundle) + */ + public static boolean signalFinish( Context context, Intent originalFireIntent, int resultCode, Bundle vars ) { + + String errorPrefix = "signalFinish: "; + + boolean okFlag = false; + + String completionIntentString = (String) getExtraValueSafe( originalFireIntent, Setting.EXTRA_PLUGIN_COMPLETION_INTENT, String.class, "signalFinish" ); + + if ( completionIntentString != null ) { + + Uri completionIntentUri = null; + try { + completionIntentUri = Uri.parse( completionIntentString ); + } + // should only throw NullPointer but don't particularly trust it + catch ( Exception e ) { + Log.w( TAG, errorPrefix + "couldn't parse " + completionIntentString ); + } + + if ( completionIntentUri != null ) { + try { + Intent completionIntent = Intent.parseUri( completionIntentString, Intent.URI_INTENT_SCHEME ); + + completionIntent.putExtra( EXTRA_RESULT_CODE, resultCode ); + + if ( vars != null ) + completionIntent.putExtra( EXTRA_VARIABLES_BUNDLE, vars ); + + context.sendBroadcast( completionIntent ); + + okFlag = true; + } + catch ( URISyntaxException e ) { + Log.w( TAG, errorPrefix + "bad URI: " + completionIntentUri ); + } + } + } + + return okFlag; + } + + /** + * Check for a hint on the timeout value the host is using. + * Used by: plugin FireReceiver. + * Requires Tasker 4.7+ + * + * @param extrasFromHost intent extras from the intent received by the FireReceiver + * @return timeoutMS the hosts timeout setting for the action or -1 if no hint is available. + * + * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER + */ + public static int getHintTimeoutMS( Bundle extrasFromHost ) { + + int timeoutMS = -1; + + Bundle hintsBundle = (Bundle) TaskerPlugin.getBundleValueSafe( extrasFromHost, EXTRA_HINTS_BUNDLE, Bundle.class, "getHintTimeoutMS" ); + + if ( hintsBundle != null ) { + + Integer val = (Integer) getBundleValueSafe( hintsBundle, BUNDLE_KEY_HINT_TIMEOUT_MS, Integer.class, "getHintTimeoutMS" ); + + if ( val != null ) + timeoutMS = val; + } + + return timeoutMS; + } + } + + // ----------------------------- CONDITION/EVENT PLUGIN ONLY --------------------------------- // + + public static class Condition { + + /** + * Used by: plugin QueryReceiver + * + * Indicates to plugin whether the host will process variables which it passes back + * + * @param extrasFromHost intent extras from the intent received by the QueryReceiver + * @see #addVariableBundle(Bundle, Bundle) + */ + public static boolean hostSupportsVariableReturn( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_CONDITION_RETURN_VARIABLES ); + } + } + + // ----------------------------- EVENT PLUGIN ONLY --------------------------------- // + + public static class Event { + + public final static String PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY = BASE_KEY + ".MESSAGE_ID"; + + private final static String EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA = EXTRAS_PREFIX + "PASS_THROUGH_DATA"; + + /** + * @param extrasFromHost intent extras from the intent received by the QueryReceiver + * @see #addPassThroughData(Intent, Bundle) + */ + public static boolean hostSupportsRequestQueryDataPassThrough( Bundle extrasFromHost ) { + return hostSupports( extrasFromHost, EXTRA_HOST_CAPABILITY_REQUEST_QUERY_DATA_PASS_THROUGH ); + } + + /** + * Specify a bundle of data (probably representing whatever change happened in the condition) + * which will be included in the QUERY_CONDITION broadcast sent by the host for each + * event instance of the plugin. + * + * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the + * with the REQUEST_QUERY that caused it. + * + * Note that for security reasons it is advisable to also store a message ID with the bundle + * which can be compared to known IDs on receipt. The host cannot validate the source of + * REQUEST_QUERY intents so fake data may be passed. Replay attacks are also possible. + * addPassThroughMesssageID() can be used to add an ID if the plugin doesn't wish to add it's + * own ID to the pass through bundle. + * + * Note also that there are several situations where REQUEST_QUERY will not result in a + * QUERY_CONDITION intent (e.g. event throttling by the host), so plugin-local data + * indexed with a message ID needs to be timestamped and eventually timed-out. + * + * This function can be called multiple times, each time all keys in data will be added to + * that of previous calls. + * + * @param requestQueryIntent intent being sent to the host + * @param data the data to be passed-through + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #retrievePassThroughData(Intent) + * @see #addPassThroughMessageID + * + */ + public static void addPassThroughData( Intent requestQueryIntent, Bundle data ) { + + Bundle passThroughBundle = retrieveOrCreatePassThroughBundle( requestQueryIntent ); + + passThroughBundle.putAll( data ); + } + + /** + * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated + * by a REQUEST_QUERY from the plugin. + * + * Note that if addPassThroughMessageID() was previously called, the data will contain an extra + * key TaskerPlugin.Event.PASS_THOUGH_BUNDLE_MESSAGE_ID_KEY. + * + * @param queryConditionIntent QUERY_REQUEST sent from host + * @return data previously added to the REQUEST_QUERY intent + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #addPassThroughData(Intent,Bundle) + */ + public static Bundle retrievePassThroughData( Intent queryConditionIntent ) { + return (Bundle) getExtraValueSafe( + queryConditionIntent, + EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, + Bundle.class, + "retrievePassThroughData" + ); + } + + /** + * Add a message ID to a REQUEST_QUERY intent which will then be included in the corresponding + * QUERY_CONDITION broadcast sent by the host for each event instance of the plugin. + * + * The minimal purpose is to enable the plugin to associate a QUERY_CONDITION to the + * with the REQUEST_QUERY that caused it. It also allows the message to be verified + * by the plugin to prevent e.g. replay attacks + * + * @param requestQueryIntent intent being sent to the host + * @return a guaranteed non-repeating within 100 calls message ID + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #retrievePassThroughData(Intent) + * @return an ID for the bundle so it can be identified and the caller verified when it is again received by the plugin + * + */ + public static int addPassThroughMessageID( Intent requestQueryIntent ) { + + Bundle passThroughBundle = retrieveOrCreatePassThroughBundle( requestQueryIntent ); + + int id = getPositiveNonRepeatingRandomInteger(); + + passThroughBundle.putInt( PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, id ); + + return id; + } + + /* + * Retrieve the pass through data from a QUERY_REQUEST from the host which was generated + * by a REQUEST_QUERY from the plugin. + * + * @param queryConditionIntent QUERY_REQUEST sent from host + * @return the ID which was passed through by the host, or -1 if no ID was found + * @see #hostSupportsRequestQueryDataPassThrough(Bundle) + * @see #addPassThroughData(Intent,Bundle) + */ + public static int retrievePassThroughMessageID( Intent queryConditionIntent ) { + + int toReturn = -1; + + Bundle passThroughData = Event.retrievePassThroughData( queryConditionIntent ); + + if ( passThroughData != null ) { + Integer id = (Integer) getBundleValueSafe( + passThroughData, + PASS_THROUGH_BUNDLE_MESSAGE_ID_KEY, + Integer.class, + "retrievePassThroughMessageID" + ); + + if ( id != null ) + toReturn = id; + } + + return toReturn; + } + + // internal use + private static Bundle retrieveOrCreatePassThroughBundle( Intent requestQueryIntent ) { + + Bundle passThroughBundle; + + if ( requestQueryIntent.hasExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA ) ) + passThroughBundle = requestQueryIntent.getBundleExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA ); + else { + passThroughBundle = new Bundle(); + requestQueryIntent.putExtra( EXTRA_REQUEST_QUERY_PASS_THROUGH_DATA, passThroughBundle ); + } + + return passThroughBundle; + } + } + // ---------------------------------- HOST ----------------------------------------- // + + public static class Host { + + /** + * Tell the plugin what capabilities the host support. This should be called when sending + * intents to any EditActivity, FireReceiver or QueryReceiver. + * + * @param toPlugin the intent we're sending + * @return capabilities one or more of the EXTRA_HOST_CAPABILITY_XXX flags + */ + public static Intent addCapabilities( Intent toPlugin, int capabilities ) { + return toPlugin.putExtra( EXTRA_HOST_CAPABILITIES, capabilities ); + } + + /** + * Add an intent to the fire intent before it goes to the plugin FireReceiver, which the plugin + * can use to signal when it is finished. Only use if @code{pluginWantsSychronousExecution} is true. + * + * @param fireIntent fire intent going to the plugin + * @param completionIntent intent which will signal the host that the plugin is finished. + * Implementation is host-dependent. + */ + public static void addCompletionIntent( Intent fireIntent, Intent completionIntent ) { + fireIntent.putExtra( + Setting.EXTRA_PLUGIN_COMPLETION_INTENT, + completionIntent.toUri( Intent.URI_INTENT_SCHEME ) + ); + } + + /** + * When a setting plugin is finished, it sends the host the intent which was passed to it + * via @code{addCompletionIntent}. + * + * @param completionIntent intent returned from the plugin when it finished. + * @return resultCode measure of plugin success, defaults to UNKNOWN + */ + public static int getSettingResultCode( Intent completionIntent ) { + + Integer val = (Integer) getExtraValueSafe( completionIntent, Setting.EXTRA_RESULT_CODE, Integer.class, "getSettingResultCode" ); + + return ( val == null ) ? Setting.RESULT_CODE_UNKNOWN : val; + } + + /** + * Extract a bundle of variables from an intent received from the FireReceiver. This + * should be called if the host previously indicated to the plugin + * that it supports setting variable return. + * + * @param resultExtras getResultExtras() from BroadcastReceiver:onReceive() + * @return variables a bundle of variable name/value pairs + * @see #addCapabilities(Intent, int) + */ + + public static Bundle getVariablesBundle( Bundle resultExtras ) { + return (Bundle) getBundleValueSafe( + resultExtras, EXTRA_VARIABLES_BUNDLE, Bundle.class, "getVariablesBundle" + ); + } + + /** + * Inform a setting plugin of the timeout value the host is using. + * + * @param toPlugin the intent we're sending + * @param timeoutMS the hosts timeout setting for the action. Note that this may differ from + * that which the plugin requests. + * @see #REQUESTED_TIMEOUT_MS_NONE, REQUESTED_TIMEOUT_MS_MAX, REQUESTED_TIMEOUT_MS_NEVER + */ + public static void addHintTimeoutMS( Intent toPlugin, int timeoutMS ) { + getHintsBundle( toPlugin, "addHintTimeoutMS" ).putInt( BUNDLE_KEY_HINT_TIMEOUT_MS, timeoutMS ); + } + + private static Bundle getHintsBundle( Intent intent, String funcName ) { + + Bundle hintsBundle = (Bundle) getExtraValueSafe( intent, EXTRA_HINTS_BUNDLE, Bundle.class, funcName ); + + if ( hintsBundle == null ) { + hintsBundle = new Bundle(); + intent.putExtra( EXTRA_HINTS_BUNDLE, hintsBundle ); + } + + return hintsBundle; + } + + public static boolean haveRequestedTimeout( Bundle extrasFromPluginEditActivity ) { + return extrasFromPluginEditActivity.containsKey( Setting.EXTRA_REQUESTED_TIMEOUT ); + } + + public static int getRequestedTimeoutMS( Bundle extrasFromPluginEditActivity ) { + return + (Integer) getBundleValueSafe( + extrasFromPluginEditActivity, Setting.EXTRA_REQUESTED_TIMEOUT, Integer.class, "getRequestedTimeout" + ) + ; + } + + public static String [] getSettingVariableReplaceKeys( Bundle fromPluginEditActivity ) { + return getStringArrayFromBundleString( + fromPluginEditActivity, Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS, + "getSettingVariableReplaceKeys" + ); + } + + public static String [] getKeysWithEncoding( Bundle fromPluginEditActivity, Encoding encoding ) { + + String [] toReturn = null; + + if ( Encoding.JSON.equals( encoding ) ) + toReturn = getStringArrayFromBundleString( + fromPluginEditActivity, TaskerPlugin.BUNDLE_KEY_ENCODING_JSON_KEYS, + "getKeyEncoding:JSON" + ); + else + Log.w( TAG, "Host.getKeyEncoding: unknown encoding " + encoding ); + + return toReturn; + } + + public static boolean haveRelevantVariables( Bundle b ) { + return b.containsKey( BUNDLE_KEY_RELEVANT_VARIABLES ); + } + + public static void cleanRelevantVariables( Bundle b ) { + b.remove( BUNDLE_KEY_RELEVANT_VARIABLES ); + } + + public static void cleanHints( Bundle extras ) { + extras.remove( TaskerPlugin.EXTRA_HINTS_BUNDLE ); + } + + public static void cleanRequestedTimeout( Bundle extras ) { + extras.remove( Setting.EXTRA_REQUESTED_TIMEOUT ); + } + + public static void cleanSettingReplaceVariables( Bundle b ) { + b.remove( Setting.BUNDLE_KEY_VARIABLE_REPLACE_STRINGS ); + } + } + + // ---------------------------------- HELPER FUNCTIONS -------------------------------- // + + private static Object getBundleValueSafe( Bundle b, String key, Class expectedClass, String funcName ) { + Object value = null; + + if ( b != null ) { + if ( b.containsKey( key ) ) { + Object obj = b.get( key ); + if ( obj == null ) + Log.w( TAG, funcName + ": " + key + ": null value" ); + else if ( obj.getClass() != expectedClass ) + Log.w( TAG, funcName + ": " + key + ": expected " + expectedClass.getClass().getName() + ", got " + obj.getClass().getName() ); + else + value = obj; + } + } + return value; + } + + private static Object getExtraValueSafe( Intent i, String key, Class expectedClass, String funcName ) { + return ( i.hasExtra( key ) ) ? + getBundleValueSafe( i.getExtras(), key, expectedClass, funcName ) : + null; + } + + private static boolean hostSupports( Bundle extrasFromHost, int capabilityFlag ) { + Integer flags = (Integer) getBundleValueSafe( extrasFromHost, EXTRA_HOST_CAPABILITIES, Integer.class, "hostSupports" ); + return + ( flags != null ) && + ( ( flags & capabilityFlag ) > 0 ) + ; + } + + public static int getPackageVersionCode( PackageManager pm, String packageName ) { + + int code = -1; + + if ( pm != null ) { + try { + PackageInfo pi = pm.getPackageInfo( packageName, 0 ); + if ( pi != null ) + code = pi.versionCode; + } + catch ( Exception e ) { + Log.e( TAG, "getPackageVersionCode: exception getting package info" ); + } + } + + return code; + } + + private static boolean variableNameIsLocal( String varName ) { + + int digitCount = 0; + int length = varName.length(); + + for ( int x = 0; x < length; x++ ) { + char ch = varName.charAt( x ); + + if ( Character.isUpperCase( ch ) ) + return false; + else if ( Character.isDigit( ch ) ) + digitCount++; + } + + if ( digitCount == ( varName.length() - 1 ) ) + return false; + + return true; + } + + private static String [] getStringArrayFromBundleString( Bundle bundle, String key, String funcName ) { + + String spec = (String) getBundleValueSafe( bundle, key, String.class, funcName ); + + String [] toReturn = null; + + if ( spec != null ) + toReturn = spec.split( " " ); + + return toReturn; + } + + private static void addStringArrayToBundleAsString( String [] toAdd, Bundle bundle, String key, String callerName ) { + + StringBuilder builder = new StringBuilder(); + + if ( toAdd != null ) { + + for ( String keyName : toAdd ) { + + if ( keyName.contains( " " ) ) + Log.w( TAG, callerName + ": ignoring bad keyName containing space: " + keyName ); + else { + if ( builder.length() > 0 ) + builder.append( ' ' ); + + builder.append( keyName ); + } + + if ( builder.length() > 0 ) + bundle.putString( key, builder.toString() ); + } + } + } + + // state tracking for random number sequence + private static int [] lastRandomsSeen = null; + private static int randomInsertPointer = 0; + private static SecureRandom sr = null; + + /** + * Generate a sequence of secure random positive integers which is guaranteed not to repeat + * in the last 100 calls to this function. + * + * @return a random positive integer + */ + public static int getPositiveNonRepeatingRandomInteger() { + + // initialize on first call + if ( sr == null ) { + sr = new SecureRandom(); + lastRandomsSeen = new int[RANDOM_HISTORY_SIZE]; + + for ( int x = 0; x < lastRandomsSeen.length; x++ ) + lastRandomsSeen[x] = -1; + } + + int toReturn; + do { + // pick a number + toReturn = sr.nextInt( Integer.MAX_VALUE ); + + // check we havn't see it recently + for ( int seen : lastRandomsSeen ) { + if ( seen == toReturn ) { + toReturn = -1; + break; + } + } + } + while ( toReturn == -1 ); + + // update history + lastRandomsSeen[randomInsertPointer] = toReturn; + randomInsertPointer = ( randomInsertPointer + 1 ) % lastRandomsSeen.length; + + return toReturn; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginActivity.java new file mode 100644 index 000000000..f0c2972e3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginActivity.java @@ -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"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginReceiver.java new file mode 100644 index 000000000..640fb269c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TaskerPluginReceiver.java @@ -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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TimePicker.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TimePicker.java new file mode 100644 index 000000000..d09171df3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/TimePicker.java @@ -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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 553b4e7a8..65e777223 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -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), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 1a7876332..31532bb67 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -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 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index afcc3a411..5524d4075 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -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: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index b1f48866a..365f3984f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridBaseSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridBaseSupport.java new file mode 100644 index 000000000..76075b948 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridBaseSupport.java @@ -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 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) { + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java new file mode 100644 index 000000000..f5b253c8b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/QHybridSupport.java @@ -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 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[] classes = new Class[]{ + BatteryLevelRequest.class, + GetStepGoalRequest.class, + GetVibrationStrengthRequest.class, + GetCurrentStepCountRequest.class, + OTAEnterRequest.class + }; + for (Class 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/AnimationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/AnimationRequest.java new file mode 100644 index 000000000..f8de2453b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/AnimationRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/BatteryLevelRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/BatteryLevelRequest.java new file mode 100644 index 000000000..4d1f13e0e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/BatteryLevelRequest.java @@ -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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/DownloadFileRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/DownloadFileRequest.java new file mode 100644 index 000000000..dcd3b64cd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/DownloadFileRequest.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EraseFileRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EraseFileRequest.java new file mode 100644 index 000000000..569326f11 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EraseFileRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EventStreamRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EventStreamRequest.java new file mode 100644 index 000000000..8ed0a9ccb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/EventStreamRequest.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/FileRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/FileRequest.java new file mode 100644 index 000000000..7fc7ee9c5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/FileRequest.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetCurrentStepCountRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetCurrentStepCountRequest.java new file mode 100644 index 000000000..e84c9dc00 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetCurrentStepCountRequest.java @@ -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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetStepGoalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetStepGoalRequest.java new file mode 100644 index 000000000..58093acbe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetStepGoalRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetTripleTapEnabledRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetTripleTapEnabledRequest.java new file mode 100644 index 000000000..09fa37219 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetTripleTapEnabledRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetVibrationStrengthRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetVibrationStrengthRequest.java new file mode 100644 index 000000000..6bcd3cc2f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/GetVibrationStrengthRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ListFilesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ListFilesRequest.java new file mode 100644 index 000000000..d6666021d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ListFilesRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/MoveHandsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/MoveHandsRequest.java new file mode 100644 index 000000000..e1ce8be11 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/MoveHandsRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEnterRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEnterRequest.java new file mode 100644 index 000000000..c33287b13 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEnterRequest.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEraseRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEraseRequest.java new file mode 100644 index 000000000..d07d591e3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAEraseRequest.java @@ -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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAResetRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAResetRequest.java new file mode 100644 index 000000000..26443a990 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/OTAResetRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PlayNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PlayNotificationRequest.java new file mode 100644 index 000000000..bb4783c94 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PlayNotificationRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PutSettingsFileRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PutSettingsFileRequest.java new file mode 100644 index 000000000..692d7bfc7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/PutSettingsFileRequest.java @@ -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]; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ReleaseHandsControlRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ReleaseHandsControlRequest.java new file mode 100644 index 000000000..443390309 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/ReleaseHandsControlRequest.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/Request.java new file mode 100644 index 000000000..07edc754d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/Request.java @@ -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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/RequestHandControlRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/RequestHandControlRequest.java new file mode 100644 index 000000000..de2d75859 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/RequestHandControlRequest.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetCurrentTimeServiceRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetCurrentTimeServiceRequest.java new file mode 100644 index 000000000..c02f4ab0c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetCurrentTimeServiceRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetStepGoalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetStepGoalRequest.java new file mode 100644 index 000000000..ac4539ec9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetStepGoalRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetTripleTapEnabledRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetTripleTapEnabledRequest.java new file mode 100644 index 000000000..d49934bee --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetTripleTapEnabledRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetVibrationStrengthRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetVibrationStrengthRequest.java new file mode 100644 index 000000000..5e17dd4b1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SetVibrationStrengthRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SettingsFilePutRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SettingsFilePutRequest.java new file mode 100644 index 000000000..30dfbe05f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/SettingsFilePutRequest.java @@ -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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/UploadFileRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/UploadFileRequest.java new file mode 100644 index 000000000..f6656160f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/UploadFileRequest.java @@ -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 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"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/VibrateRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/VibrateRequest.java new file mode 100644 index 000000000..590075964 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/VibrateRequest.java @@ -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}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 998d5bb5a..c651496a0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -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()); diff --git a/app/src/main/res/layout/activity_qhybrid_app_choser.xml b/app/src/main/res/layout/activity_qhybrid_app_choser.xml new file mode 100644 index 000000000..d9c02ff6e --- /dev/null +++ b/app/src/main/res/layout/activity_qhybrid_app_choser.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_qhybrid_settings.xml b/app/src/main/res/layout/activity_qhybrid_settings.xml new file mode 100644 index 000000000..2e73c8bbc --- /dev/null +++ b/app/src/main/res/layout/activity_qhybrid_settings.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + +