diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9c853aa9f..26a99596e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -17,6 +17,16 @@ If you got it from Google Play, please note [that version](https://github.com/Ta #### Your issue is: *If possible, please attach [logs](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Log-Files)! that might help identifying the problem.* +*Long logs can be also included but make sure to tuck them into the `details` tag:* + +
+ Click to see my log, which is a bit longer + +``` +Here go lines of that log. +``` +
+ #### Your wearable device is: diff --git a/CHANGELOG.md b/CHANGELOG.md index c4db2c15d..8c7075e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ### Changelog +#### 0.48.0 (WIP) +* Initial support for Sony SWR12 +* Initial support for Lefun Smart Bands +* InfiniTime: Improved music support for latest firmware +* Add sport activity list tab in charts +* Weather: Fix wind speed and direction not being passed properly +* Fix find your phone feature on Android 10 (need companion device pairing) + #### 0.47.2 * Amazfit Bip S: Send sunrise and sunset on latest firmware if enabled * Huami: Support new firmware update protocol (fixes firmware flashing with firmware 2.1.1.50/4.1.5.55 on Amazfit Bip S) diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index ff9fe81a6..5184aecaa 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -43,7 +43,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(30, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(31, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -74,6 +74,10 @@ public class GBDaoGenerator { addWatchXPlusHealthActivitySample(schema, user, device); addWatchXPlusHealthActivityKindOverlay(schema, user, device); addTLW64ActivitySample(schema, user, device); + addLefunActivitySample(schema, user, device); + addLefunBiometricSample(schema,user,device); + addLefunSleepSample(schema, user, device); + addSonySWR12Sample(schema, user, device); addHybridHRActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -404,6 +408,59 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addSonySWR12Sample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "SonySWR12Sample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + addHeartRateProperties(activitySample); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + return activitySample; + } + + private static Entity addLefunActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "LefunActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("distance").notNull(); + activitySample.addIntProperty("calories").notNull(); + addHeartRateProperties(activitySample); + return activitySample; + } + + private static Entity addLefunBiometricSample(Schema schema, Entity user, Entity device) { + Entity biometricSample = addEntity(schema, "LefunBiometricSample"); + biometricSample.implementsSerializable(); + + biometricSample.addIntProperty("timestamp").notNull().primaryKey(); + Property deviceId = biometricSample.addLongProperty("deviceId").primaryKey().notNull().getProperty(); + biometricSample.addToOne(device, deviceId); + Property userId = biometricSample.addLongProperty("userId").notNull().getProperty(); + biometricSample.addToOne(user, userId); + + biometricSample.addIntProperty("type").notNull(); + biometricSample.addIntProperty("value1").notNull(); + biometricSample.addIntProperty("value2"); + return biometricSample; + } + + private static Entity addLefunSleepSample(Schema schema, Entity user, Entity device) { + Entity sleepSample = addEntity(schema, "LefunSleepSample"); + sleepSample.implementsSerializable(); + + sleepSample.addIntProperty("timestamp").notNull().primaryKey(); + Property deviceId = sleepSample.addLongProperty("deviceId").primaryKey().notNull().getProperty(); + sleepSample.addToOne(device, deviceId); + Property userId = sleepSample.addLongProperty("userId").notNull().getProperty(); + sleepSample.addToOne(user, userId); + + sleepSample.addIntProperty("type").notNull(); + return sleepSample; + } + private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) { activitySample.setSuperclass(superClass); activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider"); diff --git a/README.md b/README.md index 71edbade7..7befdcca1 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,11 @@ vendor's servers. * Fossil Hybrid HR (WARNING: NEEDS FOSSIL APP WITH ACCOUNT ONCE AND COMPLICATED PROCEDURE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Fossil-Hybrid-HR) * Fossil Q Hybrid * HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus) -* iTag +* iTag [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/iTag) * ID115 +* Nut Mini 3, Nut 2 and possibly others [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Nut) * JYou Y5 +* Lefun * Lenovo Watch 9 * Lenovo Watch X (Plus) [Wiki](https://codeberg.org/mamutcho/Gadgetbridge/wiki) * Liveview @@ -94,6 +96,7 @@ Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/ma * Pavel Elagin (JYou Y5) * Taavi Eomäe (iTag) * Erik Bloß (TLW64) +* Yukai Li (Lefun) ## Contribute diff --git a/app/build.gradle b/app/build.gradle index e29b19f49..19e10474e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,13 +25,13 @@ android { targetSdkVersion 29 // Note: always bump BOTH versionCode and versionName! - versionName "0.47.2" - versionCode 181 + versionName "0.48.0" + versionCode 182 vectorDrawables.useSupportLibrary = true } buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } @@ -60,21 +60,22 @@ pmd { dependencies { // testImplementation "ch.qos.logback:logback-classic:1.1.3" // testImplementation "ch.qos.logback:logback-core:1.1.3" - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.android.support.constraint:constraint-layout:2.0.2' testImplementation "junit:junit:4.12" testImplementation "org.mockito:mockito-core:1.10.19" testImplementation "org.robolectric:robolectric:4.2.1" - testImplementation "com.google.code.gson:gson:2.8.5" + testImplementation "com.google.code.gson:gson:2.8.6" implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.preference:preference:1.1.1" implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.legacy:legacy-support-v4:1.0.0" implementation "androidx.gridlayout:gridlayout:1.0.0" - implementation "com.google.android.material:material:1.1.0" + implementation "com.google.android.material:material:1.2.1" implementation "androidx.palette:palette:1.0.0" + implementation "no.nordicsemi.android:dfu:1.11.0" implementation("com.github.tony19:logback-android-classic:1.1.1-6") { exclude group: "com.google.android", module: "android" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index befd643ca..6649b5d6a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -9,6 +9,8 @@ # Add any project specific keep options here: +-dontobfuscate + # Pebble BG-JS -keepclassmembers class * { @android.webkit.JavascriptInterface ; @@ -33,8 +35,12 @@ -keep class **$Properties { *; } +# Keep database migration classes accessed trough reflection -keep class **.gadgetbridge.database.schema.* { *; } +# Keep Nordic DFU library +-keep class no.nordicsemi.android.dfu.** { *; } + # Keep dependency android-emojify (io.wax911.emojify) uses -keep class org.hamcrest.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5e61bf86c..a7187c8b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -346,6 +346,9 @@ + mItems = new ArrayList<>(); - private ItemWithDetailsAdapter mItemAdapter; + private final List items = new ArrayList<>(); + private ItemWithDetailsAdapter itemAdapter; private ListView detailsListView; - private ItemWithDetailsAdapter mDetailsItemAdapter; - private ArrayList mDetails = new ArrayList<>(); + private ItemWithDetailsAdapter detailsAdapter; + private ArrayList details = new ArrayList<>(); - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + private final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -93,6 +95,23 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal validateInstallation(); } } + } else if (GB.ACTION_SET_PROGRESS_BAR.equals(action)) { + if (intent.hasExtra(GB.PROGRESS_BAR_INDETERMINATE)) { + setProgressIndeterminate(intent.getBooleanExtra(GB.PROGRESS_BAR_INDETERMINATE, false)); + } + + if (intent.hasExtra(GB.PROGRESS_BAR_PROGRESS)) { + setProgressIndeterminate(false); + setProgressBar(intent.getIntExtra(GB.PROGRESS_BAR_PROGRESS, 0)); + } + } else if (GB.ACTION_SET_PROGRESS_TEXT.equals(action)) { + if (intent.hasExtra(GB.DISPLAY_MESSAGE_MESSAGE)) { + setProgressText(intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE)); + } + } else if (GB.ACTION_SET_INFO_TEXT.equals(action)) { + if (intent.hasExtra(GB.DISPLAY_MESSAGE_MESSAGE)) { + setInfoText(intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE)); + } } else if (GB.ACTION_DISPLAY_MESSAGE.equals(action)) { String message = intent.getStringExtra(GB.DISPLAY_MESSAGE_MESSAGE); int severity = intent.getIntExtra(GB.DISPLAY_MESSAGE_SEVERITY, GB.INFO); @@ -103,16 +122,30 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal private void refreshBusyState(GBDevice dev) { if (dev.isConnecting() || dev.isBusy()) { - mProgressBar.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.VISIBLE); } else { - boolean wasBusy = mProgressBar.getVisibility() != View.GONE; + boolean wasBusy = progressBar.getVisibility() != View.GONE; if (wasBusy) { - mProgressBar.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); // done! } } } + public void setProgressIndeterminate(boolean indeterminate) { + progressBar.setVisibility(View.VISIBLE); + progressBar.setIndeterminate(indeterminate); + } + + public void setProgressBar(int progress) { + progressBar.setProgress(progress); + } + + public void setProgressText(String text) { + progressText.setVisibility(View.VISIBLE); + progressText.setText(text); + } + private void connect() { mayConnect = false; // only do that once per #onCreate GBApplication.deviceService().connect(device); @@ -134,28 +167,33 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal device = dev; } if (savedInstanceState != null) { - mDetails = savedInstanceState.getParcelableArrayList(ITEM_DETAILS); - if (mDetails == null) { - mDetails = new ArrayList<>(); + details = savedInstanceState.getParcelableArrayList(ITEM_DETAILS); + if (details == null) { + details = new ArrayList<>(); } } mayConnect = true; - itemListView = (ListView) findViewById(R.id.itemListView); - mItemAdapter = new ItemWithDetailsAdapter(this, mItems); - itemListView.setAdapter(mItemAdapter); - fwAppInstallTextView = (TextView) findViewById(R.id.infoTextView); - installButton = (Button) findViewById(R.id.installButton); - mProgressBar = (ProgressBar) findViewById(R.id.installProgressBar); - detailsListView = (ListView) findViewById(R.id.detailsListView); - mDetailsItemAdapter = new ItemWithDetailsAdapter(this, mDetails); - mDetailsItemAdapter.setSize(ItemWithDetailsAdapter.SIZE_SMALL); - detailsListView.setAdapter(mDetailsItemAdapter); + itemListView = findViewById(R.id.itemListView); + itemAdapter = new ItemWithDetailsAdapter(this, items); + itemListView.setAdapter(itemAdapter); + fwAppInstallTextView = findViewById(R.id.infoTextView); + installButton = findViewById(R.id.installButton); + progressBar = findViewById(R.id.installProgressBar); + progressText = findViewById(R.id.installProgressText); + detailsListView = findViewById(R.id.detailsListView); + detailsAdapter = new ItemWithDetailsAdapter(this, details); + detailsAdapter.setSize(ItemWithDetailsAdapter.SIZE_SMALL); + detailsListView.setAdapter(detailsAdapter); + setInstallEnabled(false); IntentFilter filter = new IntentFilter(); filter.addAction(GBDevice.ACTION_DEVICE_CHANGED); filter.addAction(GB.ACTION_DISPLAY_MESSAGE); - LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter); + filter.addAction(GB.ACTION_SET_PROGRESS_BAR); + filter.addAction(GB.ACTION_SET_PROGRESS_TEXT); + filter.addAction(GB.ACTION_SET_INFO_TEXT); + LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter); installButton.setOnClickListener(new View.OnClickListener() { @Override @@ -167,7 +205,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal }); uri = getIntent().getData(); - if (uri == null) { //for "share" intent + if (uri == null) { // For "share" intent uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); } installHandler = findInstallHandlerFor(uri); @@ -188,7 +226,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putParcelableArrayList(ITEM_DETAILS, mDetails); + outState.putParcelableArrayList(ITEM_DETAILS, details); } private InstallHandler findInstallHandlerFor(Uri uri) { @@ -236,7 +274,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal @Override protected void onDestroy() { - LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); + LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); super.onDestroy(); } @@ -259,19 +297,19 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal @Override public void clearInstallItems() { - mItems.clear(); - mItemAdapter.notifyDataSetChanged(); + items.clear(); + itemAdapter.notifyDataSetChanged(); } @Override public void setInstallItem(ItemWithDetails item) { - mItems.clear(); - mItems.add(item); - mItemAdapter.notifyDataSetChanged(); + items.clear(); + items.add(item); + itemAdapter.notifyDataSetChanged(); } private void addMessage(String message, int severity) { - mDetails.add(new GenericItem(message)); - mDetailsItemAdapter.notifyDataSetChanged(); + details.add(new GenericItem(message)); + detailsAdapter.notifyDataSetChanged(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SimpleChartsHost.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SimpleChartsHost.java deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingAdapter.java new file mode 100644 index 000000000..ec648091e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingAdapter.java @@ -0,0 +1,55 @@ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.content.Context; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.adapter.AbstractItemAdapter; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public class ActivityListingAdapter extends AbstractItemAdapter { + public ActivityListingAdapter(Context context) { + super(context); + } + + @Override + protected String getName(StepAnalysis.StepSession item) { + int activityKind = item.getActivityKind(); + String activityKindLabel = ActivityKind.asString(activityKind, getContext()); + Date startTime = item.getStepStart(); + Date endTime = item.getStepEnd(); + + String fromTime = DateTimeUtils.formatTime(startTime.getHours(), startTime.getMinutes()); + String toTime = DateTimeUtils.formatTime(endTime.getHours(), endTime.getMinutes()); + String duration = DateTimeUtils.formatDurationHoursMinutes(endTime.getTime() - startTime.getTime(), TimeUnit.MILLISECONDS); + + if (activityKind == ActivityKind.TYPE_UNKNOWN) { + return getContext().getString(R.string.chart_no_active_data); + } + return activityKindLabel + " " + duration + " (" + fromTime + " - " + toTime + ")"; + } + + @Override + protected String getDetails(StepAnalysis.StepSession item) { + String heartRate = ""; + if (item.getActivityKind() == ActivityKind.TYPE_UNKNOWN) { + return getContext().getString(R.string.chart_get_active_and_synchronize); + } + if (item.getHeartRateAverage() > 50) { + heartRate = " ❤️ " + item.getHeartRateAverage(); + } + + return "👣 " + item.getSteps() + heartRate; + } + + @Override + protected int getIcon(StepAnalysis.StepSession item) { + int activityKind = item.getActivityKind(); + return ActivityKind.getIconId(activityKind); + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingChartFragment.java new file mode 100644 index 000000000..593fe811a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingChartFragment.java @@ -0,0 +1,143 @@ +/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, Dikay900, Pavel Elagin + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; +import android.widget.TextView; + +import com.github.mikephil.charting.charts.Chart; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + + +public class ActivityListingChartFragment extends AbstractChartFragment { + protected static final Logger LOG = LoggerFactory.getLogger(ActivityListingChartFragment.class); + int tsDataFrom; + private View rootView; + private List activitySamples; + private ActivityListingAdapter stepListAdapter; + private TextView stepsDateView; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + rootView = inflater.inflate(R.layout.fragment_steps_list, container, false); + + ListView stepsList = rootView.findViewById(R.id.itemListView); + stepListAdapter = new ActivityListingAdapter(getContext()); + stepsList.setAdapter(stepListAdapter); + stepsDateView = rootView.findViewById(R.id.stepsDateView); + //refresh(); + return rootView; + } + + @Override + public String getTitle() { + return "Steps list"; + } + + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(ChartsHost.REFRESH)) { + // TODO: use LimitLines to visualize smart alarms? + //refresh(); + } else { + super.onReceive(context, intent); + } + } + + + @Override + protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + //trying to fit found peg into square hole of the Gb Charts fragment system + //get the data + activitySamples = getSamples(db, device); + return null; + } + + @Override + protected void updateChartsnUIThread(ChartsData chartsData) { + //top displays selected date + stepsDateView.setText(DateTimeUtils.formatDate(new Date(tsDataFrom * 1000L))); + //calculate active sessions + StepAnalysis stepAnalysis = new StepAnalysis(); + if (activitySamples != null) { + List stepSessions = stepAnalysis.calculateStepSessions(activitySamples); + if (stepSessions.toArray().length == 0) { + stepSessions = create_empty_record(); + } + //push to the adapter + stepListAdapter.setItems(stepSessions, true); + } + } + + @Override + protected void renderCharts() { + + } + + @Override + protected void setupLegend(Chart chart) { + + } + + @Override + protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + Calendar day = Calendar.getInstance(); + day.setTimeInMillis(tsTo * 1000L); //we need today initially, which is the end of the time range + day.set(Calendar.HOUR_OF_DAY, 0); //and we set time for the start and end of the same day + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + + tsFrom = (int) (day.getTimeInMillis() / 1000); + tsTo = tsFrom + 24 * 60 * 60 - 1; + tsDataFrom = tsFrom; + return getAllSamples(db, device, tsFrom, tsTo); + } + + private List create_empty_record() { + //have an "Unknown Activity" in the list in case there are no active sessions + List result = new ArrayList<>(); + int tsTo = tsDataFrom + 24 * 60 * 60 - 1; + result.add(new StepAnalysis.StepSession(new Date(tsDataFrom * 1000L), new Date(tsTo * 1000L), 0, 0, ActivityKind.TYPE_UNKNOWN)); + return result; + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java index 70f9bb13f..0aea294d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ChartsActivity.java @@ -355,14 +355,16 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts case 0: return new ActivitySleepChartFragment(); case 1: - return new SleepChartFragment(); + return new ActivityListingChartFragment(); case 2: - return new WeekSleepChartFragment(); + return new SleepChartFragment(); case 3: - return new WeekStepsChartFragment(); + return new WeekSleepChartFragment(); case 4: - return new SpeedZonesFragment(); + return new WeekStepsChartFragment(); case 5: + return new SpeedZonesFragment(); + case 6: return new LiveActivityFragment(); } return null; @@ -373,9 +375,9 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts // Show 5 or 6 total pages. DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice); if (coordinator.supportsRealtimeData()) { - return 6; + return 7; } - return 5; + return 6; } private String getSleepTitle() { @@ -402,14 +404,16 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts case 0: return getString(R.string.activity_sleepchart_activity_and_sleep); case 1: - return getString(R.string.sleepchart_your_sleep); + return getString(R.string.charts_activity_list); case 2: - return getSleepTitle(); + return getString(R.string.sleepchart_your_sleep); case 3: - return getStepsTitle(); + return getSleepTitle(); case 4: - return getString(R.string.stats_title); + return getStepsTitle(); case 5: + return getString(R.string.stats_title); + case 6: return getString(R.string.liveactivity_live_activity); } return super.getPageTitle(position); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepAnalysis.java new file mode 100644 index 000000000..774798f0b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StepAnalysis.java @@ -0,0 +1,183 @@ +/* Copyright (C) 2019-2020 Q-er + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +public class StepAnalysis { + protected static final Logger LOG = LoggerFactory.getLogger(StepAnalysis.class); + private final int MIN_SESSION_STEPS = 100; + + public List calculateStepSessions(List samples) { + List result = new ArrayList<>(); + final int MIN_SESSION_LENGTH = 60 * GBApplication.getPrefs().getInt("chart_list_min_session_length", 5); + final int MAX_IDLE_PHASE_LENGTH = 60 * GBApplication.getPrefs().getInt("chart_list_max_idle_phase_length", 5); + final int MIN_STEPS_PER_MINUTE = GBApplication.getPrefs().getInt("chart_list_min_steps_per_minute", 40); + + ActivitySample previousSample = null; + Date stepStart = null; + Date stepEnd = null; + int activeSteps = 0; + int heartRateForAverage = 0; + int heartRateToAdd = 0; + int activeSamplesForAverage = 0; + int activeSamplesToAdd = 0; + int stepsBetweenActivities = 0; + int heartRateBetweenActivities = 0; + int durationSinceLastActiveStep = 0; + int activityKind; + + for (ActivitySample sample : samples) { + if (isStep(sample)) { //TODO we could improve/extend this to other activities as well, if in database + + if (sample.getHeartRate() != 255 && sample.getHeartRate() != -1) { + heartRateToAdd = sample.getHeartRate(); + activeSamplesToAdd = 1; + } else { + heartRateToAdd = 0; + activeSamplesToAdd = 0; + } + + if (stepStart == null) { + stepStart = getDateFromSample(sample); + activeSteps = sample.getSteps(); + heartRateForAverage = heartRateToAdd; + activeSamplesForAverage = activeSamplesToAdd; + durationSinceLastActiveStep = 0; + stepsBetweenActivities = 0; + heartRateBetweenActivities = 0; + previousSample = null; + } + if (previousSample != null) { + int durationSinceLastSample = sample.getTimestamp() - previousSample.getTimestamp(); + activeSamplesForAverage += activeSamplesToAdd; + if (sample.getSteps() > MIN_STEPS_PER_MINUTE) { + activeSteps += sample.getSteps() + stepsBetweenActivities; + heartRateForAverage += heartRateToAdd + heartRateBetweenActivities; + stepsBetweenActivities = 0; + heartRateBetweenActivities = 0; + durationSinceLastActiveStep = 0; + } else { + stepsBetweenActivities += sample.getSteps(); + heartRateBetweenActivities += heartRateToAdd; + durationSinceLastActiveStep += durationSinceLastSample; + } + if (durationSinceLastActiveStep >= MAX_IDLE_PHASE_LENGTH) { + + int current = sample.getTimestamp(); + int starting = (int) (stepStart.getTime() / 1000); + int session_length = current - starting - durationSinceLastActiveStep; + int heartRateAverage = activeSamplesForAverage > 0 ? heartRateForAverage / activeSamplesForAverage : 0; + + if (session_length >= MIN_SESSION_LENGTH) { + stepEnd = new Date((sample.getTimestamp() - durationSinceLastActiveStep) * 1000L); + activityKind = detect_activity(session_length, activeSteps, heartRateAverage); + result.add(new StepSession(stepStart, stepEnd, activeSteps, heartRateAverage, activityKind)); + } + stepStart = null; + } + } + previousSample = sample; + } + } + //make sure we show the last portion of the data as well in case no further activity is recorded yet + if (stepStart != null && previousSample != null) { + int current = previousSample.getTimestamp(); + int starting = (int) (stepStart.getTime() / 1000); + int session_length = current - starting - durationSinceLastActiveStep; + int heartRateAverage = activeSamplesForAverage > 0 ? heartRateForAverage / activeSamplesForAverage : 0; + + if (session_length > MIN_SESSION_LENGTH && activeSteps > MIN_SESSION_STEPS) { + stepEnd = getDateFromSample(previousSample); + activityKind = detect_activity(session_length, activeSteps, heartRateAverage); + result.add(new StepSession(stepStart, stepEnd, activeSteps, heartRateAverage, activityKind)); + } + } + return result; + } + + private int detect_activity(int session_length, int activeSteps, int heartRateAverage) { + final int MIN_STEPS_PER_MINUTE_FOR_RUN = GBApplication.getPrefs().getInt("chart_list_min_steps_per_minute_for_run", 120); + int spm = (int) (activeSteps / (session_length / 60)); + if (spm > MIN_STEPS_PER_MINUTE_FOR_RUN) { + return ActivityKind.TYPE_RUNNING; + } + if (activeSteps > 200) { + return ActivityKind.TYPE_WALKING; + } + if (heartRateAverage > 90) { + return ActivityKind.TYPE_EXERCISE; + } + return ActivityKind.TYPE_ACTIVITY; + } + + private boolean isStep(ActivitySample sample) { + return sample.getKind() == ActivityKind.TYPE_WALKING || sample.getKind() == ActivityKind.TYPE_RUNNING || sample.getKind() == ActivityKind.TYPE_ACTIVITY; + } + + private Date getDateFromSample(ActivitySample sample) { + return new Date(sample.getTimestamp() * 1000L); + } + + public static class StepSession { + private final Date stepStart; + private final Date stepEnd; + private final int steps; + private final int heartRateAverage; + private final int activityKind; + + StepSession(Date stepStart, + Date stepEnd, + int steps, int heartRateAverage, int activityKind) { + this.stepStart = stepStart; + this.stepEnd = stepEnd; + this.steps = steps; + this.heartRateAverage = heartRateAverage; + this.activityKind = activityKind; + } + + public Date getStepStart() { + return stepStart; + } + + public Date getStepEnd() { + return stepEnd; + } + + public int getSteps() { + return steps; + } + + public int getHeartRateAverage() { + return heartRateAverage; + } + + public int getActivityKind() { + return activityKind; + } + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index a81ab8f1f..1d8d2a98b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -45,4 +45,15 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_LONGSIT_SWITCH = "pref_longsit_switch"; public static final String PREF_LONGSIT_SWITCH_NOSHED = "screen_longsit_noshed"; public static final String PREF_DO_NOT_DISTURB_NOAUTO = "do_not_disturb_no_auto"; + public static final String PREF_FIND_PHONE_ENABLED = "prefs_find_phone"; + + public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled"; + public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch"; + public static final String PREF_HYDRATION_PERIOD = "pref_hydration_period"; + public static final String PREF_AMPM_ENABLED = "pref_ampm_enabled"; + public static final String PREF_LEFUN_INTERFACE_LANGUAGE = "pref_lefun_interface_language"; + + public static final String PREF_SONYSWR12_LOW_VIBRATION = "vibration_preference"; + public static final String PREF_SONYSWR12_STAMINA = "stamina_preference"; + public static final String PREF_SONYSWR12_SMART_INTERVAL = "smart_alarm_interval_preference"; } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index bf753a4e6..fafb12b71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -41,6 +41,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference; import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALTITUDE_CALIBRATE; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_AMPM_ENABLED; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ANTILOST_ENABLED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_1_FUNCTION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_2_FUNCTION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BUTTON_3_FUNCTION; @@ -48,15 +50,22 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DATEFORMAT; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_FIND_PHONE_ENABLED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_FORCE_WHITE_COLOR; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYDRATION_PERIOD; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LEFUN_INTERFACE_LANGUAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_POWER_MODE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SCREEN_ORIENTATION; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_LOW_VIBRATION; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_SMART_INTERVAL; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_STAMINA; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION; @@ -351,11 +360,20 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat { addPreferenceHandlerFor(PREF_LONGSIT_PERIOD); addPreferenceHandlerFor(PREF_LONGSIT_SWITCH); addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO); + addPreferenceHandlerFor(PREF_FIND_PHONE_ENABLED); + addPreferenceHandlerFor(PREF_ANTILOST_ENABLED); + addPreferenceHandlerFor(PREF_HYDRATION_SWITCH); + addPreferenceHandlerFor(PREF_HYDRATION_PERIOD); + addPreferenceHandlerFor(PREF_AMPM_ENABLED); + addPreferenceHandlerFor(PREF_LEFUN_INTERFACE_LANGUAGE); addPreferenceHandlerFor(PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES); addPreferenceHandlerFor(PREF_HYBRID_HR_FORCE_WHITE_COLOR); addPreferenceHandlerFor(PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES); + addPreferenceHandlerFor(PREF_SONYSWR12_STAMINA); + addPreferenceHandlerFor(PREF_SONYSWR12_LOW_VIBRATION); + addPreferenceHandlerFor(PREF_SONYSWR12_SMART_INTERVAL); String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF); boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/itag/ITagConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/itag/ITagConstants.java index 138af862d..616e03a88 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/itag/ITagConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/itag/ITagConstants.java @@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.itag; import java.util.UUID; public final class ITagConstants { - public static final UUID UUID_SERVICE_BUTTON = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"); // Contains information about the button state - public static final UUID UUID_LINK_LOSS_ALERT_LEVEL = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb"); // Contains information about the button state + /** Contains information about the button state */ + public static final UUID UUID_SERVICE_BUTTON = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"); + /** Contains information about the button state */ + public static final UUID UUID_LINK_LOSS_ALERT_LEVEL = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb"); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java new file mode 100644 index 000000000..4af1aee5e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunConstants.java @@ -0,0 +1,88 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun; + +import java.util.UUID; + +import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID; + +/** + * Constants used with Lefun device support + */ +public class LefunConstants { + // BLE UUIDs + public static final UUID UUID_SERVICE_LEFUN = UUID.fromString(String.format(BASE_UUID, "18D0")); + public static final UUID UUID_CHARACTERISTIC_LEFUN_WRITE = UUID.fromString(String.format(BASE_UUID, "2D01")); + public static final UUID UUID_CHARACTERISTIC_LEFUN_NOTIFY = UUID.fromString(String.format(BASE_UUID, "2D00")); + + // Coordinator constants + public static final String ADVERTISEMENT_NAME = "Lefun"; + public static final String MANUFACTURER_NAME = "Teng Jin Da"; + // Commands + public static final byte CMD_REQUEST_ID = (byte) 0xab; + public static final byte CMD_RESPONSE_ID = 0x5a; + public static final int CMD_MAX_LENGTH = 20; + // 3 header bytes plus checksum + public static final int CMD_HEADER_LENGTH = 4; + public static final byte CMD_FIRMWARE_INFO = 0x00; + public static final byte CMD_BONDING_REQUEST = 0x01; + public static final byte CMD_SETTINGS = 0x02; + public static final byte CMD_BATTERY_LEVEL = 0x03; + public static final byte CMD_TIME = 0x04; + public static final byte CMD_ALARM = 0x05; + public static final byte CMD_PROFILE = 0x06; + public static final byte CMD_UI_PAGES = 0x07; + public static final byte CMD_FEATURES = 0x08; + public static final byte CMD_FIND_DEVICE = 0x09; + public static final byte CMD_FIND_PHONE = 0x0a; + public static final byte CMD_SEDENTARY_REMINDER_INTERVAL = 0x0b; + public static final byte CMD_HYDRATION_REMINDER_INTERVAL = 0x0c; + public static final byte CMD_REMOTE_CAMERA = 0x0d; + public static final byte CMD_REMOTE_CAMERA_TRIGGERED = 0x0e; + public static final byte CMD_PPG_START = 0x0f; + public static final byte CMD_PPG_RESULT = 0x10; + public static final byte CMD_PPG_DATA = 0x11; + public static final byte CMD_STEPS_DATA = 0x12; + public static final byte CMD_ACTIVITY_DATA = 0x13; + public static final byte CMD_SLEEP_TIME_DATA = 0x14; + public static final byte CMD_SLEEP_DATA = 0x15; + public static final byte CMD_NOTIFICATION = 0x17; + public static final byte CMD_LANGUAGE = 0x21; + public static final byte CMD_UNKNOWN_22 = 0x22; + public static final byte CMD_UNKNOWN_25 = 0x25; + public static final byte CMD_UNKNOWN_80 = (byte) 0x80; + public static final int PPG_TYPE_INVALID = -1; + public static final int PPG_TYPE_HEART_RATE = 0; + public static final int PPG_TYPE_BLOOD_PRESSURE = 1; + public static final int PPG_TYPE_BLOOD_OXYGEN = 2; + public static final int PPG_TYPE_COUNT = 3; + // DB activity kinds + public static final int DB_ACTIVITY_KIND_UNKNOWN = 0; + public static final int DB_ACTIVITY_KIND_ACTIVITY = 1; + public static final int DB_ACTIVITY_KIND_HEART_RATE = 2; + public static final int DB_ACTIVITY_KIND_LIGHT_SLEEP = 3; + public static final int DB_ACTIVITY_KIND_DEEP_SLEEP = 4; + // Pseudo-intensity + public static final int INTENSITY_MIN = 0; + public static final int INTENSITY_DEEP_SLEEP = 1; + public static final int INTENSITY_LIGHT_SLEEP = 2; + public static final int INTENSITY_AWAKE = 3; + public static final int INTENSITY_MAX = 4; + public static int NUM_ALARM_SLOTS = 5; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java new file mode 100644 index 000000000..3ffc33e06 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunDeviceCoordinator.java @@ -0,0 +1,173 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +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; + +import static nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants.ADVERTISEMENT_NAME; +import static nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants.MANUFACTURER_NAME; +import static nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants.NUM_ALARM_SLOTS; + +/** + * Device coordinator for Lefun band + */ +public class LefunDeviceCoordinator extends AbstractDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_NONE; + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + // There's a bunch of other names other than "Lefun", but let's just focus on one for now. + if (ADVERTISEMENT_NAME.equals(candidate.getName())) { + // The device does not advertise service UUIDs, so can't check whether it supports + // the proper service. We can check that it doesn't advertise any services, though. + // We're actually supposed to check for presence of the string "TJDR" within the + // manufacturer specific data, which consists of the device's MAC address and said + // string. But we're not being given it, so *shrug*. + if (candidate.getServiceUuids().length == 0) { + return DeviceType.LEFUN; + } + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.LEFUN; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new LefunSampleProvider(device, session); + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public int getAlarmSlotCount() { + return NUM_ALARM_SLOTS; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public String getManufacturer() { + return MANUFACTURER_NAME; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return true; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return true; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_liftwrist_display_noshed, + R.xml.devicesettings_timeformat, + R.xml.devicesettings_antilost, + R.xml.devicesettings_longsit, + R.xml.devicesettings_hydration_reminder, + R.xml.devicesettings_lefun_interface_language, + }; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunFeatureSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunFeatureSupport.java new file mode 100644 index 000000000..9b7a11df3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunFeatureSupport.java @@ -0,0 +1,61 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun; + +/** + * Feature support utilities for Lefun devices + */ +public class LefunFeatureSupport { + public static final int SUPPORT_HEART_RATE = 1 << 2; + public static final int SUPPORT_BLOOD_PRESSURE = 1 << 3; + public static final int SUPPORT_FAKE_ECG = 1 << 10; + public static final int SUPPORT_ECG = 1 << 11; + public static final int SUPPORT_WALLPAPER_UPLOAD = 1 << 12; + + public static final int RESERVE_BLOOD_OXYGEN = 1 << 0; + public static final int RESERVE_CLOCK_FACE_UPLOAD = 1 << 3; + public static final int RESERVE_CONTACTS = 1 << 5; + public static final int RESERVE_WALLPAPER = 1 << 6; + public static final int RESERVE_REMOTE_CAMERA = 1 << 7; + + /** + * Checks whether a feature is supported + * + * @param deviceSupport the feature flags from the device + * @param featureSupport the feature you want to check + * @return whether feature is supported + */ + public static boolean checkSupported(short deviceSupport, int featureSupport) { + return (deviceSupport & featureSupport) == featureSupport; + } + + /** + * Checks whether a feature is not reserved + *

+ * Reserve flags indicate a feature is not available if set. This function takes care of the + * inverting for you, so if you get true, the feature is available. + * + * @param deviceReserve the reserve flags from the device + * @param featureReserve the reserve flag you want to check + * @return whether feature is supported + */ + public static boolean checkNotReserved(short deviceReserve, int featureReserve) { + return !((deviceReserve & featureReserve) == featureReserve); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java new file mode 100644 index 000000000..606782fc8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/LefunSampleProvider.java @@ -0,0 +1,102 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +/** + * Sample provider for Lefun devices + */ +public class LefunSampleProvider extends AbstractSampleProvider { + public LefunSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getLefunActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return LefunActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return LefunActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return LefunActivitySampleDao.Properties.DeviceId; + } + + @Override + public int normalizeType(int rawType) { + switch (rawType) { + case LefunConstants.DB_ACTIVITY_KIND_ACTIVITY: + case LefunConstants.DB_ACTIVITY_KIND_HEART_RATE: + return ActivityKind.TYPE_ACTIVITY; + case LefunConstants.DB_ACTIVITY_KIND_LIGHT_SLEEP: + return ActivityKind.TYPE_LIGHT_SLEEP; + case LefunConstants.DB_ACTIVITY_KIND_DEEP_SLEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + default: + return ActivityKind.TYPE_UNKNOWN; + } + } + + @Override + public int toRawActivityKind(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return LefunConstants.DB_ACTIVITY_KIND_ACTIVITY; + case ActivityKind.TYPE_LIGHT_SLEEP: + return LefunConstants.DB_ACTIVITY_KIND_LIGHT_SLEEP; + case ActivityKind.TYPE_DEEP_SLEEP: + return LefunConstants.DB_ACTIVITY_KIND_DEEP_SLEEP; + default: + return LefunConstants.DB_ACTIVITY_KIND_UNKNOWN; + } + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity / (float) LefunConstants.INTENSITY_MAX; + } + + @Override + public LefunActivitySample createActivitySample() { + return new LefunActivitySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/AlarmCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/AlarmCommand.java new file mode 100644 index 000000000..fea4806c5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/AlarmCommand.java @@ -0,0 +1,175 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class AlarmCommand extends BaseCommand { + public static final int DOW_SUNDAY = 0; + public static final int DOW_MONDAY = 1; + public static final int DOW_TUESDAY = 2; + public static final int DOW_WEDNESDAY = 3; + public static final int DOW_THURSDAY = 4; + public static final int DOW_FRIDAY = 5; + public static final int DOW_SATURDAY = 6; + + private byte op; + private byte index; + private boolean enabled; + // Snooze is not implemented how you think it would be + // Number of snoozes is decremented every time the alarm triggers, and the alarm time + // is moved forward by number of minutes in snooze time. It never gets reset to the + // original time. + private byte numOfSnoozes; + private byte snoozeTime; + private byte dayOfWeek; + private byte hour; + private byte minute; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public byte getIndex() { + return index; + } + + public void setIndex(byte index) { + if (index < 0 || index >= LefunConstants.NUM_ALARM_SLOTS) + throw new IllegalArgumentException("Index must be between 0 and " + + (LefunConstants.NUM_ALARM_SLOTS - 1) + " inclusive"); + this.index = index; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public byte getNumOfSnoozes() { + return numOfSnoozes; + } + + public void setNumOfSnoozes(byte numOfSnoozes) { + this.numOfSnoozes = numOfSnoozes; + } + + public byte getSnoozeTime() { + return snoozeTime; + } + + public void setSnoozeTime(byte snoozeTime) { + this.snoozeTime = snoozeTime; + } + + public boolean getDayOfWeek(int day) { + if (day < 0 || day > 6) + throw new IllegalArgumentException("Invalid day of week"); + return getBit(dayOfWeek, 1 << day); + } + + public void setDayOfWeek(int day, boolean enabled) { + if (day < 0 || day > 6) + throw new IllegalArgumentException("Invalid day of week"); + dayOfWeek = setBit(dayOfWeek, 1 << day, enabled); + } + + public byte getHour() { + return hour; + } + + public void setHour(byte hour) { + if (hour < 0 || hour > 23) + throw new IllegalArgumentException("Hour must be between 0 and 23 inclusive"); + this.hour = hour; + } + + public byte getMinute() { + return minute; + } + + public void setMinute(byte minute) { + if (minute < 0 || minute > 59) + throw new IllegalArgumentException("Minute must be between 0 and 59 inclusive"); + this.minute = minute; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_ALARM); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 2) + throwUnexpectedLength(); + + op = params.get(); + index = params.get(); + if (op == OP_GET) { + if (paramsLength != 8) + throwUnexpectedLength(); + + enabled = params.get() == 1; + numOfSnoozes = params.get(); + snoozeTime = params.get(); + dayOfWeek = params.get(); + hour = params.get(); + minute = params.get(); + } else if (op == OP_SET) { + if (paramsLength != 3) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + // No alarm ID for get; all of them are returned + if (op == OP_SET) { + params.put(index); + params.put((byte)(enabled ? 1: 0)); + params.put(numOfSnoozes); + params.put(snoozeTime); + params.put(dayOfWeek); + params.put(hour); + params.put(minute); + } + return LefunConstants.CMD_ALARM; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/BaseCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/BaseCommand.java new file mode 100644 index 000000000..325beeb27 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/BaseCommand.java @@ -0,0 +1,237 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +/** + * Base class for Lefun Bluetooth commands and responses + */ +public abstract class BaseCommand { + // Common constants + /** + * Common get operation type + */ + public static final byte OP_GET = 0; + /** + * Common set operation type + */ + public static final byte OP_SET = 1; + + /** + * Calculates command checksum + * + * @param data the data to generate checksum from + * @param offset the offset in data to start calculating from + * @param length the number of bytes to include in calculation + * @return the computed checksum + */ + public static byte calculateChecksum(byte[] data, int offset, int length) { + int checksum = 0; + for (int i = offset; i < offset + length; ++i) { + byte b = data[i]; + for (int j = 0; j < 8; ++j) { + if (((b ^ checksum) & 1) == 0) { + checksum >>= 1; + } else { + checksum = (checksum ^ 0x18) >> 1 | 0x80; + } + b >>= 1; + } + } + return (byte) checksum; + } + + /** + * When implemented in a subclass, parses the response from a device + * + * @param id the command ID + * @param params the params buffer + */ + abstract protected void deserializeParams(byte id, ByteBuffer params); + + /** + * When implemented in a subclass, provides the arguments to send in the command + * + * @param params the params buffer to write to + * @return the command ID + */ + abstract protected byte serializeParams(ByteBuffer params); + + /** + * Deserialize a response from the device + * + * @param response the response data to deserialize + */ + public void deserialize(byte[] response) { + if (response.length < LefunConstants.CMD_HEADER_LENGTH || response.length < response[1]) + throw new IllegalArgumentException("Response is too short"); + if (calculateChecksum(response, 0, response[1] - 1) != response[response[1] - 1]) + throw new IllegalArgumentException("Incorrect message checksum"); + ByteBuffer buffer = ByteBuffer.wrap(response, LefunConstants.CMD_HEADER_LENGTH - 1, + response[1] - LefunConstants.CMD_HEADER_LENGTH); + buffer.order(ByteOrder.BIG_ENDIAN); + deserializeParams(response[2], buffer); + } + + /** + * Serializes a command to send to the device + * + * @return the data to send to the device + */ + public byte[] serialize() { + ByteBuffer buffer = ByteBuffer.allocate(LefunConstants.CMD_MAX_LENGTH - LefunConstants.CMD_HEADER_LENGTH); + buffer.order(ByteOrder.BIG_ENDIAN); + byte id = serializeParams(buffer); + return makeCommand(id, buffer); + } + + /** + * Builds a command given ID and parameters buffer. + * + * @param id the command ID + * @param params the parameters buffer + * @return the assembled command buffer + */ + protected byte[] makeCommand(byte id, ByteBuffer params) { + if (params.position() > LefunConstants.CMD_MAX_LENGTH - LefunConstants.CMD_HEADER_LENGTH) + throw new IllegalArgumentException("params is too long to fit"); + + int paramsLength = params.position(); + byte[] request = new byte[paramsLength + LefunConstants.CMD_HEADER_LENGTH]; + request[0] = LefunConstants.CMD_REQUEST_ID; + request[1] = (byte) request.length; + request[2] = id; + params.flip(); + params.get(request, LefunConstants.CMD_HEADER_LENGTH - 1, paramsLength); + request[request.length - 1] = calculateChecksum(request, 0, request.length - 1); + return request; + } + + /** + * Throws a standard parameters length exception + */ + protected void throwUnexpectedLength() { + throw new IllegalArgumentException("Unexpected parameters length"); + } + + /** + * Checks for valid command ID and throws if wrong ID provided + * + * @param id command ID from device + * @param expectedId expected command ID + */ + protected void validateId(byte id, byte expectedId) { + if (id != expectedId) + throw new IllegalArgumentException("Wrong command ID"); + } + + /** + * Checks for valid command ID and command length + * + * @param id command ID from device + * @param params params buffer from device + * @param expectedId expected command ID + * @param expectedLength expected params length + */ + protected void validateIdAndLength(byte id, ByteBuffer params, byte expectedId, int expectedLength) { + validateId(id, expectedId); + if (params.limit() - params.position() != expectedLength) + throwUnexpectedLength(); + } + + /** + * Gets whether a bit is set + * + * @param value the value to check against + * @param mask the bitmask + * @return whether the bits indicated by the bitmask are set + */ + protected boolean getBit(int value, int mask) { + return (value & mask) != 0; + } + + /** + * Sets a bit in a value + * + * @param value the value to modify + * @param mask the bitmask + * @param set whether to set or clear the bits + * @return the modified value + */ + protected int setBit(int value, int mask, boolean set) { + if (set) { + return value | mask; + } else { + return value & ~mask; + } + } + + /** + * Sets a bit in a value + * + * @param value the value to modify + * @param mask the bitmask + * @param set whether to set or clear the bits + * @return the modified value + */ + protected short setBit(short value, int mask, boolean set) { + if (set) { + return (short) (value | mask); + } else { + return (short) (value & ~mask); + } + } + + /** + * Sets a bit in a value + * + * @param value the value to modify + * @param mask the bitmask + * @param set whether to set or clear the bits + * @return the modified value + */ + protected byte setBit(byte value, int mask, boolean set) { + if (set) { + return (byte) (value | mask); + } else { + return (byte) (value & ~mask); + } + } + + /** + * Find index of first bit that is set + * + * @param value the value to look at + * @return the index of the lowest set bit, starting at 0 for least significant bit; -1 if no bits set + */ + protected int getLowestSetBitIndex(int value) { + if (value == 0) return -1; + + int i = 0; + while ((value & 1) == 0) { + ++i; + value >>= 1; + } + return i; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/Cmd22Command.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/Cmd22Command.java new file mode 100644 index 000000000..d95391814 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/Cmd22Command.java @@ -0,0 +1,85 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class Cmd22Command extends BaseCommand { + private byte op; + private short unknown; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public short getUnknown() { + return unknown; + } + + public void setUnknown(short unknown) { + this.unknown = unknown; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_UNKNOWN_22); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 3) + throwUnexpectedLength(); + + unknown = params.getShort(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.putShort(unknown); + } + return LefunConstants.CMD_UNKNOWN_22; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/Cmd25Command.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/Cmd25Command.java new file mode 100644 index 000000000..9c86d400d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/Cmd25Command.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class Cmd25Command extends BaseCommand { + private byte op; + private int unknown; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public int getUnknown() { + return unknown; + } + + public void setUnknown(int unknown) { + if (unknown < 0 || unknown > 65000) + throw new IllegalArgumentException("Value must be between 0 and 65000 inclusive"); + this.unknown = unknown; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_UNKNOWN_25); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 5) + throwUnexpectedLength(); + + unknown = params.getInt(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.putInt(unknown); + } + return LefunConstants.CMD_UNKNOWN_25; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FeaturesCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FeaturesCommand.java new file mode 100644 index 000000000..892d83c58 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FeaturesCommand.java @@ -0,0 +1,96 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class FeaturesCommand extends BaseCommand { + public static final int FEATURE_RAISE_TO_WAKE = 0; + public static final int FEATURE_SEDENTARY_REMINDER = 1; + public static final int FEATURE_HYDRATION_REMINDER = 2; + public static final int FEATURE_REMOTE_CAMERA = 3; + public static final int FEATURE_UNKNOWN_4 = 4; + public static final int FEATURE_ANTI_LOST = 5; + + private byte op; + private short features; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public boolean getFeature(int index) { + if (index < 0 || index > 5) + throw new IllegalArgumentException("Index must be between 0 and 5 inclusive"); + return getBit(features, 1 << index); + } + + public void setFeature(int index, boolean enabled) { + if (index < 0 || index > 5) + throw new IllegalArgumentException("Index must be between 0 and 5 inclusive"); + features = setBit(features, 1 << index, enabled); + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_FEATURES); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 3) + throwUnexpectedLength(); + + features = params.getShort(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.putShort(features); + } + return LefunConstants.CMD_FEATURES; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FindDeviceCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FindDeviceCommand.java new file mode 100644 index 000000000..aa3d694bc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FindDeviceCommand.java @@ -0,0 +1,43 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class FindDeviceCommand extends BaseCommand { + private boolean success; + + public boolean isSuccess() { + return success; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_FIND_DEVICE, 1); + + success = params.get() == 1; + } + + @Override + protected byte serializeParams(ByteBuffer params) { + return LefunConstants.CMD_FIND_DEVICE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FindPhoneCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FindPhoneCommand.java new file mode 100644 index 000000000..4190929a6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/FindPhoneCommand.java @@ -0,0 +1,35 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class FindPhoneCommand extends BaseCommand { + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_FIND_PHONE, 0); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + return LefunConstants.CMD_FIND_PHONE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetActivityDataCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetActivityDataCommand.java new file mode 100644 index 000000000..92d2a8ac8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetActivityDataCommand.java @@ -0,0 +1,110 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetActivityDataCommand extends BaseCommand { + private byte daysAgo; + private byte totalRecords; + private byte currentRecord; + private byte year; + private byte month; + private byte day; + private byte hour; + private byte minute; + private short steps; + private short distance; // m + private short calories; // calories + + public byte getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(byte daysAgo) { + if (daysAgo < 0 || daysAgo > 6) + throw new IllegalArgumentException("Days ago must be between 0 and 6 inclusive"); + this.daysAgo = daysAgo; + } + + public byte getTotalRecords() { + return totalRecords; + } + + public byte getCurrentRecord() { + return currentRecord; + } + + public byte getYear() { + return year; + } + + public byte getMonth() { + return month; + } + + public byte getDay() { + return day; + } + + public byte getHour() { + return hour; + } + + public byte getMinute() { + return minute; + } + + public short getSteps() { + return steps; + } + + public short getDistance() { + return distance; + } + + public short getCalories() { + return calories; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_ACTIVITY_DATA, 14); + + daysAgo = params.get(); + totalRecords = params.get(); + currentRecord = params.get(); + year = params.get(); + month = params.get(); + day = params.get(); + hour = params.get(); + minute = params.get(); + steps = params.getShort(); + distance = params.getShort(); + calories = params.getShort(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(daysAgo); + return LefunConstants.CMD_ACTIVITY_DATA; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetBatteryLevelCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetBatteryLevelCommand.java new file mode 100644 index 000000000..d0ff51d29 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetBatteryLevelCommand.java @@ -0,0 +1,43 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetBatteryLevelCommand extends BaseCommand { + private byte batteryLevel; + + public byte getBatteryLevel() { + return batteryLevel; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_BATTERY_LEVEL, 1); + + batteryLevel = params.get(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + return LefunConstants.CMD_BATTERY_LEVEL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetFirmwareInfoCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetFirmwareInfoCommand.java new file mode 100644 index 000000000..5dc94bd65 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetFirmwareInfoCommand.java @@ -0,0 +1,78 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetFirmwareInfoCommand extends BaseCommand { + private short supportCode; + private short devTypeReserveCode; + private String typeCode; + private short hardwareVersion; + private short softwareVersion; + private String vendorCode; + + public short getSupportCode() { + return supportCode; + } + + public short getDevTypeReserveCode() { + return devTypeReserveCode; + } + + public String getTypeCode() { + return typeCode; + } + + public short getHardwareVersion() { + return hardwareVersion; + } + + public short getSoftwareVersion() { + return softwareVersion; + } + + public String getVendorCode() { + return vendorCode; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_FIRMWARE_INFO, 16); + + supportCode = (short) (params.get() | (params.get() << 8)); + devTypeReserveCode = params.getShort(); + byte[] typeCodeBytes = new byte[4]; + params.get(typeCodeBytes); + typeCode = new String(typeCodeBytes, StandardCharsets.US_ASCII); + hardwareVersion = params.getShort(); + softwareVersion = params.getShort(); + byte[] vendorCodeBytes = new byte[4]; + params.get(vendorCodeBytes); + vendorCode = new String(vendorCodeBytes, StandardCharsets.US_ASCII); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + return LefunConstants.CMD_FIRMWARE_INFO; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetPpgDataCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetPpgDataCommand.java new file mode 100644 index 000000000..96e1e08a3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetPpgDataCommand.java @@ -0,0 +1,137 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetPpgDataCommand extends BaseCommand { + private byte ppgType; + private short totalRecords; + private short currentRecord; + private byte year; + private byte month; + private byte day; + private byte hour; + private byte minute; + private byte second; + private byte[] ppgData; + + public int getPpgType() { + return getLowestSetBitIndex(ppgType); + } + + public void setPpgType(int type) { + if (type < 0 || type > 2) + throw new IllegalArgumentException("Invalid PPG type"); + this.ppgType = (byte)(1 << type); + } + + public short getTotalRecords() { + return totalRecords; + } + + public short getCurrentRecord() { + return currentRecord; + } + + public byte getYear() { + return year; + } + + public byte getMonth() { + return month; + } + + public byte getDay() { + return day; + } + + public byte getHour() { + return hour; + } + + public byte getMinute() { + return minute; + } + + public byte getSecond() { + return second; + } + + public byte[] getPpgData() { + return ppgData; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_PPG_DATA); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 9) + throwUnexpectedLength(); + + ppgType = params.get(); + totalRecords = params.get(); + currentRecord = params.get(); + year = params.get(); + month = params.get(); + day = params.get(); + hour = params.get(); + minute = params.get(); + second = params.get(); + + int typeIndex = getPpgType(); + int dataLength; + switch (typeIndex) { + case LefunConstants.PPG_TYPE_HEART_RATE: + case LefunConstants.PPG_TYPE_BLOOD_OXYGEN: + dataLength = 1; + break; + case LefunConstants.PPG_TYPE_BLOOD_PRESSURE: + dataLength = 2; + break; + default: + throw new IllegalArgumentException("Unknown PPG type"); + } + + if (paramsLength < dataLength + 9) + throwUnexpectedLength(); + + ppgData = new byte[dataLength]; + params.get(ppgData); + + // Extended count/index + if (paramsLength == dataLength + 11) + { + totalRecords |= params.get() << 8; + currentRecord |= params.get() << 8; + } + else if (paramsLength > dataLength + 11) { + throwUnexpectedLength(); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(ppgType); + return LefunConstants.CMD_PPG_DATA; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetSleepDataCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetSleepDataCommand.java new file mode 100644 index 000000000..00fa6c0bf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetSleepDataCommand.java @@ -0,0 +1,102 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetSleepDataCommand extends BaseCommand { + public static final int SLEEP_TYPE_AWAKE = 1; + public static final int SLEEP_TYPE_LIGHT_SLEEP = 2; + public static final int SLEEP_TYPE_DEEP_SLEEP = 3; + + private byte daysAgo; + private short totalRecords; + private short currentRecord; + private byte year; + private byte month; + private byte day; + private byte hour; + private byte minute; + private byte sleepType; + + public byte getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(byte daysAgo) { + if (daysAgo < 0 || daysAgo > 6) + throw new IllegalArgumentException("Days ago must be between 0 and 6 inclusive"); + this.daysAgo = daysAgo; + } + + public short getTotalRecords() { + return totalRecords; + } + + public short getCurrentRecord() { + return currentRecord; + } + + public byte getYear() { + return year; + } + + public byte getMonth() { + return month; + } + + public byte getDay() { + return day; + } + + public byte getHour() { + return hour; + } + + public byte getMinute() { + return minute; + } + + public byte getSleepType() { + return sleepType; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_SLEEP_DATA, 11); + + daysAgo = params.get(); + totalRecords = params.getShort(); + currentRecord = params.getShort(); + year = params.get(); + month = params.get(); + day = params.get(); + hour = params.get(); + minute = params.get(); + sleepType = params.get(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(daysAgo); + return LefunConstants.CMD_SLEEP_DATA; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetSleepTimeCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetSleepTimeCommand.java new file mode 100644 index 000000000..d0f7d722e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetSleepTimeCommand.java @@ -0,0 +1,75 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetSleepTimeCommand extends BaseCommand { + private byte daysAgo; + private byte year; + private byte month; + private byte day; + private short minutes; + + public byte getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(byte daysAgo) { + if (daysAgo < 0 || daysAgo > 6) + throw new IllegalArgumentException("Days ago must be between 0 and 6 inclusive"); + this.daysAgo = daysAgo; + } + + public byte getYear() { + return year; + } + + public byte getMonth() { + return month; + } + + public byte getDay() { + return day; + } + + public short getMinutes() { + return minutes; + } + + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_SLEEP_TIME_DATA, 6); + + daysAgo = params.get(); + year = params.get(); + month = params.get(); + day = params.get(); + minutes = params.getShort(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(daysAgo); + return LefunConstants.CMD_SLEEP_TIME_DATA; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetStepsDataCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetStepsDataCommand.java new file mode 100644 index 000000000..f365c94d6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/GetStepsDataCommand.java @@ -0,0 +1,86 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class GetStepsDataCommand extends BaseCommand { + private byte daysAgo; + private byte year; + private byte month; + private byte day; + private int steps; + private int distance; // m + private int calories; // calories + + public byte getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(byte daysAgo) { + if (daysAgo < 0 || daysAgo > 6) + throw new IllegalArgumentException("Days ago must be between 0 and 6 inclusive"); + this.daysAgo = daysAgo; + } + + public byte getYear() { + return year; + } + + public byte getMonth() { + return month; + } + + public byte getDay() { + return day; + } + + public int getSteps() { + return steps; + } + + public int getDistance() { + return distance; + } + + public int getCalories() { + return calories; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_STEPS_DATA, 16); + + daysAgo = params.get(); + year = params.get(); + month = params.get(); + day = params.get(); + steps = params.getInt(); + distance = params.getInt(); + calories = params.getInt(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(daysAgo); + return LefunConstants.CMD_STEPS_DATA; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/HydrationReminderIntervalCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/HydrationReminderIntervalCommand.java new file mode 100644 index 000000000..2ec2ea956 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/HydrationReminderIntervalCommand.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class HydrationReminderIntervalCommand extends BaseCommand { + private byte op; + private byte hydrationReminderInterval; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public byte getHydrationReminderInterval() { + return hydrationReminderInterval; + } + + public void setHydrationReminderInterval(byte hydrationReminderInterval) { + if (hydrationReminderInterval == 0) + throw new IllegalArgumentException("Interval must be non-zero"); + this.hydrationReminderInterval = hydrationReminderInterval; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_HYDRATION_REMINDER_INTERVAL); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + hydrationReminderInterval = params.get(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.put(hydrationReminderInterval); + } + return LefunConstants.CMD_HYDRATION_REMINDER_INTERVAL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/NotificationCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/NotificationCommand.java new file mode 100644 index 000000000..c6dd79a3f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/NotificationCommand.java @@ -0,0 +1,125 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class NotificationCommand extends BaseCommand { + public static final byte SERVICE_TYPE_CALL = 0; + public static final byte SERVICE_TYPE_TEXT = 1; + public static final byte SERVICE_TYPE_QQ = 2; + public static final byte SERVICE_TYPE_WECHAT = 3; + public static final byte SERVICE_TYPE_EXTENDED = 4; + + public static final byte EXTENDED_SERVICE_TYPE_FACEBOOK = 1; + public static final byte EXTENDED_SERVICE_TYPE_TWITTER = 2; + public static final byte EXTENDED_SERVICE_TYPE_LINKEDIN = 3; + public static final byte EXTENDED_SERVICE_TYPE_WHATSAPP = 4; + public static final byte EXTENDED_SERVICE_TYPE_LINE = 5; + public static final byte EXTENDED_SERVICE_TYPE_KAKAOTALK = 6; + + public static final int MAX_PAYLOAD_LENGTH = 13; + public static final int MAX_MESSAGE_LENGTH = 254; + + private byte serviceType; + private byte totalPieces; + private byte currentPiece; + private byte extendedServiceType; + private byte[] payload; + + public int getServiceType() { + return getLowestSetBitIndex(serviceType); + } + + public void setServiceType(int type) { + if (type < 0 || type > 4) + throw new IllegalArgumentException("Invalid service type"); + this.serviceType = (byte) (1 << type); + } + + public byte getTotalPieces() { + return totalPieces; + } + + public void setTotalPieces(byte totalPieces) { + // This check isn't on device, but should probably be added + if (totalPieces == 0) + throw new IllegalArgumentException("Total pieces must not be 0"); + this.totalPieces = totalPieces; + } + + public byte getCurrentPiece() { + return currentPiece; + } + + public void setCurrentPiece(byte currentPiece) { + // This check isn't on device, but should probably be added + if (currentPiece == 0) + throw new IllegalArgumentException("Current piece must not be 0"); + this.currentPiece = currentPiece; + } + + public byte getExtendedServiceType() { + return extendedServiceType; + } + + public void setExtendedServiceType(byte extendedServiceType) { + this.extendedServiceType = extendedServiceType; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + if (payload == null) + throw new IllegalArgumentException("Payload must not be null"); + if (payload.length > 13) + throw new IllegalArgumentException("Payload is too long"); + this.payload = payload; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + // We should not receive a response for this + throw new UnsupportedOperationException(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + boolean hasExtendedServiceType = (serviceType & (1 << SERVICE_TYPE_EXTENDED)) != 0 + && (extendedServiceType & 0x0f) != 0; + int maxPayloadLength = MAX_PAYLOAD_LENGTH; + if (hasExtendedServiceType) maxPayloadLength -= 1; + + if (payload.length > maxPayloadLength) + throw new IllegalStateException("Payload is too long"); + + params.put(serviceType); + params.put(totalPieces); + params.put(currentPiece); + if (hasExtendedServiceType) + params.put(extendedServiceType); + params.put(payload); + + return LefunConstants.CMD_NOTIFICATION; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/PpgResultCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/PpgResultCommand.java new file mode 100644 index 000000000..1e66ee9d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/PpgResultCommand.java @@ -0,0 +1,73 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class PpgResultCommand extends BaseCommand { + private byte ppgType; + private byte[] ppgData; + + public int getPpgType() { + return getLowestSetBitIndex(ppgType); + } + + public byte[] getPpgData() { + return ppgData; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_PPG_RESULT); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + ppgType = params.get(); + + int typeIndex = getPpgType(); + int dataLength; + switch (typeIndex) { + case LefunConstants.PPG_TYPE_HEART_RATE: + case LefunConstants.PPG_TYPE_BLOOD_OXYGEN: + dataLength = 1; + break; + case LefunConstants.PPG_TYPE_BLOOD_PRESSURE: + dataLength = 2; + break; + default: + throw new IllegalArgumentException("Unknown PPG type"); + } + + if (paramsLength != dataLength + 1) + throwUnexpectedLength(); + + ppgData = new byte[dataLength]; + params.get(ppgData); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + // No handler on device side + throw new UnsupportedOperationException(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/ProfileCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/ProfileCommand.java new file mode 100644 index 000000000..e95f57d83 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/ProfileCommand.java @@ -0,0 +1,131 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class ProfileCommand extends BaseCommand { + public static final byte GENDER_FEMALE = 0; + public static final byte GENDER_MALE = 1; + + private byte op; + private byte gender; + private byte height; // cm + private byte weight; // kg + private byte age; // years + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public byte getGender() { + return gender; + } + + public void setGender(byte gender) { + if (gender != GENDER_FEMALE && gender != GENDER_MALE) + throw new IllegalArgumentException("Invalid gender"); + this.gender = gender; + } + + public byte getHeight() { + return height; + } + + public void setHeight(byte height) { + int intHeight = (int)height & 0xff; + if (intHeight < 40 || intHeight > 210) + throw new IllegalArgumentException("Height must be between 40 and 210 cm inclusive"); + this.height = height; + } + + public byte getWeight() { + return weight; + } + + public void setWeight(byte weight) { + int intWeight = (int)weight & 0xff; + if (intWeight < 5 || intWeight > 200) + throw new IllegalArgumentException("Weight must be between 5 and 200 kg inclusive"); + this.weight = weight; + } + + public byte getAge() { + return age; + } + + public void setAge(byte age) { + if (age < 0 || age > 110) + throw new IllegalArgumentException("Age must be between 0 and 110 years inclusive"); + this.age = age; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_PROFILE); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 5) + throwUnexpectedLength(); + + gender = params.get(); + height = params.get(); + weight = params.get(); + age = params.get(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.put(gender); + params.put(height); + params.put(weight); + params.put(age); + } + return LefunConstants.CMD_PROFILE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/RemoteCameraTriggeredCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/RemoteCameraTriggeredCommand.java new file mode 100644 index 000000000..f385e3fb1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/RemoteCameraTriggeredCommand.java @@ -0,0 +1,36 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class RemoteCameraTriggeredCommand extends BaseCommand { + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_REMOTE_CAMERA_TRIGGERED, 0); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + // No handler on device side + throw new UnsupportedOperationException(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/RequestBondingCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/RequestBondingCommand.java new file mode 100644 index 000000000..b4b925538 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/RequestBondingCommand.java @@ -0,0 +1,46 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class RequestBondingCommand extends BaseCommand { + public static final byte STATUS_ALREADY_BONDED = 0; + public static final byte STATUS_BONDING_SUCCESSFUL = 1; + + private byte status; + + public byte getStatus() { + return status; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_BONDING_REQUEST, 1); + + status = params.get(); + } + + @Override + protected byte serializeParams(ByteBuffer params) { + return LefunConstants.CMD_BONDING_REQUEST; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SedentaryReminderIntervalCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SedentaryReminderIntervalCommand.java new file mode 100644 index 000000000..d85bd860e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SedentaryReminderIntervalCommand.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class SedentaryReminderIntervalCommand extends BaseCommand { + private byte op; + private byte sedentaryReminderInterval; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public byte getSedentaryReminderInterval() { + return sedentaryReminderInterval; + } + + public void setSedentaryReminderInterval(byte sedentaryReminderInterval) { + if (sedentaryReminderInterval == 0) + throw new IllegalArgumentException("Interval must be non-zero"); + this.sedentaryReminderInterval = sedentaryReminderInterval; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_SEDENTARY_REMINDER_INTERVAL); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + sedentaryReminderInterval = params.get(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.put(sedentaryReminderInterval); + } + return LefunConstants.CMD_SEDENTARY_REMINDER_INTERVAL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SetLanguageCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SetLanguageCommand.java new file mode 100644 index 000000000..1ea1e7d06 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SetLanguageCommand.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class SetLanguageCommand extends BaseCommand { + private byte language; + + private boolean setSuccess; + + public byte getLanguage() { + return language; + } + + public void setLanguage(byte language) { + this.language = language; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_LANGUAGE, 1); + + setSuccess = params.get() == 1; + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(language); + return LefunConstants.CMD_LANGUAGE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SetRemoteCameraCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SetRemoteCameraCommand.java new file mode 100644 index 000000000..c4dc1ae73 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SetRemoteCameraCommand.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class SetRemoteCameraCommand extends BaseCommand { + private boolean remoteCameraEnabled; + + private boolean setSuccess; + + public boolean getRemoteCameraEnabled() { + return remoteCameraEnabled; + } + + public void setRemoteCameraEnabled(boolean remoteCameraEnabled) { + this.remoteCameraEnabled = remoteCameraEnabled; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_REMOTE_CAMERA, 1); + + setSuccess = params.get() == 1; + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put((byte)(remoteCameraEnabled ? 1 : 0)); + return LefunConstants.CMD_REMOTE_CAMERA; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SettingsCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SettingsCommand.java new file mode 100644 index 000000000..10322ecbc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/SettingsCommand.java @@ -0,0 +1,118 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class SettingsCommand extends BaseCommand { + public static final byte AM_PM_24_HOUR = 0; + public static final byte AM_PM_12_HOUR = 1; + public static final byte MEASUREMENT_UNIT_METRIC = 0; + public static final byte MEASUREMENT_UNIT_IMPERIAL = 1; + + private byte op; + private byte option1; + private byte amPmIndicator; + private byte measurementUnit; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public byte getOption1() { + return option1; + } + + public void setOption1(byte option1) { + if (option1 != (byte)0xff && (option1 < 0 || option1 > 24)) + throw new IllegalArgumentException("option1 must be between 0 and 24 inclusive"); + this.option1 = option1; + } + + public byte getAmPmIndicator() { + return amPmIndicator; + } + + public void setAmPmIndicator(byte amPmIndicator) { + if (amPmIndicator != (byte)0xff && (amPmIndicator != AM_PM_12_HOUR && amPmIndicator != AM_PM_24_HOUR)) + throw new IllegalArgumentException("Indicator must be 12 or 24 hours"); + this.amPmIndicator = amPmIndicator; + } + + public byte getMeasurementUnit() { + return measurementUnit; + } + + public void setMeasurementUnit(byte measurementUnit) { + if (measurementUnit != (byte)0xff && (measurementUnit != MEASUREMENT_UNIT_METRIC && measurementUnit != MEASUREMENT_UNIT_IMPERIAL)) + throw new IllegalArgumentException(("Unit must be metric or imperial")); + this.measurementUnit = measurementUnit; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_SETTINGS); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 4) + throwUnexpectedLength(); + + option1 = params.get(); + amPmIndicator = params.get(); + measurementUnit = params.get(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.put(option1); + params.put(amPmIndicator); + params.put(measurementUnit); + } + return LefunConstants.CMD_SETTINGS; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/StartPpgSensingCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/StartPpgSensingCommand.java new file mode 100644 index 000000000..f03403251 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/StartPpgSensingCommand.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class StartPpgSensingCommand extends BaseCommand { + private byte ppgType; + + private boolean setSuccess; + + public int getPpgType() { + return getLowestSetBitIndex(ppgType); + } + + public void setPpgType(int type) { + if (type < 0 || type > 2) + throw new IllegalArgumentException("Invalid PPG type"); + this.ppgType = (byte)(1 << type); + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateIdAndLength(id, params, LefunConstants.CMD_PPG_START, 2); + + ppgType = params.get(); + setSuccess = params.get() == 1; + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(ppgType); + return LefunConstants.CMD_PPG_START; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/TimeCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/TimeCommand.java new file mode 100644 index 000000000..dd60185f0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/TimeCommand.java @@ -0,0 +1,150 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class TimeCommand extends BaseCommand { + private byte op; + private byte year; + private byte month; + private byte day; + private byte hour; + private byte minute; + private byte second; + + private boolean setSuccess; + + public byte getOp() { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + return op; + } + + public void setOp(byte op) { + this.op = op; + } + + public byte getYear() { + return year; + } + + public void setYear(byte year) { + this.year = year; + } + + public byte getMonth() { + return month; + } + + public void setMonth(byte month) { + if (month < 1 || month > 12) + throw new IllegalArgumentException("Month must be between 1 and 12 inclusive"); + this.month = month; + } + + public byte getDay() { + return day; + } + + public void setDay(byte day) { + if (day < 1 || day > 31) + throw new IllegalArgumentException("Day must be between 1 and 31 inclusive"); + this.day = day; + } + + public byte getHour() { + return hour; + } + + public void setHour(byte hour) { + if (hour < 0 || hour > 23) + throw new IllegalArgumentException("Hour must be between 0 and 23 inclusive"); + this.hour = hour; + } + + public byte getMinute() { + return minute; + } + + public void setMinute(byte minute) { + if (minute < 0 || minute > 59) + throw new IllegalArgumentException("Minute must be between 0 and 59 inclusive"); + this.minute = minute; + } + + public byte getSecond() { + return second; + } + + public void setSecond(byte second) { + if (second < 0 || second > 59) + throw new IllegalArgumentException("Second must be between 0 and 59 inclusive"); + this.second = second; + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_TIME); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 7) + throwUnexpectedLength(); + + year = params.get(); + month = params.get(); + day = params.get(); + hour = params.get(); + minute = params.get(); + second = params.get(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.put(year); + params.put(month); + params.put(day); + params.put(hour); + params.put(minute); + params.put(second); + } + return LefunConstants.CMD_TIME; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/UiPagesCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/UiPagesCommand.java new file mode 100644 index 000000000..a304eff55 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lefun/commands/UiPagesCommand.java @@ -0,0 +1,92 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; + +public class UiPagesCommand extends BaseCommand { + // I don't know which pages since they're not implemented in my watch, so no + // constants here for now + + private byte op; + private short pages; + + private boolean setSuccess; + + public byte getOp() { + return op; + } + + public void setOp(byte op) { + if (op != OP_GET && op != OP_SET) + throw new IllegalArgumentException("Operation must be get or set"); + this.op = op; + } + + public boolean getPage(int index) { + if (index < 0 || index >= 16) + throw new IllegalArgumentException("Index must be between 0 and 15 inclusive"); + return getBit(pages, 1 << index); + } + + public void setPage(int index, boolean enabled) { + if (index < 0 || index >= 16) + throw new IllegalArgumentException("Index must be between 0 and 15 inclusive"); + pages = setBit(pages, 1 << index, enabled); + } + + public boolean isSetSuccess() { + return setSuccess; + } + + @Override + protected void deserializeParams(byte id, ByteBuffer params) { + validateId(id, LefunConstants.CMD_UI_PAGES); + + int paramsLength = params.limit() - params.position(); + if (paramsLength < 1) + throwUnexpectedLength(); + + op = params.get(); + if (op == OP_GET) { + if (paramsLength != 3) + throwUnexpectedLength(); + + pages = params.getShort(); + } else if (op == OP_SET) { + if (paramsLength != 2) + throwUnexpectedLength(); + + setSuccess = params.get() == 1; + } else { + throw new IllegalArgumentException("Invalid operation type received"); + } + } + + @Override + protected byte serializeParams(ByteBuffer params) { + params.put(op); + if (op == OP_SET) { + params.putShort(pages); + } + return LefunConstants.CMD_UI_PAGES; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java index bcae0f956..accc80782 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java @@ -20,12 +20,13 @@ package nodomain.freeyourgadget.gadgetbridge.devices.miband; import android.content.Context; import android.net.Uri; +import androidx.annotation.NonNull; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import androidx.annotation.NonNull; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -133,9 +134,8 @@ public class MiBandFWHelper extends AbstractMiBandFWHelper { * @return * @throws IllegalArgumentException when the data is not recognized as firmware data */ - public static @NonNull - AbstractMiFirmwareInfo determineFirmwareInfoFor(byte[] wholeFirmwareBytes) { + public static AbstractMiFirmwareInfo determineFirmwareInfoFor(byte[] wholeFirmwareBytes) { return AbstractMiFirmwareInfo.determineFirmwareInfoFor(wholeFirmwareBytes); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutConstants.java new file mode 100644 index 000000000..480fc6644 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutConstants.java @@ -0,0 +1,114 @@ +/* Copyright (C) 2020 Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + */ +package nodomain.freeyourgadget.gadgetbridge.devices.nut; + +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; + +public class NutConstants { + /** + * Just battery info + */ + public static final UUID SERVICE_BATTERY = GattService.UUID_SERVICE_BATTERY_SERVICE; + public static final UUID CHARAC_BATTERY_INFO = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb"); + + /** + * Device info available. + **/ + public static final UUID SERVICE_DEVICE_INFO = GattService.UUID_SERVICE_DEVICE_INFORMATION; + /** + * Firmware version. + * Used with {@link NutConstants#SERVICE_DEVICE_INFO} + */ + public static final UUID CHARAC_FIRMWARE_VERSION = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb"); + /** + * System ID. + * Used with {@link NutConstants#SERVICE_DEVICE_INFO} + */ + public static final UUID CHARAC_SYSTEM_ID = UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb"); + /** + * Hardware version. + * Used with {@link NutConstants#SERVICE_DEVICE_INFO} + */ + public static final UUID CHARAC_HARDWARE_VERSION = UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb"); + /** + * Manufacturer name. + * Used with {@link NutConstants#SERVICE_DEVICE_INFO} + */ + public static final UUID CHARAC_MANUFACTURER_NAME = UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb"); + + + /** + * Link loss alert service. + */ + public static final UUID SERVICE_LINK_LOSS = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb"); + + + /** + * Immediate alert service. + */ + public static final UUID SERVICE_IMMEDIATE_ALERT = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb"); + /** + * Immediate alert level + * Used with {@link NutConstants#SERVICE_IMMEDIATE_ALERT} + */ + public static final UUID CHARAC_LINK_LOSS_ALERT_LEVEL = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb"); + + + /** + * Proprietary command endpoint. + * TODO: Anything else in this service on other devices? + */ + public static final UUID SERVICE_PROPRIETARY_NUT = UUID.fromString("0000ff00-0000-1000-8000-00805f9b34fb"); + /** + * Shutdown or reset. + * Used with {@link NutConstants#SERVICE_PROPRIETARY_NUT} + */ + public static final UUID CHARAC_CHANGE_POWER = UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb"); + /** + * Commands for proprietary service. + * Used with {@link NutConstants#SERVICE_PROPRIETARY_NUT} + */ + public static final UUID CHARAC_DFU_PW = UUID.fromString("0000ff02-0000-1000-8000-00805f9b34fb"); + /** + * Authentication using 16-byte key? + * Used with {@link NutConstants#SERVICE_PROPRIETARY_NUT} + * TODO: Exists only on Nut Mini? + */ + public static final UUID CHARAC_AUTH_STATUS = UUID.fromString("0000ff05-0000-1000-8000-00805f9b34fb"); + + + /** + * Ringing configuration. + * TODO: Exact purpose? + */ + public static final UUID SERVICE_UNKNOWN_2 = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"); + /** + * Ringing configuration. + * Used with {@link NutConstants#SERVICE_UNKNOWN_2} + * TODO: Something else on other devices? + */ + public static final UUID CHARAC_UNKNOWN_2 = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"); + + + /** + * Very little mention online, specific to Nut devices? + */ + public static final UUID UNKNOWN_3 = UUID.fromString("00001530-0000-1000-8000-00805f9b34fb"); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutCoordinator.java new file mode 100644 index 000000000..704ef2ed3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutCoordinator.java @@ -0,0 +1,164 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti, Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.devices.nut; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.bluetooth.le.ScanFilter; +import android.content.Context; +import android.net.Uri; +import android.os.Build; + +import androidx.annotation.NonNull; + +import java.util.Collection; +import java.util.Collections; + +import nodomain.freeyourgadget.gadgetbridge.R; +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 NutCoordinator extends AbstractDeviceCoordinator { + @Override + @NonNull + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + String name = candidate.getDevice().getName(); + if (name != null && name.toLowerCase().startsWith("nut")) { + return DeviceType.NUTMINI; + } + return DeviceType.UNKNOWN; + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_ASK; + } + + @NonNull + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Collection createBLEScanFilters() { + ScanFilter filter = new ScanFilter.Builder() + .setDeviceName("nut") // Nut Mini + .build(); + return Collections.singletonList(filter); + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.NUTMINI; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_nutmini, + }; + } + + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + 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 boolean supportsScreenshots() { + 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 "Nut"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; //TODO: RRSI + } + + @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) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutKey.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutKey.java new file mode 100644 index 000000000..2b00d4270 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/nut/NutKey.java @@ -0,0 +1,266 @@ +/* Copyright (C) 2020 Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ +package nodomain.freeyourgadget.gadgetbridge.devices.nut; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.AbstractMap; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class NutKey { + private static final Logger LOG = LoggerFactory.getLogger(NutKey.class); + + /** + * Different from {@link GB#hexStringToByteArray} because + * it returns an array of zero bytes when it's given zero bytes + *

+ * https://stackoverflow.com/a/140430/4636860 + * + * @param encoded hexadecimal string like "0xcafebabe", "DEADBEEF" or "feeddead" + * @return resulting byte array + */ + public static byte[] hexStringToByteArrayNut(String encoded) { + if (encoded.startsWith("0x")) { + encoded = encoded.substring(2); + } + + if ((encoded.length() % 2) != 0) { + throw new IllegalArgumentException("Input string must contain an even number of characters"); + } + + final byte[] result = new byte[encoded.length() / 2]; + final char[] enc = encoded.toCharArray(); + for (int i = 0; i < enc.length; i += 2) { + result[i / 2] = (byte) Integer.parseInt(String.valueOf(enc[i]) + enc[i + 1], 16); + } + return result; + } + + /** + * Returns the array as hexadecimal string space delimited + * + * @param bytes bytes to return + * @return returns + */ + public static String bytesToHex2(byte[] bytes) { + char[] hexChars = new char[bytes.length * 3]; + for (int j = 0; j < bytes.length; j++) { + hexChars[j * 3] = GB.HEX_CHARS[(bytes[j] & 0xFF) >>> 4]; + hexChars[j * 3 + 1] = GB.HEX_CHARS[(bytes[j] & 0xFF) & 0x0F]; + hexChars[j * 3 + 2] = " ".toCharArray()[0]; + } + return new String(hexChars).toLowerCase(); + } + + /** + * Generates an assembled packet based on inputs + * + * @param mac the mac of the target device (AA:BB:CC:DD:EE:FF) + * @param challenge the received challenge, THE ENTIRE PAYLOAD WITH PREAMBLE! + * @param key1 first key + * @param key2 second key + * @return assembled packet (without the preamble!) + */ + public static byte[] passwordGeneration(String mac, byte[] challenge, BigInteger key1, BigInteger key2) { + if (challenge[0] != 0x01) { + throw new IllegalArgumentException("Challenge must be given with the preamble"); + } + + byte[] mac_as_bytes = macAsByteArray(mac); + byte[] correct_challenge = new byte[challenge.length - 1]; + System.arraycopy(challenge, 1, correct_challenge, 0, challenge.length - 1); + ArrayUtils.reverse(correct_challenge); + BigInteger c = new BigInteger(1, mac_as_bytes).add(new BigInteger(1, correct_challenge)); + + BigInteger max64 = BigInteger.ONE.add(BigInteger.ONE).pow(64).subtract(BigInteger.ONE); + BigInteger tmp1 = key2.xor(max64); + BigInteger result1; + if (c.compareTo(tmp1) > 0) { + result1 = c.add(key1).subtract(tmp1); + } else { + result1 = key1; + } + + BigInteger result2; + if (key2.remainder(BigInteger.ONE.add(BigInteger.ONE)).compareTo(BigInteger.ONE) == 0) { + result2 = key2.add(c); + } else { + result2 = key2.multiply(BigInteger.ONE.add(BigInteger.ONE)).add(c); + } + + return byteArraysConcatReverseWithPad(result1.toByteArray(), result2.toByteArray()); + } + + /** + * Reverses the password generation into keys + *

+ * This assumes you have: + * The MAC of the device, the challenge and response payload + *

+ * See also {@link NutKey#passwordGeneration} + * + * @param challenge the RECEIVED and COMPLETE payload (truncated accordingly) + * @param response the SENT and COMPLETE payload + * @param deviceMac colon-separated MAC address of the Nut as a string + */ + public static Map.Entry reversePasswordGeneration(byte[] challenge, + byte[] response, + String deviceMac) { + // The two arrays that were concat. with byteArraysConcatReverseWithPad(orig1, orig2) + byte[] original1 = new byte[8]; + byte[] original2 = new byte[8]; + + // The response without preamble + if (response[0] != 0x02) { + throw new IllegalArgumentException("Response always begins with 0x02"); + } + byte[] cleanResponse = new byte[16]; + System.arraycopy(response, 1, cleanResponse, 0, cleanResponse.length); + + // The challenge without preamble + byte[] cleanChall = new byte[4]; + System.arraycopy(challenge, 1, cleanChall, 0, cleanChall.length); + + // Reverse the two arrays sent as a response + byteArraysDeConcatReverseWithPad(cleanResponse, original1, original2); + + // Two common components in the equation + BigInteger a = new BigInteger(1, macAsByteArray(deviceMac)); + byte[] cleanChallTmp = cleanChall.clone(); + ArrayUtils.reverse(cleanChallTmp); + BigInteger b = new BigInteger(1, cleanChallTmp); + BigInteger c = a.add(b); + BigInteger max64 = BigInteger.ONE.add(BigInteger.ONE).pow(64).subtract(BigInteger.ONE); + + // We don't know actual keys used yet + // There's two possibilities, + // either it's directly what's in the packet + // orig1 = key1 + BigInteger key1a = new BigInteger(1, original1); + // Or it's derived from key2 using this formula :S + // orig1 = c + key1 - (key2 XOR (2^64 - 1) + // see below when it might be needed + + // It's either just + // orig2 = key2 + c + BigInteger key2a = new BigInteger(1, original2).subtract(c); + // alternatively + // orig2 = 2 * key2 + c + BigInteger key2b = new BigInteger(1, original2).multiply(BigInteger.ONE.add(BigInteger.ONE)).subtract(c); + + // Now we have key2a, key2b, key1a, + // trying to determine if we can do with just those, + // or need to continue + byte[] key1a2aresult = passwordGeneration(deviceMac, challenge, key1a, key2a); + LOG.debug("Result1a2a:02 %s\n", bytesToHex2(key1a2aresult)); + + if (java.util.Arrays.equals(key1a2aresult, cleanResponse)) { + LOG.debug("Found key1a & key2a are correct, DONE!"); + return new AbstractMap.SimpleEntry<>(key1a, key2a); + } + // Unsuccessful, let's try key1a with key2b + + byte[] key1a2bresult = passwordGeneration(deviceMac, challenge, key1a, key2b); + LOG.debug("Result1a2b:02 %s\n", bytesToHex2(key1a2bresult)); + + if (java.util.Arrays.equals(key1a2bresult, cleanResponse)) { + LOG.debug("Found key1a & key2b are correct, DONE!"); + return new AbstractMap.SimpleEntry<>(key1a, key2b); + } + + // If we're still not done, we have to calculate two possible key1b-s + // one for key2a, other for key2b + // key1 = c + (key2 XOR (2^64 - 1) + orig1 + BigInteger key1b2a = c.add(key2a.xor(max64)).add(new BigInteger(1, original1)); + byte[] key1b2aresult = passwordGeneration(deviceMac, challenge, key1b2a, key2a); + LOG.debug("Result1b2a:02 %s\n", bytesToHex2(key1b2aresult)); + + if (java.util.Arrays.equals(key1b2aresult, cleanResponse)) { + LOG.debug("Found key1b2a & key2b are correct, DONE!"); + return new AbstractMap.SimpleEntry<>(key1b2a, key2b); + } + + BigInteger key1b2b = c.add(key2b.xor(max64)).add(new BigInteger(1, original1)); + byte[] key1b2bresult = passwordGeneration(deviceMac, challenge, key1b2b, key2b); + LOG.debug("Result1b2b:02 %s\n", bytesToHex2(key1b2bresult)); + + if (java.util.Arrays.equals(key1b2bresult, cleanResponse)) { + LOG.debug("Found key1b2b & key2b are correct, DONE!"); + return new AbstractMap.SimpleEntry<>(key1b2b, key2b); + } + + LOG.warn("Input might be incorrect, a correct key was not found"); + return null; + } + + /** + * Turns the MAC address into an array of bytes + * + * @param address MAC address + * @return byte[] containing the MAC address bytes + */ + public static byte[] macAsByteArray(String address) { + //noinspection DynamicRegexReplaceableByCompiledPattern + return hexStringToByteArrayNut(address.replace(":", "")); + } + + /** + * Concatenates two byte arrays in reverse and pads with zeros + * + * @param arr1 first array to concatenate + * @param arr2 second array to concatenate + * @return 16 bytes that contain the array in reverse, zeros if any parameter is empty + */ + public static byte[] byteArraysConcatReverseWithPad(byte[] arr1, byte[] arr2) { + byte[] result = new byte[16]; + for (int i = 0; i < Math.min(arr2.length, 8); i++) { + // Reverse the array - 0-indexed - start shorter arrays from "0" - byte index to handle + result[8 - 1 - (8 - Math.min(arr2.length, 8)) - i] = arr2[i + Math.max((arr2.length - 8), 0)]; + } + for (int i = 0; i < Math.min(arr1.length, 8); i++) { + result[16 - 1 - (8 - Math.min(arr1.length, 8)) - i] = arr1[i + Math.max((arr1.length - 8), 0)]; + } + return result; + } + + /** + * De-concatenates two byte arrays in reverse, + * places them in specified destinations, + *

+ * 16 bytes that contain the array in reverse, + * zeros if any parameter is empty + * + * @param input array to de-concatenate, 16 bytes + */ + public static void byteArraysDeConcatReverseWithPad(byte[] input, byte[] dest1, byte[] dest2) { + if (input.length != 16) { + throw new IllegalArgumentException("Input must be 16 bytes!"); + } + for (int i = 0; i < 8; i++) { + dest2[8 - 1 - i] = input[i]; + } + for (int i = 8; i < 16; i++) { + dest1[16 - 1 - i] = input[i]; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeDFUService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeDFUService.java new file mode 100644 index 000000000..0cd6761f0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeDFUService.java @@ -0,0 +1,35 @@ +/* Copyright (C) 2020 Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.pinetime; + +import android.app.Activity; + +import no.nordicsemi.android.dfu.DfuBaseService; +import nodomain.freeyourgadget.gadgetbridge.BuildConfig; +import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity; + +public class PineTimeDFUService extends DfuBaseService { + @Override + protected Class getNotificationTarget() { + return FwAppInstallerActivity.class; + } + + @Override + protected boolean isDebug() { + return BuildConfig.DEBUG; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java new file mode 100644 index 000000000..d3e7a6632 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeInstallHandler.java @@ -0,0 +1,106 @@ +/* Copyright (C) 2020 Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.pinetime; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; + +public class PineTimeInstallHandler implements InstallHandler { + private static final Logger LOG = LoggerFactory.getLogger(PineTimeInstallHandler.class); + + private final Context context; + private boolean valid = false; + private String version = "(Unknown version)"; + + public PineTimeInstallHandler(Uri uri, Context context) { + this.context = context; + UriHelper uriHelper; + try { + uriHelper = UriHelper.get(uri, this.context); + } catch (IOException e) { + valid = false; + return; + } + + try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) { + byte[] bytes = new byte[32]; + int read = in.read(bytes); + if (read < 32) { + valid = false; + return; + } + } catch (Exception e) { + valid = false; + return; + } + valid = true; + } + + @Override + public void validateInstallation(InstallActivity installActivity, GBDevice device) { + if (device.isBusy()) { + installActivity.setInfoText(device.getBusyTask()); + installActivity.setInstallEnabled(false); + return; + } + + if (device.getType() != DeviceType.PINETIME_JF || !device.isConnected()) { + installActivity.setInfoText("Firmware cannot be installed"); + installActivity.setInstallEnabled(false); + return; + } + + GenericItem installItem = new GenericItem(); + installItem.setIcon(R.drawable.ic_firmware); + installItem.setName("PineTime firmware"); + installItem.setDetails(version); + + installActivity.setInfoText(context.getString(R.string.firmware_install_warning, "(unknown)")); + installActivity.setInstallEnabled(true); + installActivity.setInstallItem(installItem); + LOG.debug("Initialized PineTimeInstallHandler"); + } + + + @Override + public void onStartInstall(GBDevice device) { + } + + @Override + public boolean isValid() { + return valid; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java new file mode 100644 index 000000000..4195d66b6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java @@ -0,0 +1,20 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.pinetime; + +import java.util.UUID; + +public class PineTimeJFConstants { + public static final UUID UUID_SERVICE_MUSIC_CONTROL = UUID.fromString("c7e50001-00fc-48fe-8e23-433b3a1942d0"); + + public static final UUID UUID_CHARACTERISTICS_MUSIC_EVENT = UUID.fromString("c7e50002-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_STATUS = UUID.fromString("c7e50003-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_ARTIST = UUID.fromString("c7e50004-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_TRACK = UUID.fromString("c7e50005-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_ALBUM = UUID.fromString("c7e50006-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_POSITION = UUID.fromString("c7e50007-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_LENGTH_TOTAL = UUID.fromString("c7e50008-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_TRACK_NUMBER = UUID.fromString("c7e50009-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_TRACK_TOTAL = UUID.fromString("c7e5000a-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_PLAYBACK_SPEED = UUID.fromString("c7e5000b-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_REPEAT = UUID.fromString("c7e5000c-00fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTICS_MUSIC_SHUFFLE = UUID.fromString("c7e5000d-00fc-48fe-8e23-433b3a1942d0"); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java index 1c07cd663..4c8554701 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Andreas Shimokawa +/* Copyright (C) 2020 Andreas Shimokawa, Taavi Eomäe This file is part of Gadgetbridge. @@ -55,7 +55,8 @@ public class PineTimeJFCoordinator extends AbstractDeviceCoordinator { @Override public InstallHandler findInstallHandler(Uri uri, Context context) { - return null; + PineTimeInstallHandler handler = new PineTimeInstallHandler(uri, context); + return handler.isValid() ? handler : null; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sonyswr12/SonySWR12DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sonyswr12/SonySWR12DeviceCoordinator.java new file mode 100644 index 000000000..1a2df6cfc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sonyswr12/SonySWR12DeviceCoordinator.java @@ -0,0 +1,147 @@ +/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, José Rebelo, Matthieu Baerts, Nephiel, vanous, opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +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 SonySWR12DeviceCoordinator extends AbstractDeviceCoordinator { + + @Override + public DeviceType getDeviceType() { + return DeviceType.SONY_SWR12; + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + String name = candidate.getDevice().getName(); + if (name != null && !name.isEmpty() && name.toLowerCase().contains("swr12")) + return getDeviceType(); + } catch (Exception exc){} + return DeviceType.UNKNOWN; + } + + @Override + public String getManufacturer() { + return "Sony"; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new SonySWR12SampleProvider(device, session); + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public int getAlarmSlotCount() { + return 5; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return true; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return true; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{R.xml.devicesettings_sonyswr12}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sonyswr12/SonySWR12SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sonyswr12/SonySWR12SampleProvider.java new file mode 100644 index 000000000..c2247998d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sonyswr12/SonySWR12SampleProvider.java @@ -0,0 +1,100 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.SonySWR12Sample; +import nodomain.freeyourgadget.gadgetbridge.entities.SonySWR12SampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.SonySWR12Constants; + +public class SonySWR12SampleProvider extends AbstractSampleProvider { + public SonySWR12SampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getSonySWR12SampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return SonySWR12SampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return SonySWR12SampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return SonySWR12SampleDao.Properties.DeviceId; + } + + @Override + public int normalizeType(int rawType) { + switch (rawType) { + case SonySWR12Constants.TYPE_ACTIVITY: + return ActivityKind.TYPE_ACTIVITY; + case SonySWR12Constants.TYPE_LIGHT: + return ActivityKind.TYPE_LIGHT_SLEEP; + case SonySWR12Constants.TYPE_DEEP: + return ActivityKind.TYPE_DEEP_SLEEP; + case SonySWR12Constants.TYPE_NOT_WORN: + return ActivityKind.TYPE_NOT_WORN; + } + return ActivityKind.TYPE_UNKNOWN; + } + + @Override + public int toRawActivityKind(int activityKind) { + switch (activityKind) { + case ActivityKind.TYPE_ACTIVITY: + return SonySWR12Constants.TYPE_ACTIVITY; + case ActivityKind.TYPE_LIGHT_SLEEP: + return SonySWR12Constants.TYPE_LIGHT; + case ActivityKind.TYPE_DEEP_SLEEP: + return SonySWR12Constants.TYPE_DEEP; + case ActivityKind.TYPE_NOT_WORN: + return SonySWR12Constants.TYPE_NOT_WORN; + } + return SonySWR12Constants.TYPE_ACTIVITY; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity; + } + + @Override + public SonySWR12Sample createActivitySample() { + return new SonySWR12Sample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index 38a160781..5d9013235 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -295,7 +295,7 @@ public class NotificationListener extends NotificationListenerService { } } - String source = sbn.getPackageName().toLowerCase(); + String source = sbn.getPackageName(); Notification notification = sbn.getNotification(); Long notificationOldRepeatPreventionValue = notificationOldRepeatPrevention.get(source); 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 e3080ff83..f1faff9d2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -73,8 +73,11 @@ public enum DeviceType { TLW64(180, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_tlw64), PINETIME_JF(190, R.drawable.ic_device_pinetime, R.drawable.ic_device_pinetime_disabled, R.string.devicetype_pinetime_jf), MIJIA_LYWSD02(200, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_mijia_lywsd02), + LEFUN(210, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_lefun), ITAG(250, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_itag), + NUTMINI(251, R.drawable.ic_device_itag, R.drawable.ic_device_itag_disabled, R.string.devicetype_nut_mini), VIBRATISSIMO(300, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo), + SONY_SWR12(310, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_sonyswr12), TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test); private final int key; @DrawableRes diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java index 1097a1072..687fcae9c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GenericItem.java @@ -20,11 +20,13 @@ import android.os.Parcel; import android.os.Parcelable; import java.text.Collator; +import java.util.Objects; public class GenericItem implements ItemWithDetails { private String name; private String details; private int icon; + private boolean warning = false; public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override @@ -56,9 +58,9 @@ public class GenericItem implements ItemWithDetails { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(getName()); - dest.writeString(getDetails()); - dest.writeInt(getIcon()); + dest.writeString(name); + dest.writeString(details); + dest.writeInt(icon); } public void setName(String name) { @@ -66,6 +68,9 @@ public class GenericItem implements ItemWithDetails { } public void setDetails(String details) { + if (details.equals("(Unknown version)")) { + this.warning = true; + } this.details = details; } @@ -73,6 +78,14 @@ public class GenericItem implements ItemWithDetails { this.icon = icon; } + public boolean getWarning() { + return this.warning; + } + + public void setWarning(boolean enable) { + this.warning = enable; + } + @Override public String getName() { return name; @@ -95,32 +108,36 @@ public class GenericItem implements ItemWithDetails { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) { + return false; + } + + if (this == o) { + return true; + } GenericItem that = (GenericItem) o; - return !(getName() != null ? !getName().equals(that.getName()) : that.getName() != null); - + return Objects.equals(name, that.name); } @Override public int hashCode() { - return getName() != null ? getName().hashCode() : 0; + return name != null ? name.hashCode() : 0; } @Override public int compareTo(ItemWithDetails another) { - if (getName().equals(another.getName())) { + if (name.equals(another.getName())) { return 0; } - if (getName() == null) { + if (name == null) { return +1; } else if (another.getName() == null) { return -1; } - return Collator.getInstance().compare(getName(), another.getName()); + return Collator.getInstance().compare(name, another.getName()); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java index 8b819e773..7c51ca12f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java @@ -20,6 +20,8 @@ package nodomain.freeyourgadget.gadgetbridge.model; import java.util.Objects; public class MusicSpec { + public static final int MUSIC_UNKNOWN = -1; + public static final int MUSIC_UNDEFINED = 0; public static final int MUSIC_PLAY = 1; public static final int MUSIC_PAUSE = 2; @@ -27,12 +29,12 @@ public class MusicSpec { public static final int MUSIC_NEXT = 4; public static final int MUSIC_PREVIOUS = 5; - public String artist; - public String album; - public String track; - public int duration; - public int trackCount; - public int trackNr; + public String artist = null; + public String album = null; + public String track = null; + public int duration = MUSIC_UNKNOWN; + public int trackCount = MUSIC_UNKNOWN; + public int trackNr = MUSIC_UNKNOWN; public MusicSpec() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java index 23aa23593..ef0c44f8a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java @@ -17,20 +17,26 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.model; -/** - * Created by steffen on 07.06.16. - */ public class MusicStateSpec { - public static final int STATE_PLAYING = 0; - public static final int STATE_PAUSED = 1; - public static final int STATE_STOPPED = 2; - public static final int STATE_UNKNOWN = 3; + public static final int STATE_UNKNOWN = -1; - public byte state; - public int position; // Position of the current media in seconds - public int playRate; // Speed of playback, usually 0 or 100 (full speed) - public byte shuffle; - public byte repeat; + public static final int STATE_PLAYING = 0; + public static final int STATE_PAUSED = 1; + public static final int STATE_STOPPED = 2; + + public static final int STATE_SHUFFLE_ENABLED = 1; + + public byte state = STATE_UNKNOWN; + /** + * Position of the current media in seconds + */ + public int position = STATE_UNKNOWN; + /** + * Speed of playback, usually 0 or 100 (full speed) + */ + public int playRate = STATE_UNKNOWN; + public byte shuffle = STATE_UNKNOWN; + public byte repeat = STATE_UNKNOWN; public MusicStateSpec() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index 5d7db5401..4301ac001 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -21,6 +21,7 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; +import android.companion.CompanionDeviceManager; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; @@ -29,6 +30,7 @@ import android.net.Uri; import android.os.Build; import android.telephony.SmsManager; +import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import androidx.core.content.FileProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -195,24 +197,35 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { } } + @RequiresApi(Build.VERSION_CODES.Q) private void handleGBDeviceEventFindPhoneStartNotification() { LOG.info("Got handleGBDeviceEventFindPhoneStartNotification"); Intent intent = new Intent(context, FindPhoneActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID ); - notification + NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_HIGH_PRIORITY_ID ) .setSmallIcon(R.drawable.ic_notification) .setOngoing(false) .setFullScreenIntent(pi, true) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setContentTitle( context.getString( R.string.find_my_phone_notification ) ); + notification.setGroup("BackgroundService"); - notificationManager.notify( GB.NOTIFICATION_ID_PHONE_FIND, notification.build()); + CompanionDeviceManager manager = (CompanionDeviceManager) context.getSystemService(Context.COMPANION_DEVICE_SERVICE); + if (manager.getAssociations().size() > 0) { + notificationManager.notify(GB.NOTIFICATION_ID_PHONE_FIND, notification.build()); + context.startActivity(intent); + LOG.debug("CompanionDeviceManager associations were found, starting intent"); + } else { + notificationManager.notify(GB.NOTIFICATION_ID_PHONE_FIND, notification.build()); + LOG.warn("CompanionDeviceManager associations were not found, can't start intent"); + } } 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 e158a94a6..a3898596c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -51,16 +51,19 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.itag.ITagSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30.TeclastH30Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3.MakibesHR3DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.nut.NutSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.SonySWR12DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.tlw64.TLW64Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; @@ -248,6 +251,9 @@ public class DeviceSupportFactory { case ITAG: deviceSupport = new ServiceDeviceSupport(new ITagSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case NUTMINI: + deviceSupport = new ServiceDeviceSupport(new NutSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; case BANGLEJS: deviceSupport = new ServiceDeviceSupport(new BangleJSDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; @@ -260,6 +266,12 @@ public class DeviceSupportFactory { case SG2: deviceSupport = new ServiceDeviceSupport(new HPlusSupport(DeviceType.SG2), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case LEFUN: + deviceSupport = new ServiceDeviceSupport(new LefunDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case SONY_SWR12: + deviceSupport = new ServiceDeviceSupport(new SonySWR12DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband5/MiBand5FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband5/MiBand5FirmwareInfo.java index 801a5b49c..abad04ba7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband5/MiBand5FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband5/MiBand5FirmwareInfo.java @@ -40,11 +40,13 @@ public class MiBand5FirmwareInfo extends HuamiFirmwareInfo { crcToVersion.put(29062, "1.0.0.76"); crcToVersion.put(26302, "1.0.1.16"); crcToVersion.put(26666, "1.0.1.32"); + crcToVersion.put(54599, "1.0.2.08"); // resources crcToVersion.put(8009, "1.0.0.76"); crcToVersion.put(47040, "1.0.1.16"); crcToVersion.put(49094, "1.0.1.32"); + crcToVersion.put(18506, "1.0.2.08"); // font crcToVersion.put(31978, "1"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java new file mode 100644 index 000000000..489eedc3b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/LefunDeviceSupport.java @@ -0,0 +1,1133 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.text.format.DateFormat; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; + +import de.greenrobot.dao.query.Query; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.FeaturesCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.FindPhoneCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetActivityDataCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetPpgDataCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetSleepDataCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetStepsDataCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.PpgResultCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.SettingsCommand; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunBiometricSample; +import nodomain.freeyourgadget.gadgetbridge.entities.LefunSleepSample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +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.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.FindDeviceRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetActivityDataRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetBatteryLevelRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetEnabledFeaturesRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetFirmwareInfoRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetGeneralSettingsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetHydrationReminderIntervalRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetPpgDataRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetSedentaryReminderIntervalRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.GetSleepDataRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SendCallNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SendNotificationRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetAlarmRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetEnabledFeaturesRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetGeneralSettingsRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetHydrationReminderIntervalRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetLanguageRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetProfileRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetSedentaryReminderIntervalRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.SetTimeRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests.StartPpgRequest; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +/** + * Device support class for Lefun devices + */ +public class LefunDeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(LefunDeviceSupport.class); + + private final List inProgressRequests = Collections.synchronizedList(new ArrayList()); + private final Queue queuedRequests = new ConcurrentLinkedQueue<>(); + + private int lastStepsCount = -1; + private int lastStepsTimestamp; + + /** + * Instantiates a new instance of LefunDeviceSupport + */ + public LefunDeviceSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + addSupportedService(LefunConstants.UUID_SERVICE_LEFUN); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.setGattCallback(this); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + // Enable notification + builder.notify(getCharacteristic(LefunConstants.UUID_CHARACTERISTIC_LEFUN_NOTIFY), true); + + // Init device (get version info, battery level, and set time) + try { + GetFirmwareInfoRequest firmwareReq = new GetFirmwareInfoRequest(this, builder); + firmwareReq.perform(); + inProgressRequests.add(firmwareReq); + + SetTimeRequest timeReq = new SetTimeRequest(this, builder); + timeReq.perform(); + inProgressRequests.add(timeReq); + + GetBatteryLevelRequest batReq = new GetBatteryLevelRequest(this, builder); + batReq.perform(); + inProgressRequests.add(batReq); + + sendAmPmSettingIfNecessary(builder); + sendUnitsSetting(builder); + sendUserProfile(builder); + } catch (IOException e) { + GB.toast(getContext(), "Failed to initialize Lefun device", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + try { + TransactionBuilder builder = performInitialized(SetTimeRequest.class.getSimpleName()); + SendNotificationRequest request = new SendNotificationRequest(this, builder); + request.setNotification(notificationSpec); + request.perform(); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to send notification", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized(SetTimeRequest.class.getSimpleName()); + SetTimeRequest request = new SetTimeRequest(this, builder); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set time", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + @Override + public void onSetAlarms(ArrayList alarms) { + int i = 0; + for (Alarm alarm : alarms) { + try { + TransactionBuilder builder = performInitialized(SetAlarmRequest.class.getSimpleName()); + SetAlarmRequest request = new SetAlarmRequest(this, builder); + request.setIndex(i); + request.setEnabled(alarm.getEnabled()); + request.setDayOfWeek(alarm.getRepetition()); + request.setHour(alarm.getHour()); + request.setMinute(alarm.getMinute()); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set alarm", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + ++i; + } + } + + @Override + public void onSetCallState(CallSpec callSpec) { + switch (callSpec.command) { + case CallSpec.CALL_INCOMING: + try { + TransactionBuilder builder = performInitialized(SetTimeRequest.class.getSimpleName()); + SendCallNotificationRequest request = new SendCallNotificationRequest(this, builder); + request.setCallNotification(callSpec); + request.perform(); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to send call notification", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + break; + } + } + + @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) { + if ((dataTypes & RecordedDataTypes.TYPE_ACTIVITY) != 0) { + for (int i = 0; i < 7; ++i) { + GetActivityDataRequest req = new GetActivityDataRequest(this); + req.setDaysAgo(i); + queuedRequests.add(req); + } + + for (int i = 0; i < LefunConstants.PPG_TYPE_COUNT; ++i) { + GetPpgDataRequest req = new GetPpgDataRequest(this); + req.setPpgType(i); + queuedRequests.add(req); + } + + for (int i = 0; i < 7; ++i) { + GetSleepDataRequest req = new GetSleepDataRequest(this); + req.setDaysAgo(i); + queuedRequests.add(req); + } + + runNextQueuedRequest(); + } + } + + @Override + public void onReset(int flags) { + + } + + @Override + public void onHeartRateTest() { + try { + TransactionBuilder builder = performInitialized(StartPpgRequest.class.getSimpleName()); + StartPpgRequest request = new StartPpgRequest(this, builder); + request.setPpgType(LefunConstants.PPG_TYPE_HEART_RATE); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to start heart rate test", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + if (start) { + try { + TransactionBuilder builder = performInitialized(FindDeviceRequest.class.getSimpleName()); + FindDeviceRequest request = new FindDeviceRequest(this, builder); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to initiate find device", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + } + + @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) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + switch (config) { + case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT: { + sendAmPmSetting(null); + break; + } + case DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED: { + boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED, true); + FeaturesCommand features = getCurrentEnabledFeatures(); + features.setFeature(FeaturesCommand.FEATURE_RAISE_TO_WAKE, enabled); + sendEnabledFeaturesSetting(features); + break; + } + case DeviceSettingsPreferenceConst.PREF_ANTILOST_ENABLED: { + boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_ANTILOST_ENABLED, true); + FeaturesCommand features = getCurrentEnabledFeatures(); + features.setFeature(FeaturesCommand.FEATURE_ANTI_LOST, enabled); + sendEnabledFeaturesSetting(features); + break; + } + case DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH: { + boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH, false); + FeaturesCommand features = getCurrentEnabledFeatures(); + features.setFeature(FeaturesCommand.FEATURE_SEDENTARY_REMINDER, enabled); + sendEnabledFeaturesSetting(features); + break; + } + case DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD: { + String periodStr = prefs.getString(DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD, "60"); + try { + int period = Integer.parseInt(periodStr); + sendSedentaryReminderIntervalSetting(period); + } catch (NumberFormatException e) { + GB.toast(getContext(), "Invalid sedentary reminder interval value", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + break; + } + case DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH: { + boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH, false); + FeaturesCommand features = getCurrentEnabledFeatures(); + features.setFeature(FeaturesCommand.FEATURE_HYDRATION_REMINDER, enabled); + sendEnabledFeaturesSetting(features); + break; + } + case DeviceSettingsPreferenceConst.PREF_HYDRATION_PERIOD: { + String periodStr = prefs.getString(DeviceSettingsPreferenceConst.PREF_HYDRATION_PERIOD, "60"); + try { + int period = Integer.parseInt(periodStr); + sendHydrationReminderIntervalSetting(period); + } catch (NumberFormatException e) { + GB.toast(getContext(), "Invalid sedentary reminder interval value", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + break; + } + case SettingsActivity.PREF_MEASUREMENT_SYSTEM: { + sendUnitsSetting(null); + break; + } + case DeviceSettingsPreferenceConst.PREF_LEFUN_INTERFACE_LANGUAGE: { + String value = prefs.getString(DeviceSettingsPreferenceConst.PREF_LEFUN_INTERFACE_LANGUAGE, "0"); + int intValue = Integer.parseInt(value); + sendLanguageSetting((byte) intValue); + break; + } + } + } + + /** + * Sends unit of measurement to the device + * + * @param builder the transaction builder to append to + */ + private void sendUnitsSetting(TransactionBuilder builder) { + Prefs prefs = GBApplication.getPrefs(); + String units = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, + getContext().getString(R.string.p_unit_metric)); + + byte lefunUnits; + if (getContext().getString(R.string.p_unit_metric).equals(units)) { + lefunUnits = SettingsCommand.MEASUREMENT_UNIT_METRIC; + } else { + lefunUnits = SettingsCommand.MEASUREMENT_UNIT_IMPERIAL; + } + sendGeneralSettings(builder, (byte) 0xff, lefunUnits); + } + + /** + * Send AM/PM indicator setting based on time format pref + * + * @param builder the transaction builder to append to + */ + private void sendAmPmSetting(TransactionBuilder builder) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + String ampmSetting = prefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, + getContext().getString(R.string.p_timeformat_auto)); + + byte ampmDeviceSetting = (byte) 0xff; + if (getContext().getString(R.string.p_timeformat_auto).equals(ampmSetting)) { + if (DateFormat.is24HourFormat(getContext())) { + ampmDeviceSetting = SettingsCommand.AM_PM_24_HOUR; + } else { + ampmDeviceSetting = SettingsCommand.AM_PM_12_HOUR; + } + } else if (getContext().getString(R.string.p_timeformat_24h).equals(ampmSetting)) { + ampmDeviceSetting = SettingsCommand.AM_PM_24_HOUR; + } else if (getContext().getString(R.string.p_timeformat_am_pm).equals(ampmSetting)) { + ampmDeviceSetting = SettingsCommand.AM_PM_12_HOUR; + } + + sendGeneralSettings(builder, ampmDeviceSetting, (byte) 0xff); + } + + /** + * Send AM/PM indicator setting only if time format pref is set to auto + * + * @param builder the transaction builder to append to + */ + private void sendAmPmSettingIfNecessary(TransactionBuilder builder) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + String ampmSetting = prefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, + getContext().getString(R.string.p_timeformat_auto)); + + if (getContext().getString(R.string.p_timeformat_auto).equals(ampmSetting)) { + sendAmPmSetting(builder); + } + } + + /** + * Gets a features command with the currently enabled features set + * + * @return the features command + */ + private FeaturesCommand getCurrentEnabledFeatures() { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + boolean raiseToWakeEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED, true); + boolean antilostEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_ANTILOST_ENABLED, true); + boolean sedentaryEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH, false); + boolean hydrationEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH, false); + + FeaturesCommand cmd = new FeaturesCommand(); + cmd.setFeature(FeaturesCommand.FEATURE_RAISE_TO_WAKE, raiseToWakeEnabled); + cmd.setFeature(FeaturesCommand.FEATURE_ANTI_LOST, antilostEnabled); + cmd.setFeature(FeaturesCommand.FEATURE_SEDENTARY_REMINDER, sedentaryEnabled); + cmd.setFeature(FeaturesCommand.FEATURE_HYDRATION_REMINDER, hydrationEnabled); + + return cmd; + } + + /** + * Sends general settings to the device + * + * @param builder the transaction builder to append to + * @param amPm AM/PM indicator setting + * @param units units of measurement setting + */ + private void sendGeneralSettings(TransactionBuilder builder, byte amPm, byte units) { + boolean givenBuilder = builder != null; + try { + if (!givenBuilder) + builder = performInitialized(SetGeneralSettingsRequest.class.getSimpleName()); + SetGeneralSettingsRequest request = new SetGeneralSettingsRequest(this, builder); + request.setAmPm(amPm); + request.setUnits(units); + request.perform(); + inProgressRequests.add(request); + if (!givenBuilder) + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set settings", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + /** + * Sends the user profile to the device + * + * @param builder the transaction builder to append to + */ + private void sendUserProfile(TransactionBuilder builder) { + boolean givenBuilder = builder != null; + try { + if (!givenBuilder) + builder = performInitialized(SetProfileRequest.class.getSimpleName()); + SetProfileRequest request = new SetProfileRequest(this, builder); + ActivityUser user = new ActivityUser(); + request.setUser(user); + request.perform(); + inProgressRequests.add(request); + if (!givenBuilder) + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to send profile", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + /** + * Sends enabled features settings to the device + * + * @param cmd the features command to send + */ + private void sendEnabledFeaturesSetting(FeaturesCommand cmd) { + try { + TransactionBuilder builder = performInitialized(SetEnabledFeaturesRequest.class.getSimpleName()); + SetEnabledFeaturesRequest request = new SetEnabledFeaturesRequest(this, builder); + request.setCmd(cmd); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set enabled features", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + /** + * Sends the sedentary reminder interval setting to the device + * + * @param period the reminder interval + */ + private void sendSedentaryReminderIntervalSetting(int period) { + try { + TransactionBuilder builder = performInitialized(SetSedentaryReminderIntervalRequest.class.getSimpleName()); + SetSedentaryReminderIntervalRequest request = new SetSedentaryReminderIntervalRequest(this, builder); + request.setInterval(period); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set sedentary reminder interval", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + /** + * Sends the hydration reminder interval setting to the device + * + * @param period the reminder interval + */ + private void sendHydrationReminderIntervalSetting(int period) { + try { + TransactionBuilder builder = performInitialized(SetHydrationReminderIntervalRequest.class.getSimpleName()); + SetHydrationReminderIntervalRequest request = new SetHydrationReminderIntervalRequest(this, builder); + request.setInterval(period); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set hydration reminder interval", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + /** + * Sends the language selection to the device + * + * @param language the language selection + */ + private void sendLanguageSetting(byte language) { + try { + TransactionBuilder builder = performInitialized(SetLanguageRequest.class.getSimpleName()); + SetLanguageRequest request = new SetLanguageRequest(this, builder); + request.setLanguage(language); + request.perform(); + inProgressRequests.add(request); + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to set language", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + /** + * Stores received general settings to prefs + * + * @param amPm AM/PM indicator setting + * @param units units of measurement setting + */ + public void receiveGeneralSettings(int amPm, int units) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + boolean ampmEnabled = amPm == SettingsCommand.AM_PM_12_HOUR; + String currAmpmSetting = prefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, + getContext().getString(R.string.p_timeformat_auto)); + + SharedPreferences.Editor editor = prefs.edit(); + + // Only update AM/PM indicator setting if it is not currently set to auto + if (!getContext().getString(R.string.p_timeformat_auto).equals(currAmpmSetting)) { + String ampmValue = getContext().getString(ampmEnabled ? R.string.p_timeformat_am_pm + : R.string.p_timeformat_24h); + editor.putString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, ampmValue); + } + + editor.apply(); + } + + /** + * Stores received enabled features settings to prefs + * + * @param cmd the features command + */ + public void receiveEnabledFeaturesSetting(FeaturesCommand cmd) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + prefs.edit() + .putBoolean(DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED, + cmd.getFeature(FeaturesCommand.FEATURE_RAISE_TO_WAKE)) + .putBoolean(DeviceSettingsPreferenceConst.PREF_LONGSIT_SWITCH, + cmd.getFeature(FeaturesCommand.FEATURE_SEDENTARY_REMINDER)) + .putBoolean(DeviceSettingsPreferenceConst.PREF_HYDRATION_SWITCH, + cmd.getFeature(FeaturesCommand.FEATURE_HYDRATION_REMINDER)) + .putBoolean(DeviceSettingsPreferenceConst.PREF_ANTILOST_ENABLED, + cmd.getFeature(FeaturesCommand.FEATURE_ANTI_LOST)) + .apply(); + } + + /** + * Stores received sedentary reminder interval setting to prefs + * + * @param period the interval + */ + public void receiveSedentaryReminderIntervalSetting(int period) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + prefs.edit() + .putString(DeviceSettingsPreferenceConst.PREF_LONGSIT_PERIOD, String.valueOf(period)) + .apply(); + } + + /** + * Stores received hydration reminder interval setting to prefs + * + * @param period the interval + */ + public void receiveHydrationReminderIntervalSetting(int period) { + SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + prefs.edit() + .putString(DeviceSettingsPreferenceConst.PREF_HYDRATION_PERIOD, String.valueOf(period)) + .apply(); + } + + @Override + public void onReadConfiguration(String config) { + // Just going to read all the settings + try { + TransactionBuilder builder = performInitialized("Read settings"); + + GetGeneralSettingsRequest getGeneralSettingsRequest + = new GetGeneralSettingsRequest(this, builder); + getGeneralSettingsRequest.perform(); + inProgressRequests.add(getGeneralSettingsRequest); + + GetEnabledFeaturesRequest getEnabledFeaturesRequest + = new GetEnabledFeaturesRequest(this, builder); + getEnabledFeaturesRequest.perform(); + inProgressRequests.add(getEnabledFeaturesRequest); + + GetSedentaryReminderIntervalRequest getSedentaryReminderIntervalRequest + = new GetSedentaryReminderIntervalRequest(this, builder); + getSedentaryReminderIntervalRequest.perform(); + inProgressRequests.add(getSedentaryReminderIntervalRequest); + + GetHydrationReminderIntervalRequest getHydrationReminderIntervalRequest + = new GetHydrationReminderIntervalRequest(this, builder); + getHydrationReminderIntervalRequest.perform(); + inProgressRequests.add(getHydrationReminderIntervalRequest); + + performConnected(builder.getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to retrieve settings", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (characteristic.getUuid().equals(LefunConstants.UUID_CHARACTERISTIC_LEFUN_NOTIFY)) { + byte[] data = characteristic.getValue(); + // Parse response + if (data.length >= LefunConstants.CMD_HEADER_LENGTH && data[0] == LefunConstants.CMD_RESPONSE_ID) { + // Note: full validation is done within the request + byte commandId = data[2]; + synchronized (inProgressRequests) { + for (Request req : inProgressRequests) { + if (req.expectsResponse() && req.getCommandId() == commandId) { + try { + req.handleResponse(data); + if (req.shouldRemoveAfterHandling()) + inProgressRequests.remove(req); + return true; + } catch (IllegalArgumentException e) { + LOG.error("Failed to handle response", e); + } + } + } + } + + if (handleAsynchronousResponse(commandId, data)) + return true; + + logMessageContent(data); + LOG.error(String.format("No handler for response 0x%02x", commandId)); + return false; + } + + logMessageContent(data); + LOG.error("Invalid response received"); + return false; + } + + return super.onCharacteristicChanged(gatt, characteristic); + } + + /** + * Handles commands from the device that are not typically associated with a request + * + * @param commandId the command ID + * @param data the entire response + * @return whether the response has been handled + */ + private boolean handleAsynchronousResponse(byte commandId, byte[] data) { + // Assume data already checked for correct response code and length + switch (commandId) { + case LefunConstants.CMD_PPG_RESULT: + return handleAsynchronousPpgResult(data); + case LefunConstants.CMD_FIND_PHONE: + return handleAntiLoss(data); + case LefunConstants.CMD_STEPS_DATA: + return handleAsynchronousActivity(data); + } + return false; + } + + /** + * Handles live steps data + * + * @param data the response + * @return whether the response has been handled + */ + private boolean handleAsynchronousActivity(byte[] data) { + try { + GetStepsDataCommand cmd = new GetStepsDataCommand(); + cmd.deserialize(data); + broadcastSample(cmd); + return true; + } catch (IllegalArgumentException e) { + LOG.error("Failed to handle live activity update", e); + return false; + } + } + + // Adapted from nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3.MakibesHR3DeviceSupport.broadcastSample + + /** + * Broadcasts live sample + * + * @param command the steps data + */ + private void broadcastSample(GetStepsDataCommand command) { + Calendar now = Calendar.getInstance(); + int timestamp = (int) (now.getTimeInMillis() / 1000); + // Workaround for a world where sub-second time resolution is not a thing + if (lastStepsTimestamp == timestamp) return; + lastStepsTimestamp = timestamp; + LefunActivitySample sample = new LefunActivitySample(); + sample.setTimestamp(timestamp); + if (lastStepsCount == -1 || command.getSteps() < lastStepsCount) { + lastStepsCount = command.getSteps(); + } + int diff = command.getSteps() - lastStepsCount; + sample.setSteps(diff); + lastStepsCount = command.getSteps(); + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + + /** + * Handles PPG result from earlier request + * + * @param data the response + * @return whether the response has been handled + */ + private boolean handleAsynchronousPpgResult(byte[] data) { + try { + PpgResultCommand cmd = new PpgResultCommand(); + cmd.deserialize(data); + handlePpgData(cmd); + return true; + } catch (IllegalArgumentException e) { + LOG.error("Failed to PPG result", e); + return false; + } + } + + /** + * Handles find phone request + * + * @param data the response + * @return whether the response has been handled + */ + private boolean handleAntiLoss(byte[] data) { + try { + FindPhoneCommand cmd = new FindPhoneCommand(); + cmd.deserialize(data); + GBDeviceEventFindPhone event = new GBDeviceEventFindPhone(); + event.event = GBDeviceEventFindPhone.Event.START; + evaluateGBDeviceEvent(event); + return true; + } catch (IllegalArgumentException e) { + LOG.error("Failed to handle anti-loss", e); + return false; + } + } + + /** + * Callback when device info has been obtained + */ + public void completeInitialization() { + gbDevice.setState(GBDevice.State.INITIALIZED); + gbDevice.sendDeviceUpdateIntent(getContext()); + onReadConfiguration(""); + } + + /** + * Converts Lefun datetime format to Unix timestamp + * + * @param year the year (2 digits based on 2000) + * @param month the month + * @param day the day + * @param hour the hour + * @param minute the minute + * @param second the second + * @return Unix timestamp of the datetime + */ + private int dateToTimestamp(byte year, byte month, byte day, byte hour, byte minute, byte second) { + Calendar calendar = Calendar.getInstance(); + calendar.set( + ((int) year & 0xff) + 2000, + ((int) month & 0xff) - 1, + (int) day, + (int) hour, + (int) minute, + (int) second + ); + return (int) (calendar.getTimeInMillis() / 1000); + } + + /** + * Fetches an activity sample given the timestamp + * + * @param session DAO session + * @param timestamp the timestamp + * @return fetched activity or null if none exists + */ + private LefunActivitySample getActivitySample(DaoSession session, int timestamp) { + LefunActivitySampleDao dao = session.getLefunActivitySampleDao(); + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), session).getId(); + Query q = dao.queryBuilder() + .where(LefunActivitySampleDao.Properties.Timestamp.eq(timestamp)) + .where(LefunActivitySampleDao.Properties.DeviceId.eq(deviceId)) + .where(LefunActivitySampleDao.Properties.UserId.eq(userId)) + .build(); + return q.unique(); + } + + /** + * Processes activity data and stores it + * + * @param command the activity data + */ + public void handleActivityData(GetActivityDataCommand command) { + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + int timestamp = dateToTimestamp(command.getYear(), command.getMonth(), command.getDay(), + command.getHour(), command.getMinute(), (byte) 0); + // For the most part I'm ignoring the sample provider, because it doesn't really help + // when I need to combine sample data instead of replacing + LefunActivitySample sample = getActivitySample(session, timestamp); + if (sample == null) { + sample = new LefunActivitySample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sample.setUserId(DBHelper.getUser(session).getId()); + sample.setRawKind(LefunConstants.DB_ACTIVITY_KIND_ACTIVITY); + } + + sample.setSteps(command.getSteps()); + sample.setDistance(command.getDistance()); + sample.setCalories(command.getCalories()); + sample.setRawIntensity(LefunConstants.INTENSITY_AWAKE); + + session.getLefunActivitySampleDao().insertOrReplace(sample); + } catch (Exception e) { + LOG.error("Error handling activity data", e); + } + } + + /** + * Processes PPG data and stores it + * + * @param timestamp the timestamp + * @param ppgType the PPG type + * @param ppgData the data from the PPG operation + */ + private void handlePpgData(int timestamp, int ppgType, byte[] ppgData) { + int ppgData0 = ppgData[0] & 0xff; + int ppgData1 = ppgData.length > 1 ? ppgData[1] & 0xff : 0; + + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + + if (ppgType == LefunConstants.PPG_TYPE_HEART_RATE) { + LefunActivitySample sample = getActivitySample(session, timestamp); + if (sample == null) { + sample = new LefunActivitySample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sample.setUserId(DBHelper.getUser(session).getId()); + sample.setRawKind(LefunConstants.DB_ACTIVITY_KIND_HEART_RATE); + } + + sample.setHeartRate(ppgData0); + + session.getLefunActivitySampleDao().insertOrReplace(sample); + } + + LefunBiometricSample bioSample = new LefunBiometricSample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + bioSample.setUserId(DBHelper.getUser(session).getId()); + bioSample.setType(ppgType); + bioSample.setValue1(ppgData0); + bioSample.setValue2(ppgData1); + session.getLefunBiometricSampleDao().insertOrReplace(bioSample); + } catch (Exception e) { + LOG.error("Error handling PPG data", e); + } + } + + /** + * Processes PPG data from bulk get operation + * + * @param command the PPG data + */ + public void handlePpgData(GetPpgDataCommand command) { + int timestamp = dateToTimestamp(command.getYear(), command.getMonth(), command.getDay(), + command.getHour(), command.getMinute(), command.getSecond()); + int ppgType = command.getPpgType(); + byte[] ppgData = command.getPpgData(); + handlePpgData(timestamp, ppgType, ppgData); + } + + /** + * Processes PPG result received as a result of requesting PPG operation + * + * @param command the PPG result + */ + public void handlePpgData(PpgResultCommand command) { + int timestamp = (int) (Calendar.getInstance().getTimeInMillis() / 1000); + int ppgType = command.getPpgType(); + byte[] ppgData = command.getPpgData(); + handlePpgData(timestamp, ppgType, ppgData); + } + + /** + * Processes bulk sleep data + * + * @param command the sleep data + */ + public void handleSleepData(GetSleepDataCommand command) { + try (DBHandler handler = GBApplication.acquireDB()) { + DaoSession session = handler.getDaoSession(); + int timestamp = dateToTimestamp(command.getYear(), command.getMonth(), command.getDay(), + command.getHour(), command.getMinute(), (byte) 0); + + LefunActivitySample sample = getActivitySample(session, timestamp); + if (sample == null) { + sample = new LefunActivitySample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sample.setUserId(DBHelper.getUser(session).getId()); + } + + int rawKind; + int intensity; + switch (command.getSleepType()) { + case GetSleepDataCommand.SLEEP_TYPE_AWAKE: + rawKind = LefunConstants.DB_ACTIVITY_KIND_ACTIVITY; + intensity = LefunConstants.INTENSITY_AWAKE; + break; + case GetSleepDataCommand.SLEEP_TYPE_LIGHT_SLEEP: + rawKind = LefunConstants.DB_ACTIVITY_KIND_LIGHT_SLEEP; + intensity = LefunConstants.INTENSITY_LIGHT_SLEEP; + break; + case GetSleepDataCommand.SLEEP_TYPE_DEEP_SLEEP: + rawKind = LefunConstants.DB_ACTIVITY_KIND_DEEP_SLEEP; + intensity = LefunConstants.INTENSITY_DEEP_SLEEP; + break; + default: + rawKind = LefunConstants.DB_ACTIVITY_KIND_UNKNOWN; + intensity = LefunConstants.INTENSITY_AWAKE; + break; + } + + sample.setRawKind(rawKind); + sample.setRawIntensity(intensity); + + session.getLefunActivitySampleDao().insertOrReplace(sample); + + LefunSleepSample sleepSample = new LefunSleepSample(timestamp, + DBHelper.getDevice(getDevice(), session).getId()); + sleepSample.setUserId(DBHelper.getUser(session).getId()); + sleepSample.setType(command.getSleepType()); + session.getLefunSleepSampleDao().insertOrReplace(sleepSample); + } catch (Exception e) { + LOG.error("Error handling sleep data", e); + } + } + + /** + * Runs the next queued request + */ + public void runNextQueuedRequest() { + Request request = queuedRequests.poll(); + if (request != null) { + try { + request.perform(); + if (!request.isSelfQueue()) + performConnected(request.getTransactionBuilder().getTransaction()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to run next queued request", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/AbstractSendNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/AbstractSendNotificationRequest.java new file mode 100644 index 000000000..4cdb9f3a9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/AbstractSendNotificationRequest.java @@ -0,0 +1,101 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import android.bluetooth.BluetoothGattCharacteristic; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.NotificationCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public abstract class AbstractSendNotificationRequest extends Request { + protected AbstractSendNotificationRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + protected abstract String getMessage(); + + protected abstract byte getNotificationType(); + + protected abstract byte getExtendedNotificationType(); + + @Override + public byte[] createRequest() { + return new byte[0]; + } + + @Override + protected void doPerform() throws IOException { + byte notificationType = getNotificationType(); + byte extendedNotificationType = getExtendedNotificationType(); + boolean reserveSpaceForExtended = notificationType == NotificationCommand.SERVICE_TYPE_EXTENDED; + byte[] encoded = getMessage().getBytes(StandardCharsets.UTF_8); + ByteBuffer buffer = ByteBuffer.wrap(encoded); + + BluetoothGattCharacteristic characteristic = getSupport() + .getCharacteristic(LefunConstants.UUID_CHARACTERISTIC_LEFUN_WRITE); + + List commandList = new ArrayList<>(); + int charsWritten = 0; + for (int i = 0; i < 0xff; ++i) { + int maxPayloadLength = NotificationCommand.MAX_PAYLOAD_LENGTH; + if (reserveSpaceForExtended) maxPayloadLength -= 1; + maxPayloadLength = Math.min(maxPayloadLength, buffer.limit() - buffer.position()); + maxPayloadLength = Math.min(maxPayloadLength, NotificationCommand.MAX_MESSAGE_LENGTH - charsWritten); + if (maxPayloadLength == 0 && i != 0) break; + + byte[] payload = new byte[maxPayloadLength]; + buffer.get(payload); + + NotificationCommand cmd = new NotificationCommand(); + cmd.setServiceType(notificationType); + cmd.setExtendedServiceType(extendedNotificationType); + cmd.setCurrentPiece((byte) (i + 1)); + cmd.setPayload(payload); + charsWritten += maxPayloadLength; + + commandList.add(cmd); + } + + for (NotificationCommand cmd : commandList) { + cmd.setTotalPieces((byte) commandList.size()); + builder.write(characteristic, cmd.serialize()); + } + + if (isSelfQueue()) + getSupport().performConnected(builder.getTransaction()); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_NOTIFICATION; + } + + @Override + public boolean expectsResponse() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/FindDeviceRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/FindDeviceRequest.java new file mode 100644 index 000000000..8c4fb1d35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/FindDeviceRequest.java @@ -0,0 +1,53 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.FindDeviceCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class FindDeviceRequest extends Request { + public FindDeviceRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + FindDeviceCommand cmd = new FindDeviceCommand(); + return cmd.serialize(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_FIND_DEVICE; + } + + @Override + public void handleResponse(byte[] data) { + FindDeviceCommand cmd = new FindDeviceCommand(); + cmd.deserialize(data); + + if (!cmd.isSuccess()) + reportFailure("Could not initiate find device"); + + operationStatus = OperationStatus.FINISHED; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java new file mode 100644 index 000000000..2c1adb8c8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetActivityDataRequest.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetActivityDataCommand; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetActivityDataRequest extends MultiFetchRequest { + private int daysAgo; + + public GetActivityDataRequest(LefunDeviceSupport support) { + super(support); + } + + public int getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(int daysAgo) { + this.daysAgo = daysAgo; + } + + @Override + public byte[] createRequest() { + GetActivityDataCommand cmd = new GetActivityDataCommand(); + cmd.setDaysAgo((byte) daysAgo); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetActivityDataCommand cmd = new GetActivityDataCommand(); + cmd.deserialize(data); + + if (daysAgo != (cmd.getDaysAgo() & 0xff)) { + throw new IllegalArgumentException("Mismatching days ago"); + } + + if (totalRecords == -1) { + totalRecords = cmd.getTotalRecords() & 0xff; + } else if (totalRecords != (cmd.getTotalRecords() & 0xff)) { + throw new IllegalArgumentException("Total records mismatch"); + } + + if (totalRecords != 0) { + int currentRecord = cmd.getCurrentRecord() & 0xff; + if (lastRecord + 1 != currentRecord) { + throw new IllegalArgumentException("Records received out of sequence"); + } + lastRecord = currentRecord; + + getSupport().handleActivityData(cmd); + } else { + lastRecord = totalRecords; + } + + if (lastRecord == totalRecords) + operationFinished(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_ACTIVITY_DATA; + } + + @Override + protected String getOperationName() { + return "Getting activity data"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java new file mode 100644 index 000000000..bfef58ab3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetBatteryLevelRequest.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetBatteryLevelCommand; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class GetBatteryLevelRequest extends Request { + public GetBatteryLevelRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + GetBatteryLevelCommand cmd = new GetBatteryLevelCommand(); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetBatteryLevelCommand cmd = new GetBatteryLevelCommand(); + cmd.deserialize(data); + + GBDevice device = getSupport().getDevice(); + device.setBatteryThresholdPercent((short)15); + + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + batteryInfo.level = (short)((int)cmd.getBatteryLevel() & 0xff); + getSupport().evaluateGBDeviceEvent(batteryInfo); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_BATTERY_LEVEL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetEnabledFeaturesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetEnabledFeaturesRequest.java new file mode 100644 index 000000000..3f7759b02 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetEnabledFeaturesRequest.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.FeaturesCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class GetEnabledFeaturesRequest extends Request { + public GetEnabledFeaturesRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + FeaturesCommand cmd = new FeaturesCommand(); + + cmd.setOp(BaseCommand.OP_GET); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + FeaturesCommand cmd = new FeaturesCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_GET) { + getSupport().receiveEnabledFeaturesSetting(cmd); + } + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_FEATURES; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java new file mode 100644 index 000000000..7bdab9a78 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetFirmwareInfoRequest.java @@ -0,0 +1,66 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetFirmwareInfoCommand; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class GetFirmwareInfoRequest extends Request { + public GetFirmwareInfoRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + GetFirmwareInfoCommand cmd = new GetFirmwareInfoCommand(); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetFirmwareInfoCommand cmd = new GetFirmwareInfoCommand(); + cmd.deserialize(data); + + int hardwareVersion = cmd.getHardwareVersion() & 0xffff; + int softwareVersion = cmd.getSoftwareVersion() & 0xffff; + + GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); + versionInfo.fwVersion = String.format("%d.%d", softwareVersion >> 8, softwareVersion & 0xff); + // Last character is a \x1f? Not printable either way. + versionInfo.hwVersion = cmd.getTypeCode().substring(0, 3); + getSupport().evaluateGBDeviceEvent(versionInfo); + + GBDevice device = getSupport().getDevice(); + device.setFirmwareVersion2(String.format("%d.%d", hardwareVersion >> 8, hardwareVersion & 0xff)); + + getSupport().completeInitialization(); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_FIRMWARE_INFO; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetGeneralSettingsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetGeneralSettingsRequest.java new file mode 100644 index 000000000..9efe28fd7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetGeneralSettingsRequest.java @@ -0,0 +1,60 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.SettingsCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class GetGeneralSettingsRequest extends Request { + public GetGeneralSettingsRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + SettingsCommand cmd = new SettingsCommand(); + + cmd.setOp(BaseCommand.OP_GET); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + SettingsCommand cmd = new SettingsCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_GET) { + getSupport().receiveGeneralSettings( + (int) cmd.getAmPmIndicator() & 0xff, + (int) cmd.getMeasurementUnit() & 0xff + ); + } + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_SETTINGS; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetHydrationReminderIntervalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetHydrationReminderIntervalRequest.java new file mode 100644 index 000000000..275effa88 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetHydrationReminderIntervalRequest.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.HydrationReminderIntervalCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class GetHydrationReminderIntervalRequest extends Request { + public GetHydrationReminderIntervalRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + HydrationReminderIntervalCommand cmd = new HydrationReminderIntervalCommand(); + + cmd.setOp(BaseCommand.OP_GET); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + HydrationReminderIntervalCommand cmd = new HydrationReminderIntervalCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_GET) { + getSupport().receiveHydrationReminderIntervalSetting((int) cmd.getHydrationReminderInterval() & 0xff); + } + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_HYDRATION_REMINDER_INTERVAL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java new file mode 100644 index 000000000..eb7c4a9f0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetPpgDataRequest.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetPpgDataCommand; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetPpgDataRequest extends MultiFetchRequest { + private int ppgType; + + public GetPpgDataRequest(LefunDeviceSupport support) { + super(support); + } + + public int getPpgType() { + return ppgType; + } + + public void setPpgType(int ppgType) { + this.ppgType = ppgType; + } + + @Override + public byte[] createRequest() { + GetPpgDataCommand cmd = new GetPpgDataCommand(); + cmd.setPpgType(ppgType); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetPpgDataCommand cmd = new GetPpgDataCommand(); + cmd.deserialize(data); + + if (cmd.getPpgType() != ppgType) { + throw new IllegalArgumentException("Mismatching PPG type"); + } + + if (totalRecords == -1) { + totalRecords = cmd.getTotalRecords() & 0xffff; + } else if (totalRecords != (cmd.getTotalRecords() & 0xffff)) { + throw new IllegalArgumentException("Total records mismatch"); + } + + if (totalRecords != 0) { + int currentRecord = cmd.getCurrentRecord() & 0xffff; + if (lastRecord + 1 != currentRecord) { + throw new IllegalArgumentException("Records received out of sequence"); + } + lastRecord = currentRecord; + + getSupport().handlePpgData(cmd); + } else { + lastRecord = totalRecords; + } + + if (lastRecord == totalRecords) + operationFinished(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_PPG_DATA; + } + + @Override + protected String getOperationName() { + return "Getting PPG data"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSedentaryReminderIntervalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSedentaryReminderIntervalRequest.java new file mode 100644 index 000000000..8b17df11f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSedentaryReminderIntervalRequest.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.SedentaryReminderIntervalCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class GetSedentaryReminderIntervalRequest extends Request { + public GetSedentaryReminderIntervalRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + SedentaryReminderIntervalCommand cmd = new SedentaryReminderIntervalCommand(); + + cmd.setOp(BaseCommand.OP_GET); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + SedentaryReminderIntervalCommand cmd = new SedentaryReminderIntervalCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_GET) { + getSupport().receiveSedentaryReminderIntervalSetting((int) cmd.getSedentaryReminderInterval() & 0xff); + } + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_SEDENTARY_REMINDER_INTERVAL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java new file mode 100644 index 000000000..ab53855a3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/GetSleepDataRequest.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.GetSleepDataCommand; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class GetSleepDataRequest extends MultiFetchRequest { + private int daysAgo; + + public GetSleepDataRequest(LefunDeviceSupport support) { + super(support); + } + + public int getDaysAgo() { + return daysAgo; + } + + public void setDaysAgo(int daysAgo) { + this.daysAgo = daysAgo; + } + + @Override + public byte[] createRequest() { + GetSleepDataCommand cmd = new GetSleepDataCommand(); + cmd.setDaysAgo((byte) daysAgo); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + GetSleepDataCommand cmd = new GetSleepDataCommand(); + cmd.deserialize(data); + + if (daysAgo != (cmd.getDaysAgo() & 0xff)) { + throw new IllegalArgumentException("Mismatching days ago"); + } + + if (totalRecords == -1) { + totalRecords = cmd.getTotalRecords() & 0xff; + } else if (totalRecords != (cmd.getTotalRecords() & 0xff)) { + throw new IllegalArgumentException("Total records mismatch"); + } + + if (totalRecords != 0) { + int currentRecord = cmd.getCurrentRecord() & 0xff; + if (lastRecord + 1 != currentRecord) { + throw new IllegalArgumentException("Records received out of sequence"); + } + lastRecord = currentRecord; + + getSupport().handleSleepData(cmd); + } else { + lastRecord = totalRecords; + } + + if (lastRecord == totalRecords) + operationFinished(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_SLEEP_DATA; + } + + @Override + protected String getOperationName() { + return "Getting sleep data"; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java new file mode 100644 index 000000000..1713739f4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/MultiFetchRequest.java @@ -0,0 +1,112 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * Represents a request that receives several responses + */ +public abstract class MultiFetchRequest extends Request { + /** + * Instantiates a new MultiFetchRequest + * @param support the device support + */ + protected MultiFetchRequest(LefunDeviceSupport support) { + super(support, null); + removeAfterHandling = false; + } + + protected int lastRecord = 0; + protected int totalRecords = -1; + + @Override + protected void prePerform() throws IOException { + super.prePerform(); + builder = performInitialized(getClass().getSimpleName()); + if (getDevice().isBusy()) { + throw new IllegalStateException("Device is busy"); + } + builder.add(new SetDeviceBusyAction(getDevice(), getOperationName(), getContext())); + builder.wait(1000); // Wait a bit (after previous operation), or device sometimes won't respond + } + + @Override + protected void operationFinished() { + if (lastRecord == totalRecords) + removeAfterHandling = true; + try { + super.operationFinished(); + TransactionBuilder builder = performInitialized("Finishing operation"); + builder.setGattCallback(null); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Failed to reset callback", Toast.LENGTH_SHORT, + GB.ERROR, e); + } + unsetBusy(); + operationStatus = OperationStatus.FINISHED; + getSupport().runNextQueuedRequest(); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (characteristic.getUuid().equals(LefunConstants.UUID_CHARACTERISTIC_LEFUN_NOTIFY)) { + byte[] data = characteristic.getValue(); + // Parse response + if (data.length >= LefunConstants.CMD_HEADER_LENGTH && data[0] == LefunConstants.CMD_RESPONSE_ID) { + try { + handleResponse(data); + return true; + } catch (IllegalArgumentException e) { + log("Failed to handle response"); + operationFinished(); + } + } + + getSupport().logMessageContent(data); + log("Invalid response received"); + return false; + } + + return super.onCharacteristicChanged(gatt, characteristic); + } + + @Override + public boolean isSelfQueue() { + return true; + } + + /** + * Gets the display operation name + * @return the operation name + */ + protected abstract String getOperationName(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java new file mode 100644 index 000000000..95d50e2cc --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/Request.java @@ -0,0 +1,153 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +// Ripped from nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request + +/** + * Basic request for operations with Lefun devices + */ +public abstract class Request extends AbstractBTLEOperation { + protected TransactionBuilder builder; + protected boolean removeAfterHandling = true; + private Logger logger = (Logger) LoggerFactory.getLogger(getName()); + + /** + * Instantiates Request + * + * @param support the device support + * @param builder the transaction builder to use + */ + protected Request(LefunDeviceSupport support, TransactionBuilder builder) { + super(support); + this.builder = builder; + } + + /** + * Gets the transaction builder + * + * @return the transaction builder + */ + public TransactionBuilder getTransactionBuilder() { + return builder; + } + + @Override + protected void doPerform() throws IOException { + BluetoothGattCharacteristic characteristic = getSupport() + .getCharacteristic(LefunConstants.UUID_CHARACTERISTIC_LEFUN_WRITE); + builder.write(characteristic, createRequest()); + if (isSelfQueue()) + getSupport().performConnected(builder.getTransaction()); + } + + /** + * When implemented in a subclass, provides the request bytes to send to the device + * + * @return the request bytes + */ + public abstract byte[] createRequest(); + + /** + * When overridden in a subclass, handles the response to the current command + * + * @param data the response data + */ + public void handleResponse(byte[] data) { + operationStatus = OperationStatus.FINISHED; + } + + /** + * Gets the class name of this instance + * + * @return the class name + */ + public String getName() { + Class thisClass = getClass(); + while (thisClass.isAnonymousClass()) thisClass = thisClass.getSuperclass(); + return thisClass.getSimpleName(); + } + + /** + * Logs a debug message + * + * @param message the message to log + */ + protected void log(String message) { + logger.debug(message); + } + + /** + * When implemented in a subclass, returns the command ID associated with the current request + * + * @return the command ID + */ + public abstract int getCommandId(); + + /** + * Gets whether the request will queue itself + * + * @return whether the request is self-queuing + */ + public boolean isSelfQueue() { + return false; + } + + /** + * Gets whether the request expects a response + * + * @return whether the request expects a response + */ + public boolean expectsResponse() { + return true; + } + + /** + * Gets whether the response should be removed from in progress requests list after handling + * + * @return whether the response should be removed after handling + */ + public boolean shouldRemoveAfterHandling() { + return removeAfterHandling; + } + + /** + * Reports an error to the user + * + * @param message the message to show + */ + protected void reportFailure(String message) { + GB.toast(getContext(), message, Toast.LENGTH_SHORT, GB.ERROR); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SendCallNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SendCallNotificationRequest.java new file mode 100644 index 000000000..2b68ba517 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SendCallNotificationRequest.java @@ -0,0 +1,67 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.NotificationCommand; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class SendCallNotificationRequest extends AbstractSendNotificationRequest { + public SendCallNotificationRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + private CallSpec callNotification; + + public CallSpec getCallNotification() { + return callNotification; + } + + public void setCallNotification(CallSpec callNotification) { + this.callNotification = callNotification; + } + + @Override + protected String getMessage() { + String message = ""; + if (callNotification.number != null &&!callNotification.number.isEmpty()) { + message = callNotification.number; + } + + if (callNotification.name != null && !callNotification.name.isEmpty()) { + if (message.length() > 0) { + message += " - "; + } + message += callNotification.name; + } + + return message; + } + + @Override + protected byte getNotificationType() { + return NotificationCommand.SERVICE_TYPE_CALL; + } + + @Override + protected byte getExtendedNotificationType() { + return 0; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SendNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SendNotificationRequest.java new file mode 100644 index 000000000..689fb6f1f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SendNotificationRequest.java @@ -0,0 +1,111 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.NotificationCommand; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; + +public class SendNotificationRequest extends AbstractSendNotificationRequest { + NotificationSpec notification; + + public SendNotificationRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + protected byte getNotificationType() { + switch (notification.type) { + case GENERIC_PHONE: + return NotificationCommand.SERVICE_TYPE_CALL; + case GENERIC_SMS: + case GENERIC_EMAIL: + default: + return NotificationCommand.SERVICE_TYPE_TEXT; + case WECHAT: + return NotificationCommand.SERVICE_TYPE_WECHAT; + case FACEBOOK: + case FACEBOOK_MESSENGER: + case TWITTER: + case LINKEDIN: + case WHATSAPP: + case LINE: + case KAKAO_TALK: + return NotificationCommand.SERVICE_TYPE_EXTENDED; + } + } + + @Override + protected byte getExtendedNotificationType() { + switch (notification.type) { + case GENERIC_PHONE: + case GENERIC_SMS: + case GENERIC_EMAIL: + default: + case WECHAT: + return 0; + case FACEBOOK: + case FACEBOOK_MESSENGER: + return NotificationCommand.EXTENDED_SERVICE_TYPE_FACEBOOK; + case TWITTER: + return NotificationCommand.EXTENDED_SERVICE_TYPE_TWITTER; + case LINKEDIN: + return NotificationCommand.EXTENDED_SERVICE_TYPE_LINKEDIN; + case WHATSAPP: + return NotificationCommand.EXTENDED_SERVICE_TYPE_WHATSAPP; + case LINE: + return NotificationCommand.EXTENDED_SERVICE_TYPE_LINE; + case KAKAO_TALK: + return NotificationCommand.EXTENDED_SERVICE_TYPE_KAKAOTALK; + } + } + + public NotificationSpec getNotification() { + return notification; + } + + public void setNotification(NotificationSpec notification) { + this.notification = notification; + } + + @Override + protected String getMessage() { + // Based on nodomain.freeyourgadget.gadgetbridge.service.devices.id115.SendNotificationOperation + String message = ""; + + if (notification.phoneNumber != null && !notification.phoneNumber.isEmpty()) { + message += notification.phoneNumber + ": "; + } + + if (notification.sender != null && !notification.sender.isEmpty()) { + message += notification.sender + " - "; + } else if (notification.title != null && !notification.title.isEmpty()) { + message += notification.title + " - "; + } else if (notification.subject != null && !notification.subject.isEmpty()) { + message += notification.subject + " - "; + } + + if (notification.body != null && !notification.body.isEmpty()) { + message += notification.body; + } + + return message; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetAlarmRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetAlarmRequest.java new file mode 100644 index 000000000..bae512d30 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetAlarmRequest.java @@ -0,0 +1,118 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.AlarmCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetAlarmRequest extends Request { + private int index; + private boolean enabled; + private int dayOfWeek; + private int hour; + private int minute; + public SetAlarmRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(int dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public int getHour() { + return hour; + } + + public void setHour(int hour) { + this.hour = hour; + } + + public int getMinute() { + return minute; + } + + public void setMinute(int minute) { + this.minute = minute; + } + + @Override + public byte[] createRequest() { + AlarmCommand cmd = new AlarmCommand(); + cmd.setOp(BaseCommand.OP_SET); + cmd.setIndex((byte) index); + cmd.setEnabled(enabled); + cmd.setNumOfSnoozes((byte) 0); + cmd.setHour((byte) hour); + cmd.setMinute((byte) minute); + + // Translate GB alarm day of week to Lefun day of week + // GB starts on Monday, Lefun starts on Sunday + for (int i = 0; i < 6; ++i) { + if ((dayOfWeek & (1 << i)) != 0) { + cmd.setDayOfWeek(i + 1, true); + } + } + if ((dayOfWeek & Alarm.ALARM_SUN) != 0) { + cmd.setDayOfWeek(AlarmCommand.DOW_SUNDAY, true); + } + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + AlarmCommand cmd = new AlarmCommand(); + cmd.deserialize(data); + + if (cmd.getOp() != BaseCommand.OP_SET || cmd.getIndex() != index || !cmd.isSetSuccess()) + reportFailure("Could not set alarm"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_ALARM; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetEnabledFeaturesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetEnabledFeaturesRequest.java new file mode 100644 index 000000000..fe0cc289a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetEnabledFeaturesRequest.java @@ -0,0 +1,63 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.FeaturesCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetEnabledFeaturesRequest extends Request { + private FeaturesCommand cmd; + + public SetEnabledFeaturesRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public FeaturesCommand getCmd() { + return cmd; + } + + public void setCmd(FeaturesCommand cmd) { + this.cmd = cmd; + } + + @Override + public byte[] createRequest() { + cmd.setOp(BaseCommand.OP_SET); + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + FeaturesCommand cmd = new FeaturesCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set enabled features"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_FEATURES; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetGeneralSettingsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetGeneralSettingsRequest.java new file mode 100644 index 000000000..4848daba1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetGeneralSettingsRequest.java @@ -0,0 +1,78 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.SettingsCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetGeneralSettingsRequest extends Request { + private byte amPm; + private byte units; + + public SetGeneralSettingsRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public byte getAmPm() { + return amPm; + } + + public void setAmPm(byte amPm) { + this.amPm = amPm; + } + + public byte getUnits() { + return units; + } + + public void setUnits(byte units) { + this.units = units; + } + + @Override + public byte[] createRequest() { + SettingsCommand cmd = new SettingsCommand(); + + cmd.setOp(BaseCommand.OP_SET); + cmd.setOption1((byte) 0xff); // Don't set + cmd.setAmPmIndicator(amPm); + cmd.setMeasurementUnit(units); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + SettingsCommand cmd = new SettingsCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set settings"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_SETTINGS; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetHydrationReminderIntervalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetHydrationReminderIntervalRequest.java new file mode 100644 index 000000000..85339e189 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetHydrationReminderIntervalRequest.java @@ -0,0 +1,67 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.HydrationReminderIntervalCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetHydrationReminderIntervalRequest extends Request { + private int interval; + + public SetHydrationReminderIntervalRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public int getInterval() { + return interval; + } + + public void setInterval(int interval) { + this.interval = interval; + } + + @Override + public byte[] createRequest() { + HydrationReminderIntervalCommand cmd = new HydrationReminderIntervalCommand(); + + cmd.setOp(BaseCommand.OP_SET); + cmd.setHydrationReminderInterval((byte) interval); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + HydrationReminderIntervalCommand cmd = new HydrationReminderIntervalCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set hydration reminder interval"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_HYDRATION_REMINDER_INTERVAL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetLanguageRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetLanguageRequest.java new file mode 100644 index 000000000..db168bf8a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetLanguageRequest.java @@ -0,0 +1,65 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.SetLanguageCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetLanguageRequest extends Request { + private byte language; + + public SetLanguageRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public byte getLanguage() { + return language; + } + + public void setLanguage(byte language) { + this.language = language; + } + + @Override + public byte[] createRequest() { + SetLanguageCommand cmd = new SetLanguageCommand(); + + cmd.setLanguage(language); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + SetLanguageCommand cmd = new SetLanguageCommand(); + cmd.deserialize(data); + if (!cmd.isSetSuccess()) + reportFailure("Could not set language"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_LANGUAGE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetProfileRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetProfileRequest.java new file mode 100644 index 000000000..7b1aa29ac --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetProfileRequest.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.ProfileCommand; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetProfileRequest extends Request { + private ActivityUser user; + + public SetProfileRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public ActivityUser getUser() { + return user; + } + + public void setUser(ActivityUser user) { + this.user = user; + } + + @Override + public byte[] createRequest() { + ProfileCommand cmd = new ProfileCommand(); + + cmd.setOp(BaseCommand.OP_SET); + // No "other" option available, only male or female + cmd.setGender(user.getGender() == ActivityUser.GENDER_FEMALE + ? ProfileCommand.GENDER_FEMALE + : ProfileCommand.GENDER_MALE); + cmd.setHeight((byte) user.getHeightCm()); + cmd.setWeight((byte) user.getWeightKg()); + cmd.setAge((byte) user.getAge()); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + ProfileCommand cmd = new ProfileCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set profile"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_PROFILE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetSedentaryReminderIntervalRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetSedentaryReminderIntervalRequest.java new file mode 100644 index 000000000..c048c6ccb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetSedentaryReminderIntervalRequest.java @@ -0,0 +1,67 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.SedentaryReminderIntervalCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetSedentaryReminderIntervalRequest extends Request { + private int interval; + + public SetSedentaryReminderIntervalRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + public int getInterval() { + return interval; + } + + public void setInterval(int interval) { + this.interval = interval; + } + + @Override + public byte[] createRequest() { + SedentaryReminderIntervalCommand cmd = new SedentaryReminderIntervalCommand(); + + cmd.setOp(BaseCommand.OP_SET); + cmd.setSedentaryReminderInterval((byte) interval); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + SedentaryReminderIntervalCommand cmd = new SedentaryReminderIntervalCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set sedentary reminder interval"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_SEDENTARY_REMINDER_INTERVAL; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java new file mode 100644 index 000000000..ff2a193cf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/SetTimeRequest.java @@ -0,0 +1,66 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import java.util.Calendar; +import java.util.TimeZone; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.BaseCommand; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.TimeCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class SetTimeRequest extends Request { + public SetTimeRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + @Override + public byte[] createRequest() { + TimeCommand cmd = new TimeCommand(); + Calendar c = Calendar.getInstance(); + + cmd.setOp(BaseCommand.OP_SET); + cmd.setYear((byte)(c.get(Calendar.YEAR) - 2000)); + cmd.setMonth((byte)(c.get(Calendar.MONTH) + 1)); + cmd.setDay((byte)c.get(Calendar.DAY_OF_MONTH)); + cmd.setHour((byte)c.get(Calendar.HOUR_OF_DAY)); + cmd.setMinute((byte)c.get(Calendar.MINUTE)); + cmd.setSecond((byte)c.get(Calendar.SECOND)); + + return cmd.serialize(); + } + + @Override + public void handleResponse(byte[] data) { + TimeCommand cmd = new TimeCommand(); + cmd.deserialize(data); + if (cmd.getOp() == BaseCommand.OP_SET && !cmd.isSetSuccess()) + reportFailure("Could not set time"); + + operationStatus = OperationStatus.FINISHED; + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_TIME; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/StartPpgRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/StartPpgRequest.java new file mode 100644 index 000000000..1491a2a9d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/lefun/requests/StartPpgRequest.java @@ -0,0 +1,64 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti + Copyright (C) 2020 Yukai Li + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.requests; + +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.commands.StartPpgSensingCommand; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.lefun.LefunDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; + +public class StartPpgRequest extends Request { + public StartPpgRequest(LefunDeviceSupport support, TransactionBuilder builder) { + super(support, builder); + } + + int ppgType; + + public int getPpgType() { + return ppgType; + } + + public void setPpgType(int ppgType) { + this.ppgType = ppgType; + } + + @Override + public byte[] createRequest() { + StartPpgSensingCommand cmd = new StartPpgSensingCommand(); + cmd.setPpgType(ppgType); + return cmd.serialize(); + } + + @Override + public int getCommandId() { + return LefunConstants.CMD_PPG_START; + } + + @Override + public void handleResponse(byte[] data) { + StartPpgSensingCommand cmd = new StartPpgSensingCommand(); + cmd.deserialize(data); + + if (!cmd.isSetSuccess() || cmd.getPpgType() != ppgType) + reportFailure("Could not start PPG sensing"); + + operationStatus = OperationStatus.FINISHED; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java index 9e27f34fd..934a3dd44 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/UpdateFirmwareOperation.java @@ -84,7 +84,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand1Operation { displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR); done(); } - //the firmware will be sent by the notification listener if the band confirms that the metadata are ok. + /** the firmware will be sent by the {@link UpdateFirmwareOperation#handleNotificationNotif} if the band confirms that the metadata are ok. **/ } private void done() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nut/NutSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nut/NutSupport.java new file mode 100644 index 000000000..8ebdc8aa5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nut/NutSupport.java @@ -0,0 +1,632 @@ +/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.nut; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +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; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class NutSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(NutSupport.class); + + private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + + private final DeviceInfoProfile deviceInfoProfile; + private final BatteryInfoProfile batteryInfoProfile; + private final IntentListener listener = new IntentListener() { + @Override + public void notify(Intent intent) { + String action = intent.getAction(); + if (action.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { + handleDeviceInfo((DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } else if (action.equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) { + handleBatteryInfo((BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); + } else { + LOG.warn("Unhandled intent given to listener"); + } + } + }; + private SharedPreferences prefs = null; + /** + * It uses the proprietary Nut interface. + */ + private boolean proprietary = false; + /** + * Proprietary Nut interface needs authentication. + *

+ * Don't write characteristics until authenticated. + *

+ * Will disconnect in a minute if you don't authenticate. + */ + private boolean authenticated = true; + /** + * The two keys used for authentication + */ + private BigInteger key1; + private BigInteger key2; + + + public NutSupport() { + super(LOG); + addSupportedService(NutConstants.SERVICE_BATTERY); + addSupportedService(NutConstants.SERVICE_DEVICE_INFO); + addSupportedService(NutConstants.SERVICE_IMMEDIATE_ALERT); + addSupportedService(NutConstants.SERVICE_LINK_LOSS); + addSupportedService(NutConstants.SERVICE_PROPRIETARY_NUT); + addSupportedService(NutConstants.SERVICE_UNKNOWN_2); + + deviceInfoProfile = new DeviceInfoProfile<>(this); + deviceInfoProfile.addListener(listener); + addSupportedProfile(deviceInfoProfile); + + batteryInfoProfile = new BatteryInfoProfile<>(this); + batteryInfoProfile.addListener(listener); + addSupportedProfile(batteryInfoProfile); + } + + private void handleBatteryInfo(BatteryInfo info) { + LOG.info("Received Nut battery info"); + batteryCmd.level = (short) info.getPercentCharged(); + handleGBDeviceEvent(batteryCmd); + } + + private void handleDeviceInfo(DeviceInfo info) { + LOG.info("Received Nut device info"); + LOG.info(String.valueOf(info)); + GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); + if (info.getHardwareRevision() != null) { + versionInfo.hwVersion = info.getHardwareRevision(); + } + if (info.getFirmwareRevision() != null) { + versionInfo.fwVersion = info.getFirmwareRevision(); + } + + handleGBDeviceEvent(versionInfo); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + + // Init prefs + prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); + loadKeysFromPrefs(); + + LOG.debug("Requesting device info!"); + deviceInfoProfile.requestDeviceInfo(builder); + batteryInfoProfile.requestBatteryInfo(builder); + + // If this characteristic exists, it has proprietary Nut interface + this.proprietary = (getCharacteristic(NutConstants.CHARAC_AUTH_STATUS) != null); + + if (proprietary) { + this.authenticated = false; + /** + * Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} + * Enables proprietary notification + */ + builder.notify(getCharacteristic(NutConstants.CHARAC_AUTH_STATUS), true); + LOG.info("Enabled authentication status notify"); + + /** + * Part of {@link NutConstants.SERVICE_UNKNOWN_2} + * Enables button-press notify + */ + builder.notify(getCharacteristic(NutConstants.CHARAC_UNKNOWN_2), true); + } else { + /** + * Part of {@link NutConstants.SERVICE_UNKNOWN_1_WEIRDNESS} + * Enables button-press notify + */ + builder.notify(getCharacteristic(NutConstants.CHARAC_CHANGE_POWER), true); + } + + readDeviceInfo(); + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @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 enable) { + deviceImmediateAlert(enable); + } + + @Override + public void onSetConstantVibration(int intensity) { + } + + @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 boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + + UUID characteristicUUID = characteristic.getUuid(); + if (characteristicUUID.equals(NutConstants.CHARAC_AUTH_STATUS)) { + handleAuthResult(characteristic.getValue()); + return true; + } + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return false; + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + if (super.onCharacteristicRead(gatt, characteristic, status)) { + return true; + } + UUID characteristicUUID = characteristic.getUuid(); + + if (characteristicUUID.equals(NutConstants.CHARAC_SYSTEM_ID)) { + // TODO: Handle System ID read + return true; + } + LOG.info("Unhandled characteristic read: " + characteristicUUID); + return false; + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + /** + * Enables or disables link loss alert + */ + private void deviceLinkLossAlert(boolean enable) { + UUID charac; + if (this.proprietary) { + /** Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} */ + charac = NutConstants.CHARAC_CHANGE_POWER; + } else { + /** Part of {@link NutConstants.SERVICE_IMMEDIATE_ALERT} */ + charac = NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL; + } + + byte[] payload = new byte[]{(byte) (enable ? 0x00 : 0x01)}; + if (enable) { + writeCharacteristic("Enable link loss alert", charac, payload); + } else { + writeCharacteristic("Disable link loss alert", charac, payload); + } + } + + /** + * Should trigger an immediate alert + * + * @param enable turn on or not + */ + private void deviceImmediateAlert(boolean enable) { + UUID charac; + if (this.proprietary) { + /** Part of {@link NutConstants.SERVICE_IMMEDIATE_ALERT} */ + charac = NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL; + if (!authenticated) { + LOG.warn("Not authenticated, can't alert"); + return; + } + } else { + /** Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} */ + charac = NutConstants.CHARAC_CHANGE_POWER; + } + + if (enable) { + writeCharacteristic("Start alert", charac, new byte[]{(byte) 0x04}); + } else { + writeCharacteristic("Stop alert", charac, new byte[]{(byte) 0x03}); + } + } + + /** + * This will write a new key to the device + *

+ * However, it is irreversible, + * if you can't generate the right packets, + * the device is basically bricked! + *

+ * If you can generate the correct packets, + * it can be reset... somehow + * + * @param key key + */ + private void deviceWriteNewKey(byte[] key) { + // TODO: Determine each nuance of how this + // works before using it! + byte[] result_payload = new byte[key.length + 1]; + result_payload[0] = (byte) 0x04; + System.arraycopy(key, 0, result_payload, 1, key.length); + + writeCharacteristic("Write new key", + NutConstants.CHARAC_DFU_PW, + result_payload); + } + + /** + * Turns the device off + */ + private void deviceShutdown() { + writeCharacteristic("Shutdown", NutConstants.CHARAC_CHANGE_POWER, new byte[]{0x06}); + } + + /** + * Switches the device to Nordic's DFU mode + */ + private void deviceDFU() { + writeCharacteristic("Enable DFU mode", NutConstants.CHARAC_DFU_PW, new byte[]{0x14}); + } + + /** + * Specifies how long the alert lasts + * + * @param duration in seconds (I think) + */ + private void deviceWriteAlertDuration(int duration) { + if (duration == 0) { + duration = 15; + } + + UUID charac; + if (this.proprietary) { + charac = NutConstants.CHARAC_DFU_PW; + } else { + charac = NutConstants.CHARAC_CHANGE_POWER; + } + + writeCharacteristic("Write alert duration", + charac, + new byte[]{37, (byte) duration}); + } + + private void readHardwareInfo() { + BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_HARDWARE_VERSION); + if (characteristic != null && + ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0)) { + readCharacteristic("Device read hardware", + NutConstants.CHARAC_HARDWARE_VERSION); + } + + BluetoothGattCharacteristic characteristic1 = getCharacteristic(NutConstants.CHARAC_MANUFACTURER_NAME); + if (characteristic1 != null && + (characteristic1.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { + readCharacteristic("Read manufacturer", + NutConstants.CHARAC_MANUFACTURER_NAME); + } + } + + private void readFirmwareInfo() { + BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_FIRMWARE_VERSION); + if (characteristic != null && + (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { + readCharacteristic("Read firmware version", + NutConstants.CHARAC_FIRMWARE_VERSION); + } + } + + private void readBatteryInfo() { + BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_BATTERY_INFO); + if (characteristic != null && + (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { + readCharacteristic("Read battery info", + NutConstants.CHARAC_BATTERY_INFO); + } + } + + /** + * Loads the three keys from device-specific shared preferences + */ + private void loadKeysFromPrefs() { + if (prefs != null) { + LOG.info("Reading keys"); + key1 = new BigInteger(prefs.getString("nut_packet_key_1", "0")); + key2 = new BigInteger(prefs.getString("nut_packet_key_2", "0")); + if (key1.equals(BigInteger.ZERO) || key2.equals(BigInteger.ZERO)) { + byte[] challenge = NutKey.hexStringToByteArrayNut(prefs.getString("nut_packet_challenge", "00")); + byte[] response = NutKey.hexStringToByteArrayNut(prefs.getString("nut_response_challenge", "00")); + if (Arrays.equals(challenge, new byte[]{0x00}) || + Arrays.equals(response, new byte[]{0x00})) { + GB.toast("No key available for the device", Toast.LENGTH_LONG, GB.ERROR); + return; + } + Map.Entry key = NutKey.reversePasswordGeneration( + challenge, + response, + gbDevice.getAddress() + ); + if (key == null) { + GB.toast("No correct key available for the device", Toast.LENGTH_LONG, GB.ERROR); + return; + } + key1 = key.getKey(); + key2 = key.getValue(); + LOG.debug("Key was extracted from challenge-response packets"); + } else { + LOG.debug("Key was preset"); + } + } + } + + /** + * Processes the authentication flow of the proprietary Nut protocol + * See more: {@link NutConstants#SERVICE_PROPRIETARY_NUT} + * + * @param received the notify characteristic's content + */ + public final void handleAuthResult(byte[] received) { + if (received != null && received.length != 0) { + if (received[0] == 0x01) { + // Password is needed + // Preamble, counter, rotating key, static key + byte[] payload = new byte[1 + 1 + 3 + 12]; + + // This is a response to the challenge + payload[0] = 0x02; + + // Modify the challenge + byte[] response = NutKey.passwordGeneration(gbDevice.getAddress(), received, key1, key2); + System.arraycopy(response, 0, payload, 1, response.length); + + writeCharacteristic("Authentication", + NutConstants.CHARAC_DFU_PW, + payload + ); + LOG.debug("Successfully sent auth"); + } else if (received[0] == 0x03) { + if (received[1] == 0x55) { + LOG.debug("Successful password attempt or uninitialized"); + authenticated = true; + initChara(); + } else { + LOG.debug("Error authenticating"); + // TODO: Disconnect + } + } else if (received[0] == 0x05) { + LOG.debug("Password has been set"); + } else { + LOG.debug("Invalid packet"); + // TODO: Disconnect + } + } + } + + /** + * Initializes required characteristics + */ + private void initChara() { + if (proprietary) { + writeCharacteristic("Init alert 1", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00}); + writeCharacteristic("Init alert 2", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00}); + writeCharacteristic("Init alert 3", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00}); + } + } + + /** + * Initiates a read of all the device information characteristics + */ + private void readDeviceInfo() { + readBatteryInfo(); + readHardwareInfo(); + readFirmwareInfo(); + } + + /** + * Just wraps writing into a neat little function + * + * @param taskName something that describes the task a bit + * @param charac the characteristic to write + * @param data the data to write + */ + private void writeCharacteristic(String taskName, UUID charac, byte[] data) { + BluetoothGattCharacteristic characteristic = getCharacteristic(charac); + + TransactionBuilder builder = new TransactionBuilder(taskName); + builder.write(characteristic, data); + builder.queue(getQueue()); + } + + /** + * Just wraps reading into a neat little function + * + * @param taskName something that describes the task a bit + * @param charac the characteristic to read + */ + private void readCharacteristic(String taskName, UUID charac) { + BluetoothGattCharacteristic characteristic = getCharacteristic(charac); + + TransactionBuilder builder = new TransactionBuilder(taskName); + builder.read(characteristic); + builder.queue(getQueue()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java index 7f1a89f65..8c8ce2b52 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Andreas Shimokawa +/* Copyright (C) 2020 Andreas Shimokawa, Taavi Eomäe This file is part of Gadgetbridge. @@ -20,17 +20,31 @@ import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Intent; import android.net.Uri; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.GregorianCalendar; +import java.util.Locale; import java.util.UUID; +import no.nordicsemi.android.dfu.DfuLogListener; +import no.nordicsemi.android.dfu.DfuProgressListener; +import no.nordicsemi.android.dfu.DfuProgressListenerAdapter; +import no.nordicsemi.android.dfu.DfuServiceController; +import no.nordicsemi.android.dfu.DfuServiceInitiator; +import no.nordicsemi.android.dfu.DfuServiceListenerHelper; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; +import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeDFUService; +import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeInstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFConstants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; @@ -52,26 +66,143 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotificat import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.OverflowStrategy; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { - +public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuLogListener { private static final Logger LOG = LoggerFactory.getLogger(PineTimeJFSupport.class); private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final DeviceInfoProfile deviceInfoProfile; + /** + * These are used to keep track when long strings haven't changed, + * thus avoiding unnecessary transfers that are (potentially) very slow. + *

+ * Makes the device's UI more responsive. + */ + String lastAlbum; + String lastTrack; + String lastArtist; + PineTimeInstallHandler handler; + DfuServiceController controller; - private static final UUID UUID_SERVICE_MUSICCONTROL = UUID.fromString("c7e50001-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_EVENT = UUID.fromString("c7e50002-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_STATUS = UUID.fromString("c7e50003-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_TRACK = UUID.fromString("c7e50004-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_ARTIST = UUID.fromString("c7e50005-00fc-48fe-8e23-433b3a1942d0"); - private static final UUID UUID_CHARACTERISTICS_MUSIC_ALBUM = UUID.fromString("c7e50006-00fc-48fe-8e23-433b3a1942d0"); + private final DfuProgressListener progressListener = new DfuProgressListenerAdapter() { + private final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(getContext()); + + /** + * Sets the progress bar to indeterminate or not, also makes it visible + * + * @param indeterminate if indeterminate + */ + public void setIndeterminate(boolean indeterminate) { + manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR).putExtra(GB.PROGRESS_BAR_INDETERMINATE, indeterminate)); + } + + /** + * Sets the status text and logs it + */ + public void setProgress(int progress) { + manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR).putExtra(GB.PROGRESS_BAR_PROGRESS, progress)); + } + + /** + * Sets the text that describes progress + * + * @param progressText text to display + */ + public void setProgressText(String progressText) { + manager.sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT).putExtra(GB.DISPLAY_MESSAGE_MESSAGE, progressText)); + } + + @Override + public void onDeviceConnecting(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_connecting)); + } + + @Override + public void onDeviceConnected(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_connected)); + } + + @Override + public void onEnablingDfuMode(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_upload_starting)); + } + + @Override + public void onDfuProcessStarting(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_upload_starting)); + } + + @Override + public void onDfuProcessStarted(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_upload_started)); + } + + @Override + public void onDeviceDisconnecting(final String mac) { + this.setProgressText(getContext().getString(R.string.devicestatus_disconnecting)); + } + + @Override + public void onDeviceDisconnected(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_disconnected)); + } + + @Override + public void onDfuCompleted(final String mac) { + this.setProgressText(getContext().getString(R.string.devicestatus_upload_completed)); + this.setIndeterminate(false); + this.setProgress(100); + + handler = null; + controller = null; + DfuServiceListenerHelper.unregisterProgressListener(getContext(), progressListener); + + // TODO: Request reconnection + } + + @Override + public void onFirmwareValidating(final String mac) { + this.setIndeterminate(true); + this.setProgressText(getContext().getString(R.string.devicestatus_upload_validating)); + } + + @Override + public void onDfuAborted(final String mac) { + this.setProgressText(getContext().getString(R.string.devicestatus_upload_aborted)); + } + + @Override + public void onError(final String mac, int error, int errorType, final String message) { + this.setProgressText(getContext().getString(R.string.devicestatus_upload_failed)); + } + + @Override + public void onProgressChanged(final String mac, + int percent, + float speed, + float averageSpeed, + int segment, + int totalSegments) { + this.setProgress(percent); + this.setIndeterminate(false); + this.setProgressText(String.format(Locale.ENGLISH, + getContext().getString(R.string.firmware_update_progress), + percent, speed, averageSpeed, segment, totalSegments)); + } + }; public PineTimeJFSupport() { super(LOG); addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION); addSupportedService(GattService.UUID_SERVICE_CURRENT_TIME); addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); - addSupportedService(UUID_SERVICE_MUSICCONTROL); + addSupportedService(PineTimeJFConstants.UUID_SERVICE_MUSIC_CONTROL); deviceInfoProfile = new DeviceInfoProfile<>(this); IntentListener mListener = new IntentListener() { @Override @@ -88,34 +219,6 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { addSupportedProfile(deviceInfoProfile); } - - @Override - protected TransactionBuilder initializeDevice(TransactionBuilder builder) { - builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); - requestDeviceInfo(builder); - onSetTime(); - builder.notify(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_EVENT), true); - setInitialized(builder); - return builder; - } - - - private void setInitialized(TransactionBuilder builder) { - builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); - } - - private void requestDeviceInfo(TransactionBuilder builder) { - LOG.debug("Requesting Device Info!"); - deviceInfoProfile.requestDeviceInfo(builder); - } - - private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { - LOG.warn("Device info: " + info); - versionCmd.hwVersion = info.getHardwareRevision(); - versionCmd.fwVersion = info.getFirmwareRevision(); - handleGBDeviceEvent(versionCmd); - } - @Override public boolean useAutoConnect() { return false; @@ -137,7 +240,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { @Override public void onSetTime() { - // since this is a standard we should generalize this in Gadgetbridge (properly) + // Since this is a standard we should generalize this in Gadgetbridge (properly) GregorianCalendar now = BLETypeConversions.createCalendar(); byte[] bytes = BLETypeConversions.calendarToRawBytes(now); byte[] tail = new byte[]{0, BLETypeConversions.mapTimeZone(now.getTimeZone(), BLETypeConversions.TZ_FLAG_INCLUDE_DST_IN_TZ)}; @@ -163,7 +266,6 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } - @Override public void onEnableRealtimeSteps(boolean enable) { @@ -171,7 +273,35 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { @Override public void onInstallApp(Uri uri) { + try { + handler = new PineTimeInstallHandler(uri, getContext()); + // TODO: Check validity more closely + if (true) { + DfuServiceInitiator starter = new DfuServiceInitiator(getDevice().getAddress()) + .setDeviceName(getDevice().getName()) + .setKeepBond(true) + .setForeground(false) + .setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(false) + .setMtu(517) + .setZip(uri); + + controller = starter.start(getContext(), PineTimeDFUService.class); + DfuServiceListenerHelper.registerProgressListener(getContext(), progressListener); + DfuServiceListenerHelper.registerLogListener(getContext(), this); + + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_BAR) + .putExtra(GB.PROGRESS_BAR_INDETERMINATE, true) + ); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent(GB.ACTION_SET_PROGRESS_TEXT) + .putExtra(GB.DISPLAY_MESSAGE_MESSAGE, getContext().getString(R.string.devicestatus_upload_starting)) + ); + } else { + // TODO: Handle invalid firmware files + } + } catch (Exception ex) { + GB.toast(getContext(), getContext().getString(R.string.updatefirmwareoperation_write_failed) + ":" + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } } @Override @@ -221,11 +351,14 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { @Override public void onFindDevice(boolean start) { - onSetConstantVibration(start ? 0xff : 0x00); + TransactionBuilder builder = new TransactionBuilder("Enable alert"); + builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL), new byte[]{(byte) (start ? 0x01 : 0x00)}); + builder.queue(getQueue()); } @Override public void onSetConstantVibration(int intensity) { + } @Override @@ -253,24 +386,47 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + requestDeviceInfo(builder); + onSetTime(); + builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_EVENT), true); + setInitialized(builder); + return builder; + } + @Override public void onSetMusicInfo(MusicSpec musicSpec) { try { TransactionBuilder builder = performInitialized("send playback info"); - if (musicSpec.album != null) { - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_TRACK), musicSpec.track.getBytes()); + if (musicSpec.album != null && !musicSpec.album.equals(lastAlbum)) { + lastAlbum = musicSpec.album; + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_ALBUM, musicSpec.album.getBytes()); } - if (musicSpec.artist != null) { - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_ARTIST), musicSpec.artist.getBytes()); + if (musicSpec.track != null && !musicSpec.track.equals(lastTrack)) { + lastTrack = musicSpec.track; + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_TRACK, musicSpec.track.getBytes()); } - if (musicSpec.album != null) { - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_ALBUM), musicSpec.album.getBytes()); + if (musicSpec.artist != null && !musicSpec.artist.equals(lastArtist)) { + lastArtist = musicSpec.artist; + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_ARTIST, musicSpec.artist.getBytes()); + } + + if (musicSpec.duration != MusicSpec.MUSIC_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_LENGTH_TOTAL, intToBytes(musicSpec.duration)); + } + if (musicSpec.trackNr != MusicSpec.MUSIC_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_TRACK_NUMBER, intToBytes(musicSpec.trackNr)); + } + if (musicSpec.trackCount != MusicSpec.MUSIC_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_TRACK_TOTAL, intToBytes(musicSpec.trackCount)); } builder.queue(getQueue()); } catch (Exception e) { - LOG.error("error sending music info", e); + LOG.error("Error sending music info", e); } } @@ -279,19 +435,65 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { try { TransactionBuilder builder = performInitialized("send playback state"); - byte[] state = new byte[]{0}; - if (stateSpec.state == MusicStateSpec.STATE_PLAYING) { - state[0] = 1; + if (stateSpec.state != MusicStateSpec.STATE_UNKNOWN) { + byte[] state = new byte[1]; + if (stateSpec.state == MusicStateSpec.STATE_PLAYING) { + state[0] = 0x01; + } + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_STATUS, state); } - builder.write(getCharacteristic(UUID_CHARACTERISTICS_MUSIC_STATUS), state); + + if (stateSpec.playRate != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_PLAYBACK_SPEED, intToBytes(stateSpec.playRate)); + } + + if (stateSpec.position != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_POSITION, intToBytes(stateSpec.position)); + } + + if (stateSpec.repeat != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_REPEAT, intToBytes(stateSpec.repeat)); + } + + if (stateSpec.shuffle != MusicStateSpec.STATE_UNKNOWN) { + safeWriteToCharacteristic(builder, PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_SHUFFLE, intToBytes(stateSpec.repeat)); + } + builder.queue(getQueue()); } catch (Exception e) { - LOG.error("error sending music state", e); + LOG.error("Error sending music state", e); } } + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + if (super.onCharacteristicRead(gatt, characteristic, status)) { + return true; + } + UUID characteristicUUID = characteristic.getUuid(); + + LOG.info("Unhandled characteristic read: " + characteristicUUID); + return false; + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onReadConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { @@ -300,7 +502,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { } UUID characteristicUUID = characteristic.getUuid(); - if (characteristicUUID.equals(UUID_CHARACTERISTICS_MUSIC_EVENT)) { + if (characteristicUUID.equals(PineTimeJFConstants.UUID_CHARACTERISTICS_MUSIC_EVENT)) { byte[] value = characteristic.getValue(); GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); @@ -333,35 +535,52 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport { return false; } - @Override - public boolean onCharacteristicRead(BluetoothGatt gatt, - BluetoothGattCharacteristic characteristic, int status) { - if (super.onCharacteristicRead(gatt, characteristic, status)) { - return true; - } - UUID characteristicUUID = characteristic.getUuid(); - - LOG.info("Unhandled characteristic read: " + characteristicUUID); - return false; - } - - @Override - public void onSendConfiguration(String config) { - - } - - @Override - public void onReadConfiguration(String config) { - - } - - @Override - public void onTestNewFunction() { - - } - @Override public void onSendWeather(WeatherSpec weatherSpec) { } + + /** + * Helper function that just converts an integer into a byte array + */ + private static byte[] intToBytes(int source) { + return ByteBuffer.allocate(4).putInt(source).array(); + } + + /** + * This will check if the characteristic exists and can be written + *

+ * Keeps backwards compatibility with firmware that can't take all the information + */ + private void safeWriteToCharacteristic(TransactionBuilder builder, UUID uuid, byte[] data) { + BluetoothGattCharacteristic characteristic = getCharacteristic(uuid); + if (characteristic != null && + (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) { + builder.write(characteristic, data); + } + } + + private void setInitialized(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + } + + private void requestDeviceInfo(TransactionBuilder builder) { + LOG.debug("Requesting Device Info!"); + deviceInfoProfile.requestDeviceInfo(builder); + } + + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { + LOG.warn("Device info: " + info); + versionCmd.hwVersion = info.getHardwareRevision(); + versionCmd.fwVersion = info.getFirmwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + /** + * Nordic DFU needs this function to log DFU-related messages + */ + @Override + public void onLogEvent(final String deviceAddress, final int level, final String message) { + LOG.debug(message); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12Constants.java new file mode 100644 index 000000000..0c4b1d6c0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12Constants.java @@ -0,0 +1,34 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12; + +import java.util.UUID; + +public class SonySWR12Constants { + //accessory host service + public static final String BASE_UUID_AHS = "0000%s-37CB-11E3-8682-0002A5D5C51B"; + public static final UUID UUID_SERVICE_AHS = UUID.fromString(String.format(BASE_UUID_AHS, "0200")); + public static final UUID UUID_CHARACTERISTIC_ALARM = UUID.fromString(String.format(BASE_UUID_AHS, "0204")); + public static final UUID UUID_CHARACTERISTIC_EVENT = UUID.fromString(String.format(BASE_UUID_AHS, "0205")); + public static final UUID UUID_CHARACTERISTIC_TIME = UUID.fromString(String.format(BASE_UUID_AHS, "020B")); + public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID_AHS, "0208")); + + public static final int TYPE_ACTIVITY = 0; + public static final int TYPE_LIGHT = 1; + public static final int TYPE_DEEP = 2; + public static final int TYPE_NOT_WORN = 3; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12DeviceSupport.java new file mode 100644 index 000000000..637373b04 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12DeviceSupport.java @@ -0,0 +1,371 @@ +/* Copyright (C) 2015-2020 Andreas Böhler, Andreas Shimokawa, Carsten + Pfeiffer, Daniel Dakhno, Daniele Gobbetti, JohnnySun, José Rebelo, + opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; +import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +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; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.EventBase; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.EventFactory; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.alarm.BandAlarm; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.alarm.BandAlarms; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control.CommandCode; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control.ControlPointLowVibration; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control.ControlPointWithValue; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.time.BandTime; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +// done: +// - time sync +// - alarms (also smart) +// - fetching activity(walking, sleep) +// - stamina mode +// - vibration intensity +// - realtime heart rate +// todo options: +// - "get moving" +// - get notified: -call, -notification, -notification from, -do not disturb +// - media control: media/find phone(tap once for play pause, tap twice for next, tap triple for previous) + +public class SonySWR12DeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(SonySWR12DeviceSupport.class); + private SonySWR12HandlerThread processor = null; + + private final BatteryInfoProfile batteryInfoProfile; + private final IntentListener mListener = new IntentListener() { + @Override + public void notify(Intent intent) { + if (intent.getAction().equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) { + BatteryInfo info = intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO); + GBDeviceEventBatteryInfo gbInfo = new GBDeviceEventBatteryInfo(); + gbInfo.level = (short) info.getPercentCharged(); + handleGBDeviceEvent(gbInfo); + } + } + }; + + public SonySWR12DeviceSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); + addSupportedService(SonySWR12Constants.UUID_SERVICE_AHS); + batteryInfoProfile = new BatteryInfoProfile<>(this); + batteryInfoProfile.addListener(mListener); + addSupportedProfile(batteryInfoProfile); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + initialize(); + setTime(builder); + batteryInfoProfile.requestBatteryInfo(builder); + return builder; + } + + private SonySWR12HandlerThread getProcessor() { + if (processor == null) { + processor = new SonySWR12HandlerThread(getDevice(), getContext()); + processor.start(); + } + return processor; + } + + private void initialize() { + if (gbDevice.getState() != GBDevice.State.INITIALIZED) { + gbDevice.setFirmwareVersion("N/A"); + gbDevice.setFirmwareVersion2("N/A"); + gbDevice.setState(GBDevice.State.INITIALIZED); + gbDevice.sendDeviceUpdateIntent(getContext()); + } + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized("setTime"); + setTime(builder); + builder.queue(getQueue()); + } catch (Exception e) { + GB.toast(getContext(), "Error setting time: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void setTime(TransactionBuilder builder) { + BluetoothGattCharacteristic timeCharacteristic = getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_TIME); + builder.write(timeCharacteristic, new BandTime(Calendar.getInstance()).toByteArray()); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + BluetoothGattCharacteristic alarmCharacteristic = getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_ALARM); + TransactionBuilder builder = performInitialized("alarm"); + int prefInterval = Integer.valueOf(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()) + .getString(DeviceSettingsPreferenceConst.PREF_SONYSWR12_SMART_INTERVAL, "0")); + ArrayList bandAlarmList = new ArrayList<>(); + for (Alarm alarm : alarms) { + BandAlarm bandAlarm = BandAlarm.fromAppAlarm(alarm, bandAlarmList.size(), alarm.getSmartWakeup() ? prefInterval : 0); + if (bandAlarm != null) + bandAlarmList.add(bandAlarm); + } + builder.write(alarmCharacteristic, new BandAlarms(bandAlarmList).toByteArray()); + builder.queue(getQueue()); + } catch (Exception e) { + GB.toast(getContext(), "Error setting alarms: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + return super.onCharacteristicRead(gatt, characteristic, status); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) + return true; + UUID uuid = characteristic.getUuid(); + if (uuid.equals(SonySWR12Constants.UUID_CHARACTERISTIC_EVENT)) { + try { + EventBase event = EventFactory.readEventFromByteArray(characteristic.getValue()); + getProcessor().process(event); + } catch (Exception e) { + return false; + } + return true; + } + return false; + } + + @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) { + //doesn't support realtime steps + //supports only realtime heart rate + } + + @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) { + try { + TransactionBuilder builder = performInitialized("fetchActivity"); + builder.notify(getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_EVENT), true); + ControlPointWithValue flushControl = new ControlPointWithValue(CommandCode.FLUSH_ACTIVITY, 0); + builder.write(getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_CONTROL_POINT), flushControl.toByteArray()); + builder.queue(getQueue()); + } catch (Exception e) { + LOG.error("failed to fetch activity data", e); + } + } + + @Override + public void onReset(int flags) { + + } + + @Override + public void onHeartRateTest() { + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + try { + TransactionBuilder builder = performInitialized("HeartRateTest"); + builder.notify(getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_EVENT), enable); + ControlPointWithValue controlPointHeart = new ControlPointWithValue(CommandCode.HEARTRATE_REALTIME, enable ? 1 : 0); + builder.write(getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_CONTROL_POINT), controlPointHeart.toByteArray()); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to read heart rate from Sony device", ex); + } + } + + @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) { + try { + switch (config) { + case DeviceSettingsPreferenceConst.PREF_SONYSWR12_STAMINA: { + //stamina can be: + //disabled = 0, enabled = 1 or todo auto on low battery = 2 + int status = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(config, false) ? 1 : 0; + TransactionBuilder builder = performInitialized(config); + ControlPointWithValue vibrationControl = new ControlPointWithValue(CommandCode.STAMINA_MODE, status); + builder.write(getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_CONTROL_POINT), vibrationControl.toByteArray()); + builder.queue(getQueue()); + break; + } + case DeviceSettingsPreferenceConst.PREF_SONYSWR12_LOW_VIBRATION: { + boolean isEnabled = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(config, false); + TransactionBuilder builder = performInitialized(config); + ControlPointLowVibration vibrationControl = new ControlPointLowVibration(isEnabled); + builder.write(getCharacteristic(SonySWR12Constants.UUID_CHARACTERISTIC_CONTROL_POINT), vibrationControl.toByteArray()); + builder.queue(getQueue()); + break; + } + case DeviceSettingsPreferenceConst.PREF_SONYSWR12_SMART_INTERVAL: { + onSetAlarms(new ArrayList(DBHelper.getAlarms(gbDevice))); + } + } + } catch (Exception exc) { + LOG.error("failed to send config " + config, exc); + } + } + + @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/sonyswr12/SonySWR12HandlerThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12HandlerThread.java new file mode 100644 index 000000000..a66024cc4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12HandlerThread.java @@ -0,0 +1,151 @@ +/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12; + +import android.content.Context; +import android.content.Intent; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.SonySWR12Sample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.ActivityBase; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.ActivitySleep; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.ActivityWithData; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.EventBase; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.EventCode; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.EventWithActivity; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity.EventWithValue; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; + +public class SonySWR12HandlerThread extends GBDeviceIoThread { + private static final Logger LOG = LoggerFactory.getLogger(SonySWR12HandlerThread.class); + + public SonySWR12HandlerThread(GBDevice gbDevice, Context context) { + super(gbDevice, context); + } + + public void process(EventBase event) { + if (event instanceof EventWithValue) { + if (event.getCode() == EventCode.HEART_RATE) { + processRealTimeHeartRate((EventWithValue) event); + } + } else if (event instanceof EventWithActivity) { + processWithActivity((EventWithActivity) event); + } + } + + private void processRealTimeHeartRate(EventWithValue event) { + try { + DBHandler dbHandler = GBApplication.acquireDB(); + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + SonySWR12SampleProvider provider = new SonySWR12SampleProvider(getDevice(), dbHandler.getDaoSession()); + int timestamp = getTimestamp(); + SonySWR12Sample sample = new SonySWR12Sample(timestamp, deviceId, userId, (int) event.value, ActivitySample.NOT_MEASURED, 0, 1); + provider.addGBActivitySample(sample); + GBApplication.releaseDB(); + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, timestamp); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + private int getTimestamp() { + return (int) (System.currentTimeMillis() / 1000); + } + + private void processWithActivity(EventWithActivity event) { + List payloadList = event.activityList; + for (ActivityBase activity : payloadList) { + switch (activity.getType()) { + case WALK: + case RUN: + addActivity((ActivityWithData) activity); + break; + case SLEEP: + addSleep((ActivitySleep) activity); + break; + } + } + } + + private void addActivity(ActivityWithData activity) { + try { + DBHandler dbHandler = GBApplication.acquireDB(); + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + SonySWR12SampleProvider provider = new SonySWR12SampleProvider(getDevice(), dbHandler.getDaoSession()); + int kind = SonySWR12Constants.TYPE_ACTIVITY; + SonySWR12Sample sample = new SonySWR12Sample(activity.getTimeStampSec(), deviceId, userId, ActivitySample.NOT_MEASURED, activity.data, kind, 1); + provider.addGBActivitySample(sample); + GBApplication.releaseDB(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + + private void addSleep(ActivitySleep activity) { + try { + DBHandler dbHandler = GBApplication.acquireDB(); + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + SonySWR12SampleProvider provider = new SonySWR12SampleProvider(getDevice(), dbHandler.getDaoSession()); + int kind; + switch (activity.sleepLevel) { + case LIGHT: + kind = SonySWR12Constants.TYPE_LIGHT; + break; + case DEEP: + kind = SonySWR12Constants.TYPE_DEEP; + break; + default: + kind = SonySWR12Constants.TYPE_ACTIVITY; + break; + } + if (kind == SonySWR12Constants.TYPE_LIGHT || kind == SonySWR12Constants.TYPE_DEEP) { + //need so much samples because sleep has exact duration + //so empty samples are for right representation of sleep on activity charts + SonySWR12Sample sample = new SonySWR12Sample(activity.getTimeStampSec(), deviceId, userId, ActivitySample.NOT_MEASURED, 0, SonySWR12Constants.TYPE_NOT_WORN, 1); + provider.addGBActivitySample(sample); + sample = new SonySWR12Sample(activity.getTimeStampSec() + 2, deviceId, userId, ActivitySample.NOT_MEASURED, 0, kind, 1); + provider.addGBActivitySample(sample); + sample = new SonySWR12Sample(activity.getTimeStampSec() + activity.durationMin * 60 - 2, deviceId, userId, ActivitySample.NOT_MEASURED, 0, kind, 1); + provider.addGBActivitySample(sample); + sample = new SonySWR12Sample(activity.getTimeStampSec() + activity.durationMin * 60, deviceId, userId, ActivitySample.NOT_MEASURED, 0, SonySWR12Constants.TYPE_NOT_WORN, 1); + provider.addGBActivitySample(sample); + } + GBApplication.releaseDB(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12Util.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12Util.java new file mode 100644 index 000000000..fa9066ead --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/SonySWR12Util.java @@ -0,0 +1,38 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class SonySWR12Util { + + public static long secSince2013() { + //sony uses time on band since 2013 for some reason + final Calendar instance = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + instance.set(2013, 0, 1, 0, 0, 0); + instance.set(14, 0); + return instance.getTimeInMillis()/1000; + } + + public static String timeToString(long sec) { + SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm:ss"); + return format.format(new Date(sec * 1000)); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityBase.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityBase.java new file mode 100644 index 000000000..5f8d96247 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityBase.java @@ -0,0 +1,38 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public abstract class ActivityBase { + protected final ActivityType type; + private final long timeStampSec; + + public ActivityBase(ActivityType type, int timeOffsetMin, long timeStampSec) { + if (timeOffsetMin < 0 || timeOffsetMin > 1440) { + throw new IllegalArgumentException("activity time offset out of range: " + timeOffsetMin); + } + this.type = type; + this.timeStampSec = timeStampSec + timeOffsetMin * 60; + } + + public final int getTimeStampSec() { + return (int) (timeStampSec); + } + + public final ActivityType getType() { + return this.type; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityHeartRate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityHeartRate.java new file mode 100644 index 000000000..b9a36f86c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityHeartRate.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public class ActivityHeartRate extends ActivityBase { + public final int bpm; + + public ActivityHeartRate(int timeOffsetMin, int bpm, Long timeStampSec) { + super(ActivityType.HEART_RATE, timeOffsetMin, timeStampSec); + if (bpm < 0 || bpm > 65535) { + throw new IllegalArgumentException("bpm out of range: " + bpm); + } + this.bpm = bpm; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivitySleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivitySleep.java new file mode 100644 index 000000000..f2ad773a2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivitySleep.java @@ -0,0 +1,28 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public class ActivitySleep extends ActivityBase { + public final SleepLevel sleepLevel; + public final int durationMin; + + public ActivitySleep(int timeOffsetMin, int durationMin, SleepLevel sleepLevel, Long timeStampSec) { + super(ActivityType.SLEEP, timeOffsetMin, timeStampSec); + this.durationMin = durationMin; + this.sleepLevel = sleepLevel; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityType.java new file mode 100644 index 000000000..06c798f57 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityType.java @@ -0,0 +1,39 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public enum ActivityType { + WALK(1), + RUN(2), + SLEEP(3), + HEART_RATE(10), + END(14); + + final int value; + + ActivityType(int value) { + this.value = value; + } + + public static ActivityType fromInt(int i) { + for (ActivityType type : values()){ + if (type.value == i) + return type; + } + throw new IllegalArgumentException("wrong activity type: " + i); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityWithData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityWithData.java new file mode 100644 index 000000000..34f22c123 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/ActivityWithData.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public class ActivityWithData extends ActivityBase { + public final int data; + + public ActivityWithData(ActivityType activityType, int timeOffsetMin, int data, Long timeStampSec) { + super(activityType, timeOffsetMin, timeStampSec); + if (data < 0 || data > 65535) { + throw new IllegalArgumentException("data out of range: " + data); + } + this.data = data; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventBase.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventBase.java new file mode 100644 index 000000000..c6b21a08e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventBase.java @@ -0,0 +1,37 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public abstract class EventBase { + protected final EventCode eventCode; + + protected EventBase(EventCode eventCode) { + this.eventCode = eventCode; + } + + public EventCode getCode() { + return this.eventCode; + } + + protected ByteArrayWriter getValueWriter() { + ByteArrayWriter byteArrayWriter = new ByteArrayWriter(); + byteArrayWriter.appendUint8(this.eventCode.value); + return byteArrayWriter; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventCode.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventCode.java new file mode 100644 index 000000000..e3f25b1f1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventCode.java @@ -0,0 +1,37 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public enum EventCode { + STEPS(3), + ACTIVITY_DATA(5), + HEART_RATE(9); + + final int value; + + EventCode(int value) { + this.value = value; + } + + static EventCode fromInt(int i) { + for (EventCode code : values()){ + if (code.value == i) + return code; + } + throw new RuntimeException("wrong event code: " + i); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventFactory.java new file mode 100644 index 000000000..feab44396 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventFactory.java @@ -0,0 +1,42 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.IntFormat; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayReader; + +public class EventFactory { + + public static EventBase readEventFromByteArray(byte[] array) { + try { + ByteArrayReader byteArrayReader = new ByteArrayReader(array); + EventCode eventCode = EventCode.fromInt(byteArrayReader.readUint8()); + switch (eventCode) { + case HEART_RATE: { + long value = byteArrayReader.readInt(IntFormat.UINT32); + return new EventWithValue(eventCode, value); + } + case ACTIVITY_DATA: { + return EventWithActivity.fromByteArray(byteArrayReader); + } + default: return null; + } + } catch (Exception ex) { + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventWithActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventWithActivity.java new file mode 100644 index 000000000..5a36b66a7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventWithActivity.java @@ -0,0 +1,68 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +import java.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.SonySWR12Util; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.IntFormat; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.UIntBitReader; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayReader; + +public class EventWithActivity extends EventBase { + public final long timeStampSec; + public final List activityList; + + private EventWithActivity(long timeStampSec, List activityList) { + super(EventCode.ACTIVITY_DATA); + this.timeStampSec = timeStampSec; + this.activityList = activityList; + } + + public static EventWithActivity fromByteArray(ByteArrayReader byteArrayReader) { + long timeOffset = byteArrayReader.readInt(IntFormat.UINT32); + long timeStampSec = SonySWR12Util.secSince2013() + timeOffset; + ArrayList activities = new ArrayList<>(); + while (byteArrayReader.getBytesLeft() > 0) { + UIntBitReader uIntBitReader = new UIntBitReader(byteArrayReader.readInt(IntFormat.UINT32), 32); + ActivityType activityType = ActivityType.fromInt(uIntBitReader.read(4)); + int offsetMin = uIntBitReader.read(12); + ActivityBase activityPayload; + switch (activityType) { + case SLEEP: { + int duration = uIntBitReader.read(14); + SleepLevel sleepLevel = SleepLevel.fromInt(uIntBitReader.read(2)); + activityPayload = new ActivitySleep(offsetMin, duration, sleepLevel, timeStampSec); + break; + } + case HEART_RATE: { + int bpm = uIntBitReader.read(16); + activityPayload = new ActivityHeartRate(offsetMin, bpm, timeStampSec); + break; + } + default: { + int data = uIntBitReader.read(16); + activityPayload = new ActivityWithData(activityType, offsetMin, data, timeStampSec); + break; + } + } + activities.add(activityPayload); + } + return new EventWithActivity(timeStampSec, activities); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventWithValue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventWithValue.java new file mode 100644 index 000000000..29c59fda6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/EventWithValue.java @@ -0,0 +1,34 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public class EventWithValue extends EventBase { + public final long value; + + public EventWithValue(EventCode eventCode, long value) { + super(eventCode); + this.value = value; + } + + public byte[] toByteArray() { + ByteArrayWriter byteArrayWriter = this.getValueWriter(); + byteArrayWriter.appendUint32(this.value); + return byteArrayWriter.getByteArray(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/SleepLevel.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/SleepLevel.java new file mode 100644 index 000000000..521d631c7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/activity/SleepLevel.java @@ -0,0 +1,37 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.activity; + +public enum SleepLevel { + AWAKE(0), + LIGHT(1), + DEEP(2); + + final int value; + + SleepLevel(int value){ + this.value = value; + } + + public static SleepLevel fromInt(int i) { + for (SleepLevel level : values()){ + if (level.value == i) + return level; + } + throw new RuntimeException("wrong sleep level: " + i); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/AlarmRepeat.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/AlarmRepeat.java new file mode 100644 index 000000000..26ecaf1cb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/AlarmRepeat.java @@ -0,0 +1,69 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.alarm; + +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.UIntBitWriter; + +public class AlarmRepeat { + private final boolean[] repeat = new boolean[7]; + + public AlarmRepeat(Alarm alarm) { + super(); + setRepeatOnDay(0, alarm.getRepetition(Alarm.ALARM_MON)); + setRepeatOnDay(1, alarm.getRepetition(Alarm.ALARM_TUE)); + setRepeatOnDay(2, alarm.getRepetition(Alarm.ALARM_WED)); + setRepeatOnDay(3, alarm.getRepetition(Alarm.ALARM_THU)); + setRepeatOnDay(4, alarm.getRepetition(Alarm.ALARM_FRI)); + setRepeatOnDay(5, alarm.getRepetition(Alarm.ALARM_SAT)); + setRepeatOnDay(6, alarm.getRepetition(Alarm.ALARM_SUN)); + } + + @Override + public boolean equals(Object o) { + if (this != o) { + if (o == null || this.getClass() != o.getClass()) { + return false; + } + return Arrays.equals(this.repeat, ((AlarmRepeat) o).repeat); + } + return true; + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.repeat); + } + + public void setRepeatOnDay(int i, boolean b) { + this.repeat[i] = b; + } + + public int toInt() { + UIntBitWriter uIntBitWriter = new UIntBitWriter(7); + uIntBitWriter.appendBoolean(this.repeat[6]); + uIntBitWriter.appendBoolean(this.repeat[5]); + uIntBitWriter.appendBoolean(this.repeat[4]); + uIntBitWriter.appendBoolean(this.repeat[3]); + uIntBitWriter.appendBoolean(this.repeat[2]); + uIntBitWriter.appendBoolean(this.repeat[1]); + uIntBitWriter.appendBoolean(this.repeat[0]); + return (int) uIntBitWriter.getValue(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/AlarmState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/AlarmState.java new file mode 100644 index 000000000..7d2e7c0c4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/AlarmState.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.alarm; + +public enum AlarmState { + TRIGGERED( 0), + SNOOZED(1), + IDLE(2); + + final int value; + + AlarmState(int value) { + this.value = value; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/BandAlarm.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/BandAlarm.java new file mode 100644 index 000000000..cde517714 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/BandAlarm.java @@ -0,0 +1,76 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.alarm; + +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; + +public class BandAlarm { + public static BandAlarm fromAppAlarm(Alarm alarm, int index, int interval) { + if (!alarm.getEnabled()) return null; + //smart wakeup = (0,10..60 min)/5 + int ahsInterval = interval / 5; + return new BandAlarm(AlarmState.IDLE, index, ahsInterval, alarm.getHour(), alarm.getMinute(), new AlarmRepeat(alarm)); + } + + public AlarmState state; + public int index; + public int interval; + public int hour; + public int minute; + public AlarmRepeat repeat; + + public BandAlarm(AlarmState state, int index, int interval, int hour, int minute, AlarmRepeat repeat) { + this.state = state; + this.index = index; + this.interval = interval; + this.hour = hour; + this.minute = minute; + this.repeat = repeat; + } + + @Override + public boolean equals(Object o) { + if (this != o) { + if (o == null || this.getClass() != o.getClass()) { + return false; + } + BandAlarm bandAlarm = (BandAlarm) o; + if (this.index != bandAlarm.index) { + return false; + } + if (this.hour != bandAlarm.hour) { + return false; + } + if (this.interval != bandAlarm.interval) { + return false; + } + if (this.minute != bandAlarm.minute) { + return false; + } + if (!this.repeat.equals(bandAlarm.repeat)) { + return false; + } + return this.state == bandAlarm.state; + } + return true; + } + + @Override + public int hashCode() { + return ((((this.state.hashCode() * 31 + this.index) * 31 + this.interval) * 31 + this.hour) * 31 + this.minute) * 31 + this.repeat.hashCode(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/BandAlarms.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/BandAlarms.java new file mode 100644 index 000000000..47a14c499 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/alarm/BandAlarms.java @@ -0,0 +1,51 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.alarm; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.UIntBitWriter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public class BandAlarms { + public final List alarms; + + public BandAlarms(List alarms) { + this.alarms = alarms; + } + + public byte[] toByteArray() { + ByteArrayWriter byteArrayWriter = new ByteArrayWriter(); + if (this.alarms.size() == 0) { + byteArrayWriter.appendUint32(1073741824L); + } else { + for (BandAlarm bandAlarm : this.alarms) { + UIntBitWriter uIntBitWriter = new UIntBitWriter(32); + uIntBitWriter.append(2, 0); + uIntBitWriter.append(4, bandAlarm.index); + uIntBitWriter.append(2, bandAlarm.state.value); + uIntBitWriter.append(4, bandAlarm.interval); + uIntBitWriter.append(6, bandAlarm.hour); + uIntBitWriter.append(6, bandAlarm.minute); + uIntBitWriter.append(1, 0); + uIntBitWriter.append(7, bandAlarm.repeat.toInt()); + byteArrayWriter.appendUint32(uIntBitWriter.getValue()); + } + } + return byteArrayWriter.getByteArray(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/CommandCode.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/CommandCode.java new file mode 100644 index 000000000..1be44a199 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/CommandCode.java @@ -0,0 +1,31 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control; + +public enum CommandCode { + FLUSH_ACTIVITY(7), + HEARTRATE_REALTIME(11), + STAMINA_MODE(17), + MANUAL_ALARM(19), + LOW_VIBRATION(25); + + public final int value; + + CommandCode(int value) { + this.value = value; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPoint.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPoint.java new file mode 100644 index 000000000..9e19ce32e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPoint.java @@ -0,0 +1,33 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public abstract class ControlPoint { + protected final CommandCode code; + + public ControlPoint(CommandCode code) { + this.code = code; + } + + protected final ByteArrayWriter getValueWriter() { + final ByteArrayWriter byteArrayWriter = new ByteArrayWriter(); + byteArrayWriter.appendUint8(this.code.value); + return byteArrayWriter; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPointLowVibration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPointLowVibration.java new file mode 100644 index 000000000..408bc4f3f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPointLowVibration.java @@ -0,0 +1,44 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.UIntBitWriter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public final class ControlPointLowVibration extends ControlPoint { + public boolean smartWakeUp; + public boolean incomingCall; + public boolean notification; + + public ControlPointLowVibration(boolean isEnabled){ + super(CommandCode.LOW_VIBRATION); + this.smartWakeUp = isEnabled; + this.incomingCall = isEnabled; + this.notification = isEnabled; + } + + public final byte[] toByteArray() { + final UIntBitWriter uIntBitWriter = new UIntBitWriter(16); + uIntBitWriter.append(13, 0); + uIntBitWriter.appendBoolean(this.smartWakeUp); + uIntBitWriter.appendBoolean(this.incomingCall); + uIntBitWriter.appendBoolean(this.notification); + final ByteArrayWriter byteArrayWriter = this.getValueWriter(); + byteArrayWriter.appendUint16((int) uIntBitWriter.getValue()); + return byteArrayWriter.getByteArray(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPointWithValue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPointWithValue.java new file mode 100644 index 000000000..0bc42878b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/control/ControlPointWithValue.java @@ -0,0 +1,37 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.control; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public class ControlPointWithValue extends ControlPoint { + protected final int value; + + public ControlPointWithValue(final CommandCode commandCode, final int value) { + super(commandCode); + if (value < 0 || value > 65535) { + throw new IllegalArgumentException("command value out of range " + value); + } + this.value = value; + } + + public final byte[] toByteArray() { + final ByteArrayWriter byteArrayWriter = this.getValueWriter(); + byteArrayWriter.appendUint16(this.value); + return byteArrayWriter.getByteArray(); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandDaylightSavingTime.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandDaylightSavingTime.java new file mode 100644 index 000000000..f1e8d1ef0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandDaylightSavingTime.java @@ -0,0 +1,40 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.time; + +public enum BandDaylightSavingTime { + STANDARD_TIME(0, 0), + HALF_AN_HOUR_DST(2, 30), + DST(4, 60), + DOUBLE_DST( 8, 120); + + final int key; + private final long saving; + + BandDaylightSavingTime(int key, int min) { + this.key = key; + this.saving = 60000L * min; + } + + public static BandDaylightSavingTime fromOffset(final int dstSaving) { + for (BandDaylightSavingTime dst: values()){ + if (dst.saving == dstSaving) + return dst; + } + throw new RuntimeException("wrong dst saving: " + dstSaving); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandTime.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandTime.java new file mode 100644 index 000000000..60da07556 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandTime.java @@ -0,0 +1,78 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.time; + +import java.util.Calendar; +import java.util.TimeZone; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.IntFormat; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util.ByteArrayWriter; + +public class BandTime { + private final int year; + private final int month; + private final int dayOfMonth; + private final int hour; + private final int min; + private final int sec; + private final int dayOfWeek; + private final BandTimeZone timeZone; + private final BandDaylightSavingTime dst; + + public BandTime(Calendar calendar) { + int dayOfWeek = 7; + if (calendar == null) { + throw new IllegalArgumentException("Calendar cant be null"); + } + this.year = calendar.get(1); + if (this.year > 2099 || this.year < 2013) { + throw new RuntimeException("out of 2013-2099"); + } + this.month = calendar.get(2) + 1; + this.dayOfMonth = calendar.get(5); + int value = calendar.get(7); + if (value != 1) { + dayOfWeek = value - 1; + } + this.dayOfWeek = dayOfWeek; + this.hour = calendar.get(11); + this.min = calendar.get(12); + this.sec = calendar.get(13); + TimeZone timeZone = calendar.getTimeZone(); + this.timeZone = BandTimeZone.fromOffset(timeZone.getRawOffset()); + if (timeZone.inDaylightTime(calendar.getTime())) { + this.dst = BandDaylightSavingTime.fromOffset(timeZone.getDSTSavings()); + return; + } + this.dst = BandDaylightSavingTime.STANDARD_TIME; + } + + public byte[] toByteArray() { + ByteArrayWriter byteArrayWriter = new ByteArrayWriter(); + byteArrayWriter.appendUint16(this.year); + byteArrayWriter.appendUint8(this.month); + byteArrayWriter.appendUint8(this.dayOfMonth); + byteArrayWriter.appendUint8(this.hour); + byteArrayWriter.appendUint8(this.min); + byteArrayWriter.appendUint8(this.sec); + byteArrayWriter.appendUint8(this.dayOfWeek); + byteArrayWriter.appendValue(this.timeZone.key, IntFormat.SINT8); + byteArrayWriter.appendUint8(this.dst.key); + return byteArrayWriter.getByteArray(); + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandTimeZone.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandTimeZone.java new file mode 100644 index 000000000..f5dc0fbb1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/entities/time/BandTimeZone.java @@ -0,0 +1,77 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.entities.time; + +public enum BandTimeZone { + UTC_PLUS_06_30(26, 6, 30), + UTC_PLUS_07_00(28, 7, 0), + UTC_PLUS_08_00(32, 8, 0), + UTC_PLUS_08_45(35, 8, 45), + UTC_PLUS_09_00(36, 9, 0), + UTC_PLUS_09_30(38, 9, 30), + UTC_PLUS_10_00(40, 10, 0), + UTC_PLUS_10_30(42, 10, 30), + UTC_PLUS_11_00(44, 11, 0), + UTC_PLUS_11_30(46, 11, 30), + UTC_PLUS_12_00(48, 12, 0), + UTC_PLUS_12_45(51, 12, 45), + UTC_PLUS_13_00( 52, 13, 0), + UTC_PLUS_14_00(56, 14, 0), + UTC_MINUS_12_00(-48, -12, 0), + UTC_MINUS_11_00(-44, -11, 0), + UTC_MINUS_10_00(-40, -10, 0), + UTC_MINUS_09_30(-38, -9, -30), + UTC_MINUS_09_00(-36, -9, 0), + UTC_MINUS_08_00(-32, -8, 0), + UTC_MINUS_07_00(-28, -7, 0), + UTC_MINUS_06_00(-24, -6, 0), + UTC_MINUS_05_00(-20, -5, 0), + UTC_MINUS_04_30(-18, -4, -30), + UTC_MINUS_04_00(-16, -4, 0), + UTC_MINUS_03_30(-14, -3, -30), + UTC_MINUS_03_00(-12, -3, 0), + UTC_MINUS_02_00(-8, -2, 0), + UTC_MINUS_01_00(-4, -1, 0), + UTC_PLUS_00_00(0, 0, 0), + UTC_PLUS_01_00(4, 1, 0), + UTC_PLUS_02_00(8, 2, 0), + UTC_PLUS_03_00(12, 3, 0), + UTC_PLUS_03_30(14, 3, 30), + UTC_PLUS_04_00(16, 4, 0), + UTC_PLUS_04_30(18, 4, 30), + UTC_PLUS_05_00(20, 5, 0), + UTC_PLUS_05_30(22, 5, 30), + UTC_PLUS_05_45(23, 5, 45), + UTC_PLUS_06_00(24, 6, 0); + + final int key; + private final long rawOffset; + + BandTimeZone(int key, int hourOffset, int minOffset) { + this.key = key; + this.rawOffset = 3600000L * hourOffset + 60000L * minOffset; + } + + public static BandTimeZone fromOffset(long rawOffset) { + for (BandTimeZone zone : values()){ + if (zone.rawOffset == rawOffset) + return zone; + } + throw new RuntimeException("wrong raw offset: " + rawOffset); + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/ByteArrayReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/ByteArrayReader.java new file mode 100644 index 000000000..a87182a29 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/ByteArrayReader.java @@ -0,0 +1,69 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util; + +public class ByteArrayReader { + public final byte[] byteArray; + public int bytesRead; + + public ByteArrayReader(byte[] array) { + this.bytesRead = 0; + if (array == null || array.length <= 0) { + throw new IllegalArgumentException("wrong byte array"); + } + this.byteArray = array.clone(); + } + + public int getBytesLeft() { + return this.byteArray.length - this.bytesRead; + } + + public long readInt(IntFormat intFormat) { + if (intFormat == null) { + throw new IllegalArgumentException("wrong intFormat"); + } + int i = 0; + long n = 0L; + try { + while (i < intFormat.bytesCount) { + long n2 = this.byteArray[this.bytesRead++] & 0xFF; + int n3 = i + 1; + n += n2 << i * 8; + i = n3; + } + long n4 = n; + if (intFormat.isSigned) { + int n5 = intFormat.bytesCount * 8; + n4 = n; + if (((long) (1 << n5 - 1) & n) != 0x0L) { + n4 = ((1 << n5 - 1) - (n & (long) ((1 << n5 - 1) - 1))) * -1L; + } + } + return n4; + } catch (ArrayIndexOutOfBoundsException ex) { + throw new RuntimeException("reading outside of byte array", ex.getCause()); + } + } + + public int readUint16() { + return (int) this.readInt(IntFormat.UINT16); + } + + public int readUint8() { + return (int) this.readInt(IntFormat.UINT8); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/ByteArrayWriter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/ByteArrayWriter.java new file mode 100644 index 000000000..eb7ae6ce1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/ByteArrayWriter.java @@ -0,0 +1,77 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util; + +import java.util.Arrays; + +public class ByteArrayWriter { + public byte[] byteArray; + private int bytesWritten; + + public ByteArrayWriter() { + this.bytesWritten = 0; + } + + private void addIntToValue(long n, IntFormat intFormat) { + for (int i = 0; i < intFormat.bytesCount; ++i) { + this.byteArray[this.bytesWritten++] = (byte) (n >> i * 8 & 0xFFL); + } + } + + public void appendUint16(int n) { + this.appendValue(n, IntFormat.UINT16); + } + + public void appendUint32(long n) { + this.appendValue(n, IntFormat.UINT32); + } + + public void appendUint8(int n) { + this.appendValue(n, IntFormat.UINT8); + } + + public void appendValue(long lng, IntFormat intFormat) { + if (intFormat == null) { + throw new IllegalArgumentException("wrong int format"); + } + if (lng > intFormat.max || lng < intFormat.min) { + throw new IllegalArgumentException("wrong value for intFormat. max: " + intFormat.max + " min: " + intFormat.min + " value: " + lng); + } + this.increaseByteArray(intFormat.bytesCount); + long n = lng; + if (intFormat.isSigned) { + int n2 = intFormat.bytesCount * 8; + n = lng; + if (lng < 0L) { + n = (1 << n2 - 1) + ((long) ((1 << n2 - 1) - 1) & lng); + } + } + this.addIntToValue(n, intFormat); + } + + public void increaseByteArray(int n) { + if (this.byteArray == null) { + this.byteArray = new byte[n]; + return; + } + this.byteArray = Arrays.copyOf(this.byteArray, this.byteArray.length + n); + } + + public byte[] getByteArray() { + return this.byteArray.clone(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/IntFormat.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/IntFormat.java new file mode 100644 index 000000000..8ad493497 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/IntFormat.java @@ -0,0 +1,51 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util; + +public enum IntFormat { + UINT8(1, false), + SINT8(1, true), + UINT16( 2, false), + SINT16( 2, true), + UINT32(4, false), + SINT32(4, true); + + final int bytesCount; + final boolean isSigned; + final long max; + final long min; + + IntFormat(int bytesCount, boolean isSigned) { + this.bytesCount = bytesCount; + this.isSigned = isSigned; + int bitsCount = bytesCount * 8; + long max; + if (isSigned) { + max = (long) Math.pow(2.0, bitsCount - 1) - 1L; + } else { + max = (long) (Math.pow(2.0, bitsCount) - 1.0); + } + this.max = max; + long min; + if (isSigned) { + min = (long) (-1.0 * Math.pow(2.0, bitsCount - 1)); + } else { + min = 0L; + } + this.min = min; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/UIntBitReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/UIntBitReader.java new file mode 100644 index 000000000..573b1fb2c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/UIntBitReader.java @@ -0,0 +1,43 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util; + +public class UIntBitReader { + private final long value; + private int offset; + + public UIntBitReader(long value, int offset) { + this.value = value; + this.offset = offset; + } + + public int read(int offset) { + this.offset -= offset; + if (this.offset < 0) { + throw new IllegalArgumentException("Read out of range"); + } + return (int) ((long) ((1 << offset) - 1) & this.value >>> this.offset); + } + + public boolean readBoolean() { + boolean b = true; + if (this.read(1) == 0) { + b = false; + } + return b; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/UIntBitWriter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/UIntBitWriter.java new file mode 100644 index 000000000..3e1301cb3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sonyswr12/util/UIntBitWriter.java @@ -0,0 +1,53 @@ +/* Copyright (C) 2019-2020 opavlov + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sonyswr12.util; + +public class UIntBitWriter { + private long value; + private long offset; + + public UIntBitWriter(int offset) { + this.value = 0L; + this.offset = offset; + } + + public void append(int offset, int value) { + if (value < 0 || value > (1 << offset) - 1) { + throw new IllegalArgumentException("value is out of range: " + value); + } + this.offset -= offset; + if (this.offset < 0L) { + throw new IllegalArgumentException("Write offset out of range"); + } + this.value |= (long) value << (int) this.offset; + } + + public void appendBoolean(boolean b) { + if (b) { + this.append(1, 1); + return; + } + this.append(1, 0); + } + + public long getValue() { + if (this.offset != 0L) { + throw new IllegalStateException("value is not complete yet: " + this.offset); + } + return this.value; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java index 8622aa6a0..8df8a8f3c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BondingUtil.java @@ -277,7 +277,9 @@ public class BondingUtil { .build(); CompanionDeviceManager manager = (CompanionDeviceManager) bondingInterface.getContext().getSystemService(Context.COMPANION_DEVICE_SERVICE); + LOG.debug(String.format("Searching for %s associations", deviceCandidate.getMacAddress())); for (String association : manager.getAssociations()) { + LOG.debug(String.format("Already associated with: %s", association)); if (association.equals(deviceCandidate.getMacAddress())) { LOG.info("The device has already been bonded through CompanionDeviceManager, using regular"); // If it's already "associated", we should immediately pair @@ -287,6 +289,7 @@ public class BondingUtil { } } + LOG.debug("Starting association request"); manager.associate(pairingRequest, getCompanionDeviceManagerCallback(bondingInterface), null); 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 5956d7339..a2034e92f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -68,6 +68,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.itag.ITagCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.BFH16DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30.TeclastH30Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.y5.Y5Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.lefun.LefunDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator; @@ -76,11 +77,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator; 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.sonyswr12.SonySWR12DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator; @@ -251,11 +254,14 @@ public class DeviceHelper { result.add(new BFH16DeviceCoordinator()); result.add(new MijiaLywsd02Coordinator()); result.add(new ITagCoordinator()); + result.add(new NutCoordinator()); result.add(new MakibesHR3Coordinator()); result.add(new BangleJSCoordinator()); result.add(new TLW64Coordinator()); result.add(new PineTimeJFCoordinator()); result.add(new SG2Coordinator()); + result.add(new LefunDeviceCoordinator()); + result.add(new SonySWR12DeviceCoordinator()); return result; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index 247d8ccb2..cd587ffda 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -80,6 +80,14 @@ public class GB { public static final String DISPLAY_MESSAGE_DURATION = "duration"; public static final String DISPLAY_MESSAGE_SEVERITY = "severity"; + /** Commands related to the progress (bar) on the screen */ + public static final String ACTION_SET_PROGRESS_BAR = "GB_Set_Progress_Bar"; + public static final String PROGRESS_BAR_INDETERMINATE = "indeterminate"; + public static final String PROGRESS_BAR_MAX = "max"; + public static final String PROGRESS_BAR_PROGRESS = "progress"; + public static final String ACTION_SET_PROGRESS_TEXT = "GB_Set_Progress_Text"; + public static final String ACTION_SET_INFO_TEXT = "GB_Set_Info_Text"; + private static PendingIntent getContentIntent(Context context) { Intent notificationIntent = new Intent(context, ControlCenterv2.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK @@ -187,20 +195,29 @@ public class GB { return GBApplication.getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE); } + public static final char[] HEX_CHARS = "0123456789ABCDEF".toCharArray(); + public static String hexdump(byte[] buffer, int offset, int length) { if (length == -1) { length = buffer.length - offset; } - final char[] hexArray = "0123456789ABCDEF".toCharArray(); + char[] hexChars = new char[length * 2]; for (int i = 0; i < length; i++) { int v = buffer[i + offset] & 0xFF; - hexChars[i * 2] = hexArray[v >>> 4]; - hexChars[i * 2 + 1] = hexArray[v & 0x0F]; + hexChars[i * 2] = HEX_CHARS[v >>> 4]; + hexChars[i * 2 + 1] = HEX_CHARS[v & 0x0F]; } return new String(hexChars); } + public static String hexdump(byte[] buffer) { + return hexdump(buffer, 0, buffer.length); + } + + /** + * https://stackoverflow.com/a/140861/4636860 + */ public static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; diff --git a/app/src/main/res/drawable/ic_map.xml b/app/src/main/res/drawable/ic_map.xml new file mode 100644 index 000000000..b17843c50 --- /dev/null +++ b/app/src/main/res/drawable/ic_map.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_appinstaller.xml b/app/src/main/res/layout/activity_appinstaller.xml index 59a91a97d..26378125a 100644 --- a/app/src/main/res/layout/activity_appinstaller.xml +++ b/app/src/main/res/layout/activity_appinstaller.xml @@ -3,8 +3,7 @@ android:layout_width="fill_parent" android:layout_height="fill_parent"> - + android:contentDescription="Status icon" /> + + + android:layout_below="@+id/installProgressText" /> -