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 extends ActivitySample> 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 extends ActivitySample> 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 extends ActivitySample> 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 extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return true;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> 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 extends Activity> 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 extends ScanFilter> 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 extends Activity> 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 extends ActivitySample> 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 extends Activity> 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 extends Activity> 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 extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return true;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> 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 extends Activity> 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 extends Alarm> 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 extends Alarm> alarms) {
+
+ }
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+
+ }
+
+ @Override
+ public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
+
+ }
+
+ @Override
+ public void onSetMusicState(MusicStateSpec stateSpec) {
+
+ }
+
+ @Override
+ public void onSetMusicInfo(MusicSpec musicSpec) {
+
+ }
+
+ @Override
+ public void onEnableRealtimeSteps(boolean enable) {
+
+ }
+
+ @Override
+ public void onInstallApp(Uri uri) {
+
+ }
+
+ @Override
+ public void onAppInfoReq() {
+
+ }
+
+ @Override
+ public void onAppStart(UUID uuid, boolean start) {
+
+ }
+
+ @Override
+ public void onAppDelete(UUID uuid) {
+
+ }
+
+ @Override
+ public void onAppConfiguration(UUID appUuid, String config, Integer id) {
+
+ }
+
+ @Override
+ public void onAppReorder(UUID[] uuids) {
+
+ }
+
+ @Override
+ public void onFetchRecordedData(int dataTypes) {
+
+ }
+
+ @Override
+ public void onReset(int flags) {
+
+ }
+
+ @Override
+ public void onHeartRateTest() {
+
+ }
+
+ @Override
+ public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
+
+ }
+
+ @Override
+ public void onFindDevice(boolean 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 extends Alarm> 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" />
-
-
diff --git a/app/src/main/res/layout/fragment_steps_list.xml b/app/src/main/res/layout/fragment_steps_list.xml
new file mode 100644
index 000000000..56896de37
--- /dev/null
+++ b/app/src/main/res/layout/fragment_steps_list.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/activity_list_menu.xml b/app/src/main/res/menu/activity_list_menu.xml
index dbefb0960..b863368bd 100644
--- a/app/src/main/res/menu/activity_list_menu.xml
+++ b/app/src/main/res/menu/activity_list_menu.xml
@@ -2,22 +2,33 @@
\ No newline at end of file
diff --git a/app/src/main/res/menu/activity_take_screenshot_menu.xml b/app/src/main/res/menu/activity_take_screenshot_menu.xml
new file mode 100644
index 000000000..b7be1c569
--- /dev/null
+++ b/app/src/main/res/menu/activity_take_screenshot_menu.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 151c838f7..e74f26fca 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -595,4 +595,11 @@
Изкл.Вкл.Няма данни
+ Настройки Sony SWR12
+ Активирана ниска вибрация
+ Режимът за пестене на енергия е включен
+ Интелигентен алармен интервал в минути
+ Активирайте ниската интензивност на вибрациите на маншета
+ Режимът за пестене на енергия изключва периодичното автоматично измерване на сърдечната честота, като по този начин увеличава работното време
+ Интелигентният алармен интервал е интервал преди инсталираната аларма. В този интервал устройството се опитва да открие най-леката фаза на сън за събуждане на потребителя
\ No newline at end of file
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index e1f13efd2..b898c344c 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -343,7 +343,6 @@
Activa la vibració de la pulsera en prémer el botó d\'accióRetard màxim entre pulsacionsRetard màxim entre pulsacions del botó en mil·lisegons
- Retard després de l\'acció de botóNotificació d\'objectiuLa polsera vibrarà quan l\'objectiu diari de passes siga assolitElements a mostrar
@@ -438,7 +437,6 @@
Si l\'arribada de les dades d\'activitats no és notificada a la polsera, aquestes dades no s\'eliminaran. Aquesta opció pot ser útil si feu servir el Gadgebridge també amb altres aplicacions.Les dades d\'activitats de la Mi Band es guardaran fins i tot després de sincronitzar. Aquesta opció pot ser útil si feu servir el Gadgebridge també amb altres aplicacions.Difón el missatge enviat amb l\'esdeveniment. El paràmetre `button_id` és afegit automàticament a cada missatge.
- Retard després d\'una acció de botó (el nombre és a l\'intent extra de button_id) o 0 per procedir immediatamentDesa dades sense processar a la base de dadesDesa les dades \"tal qual\", tot augmentant l\'ús de la base de dades per tal de permetre possibles interpretacions més tard.Gestió de la base de dades
@@ -858,8 +856,6 @@
Només rellotgeEstalvi d\'energiaNormal
- Període d\'inactivitat (minuts)
- Recordatori d\'inactivitatPrem aquí per començar la calibracióCalibracióCalibració de la pressió sanguínia
@@ -878,4 +874,11 @@
Repeteix la notificació de trucadaNotificacions i trucadesCalibració del Watch X Plus
+ Configuració de Sony SWR12
+ Vibració baixa activada
+ Interval d\'alarma intel·ligent en minuts
+ Activa la baixa intensitat de vibració a la banda
+ El mode d’estalvi d’energia desactiva la mesura automàtica periòdica de la freqüència cardíaca i, per tant, augmenta el temps de treball
+ L\'interval d\'alarma intel·ligent és l\'interval abans de l\'alarma instal·lada. En aquest interval, el dispositiu intenta detectar la fase més lleugera del son per despertar l\'usuari
+ Mode d\'estalvi d\'energia activat
\ No newline at end of file
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 5bb4a1dcb..cfc32aebf 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -419,8 +419,6 @@
Zapnout vibrace náramku při stisknutí tlačítkaMaximální zpoždění mezi stiskyMaximální zpoždění mezi stisknutím tlačítka v milisekundách
- Zpoždění po provedení akce tlačítka
- Zpoždění po provedení akce tlačítka (číslo je v button_id) nebo 0 pro okamžitěChystáte se nainstalovat firmvér %s na vašem Amazfit Bip.
\n
\nProsím nezapomeňte nainstalovat soubor .fw, potom soubor .res a nakonec soubor .gps. Vaše hodinky se po instalaci souboru .fw restartují.
@@ -446,7 +444,6 @@
Složka pro uloženíČasový intervalExportovat každou %d hodinu
- Nastavení Amazfit BipCelodenní měření tepuJednou za minutuKaždých 5 minut
@@ -947,7 +944,6 @@
BazénůPlavecký stylHodnocení swolf
- ZáběryPrůměrné Tempo BazénuPrůměrné ZáběryPrůměrná Délka Záběru
@@ -1051,4 +1047,11 @@
KriketVeslovací trenažérFotbal
+ Sony SWR12 nastavení
+ Nízké vibrace povoleny
+ Režim úspory energie je zapnutý
+ Interval inteligentního alarmu v minutách
+ Povolit nízkou intenzitu vibrací na pásmu
+ Režim úspory energie vypíná periodické automatické měření srdeční frekvence a tím prodlužuje pracovní dobu
+ Interval inteligentního alarmu je interval před nainstalovaným alarmem. V tomto intervalu se zařízení pokouší detekovat nejlehčí fázi spánku, aby probudil uživatele
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 8c1fe3436..bd765ab59 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -435,7 +435,6 @@
Bandvibration aktivieren, wenn die Tastenaktion ausgelöst wurdeMaximale Verzögerung zwischen den TastendrückenMaximale Verzögerung zwischen den Tastendrücken in Millisekunden
- Verzögerung nach TastenaktionAuf Android-Gerät öffnenLautlosAntworten
@@ -488,7 +487,6 @@
Alle %d Stunden exportierenDatenbankexport fehlgeschlagen! Bitte überprüfe deine Einstellungen.Broadcast Nachricht, die mit der Aktion gesendet wird. Der Parameter „button_id“ wird automatisch zu jeder Nachricht hinzugefügt.
- Verzögerung nach einer Tastenaktion (Zahl in button_id) oder 0 für sofortSpanischEinDeine Aktivitätsverläufe
@@ -804,7 +802,6 @@
\nINSTALLATION AUF EIGENE GEFAHR!nodomain.freeyourgadget.gadgetbridge.ButtonPressedFossil Q Hybrid
- Q Hybrid Benachrichtigung abspielenUhr nicht verbundenVibrationsstärke:Ziel in Schritten
@@ -994,7 +991,6 @@
Beim EinschlafenYogaSchwimmen (Freiwasser)
- SchlägeSchlägekcalSeilspringen
@@ -1062,4 +1058,13 @@
FußballHeuteVergangenheit
+ Keine Aktivitäten gefunden.
+ Sony SWR12
+ Sony SWR12 Einstellungen
+ Geringe Vibration aktiviert
+ Der Energiesparmodus ist aktiviert
+ Intelligentes Alarmintervall in Minuten
+ Aktivieren Sie eine geringe Vibrationsintensität auf dem Band
+ Der Energiesparmodus schaltet die regelmäßige automatische Messung der Herzfrequenz aus und verlängert so die Arbeitszeit
+ Das intelligente Alarmintervall ist das Intervall vor dem installierten Alarm. In diesem Intervall versucht das Gerät, die leichteste Schlafphase zu erkennen, um den Benutzer zu wecken
\ No newline at end of file
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 94b7a7b93..1c7b0e974 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -349,8 +349,6 @@
Ενεργοποίηση δόνησης όταν πατιέται το κουμπίΜέγιστος χρόνος μεταξύ των χτυπημάτων του κουμπιούΜέγιστος χρόνος μεταξύ των χτυπημάτων του κουμπιού (σε χιλιοστά του δευτερολέπτου)
- Χρόνος αναμονής μετά το πάτημα του κουμπιού
- Καθυστέρηση μετά από μια ενέργεια αντιστοίχισης του κουμπιού (ο αριθμός καθορίζεται στο button_id) ή βάλτε 0 για να μην υπάρχει καθυστέρησηΕιδοποίηση επίτευξης στόχουΤο Mi Band θα κάνει δόνηση όταν ο ημερήσιος στόχος για τα βήματα επιτευχθείΠληροφορίες που θα εμφανίζονται
@@ -990,4 +988,11 @@
Ελλειπτικό μηχάνημαΠοδηλασία εσωτερικού χώρουΚολύμβηση (ανοιχτό νερό)
+ Ρυθμίσεις Sony SWR12
+ Ενεργοποιήθηκε χαμηλή δόνηση
+ Η λειτουργία εξοικονόμησης ενέργειας είναι ενεργοποιημένη
+ Έξυπνο διάστημα συναγερμού σε λίγα λεπτά
+ Ενεργοποιήστε τη χαμηλή ένταση των κραδασμών στη ζώνη
+ Η λειτουργία εξοικονόμησης ενέργειας απενεργοποιεί την περιοδική αυτόματη μέτρηση του καρδιακού ρυθμού αυξάνοντας έτσι το χρόνο εργασίας
+ Το διάστημα έξυπνου συναγερμού είναι διάστημα πριν από το εγκατεστημένο συναγερμό Σε αυτό το διάστημα η συσκευή προσπαθεί να ανιχνεύσει την ελαφρύτερη φάση ύπνου για να ξυπνήσει ο χρήστης
\ No newline at end of file
diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index 6b636b250..7a8ec7db0 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -104,7 +104,7 @@
DurationRing duration in secondsUse your band to play your phone\'s ringtone.
- Turn on \\\'Find phone\\\'
+ Turn on \'Find phone\'Find phoneAmazfit Bip LiteWearing left or right\?
@@ -896,4 +896,11 @@
Choose the shortcuts on the band screenShortcutsSet Alias
+ Sony SWR12 settings
+ Low vibration enabled
+ Power saving mode on
+ Smart alarm interval in minutes
+ Enable low intensity of vibration on wristband
+ Power saving mode turns off periodic auto measuring of heart rate thus increases working time
+ Smart alarm interval is interval before of installed alarm. In this interval device is trying to detect lightest phase of sleep to awake user
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 4be8b9c1b..76cbe14d6 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -430,8 +430,6 @@
Activar vibración por acciónRetardo máximo entre pulsacionesRetardo máximo entre pulsaciones en milisegundos
- Retardo después de la acción del botón
- Retardo después de una acción del botón (el número está en button_id intent extra) o bien 0 para efecto inmediatoAbrir en el dispositivo AndroidSilenciarResponder
@@ -853,4 +851,11 @@
Llamadas y NotificacionesCalibracion Watch X PlusEstablecer Alias
+ Configuración de Sony SWR12
+ Baja vibración habilitada
+ El modo de ahorro de energía está activado
+ Intervalo de alarma inteligente en minutos
+ Activar baja intensidad de vibración en la pulsera
+ El modo de ahorro de energía desactiva la medición automática periódica de la frecuencia cardíaca, lo que aumenta el tiempo de trabajo
+ El intervalo de alarma inteligente es el intervalo anterior a la alarma instalada. En este intervalo, el dispositivo está tratando de detectar la fase más ligera del sueño para despertar al usuario.
\ No newline at end of file
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 0c49d70ed..c1dcfc880 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -855,4 +855,11 @@
NimiFilterStatistika
+ Sony SWR12 sätted
+ Madal vibratsioon on lubatud
+ Energiasäästurežiim on sisse lülitatud
+ Nutika häire intervall minutites
+ Luba madala vibratsiooni intensiivsus ribal
+ Energiasäästurežiim lülitab südame löögisageduse perioodilise automaatse mõõtmise välja, pikendades seega tööaega
+ Nutika häire intervall on intervall enne installitud alarmi. Selles intervallis üritab seade kasutaja ärkamiseks tuvastada une kergemat faasi
\ No newline at end of file
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 0da36995b..3113b6652 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -125,4 +125,11 @@
مکان تعیین شده برای هواشناسی (CM/LOS)اضافه کردن برنامهها به لیست سیاهnodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ لرزش کم روی باند را فعال کنید
+ حالت صرفه جویی در مصرف انرژی اندازه گیری دوره ای ضربان قلب را خاموش می کند و در نتیجه زمان کار افزایش می یابد
+ فاصله هشدار هوشمند فاصله قبل از زنگ هشدار نصب شده است. در این فاصله دستگاه در تلاش است تا کمترین مرحله خواب را برای کاربر بیدار تشخیص دهد
+ تنظیمات Sony SWR12
+ لرزش کم فعال است
+ حالت صرفه جویی در مصرف برق روشن است
+ فاصله زنگ هوشمند در چند دقیقه
\ No newline at end of file
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 1c8f36875..fa1e5bccd 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -36,4 +36,11 @@
MääritäSiirrä ylösnodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Sony SWR12 asetukset
+ Matala tärinä käytössä
+ Virransäästötila on päällä
+ Älykäs hälytysväli minuutteina
+ Ota käyttöön matala tärinän voimakkuus kaistalla
+ Virransäästötila poistaa säännöllisen automaattisen sykemittauksen käytöstä, mikä pidentää työaikaa
+ Älykäs hälytysväli on aikaväli ennen asennettua hälytystä. Tässä välissä laite yrittää tunnistaa käyttäjän herättämisen unen kevyimmän vaiheen
\ No newline at end of file
diff --git a/app/src/main/res/values-fr-rCA/strings.xml b/app/src/main/res/values-fr-rCA/strings.xml
index ab07b88ba..ef3e2da22 100644
--- a/app/src/main/res/values-fr-rCA/strings.xml
+++ b/app/src/main/res/values-fr-rCA/strings.xml
@@ -885,4 +885,11 @@
Notifications et appelsCalibrage de Watch X PlusSystème
+ Paramètres Sony SWR12
+ Faible vibration activée
+ Le mode d\'économie d\'énergie est activé
+ Intervalle d\'alarme intelligente en minutes
+ Permettre une faible intensité de vibration sur la bande
+ Le mode d\'économie d\'énergie désactive la mesure automatique périodique de la fréquence cardiaque augmente ainsi le temps de travail
+ L\'intervalle d\'alarme intelligente est l\'intervalle avant l\'alarme installée. Dans cet intervalle, l\'appareil tente de détecter la phase de sommeil la plus légère pour réveiller l\'utilisateur
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index ed3e2368f..3aa007795 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -424,8 +424,6 @@ Temps de sommeil préféré en heures
Activer la vibration après déclenchement de l\'actionDélai maximum entre pressionsDélai maximum entre pressions en millisecondes
- Délai après action du bouton
- Délai après une pression de bouton (le nombre est dans button_id intent extra) ou 0 pour immédiatementOuvrir sur le smartphone AndroidSilencieuxRépondre
@@ -1045,4 +1043,26 @@ Temps de sommeil préféré en heures
\nNote : vous n\'avez pas à installer les fichier .res et .gps si ceux ci sont exactement les même qu\'installé précédemment.
\n
\nÀ VOS RISQUES ET PÉRILS !
+ Durée d\'activité minimale (minutes)
+ Longueur de la pause pour séparer les activités (minutes)
+ Nombre de pas minimum par minute pour détecter une activité
+ Nombre de pas minimum par minute pour détecter une course
+ Liste d\'activités
+ Avoir une activité et synchroniser l\'appareil.
+ Aucune activité détectée.
+ Lefun
+ langue de l\'interface
+ Le bracelet vibrera si votre téléphone se déconnecte du bracelet
+ Anti-perte
+ Intervalle de rappel d\'hydratation (en minutes)
+ Le bracelet vibrera pour vous rappeler de boire de l\'eau
+ Rappel d\'hydratation
+ Sony SWR12
+ Paramètres Sony SWR12
+ Faible vibration activée
+ Le mode d\'économie d\'énergie est activé
+ Intervalle d\'alarme intelligente en minutes
+ Permettre une faible intensité de vibration sur la bande
+ Le mode d\'économie d\'énergie désactive la mesure automatique périodique de la fréquence cardiaque augmente ainsi le temps de travail
+ L\'intervalle d\'alarme intelligente est l\'intervalle avant l\'alarme installée. Dans cet intervalle, l\'appareil tente de détecter la phase de sommeil la plus légère pour réveiller l\'utilisateur
\ No newline at end of file
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 1c502f3b5..5041210be 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -320,7 +320,6 @@
Activar vibración da banda en acción desencadenada por botónRetardo máximo entre pulsaciónsRetardo máximo en milisegundos entre pulsacións de botón
- Retardo despois da acción do botónNotificación de obxectivoA pulseira vai vibrar cando se acade o obxectivo diario de pasosElementos a amosar
@@ -424,7 +423,6 @@
Isto fará que as mensaxes enviadas para apps de terceiros sexan recoñecidas sempre e inmediatamenteCompensación da hora do dispositivo en horas (para detectar o sono de traballadores a turnos)Enviar mensaxe despois dun número definido de pulsacións
- Retardo despois dunha acción do botón (o número está en button_id intent extra) ou ben 0 para efecto inmediatoSe marcado, os datos son almacenados no seu formato orixinal ficando disponíbeis para posterior interpretación. Nota: A base de datos será máis grande!Verá unha notificación de emparellamento no seu dispositivo Android. Se non, acceda ás notificacións e acepte a solicitude de emparellamento. posteriormente a solicitude de emparellamento no seu Pebble.Asegúrate de que este tema esté activado na aplicación de notificación do clima para obter a información no teu Pebble.
@@ -517,4 +515,11 @@
AxustesAlipaynodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Axustes Sony SWR12
+ Vibración baixa habilitada
+ Intervalo de alarma intelixente en minutos
+ Activa a baixa intensidade de vibración na banda
+ O modo de aforro de enerxía desactiva a medición automática periódica da frecuencia cardíaca, polo que aumenta o tempo de traballo
+ O intervalo de alarma intelixente é o intervalo antes da alarma instalada. Neste intervalo o dispositivo está intentando detectar a fase máis lixeira do sono para espertar ao usuario
+ Modo de aforro de enerxía activado
\ No newline at end of file
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index e184308fa..50443d8ca 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -423,12 +423,10 @@
הפעלת רטט בצמיד עם פעולת כפתורהפרש מרבי בין לחיצותהפרש מרבי בין לחיצות על הכפתור במילישניות
- הפרש לאחר לחיצת כפתורפתיחה בהתקן Androidהשתקהתגובההפעלת JS ברקע
- המתנה לאחר התאמת פעולת כפתור אחת (המספר נמצא בתוספת של intent בשם button_id) או 0 לתגובה מיידיתפעילות תצוגת דפדפןהתחבר …כשאפשרות זו פעילה, יתאפשר למסיכות השעון להציג מזג אוויר, סוללה וכו׳.
@@ -440,7 +438,6 @@
\n
\nהמשך מעבר לנקודה זו הוא על אחריותך!הפעלת הסטה לימין/שמאל בפעילות התרשימים
- הגדרות Amazfit Bipאוטומטיסינית מפושטתסינית מסורתית
@@ -802,7 +799,6 @@
הכפתורים נדרסודריסת הכפתורים נכשלההשינויים יתבצעו בהקדם…
- הקפצת התראה ב־Q Hybridלהשתמש ביד הפעילות כמונה התראותמרחק מאזור זמןמרחק מהשעה
@@ -810,11 +806,6 @@
יש לסמן את האפשרות הזאת אם המכשיר שלך לא נמצא במהלך האיתורBangle.jsY5
- פעולה של אירוע 1
- פעולה של אירוע 2
- פעולה של אירוע 3
- הגדרות נרחבות ללחיצת כפתור
- פעולת לחיצה ארוכה על הכפתורפעולה עם אירוע 1פעולה עם אירוע 2פעולה עם אירוע 3
@@ -867,9 +858,6 @@
שעון בלבדחיסכון בחשמלרגיל
- משך חוסר פעילות (דקות)
- תזכורת חוסר פעילות
- להזכיר לי במקרה של חוסר פעילות למשך X דקותיש ללחוץ כאן כדי להתחיל כיולכיוללחץ דם סיסטולי (גבוה)
@@ -977,7 +965,6 @@
קמ״שמ׳/שנ׳צעדים
- תנועות חתירהתנועות חתירה בממוצעמרחק תנועת חתירה ממוצעס״מ
@@ -1057,4 +1044,27 @@
היוםמרחק שעברקישורים
+ משך מזערי לפעילות (דקות)
+ משך השהייה להפרדת פעילויות (דקות)
+ כמות מזערית של צעדים לדקה לזיהוי פעילות
+ כמות מזערית של צעדים לדקה לזיהוי ריצה
+ רשימת פעילויות
+ יש לעשות פעילות כלשהי ולסנכרן את ההתקן.
+ לא זוהו פעילויות.
+ Lefun
+ שפת המנשק
+ הצמיד ירוטו אם הטלפון שלך מתנתק מהצמיד
+ מונע איבוד
+ הפרש תזכורת שתייה (בדקות)
+ הצמיד ירטוט כדי להזכיר לך לשתות מים
+ תזכורת שתייה
+ Sony SWR12
+ Sony SWR12 הגדרות
+ רטט נמוך מופעל
+ מצב חיסכון בחשמל פועל
+ מרווח אזעקה חכם בדקות
+ אפשר עוצמת רטט נמוכה בלהקה
+ מצב חיסכון בחשמל מכבה מדידה אוטומטית תקופתית של דופק ובכך מגדיל את זמן העבודה
+ מרווח אזעקה חכם הוא מרווח לפני ההתראה המותקנת. במרווח זה מכשיר מנסה לאתר את שלב השינה הקל ביותר למשתמש ער
+ בסיס לחישוב רום
\ No newline at end of file
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 7506e24bd..cd38f934a 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -152,4 +152,11 @@
अपनी गतिविधि को युक्ति में रखेंसभी अलार्म बंद हैकनेक्ट नहीं.
+ मिनटों में स्मार्ट अलार्म अंतराल
+ बैंड पर कंपन की कम तीव्रता को सक्षम करें
+ पावर सेविंग मोड हृदय गति की आवधिक ऑटो माप को बंद कर देता है जिससे काम का समय बढ़ जाता है
+ स्थापित अलार्म से पहले स्मार्ट अलार्म अंतराल है। इस अंतराल डिवाइस में उपयोगकर्ता को जगाने के लिए नींद के सबसे हल्के चरण का पता लगाने की कोशिश की जा रही है
+ Sony SWR12 समायोजन
+ कम कंपन सक्षम
+ पर बिजली की बचत मोड
\ No newline at end of file
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 1220c86fd..e17fd32d4 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -61,4 +61,11 @@
SinkronizirajDonirajPostavke
+ Uključen je način uštede energije
+ Pametni interval alarma u minutama
+ Omogućite niski intenzitet vibracija na pojasu
+ Način uštede energije isključuje povremeno automatsko mjerenje brzine otkucaja srca čime se povećava radno vrijeme
+ Interval pametnog alarma je interval prije instaliranog alarma. U ovom intervalu uređaj pokušava otkriti najlakšu fazu spavanja za budnog korisnika
+ Sony SWR12 postavke
+ Omogućena slaba vibracija
\ No newline at end of file
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index c099464d0..85ef51f71 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -495,4 +495,11 @@
Értesítések közötti legrövidebb időRendszerFeketelistás Naptárak
+ Beállítások Sony SWR12
+ Alacsony rezgés engedélyezve
+ Az energiatakarékos mód be van kapcsolva
+ Intelligens riasztási intervallum percekben
+ Engedélyezze a vibráció alacsony intenzitását a sávon
+ Az energiatakarékos üzemmód kikapcsolja a pulzus időszakos automatikus mérését, így megnő a munkaidő
+ Az intelligens riasztási intervallum a telepített riasztás előtti intervallum. Ebben az intervallumban az eszköz megpróbálja észlelni az alvás legkönnyebb fázisát a felhasználó felébresztésére
\ No newline at end of file
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml
index 424b97f2f..31403b8a6 100644
--- a/app/src/main/res/values-id/strings.xml
+++ b/app/src/main/res/values-id/strings.xml
@@ -34,4 +34,11 @@
Manajer AppMelakukan factory reset akan menghapus seluruh data dari perangkat terkoneksi (jika didukung). Perangkat Xiaomi/Huami juga mengganti MAC address Bluetooth, sehingga akan muncul sebagai aplikasi baru di GadgetBridge.Debug
+ Pengaturan Sony SWR12
+ Interval alarm pintar dalam beberapa menit
+ Aktifkan getaran intensitas rendah pada pita
+ Mode hemat daya menonaktifkan pengukuran detak jantung otomatis berkala sehingga meningkatkan waktu kerja
+ Interval alarm pintar adalah interval sebelum alarm dipasang. Dalam interval ini perangkat mencoba mendeteksi fase tidur paling ringan untuk membangunkan pengguna
+ Getaran rendah diaktifkan
+ Mode hemat daya aktif
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index f57506119..d0918bc0d 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -445,8 +445,6 @@
Abilita vibrazioni band con pressione del tastoMassimo ritardo tra due pressioniMassimo ritardo tra due pressioni del pulsante in millisecondi
- Ritardo di azione del tasto
- Ritardo di esecuzione dopo rilevamento di un\'azione (il numero indica l\'extra dell\'intent per button_id); 0 per eseguire immediatamenteAutomaticoCinese semplificatoCinese tradizionale
@@ -871,7 +869,7 @@
Impossibile avviare il servizio in backgroundATTENZIONE: Errore durante la verifica delle informazioni sulla versione! Non dovresti continuare! Visto il nome della versione \"%s\"Sono richieste tutte queste autorizzazioni e l\'app potrebbe essere instabile se non concesse
- Mille grazie a tutti i collaboratori non elencati per aver contribuito con codice, supporto, idee, motivazione, segnalazioni di bug, donazioni ... ✊
+ Grazie mille a tutti i collaboratori non elencati per aver contribuito con codice, traduzioni, supporto, idee, motivazione, segnalazioni di bug, donazioni ... ✊Supporto a dispositivi aggiuntiviContributoriTeam principale (ordinati in base al primo contributo di codice)
@@ -942,9 +940,9 @@
La posizione deve essere abilitataTLW64Mostra Percorso GPS
- Durata:
- Fine:
- Inizio:
+ Durata
+ Fine
+ InizioPassiAttivitàVelocità
@@ -953,7 +951,7 @@
bpmmin/kmsec/km
- Kcal
+ kcalgiristile di nuotos
@@ -982,4 +980,73 @@
DistanzaPineTime (JF Firmware)Dettagli dell\'Attività Sportiva
+ bracc
+ bracc/s
+ Cricket
+ Yoga
+ Lemfo SG2
+ Badminton
+ Ping Pong
+ Bracciate
+ Bracciate medie
+ Distanza media bracciata
+ Misto
+ Dorso
+ Stile libero
+ Rana
+ oggi
+ passato
+ Tutti i dispositivi
+ Periodo di tempo
+ 30 giorni
+ 7 giorni
+ Mese scorso
+ Questo mese
+ Settimana scorsa
+ Questa settimana
+ Tutte le attività
+ Statistiche
+ Sì
+ Elementi selezionati individualmente
+ Aggiungi al filtro
+ Applica filtro
+ Etichetta
+ Rimuovi filtri
+ Filtra
+ A
+ Da
+ Statistiche delle attività sportive
+ Filtro delle attività sportive
+ Avvia azione
+ Invia messaggio
+ Quando non si indossa il dispositivo
+ Quando ci si sveglia
+ Quando ci si addormenta
+ Azioni sul dispositivo
+ Elevazione
+ indice swolf
+ Velocità media
+ Elevazione base
+ Salita
+ Andatura media giro
+ Collegamenti
+ Ieri
+ Oggi
+ Modifica etichetta
+ Basket
+ Remoergometro
+ Calcio
+ Salti alla corda
+ Spinning
+ Nuoto (all\'aperto)
+ No
+ km
+ Sony SWR12
+ Impostazioni Sony SWR12
+ Bassa vibrazione abilitata
+ La modalità di risparmio energetico è attiva
+ Intervallo di allarme intelligente in pochi minuti
+ Abilita una bassa intensità di vibrazione sulla banda
+ La modalità di risparmio energetico disattiva la misurazione automatica periodica della frequenza cardiaca, quindi aumenta il tempo di lavoro
+ L\'intervallo di allarme intelligente è l\'intervallo prima dell\'allarme installato. In questo intervallo il dispositivo sta cercando di rilevare la fase più leggera del sonno per svegliare l\'utente
\ No newline at end of file
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index c82cd6da4..90f5245de 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -421,8 +421,6 @@
ボタンの動作をトリガーに band の振動を有効にしますボタンを押す間の最大遅延時間ボタンを押す間隔の最大遅延時間 (ミリ秒単位)
- ボタン操作の後に遅延
- 1回のボタン操作に一致した後の遅滞 (数字は button_id インテント拡張内) 0 の場合はすぐ電話で開くミュート返信
@@ -757,4 +755,11 @@
インドネシア語切断通知距離
+ Sony SWR12 設定
+ 低振動対応
+ 省電力モードがオンになっている
+ 分単位のスマートアラーム間隔
+ バンドで低強度の振動を有効にする
+ 省電力モードでは、心拍数の定期的な自動測定がオフになり、作業時間が長くなります
+ スマートアラーム間隔は、インストールされたアラームの前の間隔です。 この間隔では、デバイスはユーザーを目覚めさせるために睡眠の最も軽い段階を検出しようとしています
\ No newline at end of file
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index 6d833162d..ae5e463fc 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -30,4 +30,11 @@
გააქტიურებაგამორტვაკონფიგურაცია
+ Sony SWR12 პარამეტრები
+ სიგნალის ჭკვიანი ინტერვალი წუთებში
+ ჩართეთ ვიბრაციის დაბალი ინტენსივობა დიაპაზონში
+ ენერგიის დაზოგვის რეჟიმი გამორთავს გულისცემის პერიოდულ ავტომატურ გაზომვას და ამით ზრდის სამუშაო დროს
+ ჭკვიანი განგაშის ინტერვალი არის დაინსტალირებული მაღვიძარის ინტერვალი. ამ ინტერვალში მოწყობილობა ცდილობს გამოავლინოს ძილის ყველაზე მსუბუქი ფაზა, რომ გაიღვიძოს მომხმარებელი
+ ჩართულია დაბალი ვიბრაცია
+ ენერგიის დაზოგვის რეჟიმი ჩართულია
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index b3c1027dd..35e2abd47 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -368,8 +368,6 @@
버튼 동작이 작동할 경우 기기 진동버튼 누름간 최대 간격버튼 누름 간격을 밀리초(Milisecond) 단위로 설정
- 버튼 동작 후 지연
- 버튼 동작을 작동하고 나서의 휴지 기간 (0으로 설정할 경우 지연 없음)목표 달성 알림목표 걸음 수 달성시 밴드 진동항목 표시
@@ -611,4 +609,11 @@
올바르지 않은 주파수87.5와 108.0 사이의 주파수를 입력하세요nodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Sony SWR12 설정
+ 저진동 가능
+ 손목 밴드에서 낮은 강도의 진동 활성화
+ 절전 모드 켜기
+ 절전 모드는주기적인 심박수 자동 측정을 해제하여 작업 시간을 늘립니다.
+ 분 단위의 스마트 알람 간격
+ 스마트 알람 간격은 설치된 알람 이전의 간격입니다. 이 간격에서 장치는 사용자를 깨우기 위해 가장 가벼운 수면 단계를 감지하려고합니다.
\ No newline at end of file
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 1ba57eb79..f466c9a57 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -308,4 +308,11 @@
SnūsteltiLeisti didelį MTUPadidinti perkėlimo greitį, bet gali ir neveikti kai kuriuose android įrenginiuose..
+ Sony SWR12 nustatymai
+ Įjungta maža vibracija
+ Įjungtas energijos taupymo režimas
+ Išmaniojo žadintuvo intervalas minutėmis
+ Įjungti mažą juostos vibracijos intensyvumą
+ Energijos taupymo režimas išjungia periodinį automatinį širdies ritmo matavimą ir taip pailgina darbo laiką
+ Išmaniojo aliarmo intervalas yra intervalas prieš įdiegtą signalizaciją. Šiuo intervalu prietaisas bando nustatyti lengviausią miego fazę, kad pažadintų vartotoją
\ No newline at end of file
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index c0fe6d32d..a0124b426 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -12,4 +12,11 @@
Pašreizējie soļi / minSoļu vēstureAtrast tālruni
+ Iespējot zemu vibrācijas intensitāti joslā
+ Enerģijas taupīšanas režīms izslēdz periodisku automātisku sirdsdarbības mērīšanu, tādējādi palielinot darba laiku
+ Viedā trauksmes intervāls ir intervāls pirms instalētās trauksmes. Šajā intervālā ierīce mēģina noteikt vieglāko miega fāzi, lai pamodinātu lietotāju
+ Sony SWR12 iestatījumi
+ Iespējota zema vibrācija
+ Ieslēgts enerģijas taupīšanas režīms
+ Viedā trauksmes intervāls minūtēs
\ No newline at end of file
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 1c1a27943..692c26523 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -80,4 +80,11 @@
ക്രമീകരണങ്ങൾഗാഡ്ജക്റ്റ് ബ്രിഡ്ജ്ഗാഡ്ജക്റ്റ് ബ്രിഡ്ജ്
+ ബാൻഡിൽ വൈബ്രേഷന്റെ കുറഞ്ഞ തീവ്രത പ്രവർത്തനക്ഷമമാക്കുക
+ പവർ സേവിംഗ് മോഡ് ഹൃദയമിടിപ്പിന്റെ ആനുകാലിക യാന്ത്രിക അളവ് ഓഫുചെയ്യുന്നത് പ്രവർത്തന സമയം വർദ്ധിപ്പിക്കുന്നു
+ ഇൻസ്റ്റാൾ ചെയ്ത അലാറത്തിന് മുമ്പുള്ള ഇടവേളയാണ് സ്മാർട്ട് അലാറം ഇടവേള. ഈ ഇടവേള ഉപകരണം ഉപയോക്താവിനെ ഉണർത്താൻ ഉറക്കത്തിന്റെ ഭാരം കുറഞ്ഞ ഘട്ടം കണ്ടെത്താൻ ശ്രമിക്കുന്നു
+ Sony SWR12 ക്രമീകരണങ്ങൾ
+ കുറഞ്ഞ വൈബ്രേഷൻ പ്രവർത്തനക്ഷമമാക്കി
+ പവർ സേവിംഗ് മോഡ് ഓണാണ്
+ മിനിറ്റുകൾക്കുള്ളിൽ സ്മാർട്ട് അലാറം ഇടവേള
\ No newline at end of file
diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml
index fa089bd6e..4caf4bc91 100644
--- a/app/src/main/res/values-my/strings.xml
+++ b/app/src/main/res/values-my/strings.xml
@@ -37,4 +37,11 @@
အခ်က္အလက္မ်ားကိုသိမ္းဆည္းမည္မခ်ိတ္ဆက္ထားျခင္းမရွိပါ , သတိေပးခ်က္မထားရေသးပါnodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ မိနစ်အတွင်းစမတ်နှိုးဆော်သံကြားကာလ
+ တီးဝိုင်းပေါ်တုန်ခါမှု၏နိမ့်ကျသောပြင်းအားကိုခွင့်ပြုပါ
+ ပါဝါချွေတာသည့်အနေဖြင့်နှလုံးခုန်နှုန်းကိုအလိုအလျောက်တိုင်းတာခြင်းကိုပိတ်ထားသဖြင့်အလုပ်ချိန်တိုးလာသည်
+ စမတ်နှိုးစက်ကြားကာလသည်တပ်ဆင်ထားသည့်နှိုးစက်မတိုင်မီကြားကာလဖြစ်သည်။ ဤကြားကာလတွင်အသုံးပြုသူသည်အိပ်ပျော်ရန်အလျင်မြန်ဆုံးအဆင့်ကိုအသုံးပြုရန်အသုံးပြုသည်
+ "Sony SWR12 ချိန်ညှိချက်များ "
+ တုန်ခါမှုနိမ့်သည်
+ ပါဝါချွေတာခြင်းမုဒ်ကိုဖွင့်ပါ
\ No newline at end of file
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index f0caf201f..9de9ac8c0 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -452,8 +452,6 @@
Skru på knappetrykk ved knappetrykkMaksimal forsinkelse mellom trykkMaksimal forsinkelse mellom knappetrykk i millisekunder
- Forsinkelse etter knappetrykkshandling
- Forsinkelse etter at én knappetrykkshandling samsvarer (nummeret er i button_id intent extra) eller 0 for umiddelbarAktiver skjerm ved løftingDatabaseoperasjoner bruker følgende sti på din enhet.
\nDenne stien er tilgjengelig for andre Android-programmer og din datamaskin.
@@ -811,11 +809,6 @@
Skru av ny BLE-skanningVelg dette hvis enheten du ikke finner enheten din under oppdagelseY5
- Handling for hendelse 1
- Handling for hendelse 2
- Handling for hendelse 3
- Detaljerte knappetrykkingsinnstillinger
- Handling for langt knappetrykkHandling for hendelse 1Handling for hendelse 2Handling for hendelse 3
@@ -931,7 +924,6 @@
mFlattRunder
- TakGjennomsnittlig rundetidGjennonsnittstakGjennomsnittlig taklengde
@@ -1021,4 +1013,15 @@
Alle enheteri dagLenker
+ Drikkepåminnelseintervall (i minutter)
+ Grensesnittspråk
+ Brukt av LineageOS-værtilbyderen, andre Android-versjoner må bruke et program som «Ditt lokale vær». Mer info er å finne på Gadgetbridge-wiki-en.
+ Sony SWR12
+ Sony SWR12 innstillinger
+ Lav vibrasjon aktivert
+ Strømsparingsmodus er på
+ Smart alarmintervall på få minutter
+ Aktiver lav vibrasjonsintensitet på båndet
+ Strømsparingsmodus slår av periodisk automatisk måling av hjertefrekvensen og øker dermed arbeidstiden
+ Smart alarmintervall er intervall før installert alarm. I dette intervallet prøver enheten å oppdage den letteste søvnfasen for å våkne brukeren
\ No newline at end of file
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 8b4f0a58a..15ade29b0 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -358,8 +358,6 @@
Schakel band vibratie in als een knopactie gedetecteerd wordtMaximum tijdsvertraging tussen knopdrukkenMaximum tijdsvertraging tussen knopdrukken in milliseconden
- Vertraging na knopactie
- Vertraag na één knopactie (nummer is button_id intent extra) of 0 voor onmiddelijkDoel meldingDe armband zal trillen als het doel voor het dagelijks aantal stappen bereikt isToon items
@@ -864,9 +862,6 @@
EnergiebesparingNormaalHorloge energie mode
- Inactiviteitsperiode (minuten)
- Inactiviteit herinnering
- Herinneren als er X minuten inactiviteit isDruk hier om te beginnen met kalibrerenKalibratieBloeddruk SYSTOLISCH (hoog)
@@ -961,4 +956,11 @@
Elliptische TrainerBinnenshuis fietsenZwemmen (Open water)
+ Sony SWR12 instellingen
+ Laag trillingsniveau ingeschakeld
+ Energiebesparende modus is ingeschakeld
+ Slimme alarminterval in minuten
+ Schakel lage trillingsintensiteit op de band in
+ De energiebesparende modus schakelt de periodieke automatische hartslagmeting uit, waardoor de werktijd toeneemt
+ Slimme alarminterval is het interval vóór het geïnstalleerde alarm. In dit interval probeert het apparaat de lichtste slaapfase te detecteren om de gebruiker wakker te maken
\ No newline at end of file
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index d15513634..562037b43 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -330,7 +330,6 @@
Uruchom wibracje opaskiMaksymalny opóźnienie pomiędzy wciśnięciamiMaksymalny opóźnienie pomiędzy wciśnięciami w milisekundach
- Opóźnienie po wciśnięciu przycisku akcjiOpaska zawibruje po osiągnięciu dziennego celu krokówNie przeszkadzaćObsługa aplikacji wysyłających powiadomienia do Pebble za pośrednictwem PebbleKit.
@@ -542,7 +541,6 @@
Nie spałeś(-aś)Wiadomość do rozgłoszeniaWiadomość broadcast nadawana wraz z zdarzeniem. Do każdej wiadomości jest automatycznie dodawany parametr `button_id`.
- Opóźnienie po wciśnięciu przycisku (liczba jest w button_id) lub 0 dla bez opóźnieniaOpaska nie będzie otrzymywać powiadomień gdy jest aktywnePoczątekKoniec
@@ -1031,4 +1029,11 @@
KrykietWiosłująca maszynaTrener eliptyczny
+ Ustawienia Sony SWR12
+ Włączono niski poziom wibracji
+ Tryb oszczędzania energii jest włączony
+ Inteligentny interwał alarmu w minutach
+ Włącz niską intensywność wibracji na paśmie
+ Tryb oszczędzania energii wyłącza okresowe automatyczne pomiary tętna, co wydłuża czas pracy
+ Interwał inteligentnego alarmu to interwał przed zainstalowanym alarmem. W tym interwale urządzenie próbuje wykryć najlżejszą fazę snu, aby obudzić użytkownika
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index db16f14d3..497c9b3d8 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -481,8 +481,6 @@
Abrir no AndroidSem somResponder
- Atrasar após ação do botão
- Atraso após uma ação do botão ser considerada válida (número está em button_id intent extra) ou 0 para imediatamenteWeb View AtividadesAutomáticoChinês simplificado
@@ -802,7 +800,6 @@
nodomain.freeyourgadget.gadgetbridge.ButtonPressedFossil Q HybridConfigurações de Q Hybrid
- Reproduzir notificação do Q HybridRelógio não conectadoforça da vibração:Meta em passos
@@ -820,11 +817,6 @@
Marque essa opção se seu dispositivo não pode ser encontrado durante descobertasBangle.jsY5
- Ação do evento 1
- Ação do evento 2
- Ação do evento 3
- Configurações detalhadas de pressionamento do botão
- Ação de pressionamento longo de botãoAção do Evento 1Ação do Evento 2Ação do Evento 3
@@ -1064,4 +1056,11 @@
hojepassado distanteLinks
+ Configurações de Sony SWR12
+ Baixa vibração habilitada
+ O modo de economia de energia está ativado
+ Intervalo de alarme inteligente em minutos
+ Ativar baixa intensidade de vibração na banda
+ O modo de economia de energia desativa a medição automática periódica da frequência cardíaca, aumentando assim o tempo de trabalho
+ O intervalo de alarme inteligente é o intervalo antes do alarme instalado. Neste intervalo, o dispositivo está tentando detectar a fase mais leve do sono para acordar o usuário
\ No newline at end of file
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 7c89871c8..715e68dcb 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -22,7 +22,7 @@
Capturar o ecrã do dispositivoDepurar
- Gestor de aplicações
+ Gestor de appsAplicações em cacheAplicações instaladasTemas instalados
@@ -42,7 +42,7 @@
Bloqueio de notificações
- Instalador de FW/Aplicação
+ Instalador de FW/appEstá prestes a instalar o %s.Estás prestes a instalar os firmwares %1$s e %2$s em vez dos que estão atualmente na sua Mi Band.O firmware foi testado e é compatível com o Gadgetbridge.
@@ -129,7 +129,7 @@
Utilizar suporte para Pebble LE em todos os Pebbles em vez do clássico BT. Necessita que se emparelhe um \"Pebble LE\" depois de dispositivos não LE se terem ligado uma vezPebble 2/LE GATT MTU limiteSe o seu Pebble2/Pebble LE não funciona como esperado, ative esta opção para limitar o tamanho das transferências (intervalo válido é 20–512)
- Activar Registo da Aplicação do Dispositivo
+ Ativar registo da app do aparelhoCausa que os registos das aplicações do relógio sejam guardados pelo Gadgetbridge (necessita de religação)Antecipar confirmações do PebbleKitIrá fazer com que as mensagens enviadas para aplicações de terceiros sejam sempre imediatamente confirmadas
@@ -363,7 +363,13 @@
A emparelhar PebbleÉ esperado que veja uma notificação de emparelhamento no seu dispositivo Android. Se isso não acontecer, aceda às notificações e aceite o pedido de emparelhamento. Depois aceite igualmente o pedido de emparelhamento no seu Pebble
- Garanta que este tema se encontra ativado na Aplicação de Meteorologia para obter informação meteorológica atualizada no seu Pebble.\n\nNão é necessária qualquer configuração aqui.\n\nPode ativar a aplicação nativa de meteorologia do seu Pebble através da gestão de aplicações.\n\nTemas de relógio irão apresentar a informação meteorológica automaticamente.
+ Garanta que este tema se encontra ativado na App de Meteorologia para obter informação meteorológica atualizada no seu Pebble.
+\n
+\nNão é necessária qualquer configuração aqui.
+\n
+\nPode ativar a app nativa de meteorologia do seu Pebble através da gestão de apps.
+\n
+\nTemas de relógio irão apresentar a informação meteorológica automaticamente.Ativar o emparelhamento BluetoothDesative isto caso tenha problemas na ligaçãoMétrico
@@ -440,7 +446,6 @@
Exportação da base de dados falhou. Por favor, verifique as configurações.Ações do botãoEspecificar ações ao carregar no botão
- Atraso após ação do botãoDesbloqueio do ecrã da pulseiraAutomáticoChinês Simplificado
@@ -535,7 +540,7 @@
Exportar a base de dados…O local de exportação automática da base de dados foi definido para:Relógio Mundial
- Usado para o provedor do clima do LineageOS, outras versões do Android precisam usar uma aplicação como o \"Notificação de clima\". Encontrará mais informações no wiki do Gadgetbridge.
+ Usado para o provedor do clima do LineageOS, outras versões do Android precisam usar uma app como o \"Notificação de clima\". Encontrará mais informações no wiki do Gadgetbridge.Muito obrigado a todos os contribuidores não listados aqui por contribuição com código, suporte, ideias, motivação, relatórios de erros, dinheiro… ✊Suporte a aparelhos adicionaisContribuidores
@@ -694,7 +699,7 @@
Amazfit Bip LiteMakibes HR3Configurações de Makibes HR3
- Para visualizar o rastreamento de atividade, instale uma aplicação que consegue manipular ficheiros GPX.
+ Para visualizar o rastreamento de atividade, instale uma app que consegue manipular ficheiros GPX.Visível apenas se nenhum aparelho estiver adicionadoSempre visívelConectar novo botão de aparelho
@@ -827,7 +832,7 @@
Não filtrarFiltro de notificação gravadoInsira as palavras desejadas, nova linha para cada uma
- Para ser configurada, a aplicação não pode estar na lista negra
+ Para ser configurada, a app não pode estar na lista negraFiltro de notificaçãoCasio GB-6900Frequência cardíaca atual / máx: %1$d / %2$d
@@ -1041,4 +1046,11 @@
Ritmo médio de voltasMédia de braçadasDistância média das braçadas
+ Configurações de Sony SWR12
+ Baixa vibração habilitada
+ O modo de economia de energia está ativado
+ Intervalo de alarme inteligente em minutos
+ Ativar baixa intensidade de vibração na banda
+ O modo de economia de energia desativa a medição automática periódica da frequência cardíaca, aumentando assim o tempo de trabalho
+ O intervalo de alarme inteligente é o intervalo antes do alarme instalado. Neste intervalo, o dispositivo está tentando detectar a fase mais leve do sono para acordar o usuário
\ No newline at end of file
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index 2edb5b68d..a05411b86 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -129,4 +129,11 @@
Salvare ConfigurațieDeconectat(ă), alarma nu este setată.nodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Setari Sony SWR12
+ Modul de economisire a energiei este activat
+ Interval de alarmă inteligentă în minute
+ Activați intensitatea redusă a vibrațiilor pe bandă
+ Modul de economisire a energiei dezactivează măsurarea automată periodică a ritmului cardiac, crescând astfel timpul de lucru
+ Intervalul de alarmă inteligentă este intervalul înainte de alarma instalată. În acest interval, dispozitivul încearcă să detecteze cea mai ușoară fază de somn pentru a trezi utilizatorul
+ Vibrație scăzută activată
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index a5d774196..02024c6e2 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -438,8 +438,6 @@
Включить вибро-отклик браслета в ответ на исполнение действия при нажатииМаксимальная задержка между нажатиямиМаксимальная задержка между нажатиями в миллисекундах
- Задержка после действия при нажатии
- Задержка при выполнении заданного количества нажатий для однократного действия (число указано в button_id). Это число равно нулю для действий без задержкиУведомления о достижении целиБраслет завибрирует, когда будет выполнена дневная норма шаговЧто показывать на экране
@@ -765,7 +763,7 @@
Makibes HR3Amazfit Bip LiteНайти телефон
- Включить \\\'Найти телефон\\\'
+ Включить \'Найти телефон\'Использовать ваш браслет для проигрывания рингтонов.Продолжительность звонка в секундахПродолжительность
@@ -939,7 +937,6 @@
Взмахивзмвзм/с
- ВзмахиСреднее количество взмаховВыполнить действиеПри пробуждении
@@ -1053,4 +1050,26 @@
далёкое прошлоесегодняСсылки
+ Минимальная продолжительность активностей (минуты)
+ Продолжительность паузы для разделения активностей (минуты)
+ Минимальные шаги в минуту для обнаружения активности
+ Минимальные шаги в минуту для обнаружения бега
+ Список активностей
+ Сделайте некоторую активность и синхронизируйте устройство.
+ Никакой активности не обнаружено.
+ Интервал напоминания о гидратации (в минутах)
+ Напоминание о гидратации
+ Браслет будет вибрировать, если ваш телефон отключится от браслета
+ Браслет будет вибрировать, напоминая вам о питье воды
+ Lefun
+ Язык интерфейса
+ Защита от потери
+ Sony SWR12
+ Настройки Sony SWR12
+ Слабая вибрация
+ Режима энергосбережения включен
+ Интервал умного будильника в минутах
+ Включить слабую вибрации на браслете
+ Режим энергосбережения отключает периодическое автоматическое измерение пульса, что увеличивает время работы
+ Интервал умного будильника - это интервал до установленного будильника. В этом интервале устройство пытается определить самую легкую фазу сна, чтобы разбудить пользователя.
\ No newline at end of file
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 4764b74f2..8cff0a970 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -355,8 +355,6 @@
Zapnúť vibrácie náramku pri stlačení tlačidlaMaximálne oneskorenie medzi stlačeniamiMaximálne oneskorenie medzi stlačením tlačidla v milisekundách
- Oneskorenie po vykonaní akcie tlačidla
- Oneskorenie po vykonaní akcie tlačidla (číslo je v button_id) alebo 0 pre okamžiteUpozornenie na cieľNáramok zavibruje pri dosiahnutí cieľa denného počtu krokovZobrazenie položiek
@@ -493,4 +491,11 @@
PripojiťZap.nodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Nastavenia Sony SWR12
+ Nízke vibrácie sú povolené
+ Režim úspory energie je zapnutý
+ Interval inteligentného alarmu v minútach
+ Povoľte nízku intenzitu vibrácií na náramku
+ Režim úspory energie vypne pravidelné automatické meranie srdcovej frekvencie, čím sa predĺži pracovná doba
+ Interval inteligentného alarmu je interval pred nainštalovaným alarmom. V tomto intervale sa zariadenie pokúša zistiť najľahšiu fázu spánku, aby sa používateľ zobudil
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 61daafb9c..6a8f73ae5 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -48,4 +48,11 @@
Aktivera pulsmätareAvaktivera pulsmätareKonfigurera
+ Inställningar Sony SWR12
+ Låg vibration aktiverad
+ Energisparläge är på
+ Smart larmintervall på några minuter
+ Aktivera låg vibrationsintensitet på armbandet
+ Energisparläget stänger av periodisk automatisk mätning av hjärtfrekvensen, vilket ökar arbetstiden
+ Smart alarmintervall är intervall före installerat larm. I detta intervall försöker enheten att upptäcka den lättaste fasen av sömn för att vakna användare
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 4f6f7aae8..e7d5f2fd5 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -1,177 +1,177 @@
İptal
- "KalpRitmi: "
+ "Kalp ritmi: "Sil
- %1$s den itibaren aktarmak üzere
+ %1$s tarihinden itibaren verileri aktarmak üzere(%1$s)Sessiz
- Android Cihazda Aç
+ Android aygıtında açCevapla(bilinmeyen)Etkinlik
- Derin Uyku
- Hafif Uyku
- BilgiYok
- VeriTabanı Yönetimi
+ Derin uyku
+ Hafif uyku
+ Giyilmedi
+ Veri tabanı yönetimiHata Ayıklama
- Yeni Cihaz Bağla
+ Yeni aygıt bağlaBağış YapÇıkışAyarlar
- VeriTabanını Dışa Aktar
- VeriTabanını Sil
- VeriTabanını Boşalt
- VeriTabanını İçe Aktar
+ Veri tabanını dışa aktar
+ Eski veri tabanını sil
+ Veri tabanını boşalt
+ Veri tabanını içe aktarOtomatik Dışa Aktarmayı Başlat
- VeriTabanı Dışa Aktarılıyor…
- VeriTabanının Otomatik Dışa Aktarılacağı Yer:
+ Veri tabanı dışa aktarılıyor…
+ Veri tabanı otomatik dışa aktarma konumu:Otomatik Dışa Aktarma
- VeriTabanını Boşalt
- Dikkat! Bu düğmeye basarak veritabanınızı silecek ve sıfırdan başlayacaksınız.
- DışaAktarma ve İçeAktarma
- Veritabanı işlemleri cihazınızda aşağıdaki yolu kullanır.
-\nBu yola diğer Android uygulamaları ve bilgisayarınız tarafından erişilebilir.
-\nDışa aktarılan veritabanınızı bulmayı bekleyin (veya içe aktarmak istediğiniz veritabanını yerleştirin):
- Eski VeriTabanını Silme
- Aktivite izlemesini görüntülemek için GPX dosyalarını işleyebilen bir uygulama yükleyin.
+ Veri tabanını boşalt
+ Dikkat! Bu düğmeye basarak veri tabanınızı silecek ve sıfırdan başlayacaksınız.
+ Dışa ve İçe Aktar
+ Veri tabanı işlemleri aygıtınızda aşağıdaki yolu kullanır.
+\nBu yola diğer Android uygulamaları ve bilgisayarınız tarafından erişilebilir.
+\nDışa aktarılan veri tabanınızı burada bulmayı bekleyin (veya içe aktarmak istediğiniz veri tabanını buraya koyun):
+ Eski veri tabanını silme
+ Etkinlik izlemesini görüntülemek için GPX dosyalarını işleyebilen bir uygulama kurun.Hakkınızda
- Günlük Hedef: Dakika Olarak Aktif Zaman
- EnYüksek Kalp Ritmi
- EnDüşük Kalp Ritmi
- Günlük Hedef: Harcanacak Kalori
- EnYüksek Kalp Atış Ritmi
- EnDüşük Kalp Atış Ritmi
- Gösterge Ayarları
- Günlük Hedef: Metre Mesafe
+ Günlük hedef: dakika cinsinden etkin zaman
+ En yüksek kalp ritmi
+ En düşük kalp ritmi
+ Günlük hedef: harcanan kalori
+ En yüksek kalp ritmi
+ En düşük kalp ritmi
+ Çizelge ayarları
+ Günlük hedef: metre cinsinden mesafeCinsiyet
- Boy Santim
- Hedeflenen Uyku Saati Süresi
- Ağırlık Kilogram
- Doğum Yılı
+ Boy (cm)
+ Tercih edilen uyku süresi (saat cinsinden)
+ Ağırlık (kg)
+ Doğum yılıEtkinlikSpor EtkinlikleriEtkinlikBisiklet
- Derin Uyku
+ Derin uykuEgzersiz
- Hafif Uyku
+ Hafif uykuÖlçülmedi
- Cihaz Kullanılmamış
+ Aygıt giyilmediKoşuYüzmeKoşu bandıBilinmeyen etkinlikYürüme
- Aktivite Web Görünümü
- AnaSayfa Göstergesi (Widget) Ekle
+ Etkinlik Web Görünümü
+ Widget ekleCumPztCts
- Akıllı Uyandırma
- Durdur
+ Akıllı uyandırma
+ ErtelePazPerSalÇar
- Daima Bildir
- Cihazı eşleştirmek için Android Bluetooth Eşleştirme İletişimini kullan.
+ Her zaman
+ Aygıtı eşleştirmek için Android Bluetooth eşleştirme iletişimini kullan.Yapılandır
- Aşağıdaki uygulamayı yüklemek üzeresiniz: Sürüm:
+ Aşağıdaki uygulamayı kurmak üzeresiniz:
\n
\n
-\n%1$s - %2$s Sahibi: %3$s
+\n%1$s Sürüm %2$s, %3$s tarafından
\n
- Yukarı Taşı
- GadgetBridge
+ Yukarı taşı
+ GadgetbridgeKur
- Pebble Uygulama Mağazasında Ara
- ÖnBellekteki Uygulamalar
- Aktifleştir
- Pasifleştir
- KalpRitmi Kontrolünü Aktifleştir
- KalpRitmi Kontrolünü Pasifleştir
- Yüklenmiş Uygulamalar
- Yüklenmiş Saat AraYüzleri
- Sistem Hava Durumu uygulamasını Aktifleştir
- Sistem Hava Durumu uygulamasını Pasifleştir
- Weather Notification Uygulamasını Yükle
+ Pebble uygulama mağazasında ara
+ Önbellekteki uygulamalar
+ Etkinleştir
+ Devre dışı bırak
+ Kalp ritmi izlemeyi etkinleştir
+ Kalp ritmi izlemeyi devre dışı bırak
+ Kurulu uygulamalar
+ Kurulu saat arayüzleri
+ Sistem hava durumu uygulamasını etkinleştir
+ Sistem hava durumu uygulamasını devre dışı bırak
+ Hava durumu bildirim uygulaması kurSil
- Sil ve ÖnBellekten Kaldır
- Yeniden Kur
- %1$s Sahibi %2$s
- Bağlanmadı, Alarm Kurulmadı.
- Alarmı Kur %1$02d:%2$02d
+ Sil ve önbellekten kaldır
+ Yeniden kur
+ %1$s, %2$s tarafından
+ Bağlı değil, alarm kurulmadı.
+ %1$02d:%2$02d için alarm kurUyku AlarmıZzz
- Arabic
- Kimlik Doğrulanıyor
- Kimlik Doğrulama Gerekiyor
+ Arapça
+ Kimlik doğrulanıyor
+ Kimlik doğrulama gerekiyorOtomatikOrtalama: %1$s
- Batarya
- Bildirimler için Hepsini KaraListeye Al
- Bluetooth Kapalı.
- Bluetooth Desteklenmiyor.
+ Pil
+ Bildirimler için hepsini kara listeye al
+ Bluetooth devre dışı.
+ Bluetooth desteklenmiyor.Etkinlik verileri getiriliyorKalori
- Cihaz Görüntüsü
+ Aygıt görüntüsüBağlanamıyor: %1$s
- Bağlanamadı. Bluetooth Adresi Geçersiz\?
- Veri Yok! Cihazı Senkronize Et\?
+ Bağlanamıyor. Bluetooth adresi geçersiz mi\?
+ Veri yok! Aygıt eşzamanlansın mı\?Adım
- Kalp Ritmi
- Dışa Aktarılacak Yeri Seç
+ Kalp ritmi
+ Dışa aktarma konumunu seçSaatBağlandıBağlanıyor
- Titreşimi Durdurmak için iptal et.
- Kayıp Cihaz aranıyor
- Cihazı Kalibre Et
+ Titreşimi durdurmak için iptal et.
+ Kayıp aygıtı bul
+ Aygıtı Kalibre EtFM Frekansını DeğiştirLED Rengini DeğiştirBağlan…
- Cihazı Sil
- Bu Cihaz ve ilişkili veriler silinecek!
+ Aygıtı Sil
+ Aygıt ve tüm ilişkili verileri silinecek!%1$s SilBağlantıyı Kes
- Senkronize Et
- Kayıp Cihazı Bul
- Gezinti Çizimini Kapat
- Gezinti Çizimini Aç
+ Eşzamanla
+ Kayıp aygıtı bul
+ Gezinme çekmecesini kapat
+ Gezinme çekmecesini açBağlanıyor…Bağlantı Kesiliyor
- Bağlantıyı kesmek için Cihazın İsmine uzunca basın
- Cihazın Ekran Görüntüsü Alma
- Aktivite İzlemeleriniz
- Aktiviteniz (ALPHA)
- Alarmları Yapılandır
+ Bağlantıyı kesmek için karta uzun basın
+ Aygıtın ekran görüntüsü alınıyor
+ Etkinlik izlemeleriniz
+ Etkinliğiniz (ALPHA)
+ Alarmları yapılandırEkran Görüntüsü Al
- Zaman & Tarih
- Zaman
- "'%1$s' İşleme Hatası"
- Veri Silindi.
- VeriTabanı Silmede Hata.
- Aktivite Verisi Silinsin mi?
- Eski Aktiviteler VeriTabanı Silinsin mi?
- Eski Aktiviteler VeriTabanı Gerçekten Silinsin mi? İçe aktarılmayan aktivite verileri kaybolacak.
- VeriTabanı DışaAktarma Hatası: %1$s
- Tercihi DışaAktarma Hatası: %1$s
- VeriTabanı İçe Aktarma Hatası: %1$s
- Tercihi İçeAktarmada Hata: %1$s
- Dışa Aktarıldı: %1$s
- VeriTabanı İçe Aktarılsın mı?
- İçe Aktarıldı.
- Eski Aktiviteler VeriTabanı Silmede Hata.
- Eski Aktiviteler VeriTabanı Silindi.
+ Saat ve tarih
+ Saat
+ \'%1$s\' çalıştırılırken hata oluştu
+ Veriler silindi.
+ Veri tabanını silme başarısız oldu.
+ Etkinlik Verileri Silinsin mi\?
+ Eski Etkinlik Veri Tabanı Silinsin mi\?
+ Eski etkinlik veri tabanı gerçekten silinsin mi\? İçe aktarılmayan etkinlik verileri kaybolacak.
+ Veri tabanı dışa aktarılırken hata oluştu: %1$s
+ Tercih dışa aktarılırken hata oluştu: %1$s
+ Veri tabanı içe aktarılırken hata oluştu: %1$s
+ Tercih içe aktarılırken hata oluştu: %1$s
+ Dışa aktarıldı: %1$s
+ Veriler İçe Aktarılsın mı\?
+ İçe aktarıldı.
+ Eski etkinlik veri tabanının silinmesi başarısız oldu.
+ Eski etkinlik verileri silindi.Üzerine Yaz
- Gerçekten Mevcut VeriTabanı Üzerine Yazılsın mı? Mevcut Aktivite Verisi Kaybedilecek.
- Gerçekten Mevcut VeriTabanı Silinsin mi? Mevcut Aktivite Veriniz ve Cihaz Bilgileriniz Silinecek.
- Dışa Aktarma Yoluna Erişilemiyor. Lütfen Geliştiricilere Başvurun.
- Fabrika ayarlarına sıfırlama yapılması bağlı cihazdaki tüm verileri silecektir. Xiaomi / Huami cihazları da Bluetooth MAC adresini değiştirir, bu nedenle GadgetBridge yeni bir cihaz olarak görür.
- Gerçekten Fabrika Ayarlarına Sıfırlansın mı?
- Firmware Sürümü: %1$s
- Donanım Revizyonu: %1$s
+ Geçerli veri tabanının gerçekten üzerine yazılsın mı\? Tüm etkinlik verileriniz (varsa) kaybolacak.
+ Veri tabanının tamamı gerçekten silinsin mi\? Tüm etkinlik verileriniz ve aygıtlarınızla ilgili bilgileriniz kaybolacak.
+ Dışa aktarma yoluna erişilemiyor. Lütfen geliştiricilerle iletişime geçin.
+ Fabrika ayarlarına sıfırlama yapılması bağlı aygıttaki tüm verileri (destekleniyorsa) silecektir. Xiaomi/Huami aygıtları ayrıca Bluetooth MAC adresini değiştirir, bu nedenle Gadgetbridge tarafından yeni bir aygıt olarak görülürler.
+ Gerçekten fabrika ayarlarına sıfırlansın mı\?
+ Ürün yazılımı sürümü: %1$s
+ Donanım revizyonu: %1$sBağlanmadı.%1$s (%2$s)AmazFit Bip
@@ -205,319 +205,319 @@
RoidmiRoidmi 3Teclast H30
- Test Cihazı
- Bilinmeyen Cihaz
+ Test Aygıtı
+ Bilinmeyen AygıtVibratissimoWatch 9XWatchY5%1$s ile eşleşmeye çalışılıyor%1$s ile etkileşim anında başarısız oldu.
- Cihazınızı Bluetooth araması için Görünür Yapın. Halen Eşleşmiş Cihazlar Muhtemelen Bulunmayabilir. Bu durumda Bluetooth üzerindeki eşleşmeyi kaldırıp yeniden deneyin. Android 6 ve sonraki sürümlerinde konumu (ör. GPS) etkinleştirin. GadgetBridge için Privacy Guardı devre dışı bırakın, çünkü telefonunuz çökebilir ve yeniden başlatılabilir. Birkaç dakika sonra hiçbir cihaz bulunmazsa, mobil cihazınızı yeniden başlattıktan sonra tekrar deneyin.
- "Eşlenmesin"
- Cihazları bulmak için Bluetoothu aktifleştirin.
- Bu cihaz için bir Gizli Kimlik Doğrulama Anahtarı gerekiyor, Girmek için uzunca basın. Bilgi için ilgili wiki maddesini okuyun.
+ Aygıtınızı bulunabilir hale getirin. Şu anda bağlı aygıtlar muhtemelen keşfedilmeyecek. Android 6+ için konumu (örn. GPS) etkinleştirin. Gadgetbridge için Gizlilik Korumasını (Privacy Guard) devre dışı bırakın, çünkü çökebilir ve telefonunuzu yeniden başlatabilir. Birkaç dakika sonra hiçbir aygıt bulunmazsa, mobil aygıtınızı yeniden başlattıktan sonra tekrar deneyin.
+ Eşleştirme
+ Aygıtları bulmak için Bluetooth\'u etkinleştir.
+ Bu aygıt için gizli bir kimlik doğrulama anahtarı gerekiyor, girmek için aygıta uzun basın. Wiki sayfasını okuyun.Not:
- Cihazlarınızı eşleştirmek için Eşleştiri seçin. Bu başarısız olursa, Eşleştirmeyi tekrar deneyin.
+ Aygıtlarınızı eşleştirmek için Eşleştir seçeneğini seçin. Bu başarısız olursa, eşleştirme olmadan tekrar deneyin.%1$s ile eşleştirilsin mi?
- Taramayı Başlat
- Taramayı Durdur
+ Taramayı başlat
+ Taramayı durdurBağlanacak: %1$s.
- Bağlanmaya Çalışıyor: %1$s
+ Bağlanmaya çalışıyor: %1$sEşleştirMesafe
- Dutch
- Her bir kelimeyi yeni bir satıra girin
- English
+ Flemenkçe
+ Her bir sözcüğü yeni bir satıra girin
+ İngilizceGünlük dosyaları oluşturmada hata: %1$s
- Taramanın düzgün çalışması için Konum Erişimi verilmeli ve etkinleştirilmelidir
- Bayan
+ Taramanın düzgün çalışması için konum erişimi verilmeli ve etkinleştirilmelidir
+ KadınFiltreleme Modu
- Kelimeler bulunduğunda engelle
- Filtreleme Yapılmasın
- Kelimeler bulunduğunda göster
- Kelimelerin Hepsi
- Kelimelerin EnAz Birisi
+ Sözcükler bulunduğunda engelle
+ Filtreleme yapılmasın
+ Sözcükler bulunduğunda göster
+ Sözcüklerin tümü
+ Sözcüklerden en az biriBuldunuz!
- Telefonu Bul!
- Telefon Bulundu
- "Riski size ait olan bir FIRMWARE yüklüyorsunuz.
-
-
- Bu Firmware şu HW revizyonu içindir: %s"
- French
- Mevcut Mi Band cihazınıza %1$s ve %2$s için Firmware yüklemek üzeresiniz.
- %s yüklemek üzeresiniz.
- AmazFit Bip cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ Buldunuz!
+ Telefonumu bul
+ BİR ÜRÜN YAZILIMI KURMAYA ÇALIŞIYORSUNUZ, RİSKİ GÖZE ALARAK İLERLEYİN.
\n
-\nLütfen .fw dosyasını, ardından .res dosyasını ve son olarak .gps dosyasını yüklediğinizden emin olun. Saatiniz .fw dosyasını yükledikten sonra yeniden başlatılacak.
\n
-\nNot: Bu dosyalar, daha önce yüklenenler ile aynıysa, .res ve .gps yüklemeniz gerekmez.
+\n Bu ürün yazılımı şu donanım revizyonu içindir: %s
+ Fransızca
+ Şu anda Mi Band\'ınızda bulunanların yerine %1$s ve %2$s ürün yazılımını kurmak üzeresiniz.
+ %s kurmak üzeresiniz.
+ AmazFit Bip aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
+\n
+\nLütfen .fw dosyasını, ardından .res dosyasını ve son olarak .gps dosyasını kurduğunuzdan emin olun. Saatiniz .fw dosyasını kurduktan sonra yeniden başlatılacak.
+\n
+\nNot: Bu dosyalar, daha önce kurulanlarla tamamen aynıysa, .res ve .gps kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
- AmazFit Bip Lite cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ AmazFit Bip Lite aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra saatiniz yeniden başlatılacak.
\n
-\nNote: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
- AmazFit Core cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ AmazFit Core aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra bilekliğiniz yeniden başlatılacak.
\n
-\nNot: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
+\nNot: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
- AmazFit Core 2 cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ AmazFit Core 2 aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra bilekliğiniz yeniden başlatılacak.
\n
-\nNot: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
+\nNot: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
\n
-\n TAM OLARAK TEST EDİLMEMİŞTİR, CİHAZINIZIN İSMİ \"Amazfit Band 2\" İSE BEATS_W ÜRÜN YAZILIMINI YÜKLEMENİZ GEREKEBİLİR
- AmazFit GTR cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+\n TAM OLARAK TEST EDİLMEMİŞTİR, AYGITINIZIN İSMİ \"Amazfit Band 2\" İSE BEATS_W ÜRÜN YAZILIMINI KURMANIZ GEREKEBİLİR
+ AmazFit GTR aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra saatiniz yeniden başlatılacak.
\n
-\nNote: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
- AmazFit GTS cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ AmazFit GTS aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra saatiniz yeniden başlatılacak.
\n
-\nNote: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
- "Mi Band 3 üzerine %s Firmware yüklemek üzeresiniz.
-
-Lütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra Saatiniz yeniden başlatılacak.
-
-Note: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
-
-YÜKLEME RİSKİ RİZE AİTTİR!"
- "Mi Band 4 üzerine %s Firmware yüklemek üzeresiniz.
-
-Lütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra Saatiniz yeniden başlatılacak.
-
-Note: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
-
-YÜKLEME RİSKİ RİZE AİTTİR!"
- Dosya kurulamadı, Cihaz hazır değil.
- Bağlanacak Cihaz: %1$s
- Bu Firmware bu cihaza uyumlu değil
- GadgetBridge Çalışıyor
- German
- Kalp Ritmi
+ Mi Band 3 aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
+\n
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra bilekliğiniz yeniden başlatılacak.
+\n
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
+\n
+\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
+ Mi Band 4 aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
+\n
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra bilekliğiniz yeniden başlatılacak.
+\n
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
+\n
+\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!
+ Dosya kurulamıyor, aygıt hazır değil.
+ Aygıta bağlantı: %1$s
+ Bu ürün yazılımı bu aygıtla uyumlu değil
+ Gadgetbridge çalışıyor
+ Almanca
+ Kalp ritmiYatay
- Çevirme
+ İşe Gidip GelmeKronometreSağlıkEgzersiz
- Aktif Dakikalar
- Batarya
+ Etkin dakikalar
+ PilKaloriTarih
- Kalp Ritmi
- HiçbirŞey
+ Kalp ritmi
+ Hiçbir ŞeyAdımHava DurumuSimge
- Indonesian
- Başlatılmış
+ Endonezce
+ başlatıldıBaşlatılıyor
- Yükleme Hatası
- Yüklendi
- Bu dosyayı yüklemek için bir İşleyici bulunamıyor.
- Yükleme durumundayken bekleyiniz…
+ Kurulum başarısız oldu
+ Kuruldu
+ Bu dosyayı kurmak için bir işleyici bulunamıyor.
+ Kurulum durumu belirlenirken lütfen bekleyin…%1$s: %2$s %3$s
- %1$d/%2$d Yükleniyor
- Her 15 Dakikada
- Her 5 Dakikada
- Her 45 Dakikada
- Saatte 1 Kez
- Dakikada 1 Kez
- Her 10 Dakikada
- Her 30 Dakikada
- Italian
- Japanese
- Firmware
- YazıTipi
- GPS Firmware
+ %1$d/%2$d kuruluyor
+ her 15 dakikada bir
+ her 5 dakikada bir
+ her 45 dakikada bir
+ saatte 1 defa
+ dakikada 1 defa
+ her 10 dakikada bir
+ her 30 dakikada bir
+ İtalyanca
+ Japonca
+ Ürün yazılımı
+ Yazı tipi
+ GPS Ürün YazılımıGPS AlmanakGPS Hata Düzeltme
- Geçersiz Veri
+ Geçersiz veriKaynaklar
- Saat AraYüzü
- Korean
- Eksik Uyku: %1$s
- Eksik Adım: %1$d
- Dil ve Bölge Ayarları
+ Saat Arayüzü
+ Korece
+ Eksik uyku: %1$s
+ Eksik adım sayısı: %1$d
+ Dil ve bölge ayarlarıSol
- Mevcut Adımlar/Dakika
- Kalp Ritmi
- Mevcut / EnYüksek Kalp Ritmi: %1$d / %2$d
- Etkinliğinizi Başlatın
- Geçmişte Adımlar
- Geçmişte Dakikada Adımlar
- Toplam Adımlar
- Canlı Aktivite
+ Şu anki adım sayısı/dakika
+ Kalp ritmi
+ Şu anki / En yüksek kalp ritmi: %1$d / %2$d
+ Etkinliğinizi başlatın
+ Adım sayısı geçmişi
+ Dakikada adım sayısı geçmişi
+ Toplam adım sayısı
+ Canlı etkinlikErkekSüreEtkinlikAlarm
- AliPay
+ AlipayPusula
- DahaFazla
+ Daha FazlaMüzikNFCBildirimlerAyarlar
- AliPay (KısaYol)
- HavaDurumu (KısaYol)
+ Alipay (Kısayol)
+ Hava Durumu (Kısayol)DurumZamanlamaHava Durumu
- MAC Adresi geçilemedi, Eşlenilemez.
- Otomatik (Uyku Algılama)
+ MAC adresi verilmedi, eşleştirilemiyor.
+ Otomatik (uyku algılama)Kapalı
- Zamanlanmış (Zaman Aralığı)
- Metin Bildirimleri
- Firmware gerekiyor >= 1.0.1.28 ve Mili_pro.ft* yüklendi.
- Bu Firmware yüklenmeden önce %1$s Sürümü yüklenmelidir!
- Kolumu Kaldırınca Ekranı Aç
- Düğme Basma Eylemini Etkinleştir
- Belirtilen sayıda basıldığında eylemi etkinleştir
- Band Titreşimini Etkinleştir
- Tetiklenen Basma eyleminde bant titreşimini etkinleştir
- Basma Eylemleri
- Basma Eylemlerini Belirleme
- Gönderilecek Mesajı Yayınla
+ Planlandı (zaman aralığı)
+ Metin bildirimleri
+ 1.0.1.28 veya üstü bir ürün yazılımı ve Mili_pro.ft* kurulu olması gerekiyor.
+ Bu ürün yazılımını kurmadan önce %1$s sürümünü kurmalısınız!
+ Kaldırma anında ekranı etkinleştir
+ Düğme eylemini etkinleştir
+ Belirtilen sayıda düğmeye basıldığında eylemi etkinleştir
+ Bileklik titreşimini etkinleştir
+ Tetiklenen düğme eyleminde bileklik titreşimini etkinleştir
+ Düğme eylemleri
+ Düğme basma eylemlerini belirtin
+ Gönderilecek yayınlama mesajınodomain.freeyourgadget.gadgetbridge.ButtonPressed
- Aktiviteyle gönderilip Yayınlanan Mesaj. `button_id` Değeri otomatik olarak her mesaja eklenecek.
- Basma Eylemi Sayısı
+ Etkinlikle birlikte gönderilen yayınlama mesajı. `button_id` parametresi otomatik olarak her mesaja eklenir.
+ Düğme basma sayısıBasmalar arasındaki azami gecikmeMilisaniye cinsinden basmalar arasındaki azami gecikme
- Etkinliği tetiklemek için Basma Sayısı. Aynı miktarda Basma ikinci etkinliği tetikler.
- Görüntülenecek Eylemler
- Band Ekranında Görüntülenecek Eylemleri Belirleyin
- Rahatsız Etmeyin
- Bitiş Zamanı
- Başlama Zamanı
- "Aktifken Band Bildirim Almaz"
- Hedefe Ulaşma Bildirimi
- Günlük Adım hedefine ulaşıldığında Band titreyecek
- Hareketsizlik Uyarıları
+ Etkinlik 1\'i tetiklemek için gerekli düğme basma sayısı. Sonraki aynı miktarda basmalar Etkinlik 2\'yi tetikler ve bu böyle devam eder.
+ Görüntülenecek ögeler
+ Bileklik ekranında görüntülenen ögeleri seçin
+ Rahatsız Etme
+ Bitiş zamanı
+ Başlama zamanı
+ Etkinken bileklik bildirim almayacak
+ Hedefe ulaşma bildirimi
+ Günlük adım sayısı hedefine ulaşıldığında bileklik titreyecek
+ Hareketsizlik uyarılarıBelirli zaman arasında Hareketsizlik Uyarısı vermesin
- Belirli süre Hareketsiz kaldığınızda Band titreyecek
- Hareketsizlik Dakika Sınırı
- Ekran bilgisini değiştirmek için Bileği döndür
- Gün Batımında
- Band Ekran Kilidini Açma
- "Band Ekran Kilidi açmak için Yukarı kaydır"
- Gece Modu
- Geceleri Otomatik olarak DahaAz Ekran Parlaklığı
- Tarih Formatı
- Mi2: Zaman Formatı
- Bu ürün yazılımı test edilmiştir ve GadgetBridge ile uyumlu olduğu bilinmektedir.
- Her şey düzgün çalışıyorsa, lütfen GadgetBridge geliştiricilerine %s ürün yazılımı sürümünü beyaz listeye eklemelerini söyleyin.
- Bu Firmware test edilmemiştir ve GadgetBridge ile uyumlu olmayabilir.
-\n
-\nFormat atmanız gerekebilir!
- Uyumlu Sürüm
- Uyumsuz Firmware
- Test edilmemiş Sürüm!
- Bandınız titreşip yanıp söndüğünde, arka arkaya birkaç kez dokunun.
- Geçerli kullanıcı verisi alınmadı, şimdilik model kullanıcı verisi kullanıyor.
- İsim/TakmaAd
- Vardiyalı Çalışanlar (Uyku Zamanı) için Saat Farklılığı
- Günlük Adım Hedefi
- Uyku algılamasını iyileştirmek için Kalp Ritmi sensörünü kullan
- Gelecek etkinlikler için ayrılacak Alarmlar
+ Belirli süre hareketsiz kaldığınızda bileklik titreyecek
+ Hareketsizlik sınırı (dakika olarak)
+ Bilgileri değiştirmek için bileği döndür
+ Gün batımında
+ Bileklik ekran kilidini açma
+ Bileklik ekran kilidini açmak için yukarı kaydır
+ Gece modu
+ Geceleri bilekliğin ekran parlaklığını otomatik olarak azalt
+ Tarih biçimi
+ Mi2: Zaman biçimi
+ Bu ürün yazılımı test edilmiştir ve Gadgetbridge ile uyumlu olduğu bilinmektedir.
+ Her şey düzgün çalışıyorsa, lütfen Gadgetbridge geliştiricilerine %s ürün yazılımı sürümünü beyaz listeye eklemelerini söyleyin.
+ Bu ürün yazılımı test edilmemiştir ve Gadgetbridge ile uyumlu olmayabilir.
+\n
+\nKurulması TAVSİYE EDİLMEZ!
+ Uyumlu sürüm
+ Uyumsuz ürün yazılımı
+ Test edilmemiş sürüm!
+ Bilekliğiniz titreşip yanıp söndüğünde, arka arkaya birkaç kez dokunun.
+ Geçerli kullanıcı verisi verilmedi, şimdilik model kullanıcı verisi kullanılıyor.
+ İsim/Takma ad
+ Saat cinsinden aygıt zamanı farklılığı (vardiyalı çalışanların uykusunu algılamak için)
+ Günlük adım sayısı hedefi
+ Uyku algılamasını iyileştirmek için kalp ritmi sensörünü kullan
+ Gelecek etkinlikler için ayrılacak alarmlarTitreşim
- 1 Dakika
- 10 Dakika
- 30 Dakika
- 5 Dakika
+ 1 dakika
+ 10 dakika
+ 30 dakika
+ 5 dakikaMod YapılandırmasıYOK
- Bildirim Yapılmasın
- VeriYok
- Limitsiz
- Norwegian Bokmål
- Bağlı Değil
- %1$s Batarya Düşük
+ Asla
+ Veri yok
+ Sınırsız
+ Bokmål Norveççe
+ Bağlı değil
+ %1$s pil düşükSon şarj: %s
\n
- Sarj Oranı: %s
- %1$s Batarya Az: %2$s
- %1$s Batarya kaldı: %2$s%%
- Cihaz Bataryası Az!
- VeriTabanı DışaAktarma Hata verdi. Ayarları kontrol edin.
- GadgetBridge Bildirimlerine Yüksek Öncelik
- GadgetBridge Bildirimleri
+ Şarj sayısı: %s
+ %1$s pil düşük: %2$s
+ %1$s pil kaldı: %2$s%%
+ Aygıt pili düşük!
+ Veri tabanını dışa aktarma başarısız oldu! Lütfen ayarlarınızı gözden geçirin.
+ Gadgetbridge bildirimlerine yüksek öncelik
+ Gadgetbridge bildirimleriKapalıTamamAçıkDiğer
- Uyku Süresi: %1$s
- Uyku Aşımı: %1$d
- %s ile Eşleşiyor…
+ Fazla uyku: %1$s
+ Fazla adım: %1$d
+ %s ile eşleştiriliyor…%1$s (%2$s) ile zaten etkileşimde, bağlanıyor…%1$s (%2$s) ile etkileşim başlıyorEtkileşim sürüyor: %1$s (%2$s)
- %1$s (%2$s) ile Eşleşme geçersiz
- "Belirtilen Firmware yüklenemiyor: Pebble donanım revizyonuyla eşleşmiyor."
- Dosya Yükleme Hatası: %1$s
+ %1$s (%2$s) ile eşleştirilemiyor
+ Verilen ürün yazılımı kurulamıyor: Pebble aygıtınızın donanım revizyonuyla eşleşmiyor.
+ Verilen dosya kurulamıyor: %1$sPebble Sürümü %1$s%1$s (%2$s)
- Donanım Revizyonu Doğru
- Donanım Revizyonu Benzemiyor!
- Android cihazınızda bir eşleşme iletişim kutusu açılır. Değilse, bildirim kutusuna bakın ve eşleştirme isteğini kabul edin. Daha sonra Pebble üzerinde onaylayın.
- Polish
- Portuguese
+ Donanım revizyonu doğru
+ Donanım revizyonu eşleşmiyor!
+ Android aygıtınızda bir eşleşme iletişim kutusu açılacak. Açılmazsa, bildirim çekmecesine bakın ve eşleştirme isteğini kabul edin. Daha sonra Pebble üzerinde kabul edin.
+ Lehçe
+ PortekizceEtkinlik verilerini otomatik getirGetirmeler arasındaki asgari süreHer %d dakikada bir getirir
- Getirme, ekran kilidi açıldığında gerçekleşir. Sadece bir kilit mekanizması ayarlanmışsa çalışır!
- KaraListe Uygulamaları
- KaraListe Takvimleri
+ Getirme, ekran kilidi açıldığında gerçekleşir. Yalnızca bir kilit mekanizması ayarlanmışsa çalışır!
+ Kara Liste Uygulamaları
+ Kara Liste TakvimleriEngellenmişBilinmeyen
- İsim ve Numarayı Gizle
- İsim Gizle Numara Göster
- Numara Gizle İsim Göster
- İsim ve Numara Göster
- Portakal
+ İsim ve numarayı gizle
+ İsmi gizle ama numarayı göster
+ Numarayı gizle ama ismi göster
+ İsim ve numara göster
+ TuruncuKırmızı
- Öğleden Öğleye
- Son 24 Saat
- Haftalık Ortalama Çizelgesi
- Aylık Ortalama Çizelgesi
- Varsayılan
- Yeni BLE (BlueTooth Düşük Enerji) Tarama Yapılmasın
- Yeni Cihaz Bağlama Simgesi
- Sadece hiçbir Cihaz eklenmediğinde görünür
- Daima Göster
- Aktivite İzleyiciler
- Otomatik DışaAktar
+ Öğleden öğleye
+ Son 24 saat
+ Haftalık ortalama çizelgesi
+ Aylık ortalama çizelgesi
+ Öntanımlı
+ Yeni BLE (BlueTooth Düşük Enerji) taramasını devre dışı bırak
+ Yeni aygıt bağla düğmesi
+ Yalnızca hiçbir aygıt eklenmediğinde görünür
+ Her zaman göster
+ Etkinlik izleyiciler
+ Otomatik dışa aktarOtomatik getir
- Hazır Mesajlar
- Gösterge Ayarları
- Tarih ve Zaman
+ Mesaj şablonları
+ Çizelge Ayarları
+ Tarih ve SaatGelişmiş SeçeneklerGenel AyarlarKonumBildirimler
- Pebble ZamanÇizgisi
+ Pebble zaman çizelgesiGizlilik
- Titreme Sayısı
- Titreme Ayarları
- Lütfen 87.5 ve 108.0 arası bir Frekans girin
- Geçersiz Frekans
+ Titreşim sayısı
+ Titreme ayarları
+ Lütfen 87.5 ve 108.0 arasında bir frekans girin
+ Geçersiz frekansSonrakine GeçSONRAKİSonraki Medya
@@ -536,276 +536,276 @@ YÜKLEME RİSKİ RİZE AİTTİR!"SESDÜŞÜRSesi YükseltSESYÜKSELT
- Sadece Bildirim Simgesini Göster
+ Yalnızca bildirim simgesini gösterBildirim metnini ekran dışına kaydır
- Normal Bildirimler
- Sağdan Sola EnFazla Satır Uzunluğu
- Sağdan Sola Metin Satırlarını Uzatma veya Kısaltma
- Alarm Saati
- Kayıp Cihaz Uyarısı
- Takvim Bildirimi
- E-Posta Bildirimi
- Genel Bildirim
+ Normal bildirimler
+ Sağdan Sola En Fazla Satır Uzunluğu
+ Sağdan sola metinlerin ayrıldığı satırları uzatır veya kısaltır
+ Alarm saati
+ Kayıp önleme uyarısı
+ Takvim bildirimi
+ E-Posta bildirimi
+ Genel bildirimSohbetNavigasyon
- Sosyal Ağ
- Hareketsizlik Bildirimi
- GelenÇağrı Bildirimi
- Düşük Batarya Uyarısı
- Cevapsız Çağrı Bildirimi
- SMS Bildirimi
- Titreşim Kurgusu
- Aktarım hızını artırır, ancak bazı Android cihazlarda çalışmayabilir.
- Kimlik doğrulama anahtarını, bağlanmak istediğiniz tüm Android cihazlarınızda ortak bir anahtar olarak değiştirin. Tüm cihazlar için önceki varsayılan anahtar: 0123456789 @ ABCDE
- Her %d saatte dışa aktar
- Android cihazdan silinen bildirimleri Pebble cihazından da otomatik olarak kaldır
- Arabça içeriği desteklemek için bunu Etkinleştirin
- Bağlandığında GadgetBridge simgesi yerine cihaza özel bir Android bildirim simgesi göster
- Android cihazda Saat ve Saat Dilimi değiştiğinde, GadgetBridge bağlanırken Zamanı senkronize et
- Tarama sırasında cihazınız bulunmaz ise bu seçeneği işaretleyin
- Banda onay gönderilmezse, aktivite verileri temizlenmez. GB diğer uygulamalarla birlikte kullanılıyorsa kullanışlıdır.
- Takvim etkinliklerini Zaman Çizgisine gönder
- Bunun Pasifleştirilmesi, Pebble 2/LE cihazındaki giden aramalarda titremeyi durduracaktır
+ Sosyal ağ
+ Hareketsizlik bildirimi
+ Gelen çağrı bildirimi
+ Düşük pil uyarısı
+ Cevapsız çağrı bildirimi
+ SMS bildirimi
+ Titreşim profili
+ Aktarım hızını artırır, ancak bazı Android aygıtlarda çalışmayabilir.
+ Kimlik doğrulama anahtarını, bağlanmak istediğiniz tüm Android aygıtlarınızda ortak bir anahtar olarak değiştirin. Tüm aygıtlar için önceki öntanımlı anahtar: 0123456789@ABCDE
+ Her %d saatte bir dışa aktar
+ Bildirimler Android aygıtından silindiğinde otomatik olarak Pebble üzerinden kaldırılır
+ Arapça içeriği desteklemek için bunu etkinleştirin
+ Bağlandığında Gadgetbridge simgesi yerine aygıta özel bir Android bildirim simgesi göster
+ Bağlanırken ve Android aygıtında saat ve saat dilimi değiştiğinde saati Gadgetbridge aygıtına eşzamanla
+ Tarama sırasında aygıtınız bulunamıyorsa bu seçeneği işaretleyin
+ Bilekliğe onay (ACK) gönderilmezse, etkinlik verileri temizlenmez. GB diğer uygulamalarla birlikte kullanılıyorsa kullanışlıdır.
+ Takvim etkinliklerini zaman çizelgesine gönder
+ Bunun devre dışı bırakılması, Pebble 2/LE aygıtının giden aramalarda titremesini durduracaktırPebbleKit kullanırken Android uygulamaları için deneysel desteği etkinleştirin
- GadgetBridge bağlıyken diğer uygulamaların verilere gerçek zamanlı erişmesine izin ver
+ Gadgetbridge bağlıyken diğer uygulamaların kalp ritmi verilerine gerçek zamanlı erişmesine izin verirSaatinizin karanlık görünümü varsa kullanışlıdır
- Senkronizasyondan sonra bile Aktivite verilerini Mi Bandında tutar. GB diğer uygulamalarla birlikte kullanılıyorsa kullanışlıdır.
+ Eşzamanlamadan sonra bile etkinlik verilerini Mi Band üzerinde tutar. GB diğer uygulamalarla birlikte kullanılıyorsa kullanışlıdır.Geçerli konumu çalışma zamanında almaya çalış, depolanan konumu yedek olarak kullan
- Bu, Ürün Firmware yüklemesinin başarısız olduğu cihazlarda yardımcı olabilir.
- Durum Çubuğunda Simge ve Kilit Ekranında Bildirim gösterilir
- Durum Çubuğunda Simge ve Kilit Ekranında Bildirim gizlenir
- İstenmeyen Bildirimler, bu modda durdurulmuştur
- PebbleKit üzerinden Pebble cihazına bildirim gönderen uygulamalar için destek.
- Harici 3.Taraf uygulamalarına gönderilen iletilerin her zaman ve hemen onaylanmasına neden olur
- Saat uygulamalarındaki günlüklerin GadgetBridge tarafından günlüğe kaydedilmesine neden olur (yeniden bağlanma gerekir)
- Etkinleştirildiği zaman, Hava Durumu, Batarya ve benzeri bilgileri Bileklik arayüzüne gönderir.
- Klasik BT yerine tüm Pebbleler için deneysel Pebble LE desteği kullanın. Bu önce LE olmayan ile ve daha sonra Pebble LE ile eşleştirme gerektirir
+ Bu, ürün yazılımı kurulmasının başarısız olduğu aygıtlarda yardımcı olabilir.
+ Durum çubuğunda simge ve kilit ekranında bildirim gösterilir
+ Durum çubuğunda simge ve kilit ekranında bildirim gizlenir
+ İstenmeyen bildirimler, bu modda durdurulmuştur
+ PebbleKit üzerinden Pebble aygıtına bildirim gönderen uygulamalar için destek.
+ Harici 3. taraf uygulamalarına gönderilen mesajların her zaman ve hemen onaylanmasına neden olur
+ Saat uygulamalarındaki günlüklerin Gadgetbridge tarafından günlüğe kaydedilmesine neden olur (yeniden bağlanma gerekir)
+ Etkinleştirildiğinde, saat arayüzlerinin hava durumu, pil bilgisi vs. göstermesine izin verir.
+ Klasik BT yerine tüm Pebble\'ler için deneysel Pebble LE desteği kullanın. Bu önce LE olmayan ile ve daha sonra Pebble LE ile eşleştirme gerektirirBu seçenek, ürün yazılımı sürümüne bağlı olarak en son bildirim protokolünü kullanmaya zorlar. NE YAPTIĞINIZI BİLİN!Test edilmemiş özellikleri etkinleştir. NE YAPTIĞINIZI BİLİN!
- Bu sadece Pebble 2 ve Deneysel içindir, bağlantı sorunlarınız varsa bunu deneyin
- Verileri \"olduğu gibi\" saklar, daha sonra yorumlanabilmesi için veritabanı kullanımını artırır.
+ Bu yalnızca Pebble 2 içindir ve deneyseldir, bağlantı sorunlarınız varsa bunu deneyin
+ Verileri \"olduğu gibi\" saklar, daha sonra yorumlanabilmesi için veri tabanı kullanımını artırır.Eğer Pebble 2/Pebble LE beklendiği gibi çalışmazsa, MTU sınırlaması için bu ayarı deneyin (geçerli aralık 20–512)
- Firmware kontrollerini gevşet
- Cihazınız sağdan sola dilleri gösteremiyorsa bunu etkinleştirin
+ Ürün yazılımı denetimlerini gevşet
+ Aygıtınız sağdan sola dilleri gösteremiyorsa bunu etkinleştirinBağlanma konusunda sorun yaşıyorsanız bunu devre dışı bırakın
- Konum üzerindeki GünDoğumu ve GünBatımı zamanlarını, Pebble Zaman Çizgisine gönder
- Bağlantı kesildiğinde bile Takvim Alarmlarını etkinleştirir
- Cihazınızda, Dilinizin YazıTipi için destek yoksa bunu etkinleştirin
- Cihazınız, Emoji desteği için özel bir YazıTipi yazılımına sahipse bunu etkinleştirin
- Karanlık
- Aydınlık
+ Pebble zaman çizelgesine konuma göre gün doğumu ve gün batımı saatlerini gönder
+ Bağlantı kesildiğinde bile takvim uyarılarını etkinleştirir
+ Aygıtınızda dilinize ait yazı tipi desteği yoksa bunu etkinleştirin
+ Aygıtınız, Emoji desteği için özel bir yazı tipi yazılımına sahipse bunu etkinleştirin
+ Koyu
+ AçıkkaranlıkaydınlıkYüksek MTU\'ya izin verTercih Edilen Ses OynatıcıKimlik Doğrulama Anahtarı
- Otomatik DışaAktarma Açık
- Dışa Aktarma Aralığı
- Dışa Aktarma Konumu
- Yoksayılan Bildirimleri Otomatik Kaldır
- Çağrı Gizlilik Modu
- Çağrı YokSayımı
+ Otomatik dışa aktarma etkinleştirildi
+ Dışa aktarma aralığı
+ Dışa aktarma konumu
+ Yok sayılan bildirimleri otomatik kaldır
+ Çağrı gizlilik modu
+ Çağrı Yok SaymaPebble GüncellemeCevaplar
- Ortak SonEk
- Kalp Ritmi Rengi
- Uyku Aralığı
- Çizelgelerdeki Ortalamaları Göster
+ Ortak son ek
+ Kalp ritmi rengi
+ Uyku aralığı
+ Çizelgelerde ortalamaları gösterÇizelge Aralığı
- Aktivite Çizelgelerini sola / sağa hızlıca kaydırmayı etkinleştir
- Arabça İçerik
- Cihaz Özel Bildirim Simgesini Göster
- Senkronizasyon Zamanı
- Mi Band Bluetooth Adresi
- ACK Aktivite veri aktarımı yapma
- Takvimi Senkronize Et
- Giden Çağrıları Destekle
- 3.Taraf Android Uygulama erişimine izin ver
- 3.Taraf gerçek zamanlı KalpRitmi erişimi
- Beyaz renk şemasında siyahı zorla
- Bluetooth açıkken GadgetBridge cihazına Bağlan
- Otomatik Yeniden Bağlan
- Otomatik Başlat
- Verileri Cihazda Sakla
- Lisan
- Konumu Al
- Konumu Güncel Tut
+ Etkinlik çizelgelerinde sola/sağa kaydırmayı etkinleştir
+ Arapça İçerik
+ Aygıta özel bildirim simgesini göster
+ Saati eşzamanla
+ Mi Band adresi
+ Etkinlik veri aktarımını ACK (onaylama) yapma
+ Takvimi eşzamanla
+ Giden çağrıları destekle
+ 3. taraf Android Uygulama erişimine izin ver
+ 3. taraf gerçek zamanlı kalp ritmi erişimi
+ Beyaz üzerinde siyah renk şemasını zorla
+ Bluetooth açıkken Gadgetbridge aygıtına bağlan
+ Otomatik yeniden bağlan
+ Otomatik başlat
+ Etkinlik verilerini aygıtta sakla
+ Dil
+ Konumu al
+ Konumu güncel tutEnlemBoylam
- Firmware yüklerken Düşük-Gecikme Modu kullan
- Azaltma Butonu
- Orta Butonu
- GadgetBridge Bildirimini Gizle
+ Ürün yazılımı kurarken düşük gecikme modu kullan
+ Alt Düğme
+ Orta Düğme
+ Gadgetbridge bildirimini gizleRahatsız EtmeTelefon Çağrıları
- Genel Bildirim Desteği
+ Genel bildirim desteğiPebble MesajlarıTekrarlarSMSBildirimler arasındaki asgari süre
- Tercih Edilen Aktivite İzleyici
+ Tercih edilen etkinlik izleyiciErken ACK PebbleKitİzleme uygulaması kaydetmeyi etkinleştir
- ArkaPlanda JS Etkinleştir
- Daima BLE (Bluetooth Düşük Enerji) TercihEt
- Bildirim Protokolünü Zorla
+ Arka plan JS\'sini etkinleştir
+ Her zaman BLE (Bluetooth Düşük Enerji) tercih et
+ Bildirim protokolünü zorlaTest edilmemiş özellikleri etkinleştir
- Sadece GATT İstemcisi
- Ham kaydı veritabanında depola
- Pebble 2/LE GATT MTU Limiti
- Gizlilik Modu
- Yeniden Bağlanma Denemeleri
- Pebble Ayarları
- Pebble Health Senkronizasyonu
- Misfit Senkronizasyonu
- Morpheuz Sentronizasyonu
- Cihazınız için tasarlanmamış bir ürün yazılımı yüklemek istiyorsanız bunu etkinleştirin (sorumluluk size aittir)
+ Yalnızca GATT istemcisi
+ Ham kaydı veri tabanında depola
+ Pebble 2/LE GATT MTU sınırı
+ Gizlilik modu
+ Yeniden bağlanma denemeleri
+ Pebble ayarları
+ Pebble Health Eşzamanlaması
+ Misfit Eşzamanlaması
+ Morpheuz Eşzamanlaması
+ Aygıtınız için tasarlanmamış bir ürün yazılımı kurmak istiyorsanız bunu etkinleştirin (sorumluluk size aittir)Sağdan Sola
- Zaman Gösterimi Ekranı
- Bluetooth Paylaşımını Etkinleştir
- GünDoğumu ve GünBatımı
- VoIP Çağrılarını Etkinleştir
- Takvim Etkinliklerini Senkronize Et
- Görünüm
- Zaman Formatı
- KarakterÇevirisi
+ Ekran açık süresi
+ Bluetooth eşleştirmeyi etkinleştir
+ Gün doğumu ve gün batımı
+ VoIP çağrılarını etkinleştir
+ Takvim etkinliklerini eşzamanla
+ Tema
+ Zaman biçimi
+ Karakter çevirisiBirimler
- Yukarı Butonu
- Özel Font Kullan
- Titreşim Gücü
+ Üst Düğme
+ Özel yazı tipi kullan
+ Titreşim gücüHava DurumuHava durumu konumu (LineageOS hava durumu sağlayıcısı için)
- Ekran Açıkken de Genel Bildirim
- Günlük Dosyalarını Yaz
- Cihaz Özel Ayarları
+ …ayrıca ekran açıkken
+ Günlük dosyalarını yaz
+ Aygıta özel ayarlarFM Frekansı
- HPlus/Makibes Ayarları
+ HPlus/Makibes ayarlarıLED Rengi
- Makibes HR3 Ayarları
- Mi Band / AmazFit Ayarları
+ Makibes HR3 ayarları
+ Mi Band / AmazFit ayarlarıQ Hybrid AyarlarıSağdan Sola Desteği
- Aktivite 2 Eylemi
- Uzun Basma Eylemi
- Aktivite 1 Eylemi
- Aktivite 3 Eylemi
- Ayrıntılı Basma Eylemi Ayarları
- Bağlantı Kesilme Bildirimi
- \\\'Telefonu Bul\\\' Etkinleştir
- Telefonu Bul
- Zil Süresi Saniye
+ Etkinlik 2 eylemi
+ Uzun basma eylemi
+ Etkinlik 1 eylemi
+ Etkinlik 3 eylemi
+ Ayrıntılı basma eylemi ayarları
+ Bağlantı kesilme bildirimi
+ \'Telefonu bul\' özelliğini aç
+ Telefonu bul
+ Saniye cinsinden zil süresiTelefonunuzun zilini çalmak için bilekliğinizi kullanın.
- Spor etkinliği sırasında Kalp Ritmi alarmı
- EnYüksek Limit
- EnDüşük Limit
- Ekran Yönü
- Tüm gün Kalp Ritmi ölçümü
- Tüm gün KalpRitmi ölçümü
- Saatiniz Sağda mı Solda mı?
- Üzerine Yazma Butonları Hatası
- Üzerine Yazma Butonları
+ Spor etkinliği sırasında kalp ritmi alarmı
+ Üst sınır
+ Alt sınır
+ Ekran yönü
+ Tüm gün kalp ritmi ölçümü
+ Tüm gün kalp ritmi ölçümü
+ Sola mı sağa mı giyiyorsunuz\?
+ Düğmelerin üzerine yazılırken hata oluştu
+ Düğmelerin üzerine yazıldıdeğişiklik birkaç saniye sürebilir…
- Adım Hedefi
- Zaman Dengeleyici
- SaatDilimi Dengeleyici
- Üzerine yazma butonları
+ Adım sayısı hedefi
+ saati kaydır
+ saat dilimini kaydır
+ düğmelerin üzerine yazEtkinleştirmek için lütfen adım sayısını bir milyona ayarlayın.UTC olarak ikinci saat dilimi farkı
- Zaman Değişimi
- Aktivite elini bildirim sayacı olarak kullan
- Titreşim Gücü:
+ zaman değişimi
+ etkinlik elini bildirim sayacı olarak kullan
+ titreşim gücü:Getirme tarihini sıfırlaSağ
- Russian
+ RusçaYapılandırmayı Kaydet
- 10 Saniye
- 20 Saniye
- 30 Saniye
- 5 Dakika
- Hepsini Seç
+ 10 saniye
+ 20 saniye
+ 30 saniye
+ 5 saniye
+ Tümünü seçPaylaş
- Günlüğü Paylaş
- "Genel Sorun Raporuna ait bir günlük dosyasını göndermeden önce Lütfen Şunu Unutmayın: Gadgetbridge, sağlık verileri, benzersiz tanımlayıcılar (bir cihazın MAC adresi gibi), müzik tercihleri vb. bilgiler yanında çok sayıda kişisel bilgi içerebilecek konuları da günlüğe kaydeder."
- Simplified Chinese
+ Günlüğü paylaş
+ Gadgetbridge\'in sağlık verileri, benzersiz tanımlayıcılar (bir aygıtın MAC adresi gibi), müzik tercihleri vb. dahil ancak bunlarla sınırlı olmamak üzere birçok kişisel bilgiyi günlük dosyalarına kaydettiğini lütfen unutmayın. Dosyayı herkese açık sorun bildirimi olarak göndermeden önce bu dosyayı düzenlemeyi ve bu bilgileri kaldırmayı göz önünde bulundurun.
+ Basitleştirilmiş ÇinceSaat %1$s ile %2$s arasıUyku
- Spanish
- Hız Dilimleri
- Toplam Dakika
- Dakikada Adım
- Bağlanmak için cihaza dokunun
- Aktivite için bağlı cihaza hafifçe dokunun
- Uygulama yöneticisi için bağlı cihaza hafifçe dokunun
- Titreşim için bağlı cihaza hafifçe dokunun
+ İspanyolca
+ Hız bölgeleri
+ Toplam dakika
+ Dakikada adım sayısı
+ Bağlanmak için bir aygıta dokunun
+ Etkinlik için bağlı aygıta hafifçe dokunun
+ Uygulama yöneticisi için bağlı aygıta hafifçe dokunun
+ Titreşim için bağlı aygıta hafifçe dokununTest
- Test Bildirimi
- Thai
- Bu GadgetBridge üzerinden bir test bildirimidir
- 24Saat
+ Test bildirimi
+ Tayca
+ Bu Gadgetbridge üzerinden bir test bildirimidir
+ 24 saatÖÖ/ÖS
- Alarm Detayları
- Cihaz Eşleştir
- Bildirim KaraListesi
+ Alarm ayrıntıları
+ Aygıt eşleştir
+ Bildirim kara listesiUygulama Yöneticisi
- KaraListedeki Takvimler
- Aktivite ve Uyku Grafiği
- GadgetBridge
- VeriTabanı Yönetimi
+ Kara Listeye Alınan Takvimler
+ Etkinlik ve Uyku
+ Gadgetbridge
+ Veri tabanı yönetimiHata Ayıklama
- Cihaz Özel Ayarları
- Cihaz Arama
- FW/App Yükleme
- Mi Bandını Eşleştir
+ Aygıta özel ayarlar
+ Aygıt arama
+ Ürün yazılımı/uygulama kurucu
+ Mi Band\'ınızı eşleştirinBildirim FiltresiPebble Eşleştirme
- Alarmları Yapılandır
+ Alarmları yapılandırAyarlar
- Uyku Göstergesi
+ Uyku izlemeTitreşim
- Watch 9 Ayarı
- Watch 9 Eşleştirme
- Yapılandırılacak Uygulama KaraListeye alınmamalıdır
- Konum Alındı
- Lütfen Ağ Konumlamayı Etkinleştirin
- Bildirim Filtresi Kaydedildi
- Lütfen EnAz 1 Kelime Girin
- Traditional Chinese
+ Watch 9 kalibrasyonu
+ Watch 9 eşleştirme
+ Uygulamanın yapılandırılması için kara listeye alınmaması gerekir
+ konum alındı
+ Lütfen ağ konumunu etkinleştirin
+ Bildirim filtresi kaydedildi
+ Lütfen en az bir sözcük girin
+ Geleneksel ÇinceTürkçe
- Ukrainian
- Imperyal
+ Ukraynaca
+ İngiliz birimleriMetrik
- Bilinmeyen Durum
- Firmware Gönderilmedi
- Firmware Meta Veri Aktarımı Sorunu
- Firmware Yükleme Tamam
- Firmware Yükleme Tamamlandı, Cihaz Yeniden Başlatılıyor…
- Firmware Yazılıyor
- Firmware Aktarımı Sorunu. Mi Bandınızı YENİDEN BAŞLATMAYIN!
- Firmware Yazımı Başarısız
- Firmware Yazılıyor…
- Bütün Alarmlar Etkisizleştirildi
- Veri Aktarımı Durumu: %1$s / %2$s
+ Bilinmeyen durum
+ Ürün yazılımı gönderilmedi
+ Ürün yazılımı üst veri aktarımıyla ilgili sorun
+ Ürün yazılımı kurulumu tamamlandı
+ Ürün yazılımı kurulumu tamamlandı, aygıt yeniden başlatılıyor…
+ Ürün yazılımı kuruluyor
+ Ürün yazılımı aktarımıyla ilgili sorun. Mi Band\'ınızı YENİDEN BAŞLATMAYIN!
+ Ürün yazılımı kurulması başarısız oldu
+ Ürün yazılımı kuruluyor…
+ Bütün alarmlar devre dışı
+ Veri aktarımı durumu: %1$s / %2$sAlarmlar ayarlanırken bir hata oluştu, lütfen tekrar deneyin.
- Alarmlar Cihaza Gönderildi.
+ Alarmlar aygıta gönderildi.Dikey
- Alarm Saati
+ Alarm saatiUzunOrta
- ZilSesi
+ Zil sesiKısa
- KesikKesik
- SuDamlası
- DenemeYap
- Vietnamese
- Bağlanmak için bekliyor
+ Kesik kesik
+ Su damlası
+ Dene
+ Vietnamca
+ Yeniden bağlanmak için bekliyorDikkat!Kalibrasyon
- Cihazınızın şu anda size göstereceği zamanı ayarlayın.
- Saatiniz titreştiğinde cihazı sallayın veya düğmesine basın.
+ Aygıtınızın şu anda size gösterdiği saati ayarlayın.
+ Saatiniz titreştiğinde aygıtı sallayın veya düğmesine basın.Saat:Dakika:Saniye:
- Saat Bağlanmadı
+ Saat bağlanmadıPebble üzerinde hava durumu bilgisi almak için bu dış görünümün Hava Durumu Bildirimi uygulamasında etkinleştirildiğinden emin olun.
\n
\nBurada herhangi bir yapılandırma gerekmez.
@@ -813,127 +813,127 @@ YÜKLEME RİSKİ RİZE AİTTİR!"
\nPebble sistem hava durumu uygulamasını uygulama yönetiminden etkinleştirebilirsiniz.
\n
\nDesteklenen saat yüzleri hava durumunu otomatik olarak gösterir.
- Aylık Uykular
- Haftalık Uykular
- Bugünkü Uyku, Hedef: %1$s
- Bugünkü Adımlar, Hedef: %1$s
- Aylık Adımlar
- Haftalık Adımlar
- Ekran Kapalı Olduğu Zaman
- Bildirimler için Hepsini BeyazListeye al
- 10 Dakika
- 1 Saat
- 20 Dakika
- 5 Dakika
+ Aylık uyku
+ Haftalık uyku
+ Bugünkü uyku, hedef: %1$s
+ Bugünkü adım sayısı, hedef: %1$s
+ Aylık adım sayısı
+ Haftalık adım sayısı
+ Ekran kapalı olduğunda
+ Bildirimler için hepsini beyaz listeye al
+ 10 dakika
+ 1 saat
+ 20 dakika
+ 5 dakikaDurum ve Alarmlar
- Alarm Ayarla:
+ Alarm kur:Uyku: %1$sAdım: %1$02dUyumadınızSaat %1$s ile %2$s arası uyudunuz
- Aktivite İzleme
- Aktivite İzlemenin Açılması, adımları sayacaktır.
- Analog Modu
- Sadece Ellerle
- Eller ve Adımlar
- Kalori Cinsi
- Sadece Aktif Harcanan Kalori
- Aktif ve Pasif Harcama Kalorisi
- Tarih Formatı
- YL/AY/GN
- GN/AY/YL
- AY/GN/YL
- El Hareketi
- Ekranı Açmak veya Kapamak için bileğinizi döndürün.
- Kalp Ritmi Alarmını Etkinleştir
+ Etkinlik izleme
+ Etkinlik izlemenin açılması adımlarınızı sayacaktır.
+ Analog modu
+ Yalnızca eller
+ Eller ve adımlar
+ Kalori türü
+ Yalnızca etkin harcanan kaloriler
+ Etkin ve etkin değilken harcanan kaloriler
+ Tarih biçimi
+ YY/AA/GG
+ GG/AA/YY
+ AA/GG/YY
+ El hareketi
+ Ekranı açmak veya kapatmak için bileğinizi döndürün.
+ Kalp ritmi alarmını etkinleştirCumaPazartesi
- Tekrarlama Sayısı
+ TekrarlarCumartesiPazarPerşembeSalıÇarşamba
- Sürekli Bip Sesi
- 1 Kez Bip Sesi
- 2 Kez Bip Sesi
- Sessizlik
- Sürekli Titreşim
- Sürekli Titreşim ve Bipleme
- 1 Kez Titreşim ve Bipleme
- 1 Kez Titreşim
- 2 Kez Titreşim
- Alarm için Sinyal Türü Ayarla
- Kalp Ritmi Alarmı
- Kalp Ritminiz sınırı aşarsa, Saat sizi uyaracaktır.
- Kalp Ritmi Ayarları
- Zaman Durumu Ekranında Saniyeler
- ZeTime Ayarları
+ Sürekli bip sesi
+ 1 defa bip sesi
+ 2 defa bip sesi
+ Sessiz
+ Sürekli titreşim
+ Sürekli titreşim ve bip sesi
+ 1 defa titreşim ve bip sesi
+ 1 defa titreşim
+ 2 defa titreşim
+ Alarm için sinyal türünü ayarla
+ Kalp ritmi alarmı
+ Kalp ritminiz sınırı aşarsa, saat sizi uyaracaktır.
+ Kalp ritmi ayarları
+ Saniye cinsinden ekran açık süresi
+ ZeTime ayarları
- %d Saat
- %d Saat
+ %d saat
+ %d saat
- AnaSayfa Göstergesi (Widget) Çevresi Çiz
- Watch X Plus Kalibrasyonu
- Ham aktivite dosyalarını kaydet
+ Widget çemberleri çiz
+ Watch X Plus kalibrasyonu
+ Ham etkinlik dosyalarını kaydetEgzersizEtkinlik HatırlatıcıKalp RitmiPAIWatch X PlusWatch X
- Sadece Saat
- Güç Tasarrufu
+ Yalnızca saat
+ Güç tasarrufuNormal
- Saat Güç Modu
+ Saat güç moduKalibrasyona başlamak için buraya basınKalibrasyonSistolik Kan Basıncı (Yüksek Tansiyon)Diyastolik Kan Basıncı (Düşük Tansiyon)Kan Basıncı Kalibrasyonu
- Yükseklik (irtifa) Kalibrasyonu
+ Yükseklik (irtifa) kalibrasyonuSensörler Kalibrasyonu
- Aktivite grafiğinde RAW verilerini göster
- Yeniden bağlandığında Otomatik Senkronizasyonu zorla. Elle müdahalede yanlış zaman gösterebilir!
- Senkronizasyon Zamanını Zorla
- Cihaz Ayarları
- İzleme Butonu Eylemini Çoğaltma
- Bileği Sallayarak Çağrıyı YokSayma/Reddetme
- Çağrıyı YokSayma/Reddetme Butonu
- YokSayma Kapalı, Reddetme Açık
+ Etkinlik grafiğinde RAW verilerini göster
+ Yeniden bağlandığında saati eşzamanlamayı zorla. Analog saatler yanlış zaman gösterebilir!
+ Saati eşzamanlamayı zorla
+ Aygıt ayarları
+ Saat düğmesi eylemini kopyalar
+ Bileği sallayarak çağrıyı yok say/reddet
+ Düğmeyle çağrıyı yok say/reddet
+ Kapalı - yok say, Açık - reddetÇağrı Yönetimi
- X Dakikada Tekrarla
- Dakika Başı Tekrarlar
- Cevapsız Çağrı Bildirimi
- Telefon Çalarken Bildirim
- Çağrı Bildirimini Tekrarla
+ X dakikada tekrarla
+ Her dakikada tekrarlar
+ Cevapsız çağrı bildirimi
+ Telefon çalarken bildirim
+ Çağrı bildirimini tekrarlaBildirimler ve ÇağrılarSistem
- Czech
- Swedish
- Hebrew
- Greek
- Hungarian
- Romanian
+ Çekçe
+ İsveççe
+ İbranice
+ Yunanca
+ Macarca
+ RumenceBilinmeyen
- Band Ekranı üzerindeki KısaYolları seçin
- KısaYollar
+ Bileklik ekranındaki kısayolları seçin
+ Kısa yollarSon bildirimTakma Ad Ayarla
- Amazfit T-Rex cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ Amazfit T-Rex aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasını, ve son olarak .gps dosyasını kurun. .fw dosyasını kurduktan sonra saatiniz yeniden başlatılacak.
\n
-\nNote: Bu dosyalar önceden yüklenmiş olanlarla tamamen aynıysa .res ve .gps dosyalarını yüklemeniz gerekmez.
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res ve .gps dosyalarını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!Amazfit T-RexListelenmeyen herkese kod, çeviri, destek, fikirler, motivasyon, hata raporları, para… ile katkıda bulundukları için çok teşekkürler ✊
- Ek cihaz desteği
+ Ek aygıt desteğiKatkıda bulunanlarÇekirdek Ekip (ilk kod katkıda bulunma sırasına göre)Aygıt sağlayıcılarınızın kapalı kaynaklı Android uygulamalarının yerine özgür ve bulut gerektirmeyen bir alternatif.
- Gadgetbridge Hakkında
+ Gadgetbridge hakkındaHakkındaLineageOS hava durumu sağlayıcısı için kullanılır, diğer Android sürümleri \"Weather notification\" gibi bir uygulama kullanmalıdır. Gadgetbridge wiki sayfasında daha fazla bilgi bulabilirsiniz.Dünya Saati
@@ -946,40 +946,39 @@ YÜKLEME RİSKİ RİZE AİTTİR!"DöngülerNefes AlmaMi Band 5
- Mi Band 5 cihazınıza %s ürün yazılımını yüklemek üzeresiniz.
+ Mi Band 5 aygıtınıza %s ürün yazılımını kurmak üzeresiniz.
\n
-\nLütfen önce .fw dosyasını, sonra .res dosyasını yükleyin. .fw dosyasını yükledikten sonra saatiniz yeniden başlatılacak.
+\nLütfen önce .fw dosyasını, sonra .res dosyasın kurun. .fw dosyasını kurduktan sonra bilekliğiniz yeniden başlatılacak.
\n
-\nNot: Önceden yüklenmiş olanla tamamen aynıysa .res dosyasını yüklemeniz gerekmez.
+\nNote: Bu dosyalar önceden kurulanlarla tamamen aynıysa .res dosyasını kurmanız gerekmez.
\n
\nİLERLEMENİZ DURUMUNDA RİSK SİZE AİTTİR!TLW64
- Hizmetin arka planda yeniden başlatılması gerekiyorsa güvenilirliği artıracak yeni CompanionDevice API desteğini etkinleştirir (sadece Android 8 veya üzerinde bir etkisi vardır), etkili olması için Gadgetbridge kullanarak yeniden eşleştirme gerektirir
- Bu seçeneğin etkinleştirilmesi, tarama sırasında önceden etkileşimde/eşleştirilmiş olan cihazları yok sayar
- Etkileşimde olan cihazları yok say
+ Hizmetin arka planda yeniden başlatılması gerekiyorsa güvenilirliği artıracak yeni CompanionDevice API desteğini etkinleştirir (yalnızca Android 8 veya üzerinde bir etkisi vardır), etkili olması için Gadgetbridge kullanarak yeniden eşleştirme gerektirir
+ Bu seçeneğin etkinleştirilmesi, tarama sırasında önceden etkileşimde/eşleştirilmiş olan aygıtları yok sayar
+ Etkileşimde olan aygıtları yok sayZATEN ETKİLEŞİMDEANAHTAR GEREKLİ
- Cihazları taramak için konumun açık olması gerekmektedir
- Veri tabanından cihazlar alınırken hata oluştu
+ Aygıtları taramak için konumun açık olması gerekmektedir
+ Veri tabanından aygıtlar alınırken hata oluştuTakma ad ayarlanırken hata oluştu:
- Cihaza özgü tercihler dışa aktarılırken hata oluştu
- Anında gerekmediklerinde bile eksik izinleri denetleyin ve isteyin. Bunu sadece cihazlarınız bu özelliklerin hiçbirini desteklemiyorsa devre dışı bırakın. İzin verilmemesi sorunlara neden olabilir!
+ Aygıta özel tercihler dışa aktarılırken hata oluştu
+ Anında gerekmediklerinde bile eksik izinleri denetleyin ve isteyin. Bunu yalnızca aygıtlarınız bu özelliklerin hiçbirini desteklemiyorsa devre dışı bırakın. İzin verilmemesi sorunlara neden olabilir!İzin durumuna göz atCompanionDevice EşleştirmeKonum etkinleştirilmelidirPineTime (JF Ürün Yazılımı)ArtanAzalma
- GPS Takibini Göster
+ GPS İzlemesini GösterYükseklikKulaçbpmkulaçkulaç/sDüz
- yüzme tarzı
- swolf indeksi
- Kulaç
+ Yüzme Tarzı
+ Swolf İndeksiOrtalama KulaçOrtalama Kulaç UzunluğuEtkinlik
@@ -1005,7 +1004,7 @@ YÜKLEME RİSKİ RİZE AİTTİR!"Ortalama Tur HızıOrtalama AdımHız
- Nabız
+ Kalp ritmiToplam adımEn Yüksek HızAzami
@@ -1024,8 +1023,8 @@ YÜKLEME RİSKİ RİZE AİTTİR!"GiyilmediğindeUyandığındaUykuya Dalarken
- Cihaz eylemleri
- Eylemleri ve Android yayınlarını tetiklemek için cihaz olaylarını kullan
+ Aygıt eylemleri
+ Eylemleri ve Android yayınlarını tetiklemek için aygıt olaylarını kullanYogaİp AtlamaYüzme (Açık su)
@@ -1054,7 +1053,7 @@ YÜKLEME RİSKİ RİZE AİTTİR!"Bugünbugünuzak geçmiş
- Tüm cihazlar
+ Tüm aygıtlarZaman dilimi30 gün7 gün
@@ -1075,4 +1074,26 @@ YÜKLEME RİSKİ RİZE AİTTİR!"KriketKürek MakinesiFutbol
+ En düşük etkinlik uzunluğu (dakika)
+ Etkinlikleri ayırmak için duraklama uzunluğu (dakika)
+ Etkinlik algılamak için dakikada en az adım sayısı
+ Çalışmayı algılamak için dakikada en az adım sayısı
+ Etkinlik listesi
+ Biraz etkinlik yapın ve aygıtı eşzamanlayın.
+ Etkinlik algılanmadı.
+ Telefonunuzun bileklikle bağlantısı kesilirse bileklik titreyecek
+ Lefun
+ Arayüz dili
+ Kayıp önleme
+ Sıvı alma hatırlatma aralığı (dakika olarak)
+ Bileklik su içmenizi hatırlatmak için titreyecek
+ Sıvı alma hatırlatıcı
+ Sony SWR12
+ Sony SWR12 Ayarları
+ Düşük titreşim etkin
+ Dakikalar içinde akıllı alarm aralığı
+ Bileklik üzerinde düşük titreşim yoğunluğu sağlayın
+ Güç tasarrufu modu, kalp atış hızının periyodik otomatik ölçümünü kapatır ve böylece çalışma süresini uzatır
+ Akıllı alarm aralığı, kurulu alarmdan önceki aralıktır. Bu aralıkta cihaz, kullanıcıyı uyandırmak için uykunun en hafif aşamasını tespit etmeye çalışıyor
+ Güç tasarrufu modu açık
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 8ebf505b5..394ab5c4e 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -1052,4 +1052,11 @@
\nПримітка: вам не потрібно встановлювати .res файл, якщо він такий самий був встановлений раніше.
\n
\nДІЄТЕ НА ВЛАСНИЙ РИЗИК!
+ Sony SWR12 налаштування
+ Cлабка вібрація
+ Режим енергозбереження ввімкнено
+ Інтелектуальний інтервал тривоги в хвилинах
+ Увімкніть низьку інтенсивність вібрації на браслеті
+ Режим енергозбереження вимикає періодичне автоматичне вимірювання частоти серцевих скорочень, таким чином збільшуючи час роботи
+ Інтервал інтелектуальної тривоги - це інтервал до встановленої сигналізації. У цей інтервал пристрій намагається виявити найлегшу фазу сну для пробудження користувача
\ No newline at end of file
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index a5c98d6f0..7bea7ae23 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -190,4 +190,11 @@
GiờGiờ và ngàynodomain.freeyourgadget.gadgetbridge.ButtonPressed
+ Cài đặt Sony SWR12
+ Chế độ tiết kiệm pin đang bật
+ Khoảng thời gian báo thức thông minh trong vài phút
+ Bật cường độ rung thấp trên thiết bị đeo tay
+ Chế độ tiết kiệm năng lượng tắt tính năng đo nhịp tim tự động định kỳ do đó tăng thời gian làm việc
+ Khoảng thời gian báo động thông minh là khoảng thời gian trước khi báo động được cài đặt. Trong khoảng thời gian này, thiết bị đang cố gắng phát hiện giai đoạn ngủ nhẹ nhất để người dùng tỉnh táo
+ Độ rung thấp được kích hoạt
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 7794b1102..ef7bdd9ad 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -759,7 +759,7 @@
Makibes HR3华米手表青春版 Lite查找手机
- 启用查找手机
+ 启用“查找手机”使用您的手环以在手机上播放铃声。铃声将持续数秒持续
@@ -1055,4 +1055,26 @@
今天遥远的过去连接
+ 最小活动长度(分钟)
+ 以暂停长度来分割活动(分钟)
+ 每分钟检测到的最小活动步数
+ 每分钟检测跑步的最小步数
+ 活动列表
+ 做一些运动来同步设备。
+ 未检测到活动。
+ Lefun
+ 界面语言
+ 如果您的手机断开与手环的连接,将会震动
+ 防丢失
+ 水合提醒间隔(分钟)
+ 当需要提醒喝水时,手环会震动
+ 水合提醒
+ Sony SWR12
+ Sony SWR12 设置
+ 低振动启用
+ 省电模式已开启
+ 智能警报间隔(以分钟为单位)
+ 降低频带上的振动强度
+ 省电模式会关闭定期自动测量心率,从而增加工作时间
+ 智能警报间隔是安装警报之前的间隔。 在此间隔内,设备尝试检测最轻的睡眠阶段以唤醒用户
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 1bba745d1..e56bc856d 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -585,4 +585,11 @@
游泳這是一個來自 Gadgetbridge 的測試通知測試通知
+ Sony SWR12 设置
+ 低振动启用
+ 降低频带上的振动强度
+ 省电模式已开启
+ 省电模式会关闭定期自动测量心率,从而增加工作时间
+ 智能警报间隔(以分钟为单位)
+ 智能警报间隔是安装警报之前的间隔。 在此间隔内,设备尝试检测最轻的睡眠阶段以唤醒用户
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 346b118c0..1619bac8f 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -976,4 +976,32 @@
7days30days
+
+
+ @string/english
+ @string/simplified_chinese
+
+
+
+ 0
+ 1
+
+
+ 0 minutes
+ 10 minutes
+ 20 minutes
+ 30 minutes
+ 40 minutes
+ 50 minutes
+ 60 minutes
+
+
+ 0
+ 10
+ 20
+ 30
+ 40
+ 50
+ 60
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c6782d0f0..7ef777ec3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
-
+GadgetbridgeGadgetbridgeSettings
@@ -418,6 +418,8 @@
There was an error setting the alarms, please try again.Alarms sent to device.No data. Synchronize device?
+ No activities detected.
+ Do some activity and synchronize device.About to transfer %1$s of data starting from %2$sDaily step targetError executing \'%1$s\'
@@ -439,9 +441,24 @@
Sleep today, target: %1$sSteps per weekActivity
+ Activity listLack of sleep: %1$sOverslept: %1$s
+
+
+ Device is connecting
+ Device is connected
+ Upload is starting
+ Upload has started
+ Device is disconnecting!
+ Device has disconnected!
+ Upload has completed
+ Upload is being validated
+ Upload has been aborted!
+ Upload has failed
+ Upload is in progress\n%1d%% at %.2fkbps (average %.2fkbps)\nPart %1d of %1dFlashing firmware…
+
File cannot be installed, device not ready.%1$s: %2$s %3$sCompatible version
@@ -502,7 +519,7 @@
Use heart rate sensor to improve sleep detectionDevice time offset in hours (for detecting sleep of shift workers)Find phone
- Turn on \\\'Find phone\\\'
+ Turn on \'Find phone\'Use your band to play your phone\'s ringtone.Ring duration in secondsDate format
@@ -544,6 +561,12 @@
Choose the shortcuts on the band screenForce black on white color schemeUseful if you your watch has dark hands
+ Hydration reminder
+ The band will vibrate to remind you to drink water
+ Hydration reminder interval (in minutes)
+ Anti-loss
+ The band will vibrate if your phone disconnects from the band
+ Interface languageAutomaticSimplified ChineseTraditional Chinese
@@ -596,6 +619,10 @@
Sleep rangePast 24 hoursNoon to noon
+ Minimal steps per minute to detect run
+ Minimal steps per minute to detect activity
+ Pause length to separate activities (minutes)
+ Minimal activity length (minutes)AuthenticatingAuthentication requiredPreferred sleep duration in hours
@@ -719,6 +746,7 @@
TreadmillExerciseTo view activity trace, install app which can handle GPX files.
+ To share this screenshot, install an app which can handle image files.Select allShareReset fetch date
@@ -772,11 +800,13 @@
Bangle.jsTLW64PineTime (JF Firmware)
+ Sony SWR12Choose export locationGadgetbridge notificationsGadgetbridge notifications high priorityAmazfit GTSLemfo SG2
+ LefunAlipay (Shortcut)Weather (Shortcut)
@@ -920,6 +950,13 @@
Ignore bonded devicesEnabling this option will ignore devices that have been bonded/paired already when scanningLocation must be turned on to scan for devices
+ Sony SWR12 Settings
+ Low vibration enabled
+ Enable low intensity of vibration on wristband
+ Power saving mode on
+ Power saving mode turns off periodic auto measuring of heart rate thus increases working time
+ Smart alarm interval in minutes
+ Smart alarm interval is interval before of installed alarm. In this interval device is trying to detect lightest phase of sleep to awake userDistanceUphill
@@ -1021,6 +1058,7 @@
FreestyleBackstrokeMedley
+ Nut Mini%d hour
diff --git a/app/src/main/res/xml/charts_preferences.xml b/app/src/main/res/xml/charts_preferences.xml
index 675fee091..2c625fb51 100644
--- a/app/src/main/res/xml/charts_preferences.xml
+++ b/app/src/main/res/xml/charts_preferences.xml
@@ -53,7 +53,38 @@
android:summaryOff="@string/pref_charts_range_off"
android:summaryOn="@string/pref_charts_range_on"
android:title="@string/pref_title_charts_range" />
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_antilost.xml b/app/src/main/res/xml/devicesettings_antilost.xml
new file mode 100644
index 000000000..a4c9dc705
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_antilost.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_hydration_reminder.xml b/app/src/main/res/xml/devicesettings_hydration_reminder.xml
new file mode 100644
index 000000000..95edf232b
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_hydration_reminder.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_lefun_interface_language.xml b/app/src/main/res/xml/devicesettings_lefun_interface_language.xml
new file mode 100644
index 000000000..b6dab3ceb
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_lefun_interface_language.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_nutmini.xml b/app/src/main/res/xml/devicesettings_nutmini.xml
new file mode 100644
index 000000000..323dfcd73
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_nutmini.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_sonyswr12.xml b/app/src/main/res/xml/devicesettings_sonyswr12.xml
new file mode 100644
index 000000000..e67a0ba1d
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_sonyswr12.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nut/NutUtilsTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nut/NutUtilsTest.java
new file mode 100644
index 000000000..0df021164
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/nut/NutUtilsTest.java
@@ -0,0 +1,247 @@
+/* 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.service.devices.nut;
+
+import org.junit.Test;
+
+import java.math.BigInteger;
+import java.util.AbstractMap;
+import java.util.Map;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.fail;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.byteArraysConcatReverseWithPad;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.byteArraysDeConcatReverseWithPad;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.bytesToHex2;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.hexStringToByteArrayNut;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.macAsByteArray;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.passwordGeneration;
+import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.reversePasswordGeneration;
+
+public class NutUtilsTest {
+ @Test
+ public void testPasswordGen() {
+ String result = bytesToHex2(passwordGeneration(
+ "00:00:00:00:00:00",
+ new byte[]{1, 0, 0, 0, 0, 0, 0},
+ BigInteger.ZERO,
+ BigInteger.ZERO)
+ );
+ String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testPasswordGen2() {
+ String result = bytesToHex2(passwordGeneration(
+ "00:00:00:00:00:00",
+ new byte[]{1, 0, 0, 0, 0, 0, 0},
+ BigInteger.ZERO,
+ BigInteger.ZERO)
+ );
+ String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testPasswordGen3() {
+ String result = bytesToHex2(passwordGeneration(
+ "00:00:00:00:00:00",
+ new byte[]{1, 1, 0, 0, 0, 0, 0},
+ BigInteger.ZERO,
+ BigInteger.ZERO)
+ );
+ String expected = bytesToHex2(new byte[]{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testPasswordGen4() {
+ String result = bytesToHex2(passwordGeneration(
+ "00:00:00:00:00:00",
+ new byte[]{1, 0, 0, 0, 0, 0, 0},
+ new BigInteger(1, new byte[]{1, 0, 0, 0, 0, 0}),
+ BigInteger.ZERO)
+ );
+ String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testPasswordGen5() {
+ String result = bytesToHex2(passwordGeneration(
+ "00:00:00:00:00:00",
+ new byte[]{1, 0, 0, 0, 0, 0, 0, 0, 0},
+ BigInteger.ZERO,
+ new BigInteger(1, new byte[]{1, 1, 1, 1, 1, 1, 0, 0}))
+ );
+ String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testPasswordGen6() {
+ String result = bytesToHex2(passwordGeneration(
+ "01:02:03:04:05:06",
+ new byte[]{1, 1, 2, 3, 4, 5, 6, 7, 8},
+ BigInteger.ZERO,
+ BigInteger.ZERO)
+ );
+ String expected = bytesToHex2(new byte[]{0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testPasswordGen7() {
+ String result = bytesToHex2(passwordGeneration(
+ "01:02:03:04:05:06",
+ new byte[]{1, 1, 2, 3, 4, 5, 6, 7, 8},
+ new BigInteger(1, new byte[]{1, 2, 3, 4, 5, 6, 7, 8}),
+ new BigInteger(1, new byte[]{1, 2, 3, 4, 5, 6, 7, 8}))
+ );
+ String expected = bytesToHex2(new byte[]{0x17, 0x15, 0x13, 0x11, 0x0f, 0x0d, 0x0b, 0x0a, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01});
+ assertEquals(expected, result);
+ }
+
+ @Test
+ public void testReversePassword() {
+ String challenge = "01e8f0340d000000000000000000000000";
+ String response = "029bbd0fa25aed0000dcfd0c0000000000";
+ String device_mac = "ED:5A:94:CB:98:E4";
+
+ Map.Entry key = reversePasswordGeneration(
+ hexStringToByteArrayNut(challenge),
+ hexStringToByteArrayNut(response),
+ device_mac
+ );
+
+ assertNotNull(key);
+ assertEquals(new AbstractMap.SimpleEntry<>(new BigInteger("851420"), new BigInteger("996303")), key);
+ }
+
+ @Test
+ public void testPassword() {
+ String challenge = "011a9b826c000000000000000000000000";
+ String response = "02cd675d015bed0000dcfd0c0000000000";
+ String device_mac = "ED:5A:94:CB:98:E4";
+
+ Map.Entry key = new AbstractMap.SimpleEntry<>(new BigInteger("851420"), new BigInteger("996303"));
+
+ byte[] result = new byte[17];
+ result[0] = 0x02;
+ System.arraycopy(passwordGeneration(device_mac, hexStringToByteArrayNut(challenge), key.getKey(), key.getValue()), 0, result, 1, 16);
+
+ assertEquals(bytesToHex2(hexStringToByteArrayNut(response)), bytesToHex2(result));
+ }
+
+ @Test
+ public void testInvalidResponse() {
+ String challenge = "00";
+ String response = "00";
+ String device_mac = "0:00:00:00:00:00";
+ try {
+ reversePasswordGeneration(
+ hexStringToByteArrayNut(challenge),
+ hexStringToByteArrayNut(response),
+ device_mac
+ );
+ } catch (IllegalArgumentException e) {
+ // This is intended behaviour
+ assertNotNull(e);
+ return;
+ }
+ fail();
+ }
+
+ @Test
+ public void testHexToByteArray() {
+ byte[] arr = hexStringToByteArrayNut("0x0000000000");
+ assertEquals(bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00}), bytesToHex2(arr));
+ }
+
+ @Test
+ public void testHexToByteArray2() {
+ byte[] arr = hexStringToByteArrayNut("cafebabe");
+ assertEquals(bytesToHex2(new byte[]{(byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe}), bytesToHex2(arr));
+ }
+
+ @Test
+ public void testMACToByteArray() {
+ byte[] arr = macAsByteArray("AA:BB:CC:DD:EE:FF");
+ assertEquals(bytesToHex2(
+ new byte[]{
+ (byte) 0xaa,
+ (byte) 0xbb,
+ (byte) 0xcc,
+ (byte) 0xdd,
+ (byte) 0xee,
+ (byte) 0xff
+ }
+ ), bytesToHex2(arr));
+ }
+
+ @Test
+ public void testConcat() {
+ byte[] src1 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
+ byte[] src2 = new byte[]{9, 10, 11, 12, 13, 14, 15, 16};
+ assertEquals(
+ bytesToHex2(new byte[]{0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}),
+ bytesToHex2(byteArraysConcatReverseWithPad(
+ src1,
+ src2)
+ )
+ );
+ }
+
+ @Test
+ public void testConcatDeConcat() {
+ byte[] src1 = new byte[]{0, 0, 3, 4, 5, 6, 0, 0};
+ byte[] src2 = new byte[]{0, 0, 7, 8, 9, 1, 0, 0};
+ byte[] dst1 = new byte[8];
+ byte[] dst2 = new byte[8];
+ byteArraysDeConcatReverseWithPad(
+ byteArraysConcatReverseWithPad(
+ src1,
+ src2
+ ),
+ dst1,
+ dst2
+ );
+ assertEquals(bytesToHex2(src1), bytesToHex2(dst1));
+ assertEquals(bytesToHex2(src2), bytesToHex2(dst2));
+ }
+
+ @Test
+ public void testConcatDeConcat2() {
+ byte[] src1 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
+ byte[] src2 = new byte[]{9, 10, 11, 12, 13, 14, 15, 16};
+ byte[] dst1 = new byte[8];
+ byte[] dst2 = new byte[8];
+ byteArraysDeConcatReverseWithPad(
+ byteArraysConcatReverseWithPad(
+ src1,
+ src2
+ ),
+ dst1,
+ dst2
+ );
+ assertEquals(bytesToHex2(src1), bytesToHex2(dst1));
+ assertEquals(bytesToHex2(src2), bytesToHex2(dst2));
+ }
+}
diff --git a/build.gradle b/build.gradle
index 7f21fe3e0..25667ee85 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,7 @@ buildscript {
}
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath 'com.android.tools.build:gradle:4.0.2'
classpath 'gradle.plugin.com.github.spotbugs:spotbugs-gradle-plugin:2.0.0'
// NOTE: Do not place your application dependencies here; they belong