1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 04:46:51 +01:00

Merge remote-tracking branch 'origin/master'

This commit is contained in:
Daniel Dakhno 2020-10-12 18:15:11 +02:00
commit 2335a7a337
184 changed files with 12121 additions and 951 deletions

View File

@ -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:*
<details>
<summary>Click to see my log, which is a bit longer</summary>
```
Here go lines of that log.
```
</details>
#### Your wearable device is:

View File

@ -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)

View File

@ -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");

View File

@ -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

View File

@ -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"
}

View File

@ -9,6 +9,8 @@
# Add any project specific keep options here:
-dontobfuscate
# Pebble BG-JS
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
@ -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.** { *; }

View File

@ -346,6 +346,9 @@
</service>
<service android:name=".service.NotificationCollectorMonitorService" />
<service android:name=".service.DeviceCommunicationService" />
<service
android:name=".devices.pinetime.PineTimeDFUService"
android:label="PineTime Nordic DFU service" />
<receiver
android:name=".externalevents.WeatherNotificationReceiver"

View File

@ -19,21 +19,26 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.text.InputType;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
@ -43,12 +48,16 @@ import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Date;
@ -65,28 +74,49 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.SwipeEvents;
//import nodomain.freeyourgadget.gadgetbridge.util.OnSwipeTouchListener;
public class ActivitySummaryDetail extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryDetail.class);
private GBDevice gbDevice;
private boolean show_raw_data = false;
BaseActivitySummary currentItem = null;
private GBDevice gbDevice;
private boolean show_raw_data = false;
private int alternateColor;
//private Object BottomSheetBehavior;
private Menu mOptionsMenu;
public static int getAlternateColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.alternate_row_background, typedValue, true);
return typedValue.data;
}
public static Bitmap getScreenShot(View view, int height, int width, Context context) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (GBApplication.isDarkThemeEnabled()) {
canvas.drawColor(GBApplication.getBackgroundColor(context));
} else {
canvas.drawColor(Color.WHITE);
}
view.draw(canvas);
return bitmap;
}
@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context appContext = this.getApplicationContext();
if (appContext instanceof GBApplication) {
setContentView(R.layout.activity_summary_details);
}
Intent intent = getIntent();
Bundle bundle = intent.getExtras();
@ -137,9 +167,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
currentItem = newItem;
makeSummaryHeader(newItem);
makeSummaryContent(newItem);
activitySummariesChartFragment.setDateAndGetData(gbDevice, currentItem.getStartTime().getTime()/1000, currentItem.getEndTime().getTime()/1000);
activitySummariesChartFragment.setDateAndGetData(gbDevice, currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
layout.startAnimation(animFadeRight);
show_hide_gpx_menu();
} else {
layout.startAnimation(animBounceRight);
}
@ -152,8 +182,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
currentItem = newItem;
makeSummaryHeader(newItem);
makeSummaryContent(newItem);
activitySummariesChartFragment.setDateAndGetData(gbDevice, currentItem.getStartTime().getTime()/1000, currentItem.getEndTime().getTime()/1000);
activitySummariesChartFragment.setDateAndGetData(gbDevice, currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
layout.startAnimation(animFadeLeft);
show_hide_gpx_menu();
} else {
layout.startAnimation(animBounceLeft);
}
@ -164,7 +195,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
activitySummariesChartFragment.setDateAndGetData(gbDevice, currentItem.getStartTime().getTime()/1000, currentItem.getEndTime().getTime()/1000);
activitySummariesChartFragment.setDateAndGetData(gbDevice, currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000);
}
@ -190,7 +221,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
String name = currentItem.getName();
input.setText((name != null) ? name : "");
FrameLayout container = new FrameLayout(ActivitySummaryDetail.this);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
input.setLayoutParams(params);
@ -224,22 +255,6 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
private void makeSummaryHeader(BaseActivitySummary item) {
//make view of data from main part of item
final String gpxTrack = item.getGpxTrack();
Button show_track_btn = findViewById(R.id.showTrack);
show_track_btn.setVisibility(View.GONE);
if (gpxTrack != null) {
show_track_btn.setVisibility(View.VISIBLE);
show_track_btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
try {
AndroidUtils.viewFile(gpxTrack, Intent.ACTION_VIEW, ActivitySummaryDetail.this);
} catch (IOException e) {
GB.toast(getApplicationContext(), "Unable to display GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
}
});
}
String activitykindname = ActivityKind.asString(item.getActivityKind(), getApplicationContext());
String activityname = item.getName();
Date starttime = item.getStartTime();
@ -292,7 +307,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
TableRow label_row = new TableRow(ActivitySummaryDetail.this);
TextView label_field = new TextView(ActivitySummaryDetail.this);
label_field.setTextSize(16);
label_field.setPadding(0,10,0,0);
label_field.setPadding(0, 10, 0, 0);
label_field.setTypeface(null, Typeface.BOLD);
label_field.setText(String.format("%s", getStringResourceByName(key)));
label_row.addView(label_field);
@ -361,14 +376,6 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
}
}
public static int getAlternateColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.alternate_row_background, typedValue, true);
return typedValue.data;
}
private String getStringResourceByName(String aString) {
String packageName = getPackageName();
int resId = getResources().getIdentifier(aString, "string", packageName);
@ -387,9 +394,82 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
// back button
finish();
return true;
case R.id.activity_action_take_screenshot:
take_share_screenshot(ActivitySummaryDetail.this);
return true;
case R.id.activity_action_share_gpx:
share_gpx_track(ActivitySummaryDetail.this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void take_share_screenshot(Context context) {
final ScrollView layout = findViewById(R.id.activity_summary_detail_scroll_layout);
int width = layout.getChildAt(0).getHeight();
int height = layout.getChildAt(0).getWidth();
Bitmap screenShot = getScreenShot(layout, width, height, context);
String fileName = FileUtils.makeValidFileName("Screenshot-" + ActivityKind.asString(currentItem.getActivityKind(), context).toLowerCase() + "-" + DateTimeUtils.formatIso8601(currentItem.getStartTime()) + ".png");
try {
File targetFile = new File(FileUtils.getExternalFilesDir(), fileName);
FileOutputStream fOut = new FileOutputStream(targetFile);
screenShot.compress(Bitmap.CompressFormat.PNG, 85, fOut);
fOut.flush();
fOut.close();
shareScreenshot(targetFile, context);
GB.toast(getApplicationContext(), "Screenshot saved", Toast.LENGTH_LONG, GB.INFO);
} catch (IOException e) {
e.printStackTrace();
}
}
private void shareScreenshot(File targetFile, Context context) {
Uri contentUri = FileProvider.getUriForFile(context,
context.getApplicationContext().getPackageName() + ".screenshot_provider", targetFile);
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("image/*");
String shareBody = "Sports Activity";
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Sports Activity");
sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
sharingIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
try {
startActivity(Intent.createChooser(sharingIntent, "Share via"));
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.activity_error_no_app_for_png, Toast.LENGTH_LONG).show();
}
}
private void share_gpx_track(Context context) {
final String gpxTrack = currentItem.getGpxTrack();
if (gpxTrack != null) {
try {
AndroidUtils.viewFile(gpxTrack, Intent.ACTION_VIEW, context);
} catch (IOException e) {
GB.toast(getApplicationContext(), "Unable to display GPX track: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
} else {
GB.toast(getApplicationContext(), "No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO);
}
}
private void show_hide_gpx_menu() {
final String gpxTrack = currentItem.getGpxTrack();
if (gpxTrack == null) {
mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_share_gpx).setVisible(false);
} else {
mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_share_gpx).setVisible(true);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
mOptionsMenu = menu;
getMenuInflater().inflate(R.menu.activity_take_screenshot_menu, menu);
show_hide_gpx_menu();
return true;
}
}

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, Lem Dulfo
Gobbetti, Lem Dulfo, Taavi Eomäe
This file is part of Gadgetbridge.
@ -31,14 +31,15 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.app.NavUtils;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.NavUtils;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter;
@ -64,16 +65,17 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal
private InstallHandler installHandler;
private boolean mayConnect;
private ProgressBar mProgressBar;
private ProgressBar progressBar;
private TextView progressText;
private ListView itemListView;
private final List<ItemWithDetails> mItems = new ArrayList<>();
private ItemWithDetailsAdapter mItemAdapter;
private final List<ItemWithDetails> items = new ArrayList<>();
private ItemWithDetailsAdapter itemAdapter;
private ListView detailsListView;
private ItemWithDetailsAdapter mDetailsItemAdapter;
private ArrayList<ItemWithDetails> mDetails = new ArrayList<>();
private ItemWithDetailsAdapter detailsAdapter;
private ArrayList<ItemWithDetails> 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();
}
}

View File

@ -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<StepAnalysis.StepSession> {
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<StepAnalysis.StepSession> 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<StepAnalysis.StepSession> create_empty_record() {
//have an "Unknown Activity" in the list in case there are no active sessions
List<StepAnalysis.StepSession> 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;
}
}

View File

@ -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);

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<StepSession> calculateStepSessions(List<? extends ActivitySample> samples) {
List<StepSession> 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;
}
}
}

View File

@ -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";
}

View File

@ -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);

View File

@ -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");
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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,
};
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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
* <p>
* 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<LefunActivitySample> {
public LefunSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<LefunActivitySample, ?> 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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");
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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) {
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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
* <p>
* 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
* <p>
* This assumes you have:
* The MAC of the device, the challenge and response payload
* <p>
* 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<BigInteger, BigInteger> 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,
* <p>
* 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];
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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");
}

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>. */
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};
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<SonySWR12Sample> {
public SonySWR12SampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<SonySWR12Sample, ?> 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();
}
}

View File

@ -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);

View File

@ -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

View File

@ -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<GenericItem>() {
@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());
}
}

View File

@ -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() {

View File

@ -17,20 +17,26 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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() {

View File

@ -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");
}
}

View File

@ -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);

View File

@ -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");

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<NotificationCommand> 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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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";
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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();
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<LefunDeviceSupport> {
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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() {

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<NutSupport> deviceInfoProfile;
private final BatteryInfoProfile<NutSupport> 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.
* <p>
* Don't write characteristics until authenticated.
* <p>
* 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
* <p>
* However, <b>it is irreversible</b>,
* if you can't generate the right packets,
* the device is basically bricked!
* <p>
* 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<BigInteger, BigInteger> 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());
}
}

View File

@ -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<PineTimeJFSupport> deviceInfoProfile;
/**
* These are used to keep track when long strings haven't changed,
* thus avoiding unnecessary transfers that are (potentially) very slow.
* <p>
* 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
* <p>
* 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<SonySWR12DeviceSupport> 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<BandAlarm> 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) {
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<ActivityBase> 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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

Some files were not shown because too many files have changed in this diff Show More