diff --git a/CHANGELOG.md b/CHANGELOG.md index 65a2286b9..958a74253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,51 @@ ### Changelog -#### Version 0.28.0 (WIP) -* Inital support for ZeTime: time,weather and activity data sync, notification support and music playback control is working +#### Version 0.29.1 +* Mi Band 3: Support setting language to to German, Italian, French, Polish, Japanese, Korean (read wiki) +* Mi Band 3: Support flashing latest RES files +* Mi Band 3: Fix notification text not being displayed +* Mi Band 3/Cor/Bip: Display app name when no app specific icon is available +* Teclast: add/improve H1 and H3 watch recognition +* Support transliteration for Lithuanian and Bengali +* Fix BLE reconnect issues in certain conditions +* Various fixes for display issues on small screens +* Fix some potential NPEs +* WIP: Display start and end of sleep in statistics + +#### Version 0.29.0 +* New Device: Initial support for ID115 +* New Device: Initial support for Lenovo Watch9 +* Show splash screen during startup +* Vertically align device icon in main activity +* Try to support the google clock application (untested) +* Amazfit Cor: Allow to configure displayed menu items +* Amazfit Cor: Support basic music control +* Amazfit Cor: Fix flashing font files +* Amazfit Bip: improved GPX export +* Amazfit Bip: Fix exported GPX file names for *FAT storage +* Amazfit Bip: Fix current weather not being displayed with later firmwares +* Amazfit Bip/Cor: Try to fix device being sometimes stuck in connecting state +* Mi Band 2: Put some device specific settings into its own settings category +* Mi Band 3: Support disabling of on-device menu items +* Mi Band 3: Support locking the Mi Band sceen (swipe up to unlock) +* Mi Band 2/3: New icon +* NO1 F1: Set time during initialization + +#### Version 0.28.1 +* Fix wrong weather icon mapping in rare cases +* Fix device discovery on Android 4.4 +* Amazfit Bip: Use UTC in gpx tracks for better compatibility with external software +* Amazfit Bip: Add the (localized) activity type to the gpx filename +* Amazfit Bip: Fix weather on latest firmwares + +#### Version 0.28.0 +* Initial support for ZeTime: time, weather and activity data sync, notification support and music playback control is working * Amazfit Bip/Cor: Rework firmware detection to cope with new version scheme +* Amazfit Bip: Support setting language to Russian +* Amazfit Cor: Support language switching on newer firmwares * Mi Band 3: support setting language (english and spanish tested) +* Mi Band 3: Fix pairing +* Mi Band 3: Send AQI to enable display of current temperature #### Version 0.27.1 * Pebble: Change appstore search to point to RomanPort's pebble appstore diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index d43487b68..afe301ebc 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -26,17 +26,21 @@ * Carsten Pfeiffer * Daniele Gobbetti * João Paulo Barraca -* Jonas * Yaron Shahrabani +* Jonas * postsorino -* protomors +* Sebastian Kranz +* Vadim Kaushan * Allan Nordhøy +* protomors +* José Rebelo +* TaaviE * mueller-ma * ivanovlev * naofum * youzhiran <2668760098@qq.com> * Tijl Schepens -* TaaviE +* mesnevi * Julien Pivotto * Taavi Eomäe * Steffen Liebergeld @@ -45,52 +49,63 @@ * Felix Konstantin Maurer * Sergey Trofimov * Robert Barat -* José Rebelo +* Pavel Elagin * JohnnySun * Uwe Hermann +* Kranz * Edoardo Rosa * Alberto * Vladislav Serkov * Vebryn * Gilles Émilien MOREL * Gergely Peidl +* Emre * Bożydar * 0nse <0nse@users.noreply.github.com> * Максим Якимчук * Rimas Raguliūnas +* nautilusx * masakoodaa +* Marius Cornescu * Lukas Veneziano * Kompact +* K0L0B0G * Jasper * Christian Fischer * c4ndel4 * 6arms1leg * Zhong Jianxin * walkjivefly +* Thomas * Ted Stein +* petronovak +* Pascal * NotAFIle * Normano64 * NicoBuntu -* nautilusx * Minori Hiraoka (미노리) * Michal Novotny -* mesnevi +* Martin * LL * Jesús * exit-failure +* Denis * Avamander * AnthonyDiGirolamo * Andreas Kromke * Ⲇⲁⲛⲓ Φi +* Your Name * Yar * xzovy * xphnx * Vitaliy Shuruta +* Vincèn PUJOL * Tomer Rosenfeld * Tomas Radej * tiparega <11555126+tiparega@users.noreply.github.com> * Tarik Sekmen * Szymon Tomasz Stefanek +* szilardx <15869670+szilardx@users.noreply.github.com> * Sergio Lopez * Sami Alaoui <4ndroidgeek@gmail.com> * Roman Plevka @@ -98,47 +113,61 @@ * redking * Quallenauge * Pavel Motyrev -* Pascal * Olexandr Nesterenko * Nicolò Balzarotti * Natanael Arndt +* Molnár Barnabás * Moarc +* Mike van Rossum * Michal Novak * michaelneu * McSym28 * MaxL -* Martin +* maxirnilian * Martin Piatka +* Margreet * Marc Schlaich +* Marcel pl (m4rcel) * Manuel Soler * Luiz Felipe das Neves Lopes * Leonardo Amaral * lazarosfs * ladbsoft <30509719+ladbsoft@users.noreply.github.com> * Kristjan Räts +* Konrad Iturbe * kevlarcade * Kevin Richter * Kaz Wolfe * Kasha +* kalaee * Joseph Kim +* jonnsoft <> * Jan Lolek * Jakub Jelínek * Ivan * Hasan Ammar +* Grzegorz Dznsk * Gilles MOREL * Gideão Gomes Ferreira * Gabe Schrecker * freezed-or-frozen * Frank Slezak +* Dreamwalker +* Dougal19 <4662351+Dougal19@users.noreply.github.com> * Davis Mosenkovs * Daniel Hauck +* dakhnod * criogenic +* clach04 * Chris Perelstein * chabotsi * Carlos Ferreira * bucala +* boun * batataspt@gmail.com * atkyritsis +* Aniruddha Adhikary +* andrewlytvyn * AndrewH <36428679+andrewheadricke@users.noreply.github.com> * andre * Allen B <28495335+Allen-B1@users.noreply.github.com> diff --git a/FEATURES.md b/FEATURES.md index 4ed6a00e5..98e1da537 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,27 +1,28 @@ ## Feature Matrix -| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Amazfit Bip | -|-----------------------------------| ----------|---------------|---------|-----------|-------------| -|Calls Notification | YES | YES | YES | YES | YES | -|Reject Calls | YES | YES | NO | NO | YES | -|Accept Calls | NO(2) | NO(2) | NO | NO | NO | -|Generic Notification | YES | YES | YES | YES | YES | -|Dismiss Notifications on Phone | YES | YES | NO | NO | NO | -|Predefined Replies | YES | YES | NO | NO | NO | -|Voice Replies | N/A | NO(3) | N/A | N/A | N/A | -|Calendar Sync | YES | YES | NO | NO | NO | -|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES | -|Smart alarms | NO(1) | YES | YES | NO | NO | -|Weather | NO(1) | YES | NO | NO | YES | -|Activity Tracking | NO(1) | YES | YES | YES | YES | -|Sleep Tracking | NO(1) | YES | YES | YES | YES | -|HR Tracking | N/A | YES | YES | YES | YES | -|Realtime Activity Tracking | NO | NO | YES | YES | YES | -|Music Control | YES | YES | NO | NO | NO | -|Watchapp/face Installation | YES | YES | NO | NO | YES | -|Firmware Installation | YES | YES | YES | YES | YES | -|Taking Screenshots | YES | YES | NO | NO | NO | -|Support Android Companion Apps | YES | YES | NO | NO | NO | +| | Pebble OG | Pebble Time/2 | Mi Band | Mi Band 2 | Mi Band 3 | Amazfit Bip | Amazfit Cor | +|-----------------------------------| ----------|---------------|---------|-----------|-----------|-------------|-------------| +|Calls Notification | YES | YES | YES | YES | YES | YES | YES | +|Reject Calls | YES | YES | NO | NO | YES | YES | YES | +|Accept Calls | NO(2) | NO(2) | NO | NO | NO | NO | NO | +|Generic Notification | YES | YES | YES | YES | YES | YES | YES | +|Dismiss Notifications on Phone | YES | YES | NO | NO | NO | NO | NO | +|Predefined Replies | YES | YES | NO | NO | NO | NO | NO | +|Voice Replies | N/A | NO(3) | N/A | N/A | N/A | N/A | N/A | +|Calendar Sync | YES | YES | NO | NO | NO | NO(3) | NO | +|Configure alarms from Gadgetbridge | NO | NO | YES | YES | YES | YES | YES | +|Smart alarms | NO(1) | YES | YES | NO | NO | NO | NO | +|Weather | NO(1) | YES | NO | NO | YES | YES | YES | +|Activity Tracking | NO(1) | YES | YES | YES | YES | YES | YES | +|GPS tracks import | NO | NO | NO | NO | NO | YES | NO | +|Sleep Tracking | NO(1) | YES | YES | YES | YES | YES | YES | +|HR Tracking | N/A | YES | YES | YES | YES | YES | YES | +|Realtime Activity Tracking | NO | NO | YES | YES | YES | YES | YES | +|Music Control | YES | YES | NO | NO | NO | NO | YES | +|Watchapp/face Installation | YES | YES | NO | NO | NO | YES | YES | +|Firmware Installation | YES | YES | YES | YES | YES | YES | YES | +|Taking Screenshots | YES | YES | NO | NO | NO | NO | NO | +|Support Android Companion Apps | YES | YES | NO | NO | NO | NO | NO | (1) Possible via 3rd Party Watchapp (2) Theoretically possible (works on iOS, would need lot of work) diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 8aed0d76e..4ef0d8a1b 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -70,6 +70,7 @@ public class GBDaoGenerator { addNo1F1ActivitySample(schema, user, device); addXWatchActivitySample(schema, user, device); addZeTimeActivitySample(schema, user, device); + addID115ActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -301,6 +302,18 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addID115ActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "ID115ActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty("caloriesBurnt"); + activitySample.addIntProperty("distanceMeters"); + activitySample.addIntProperty("activeTimeMinutes"); + return activitySample; + } + private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) { activitySample.setSuperclass(superClass); activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider"); diff --git a/Get_it_on_F-Droid.svg.png b/Get_it_on_F-Droid.svg.png deleted file mode 100644 index 75d6992e8..000000000 Binary files a/Get_it_on_F-Droid.svg.png and /dev/null differ diff --git a/README.md b/README.md index 7648d0cfc..ca18a09b2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Gadgetbridge ============ Gadgetbridge is an Android (4.4+) application which will allow you to use your -Pebble, Mi Band, Amazfit Bit and HPlus device (and more) without the vendor's closed source application +Pebble, Mi Band, Amazfit Bip and HPlus device (and more) without the vendor's closed source application and without the need to create an account and transmit any of your data to the vendor's servers. @@ -13,27 +13,32 @@ vendor's servers. [![Donate](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/Gadgetbridge/donate) + [![Build](https://travis-ci.org/Freeyourgadget/Gadgetbridge.svg?branch=master)](https://travis-ci.org/Freeyourgadget/Gadgetbridge) ## Download -[![Gadgetbridge on F-Droid](/Get_it_on_F-Droid.svg.png?raw=true "Download from F-Droid")](https://f-droid.org/repository/browse/?fdid=nodomain.freeyourgadget.gadgetbridge) +[Get it on F-Droid](https://f-droid.org/app/nodomain.freeyourgadget.gadgetbridge) [List of changes](https://github.com/Freeyourgadget/Gadgetbridge/blob/master/CHANGELOG.md) ## Supported Devices -* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble) -* Pebble 2 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble) -* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band) -* Mi Band 2 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2) * Amazfit Bip [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip) * Amazfit Cor (no maintainer) [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor) * HPlus Devices (e.g. ZeBand) [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus) -* Teclast H10, H30 (WIP) +* ID115 (WIP) +* Lenovo Watch 9 (WIP) +* Liveview (WIP) +* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band) +* Mi Band 2 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2) +* Mi Band 3 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3) * NO.1 F1 (WIP) -* Liveview -* Vibratissimo (experimental) +* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble) +* Pebble 2 [Wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble) +* Teclast H10, H30 (WIP) * XWatch (Affordable Chinese Casio-like smartwatches) +* Vibratissimo (experimental) +* ZeTime (WIP) ## Features @@ -92,6 +97,10 @@ For more information read [this wiki article](https://github.com/Freeyourgadget/ * João Paulo Barraca (HPlus) * Vitaly Svyastyn (NO.1 F1) * Sami Alaoui (Teclast H30) +* "ladbsoft" (XWatch) +* Sebastian Kranz (ZeTime) +* Vadim Kaushan (ID115) +* "maxirnilian" (Lenovo Watch 9) ## Contribute diff --git a/app/build.gradle b/app/build.gradle index 13bb7b231..0392369ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,8 +25,8 @@ android { targetSdkVersion 27 // Note: always bump BOTH versionCode and versionName! - versionName "0.28.0" - versionCode 134 + versionName "0.29.1" + versionCode 137 vectorDrawables.useSupportLibrary = true } buildTypes { @@ -85,7 +85,7 @@ dependencies { implementation "org.greenrobot:greendao:2.2.1" implementation "org.apache.commons:commons-lang3:3.5" implementation "org.cyanogenmod:platform.sdk:6.0" - + implementation 'com.jaredrummler:colorpicker:1.0.2' // implementation project(":DaoCore") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bf3ae12fb..c9ecb863a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ --> + @@ -45,7 +46,7 @@ + android:theme="@style/SplashTheme"> @@ -387,6 +388,12 @@ + + . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import android.app.AlertDialog; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.NavUtils; import android.support.v4.app.NotificationCompat; @@ -39,6 +43,7 @@ import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.util.ArrayList; import java.util.Objects; @@ -53,9 +58,9 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; import nodomain.freeyourgadget.gadgetbridge.util.GB; +import static android.content.Intent.EXTRA_SUBJECT; import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID; - public class DebugActivity extends AbstractGBActivity { private static final Logger LOG = LoggerFactory.getLogger(DebugActivity.class); @@ -235,12 +240,58 @@ public class DebugActivity extends AbstractGBActivity { testNewFunctionality(); } }); + + Button shareLogButton = findViewById(R.id.shareLog); + shareLogButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showWarning(); + } + }); + } + + private void showWarning() { + new AlertDialog.Builder(this) + .setCancelable(true) + .setTitle(R.string.warning) + .setMessage(R.string.share_log_warning) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String fileName = GBApplication.getLogPath(); + if (fileName != null && fileName.length() > 0) { + Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); + emailIntent.setType("*/*"); + emailIntent.putExtra(EXTRA_SUBJECT, "Gadgetbridge log file"); + emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(fileName))); + startActivity(Intent.createChooser(emailIntent, "Share File")); + } + } + }) + .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // do nothing + } + }) + .show(); } private void testNewFunctionality() { GBApplication.deviceService().onTestNewFunction(); } + private void shareLog() { + String fileName = GBApplication.getLogPath(); + if(fileName != null && fileName.length() > 0) { + Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND); + emailIntent.setType("*/*"); + emailIntent.putExtra(EXTRA_SUBJECT, "Gadgetbridge log file"); + emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(fileName))); + startActivity(Intent.createChooser(emailIntent, "Share File")); + } + } + private void testNotification() { Intent notificationIntent = new Intent(getApplicationContext(), DebugActivity.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java index b53f5fd98..26d48dbbe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java @@ -1,5 +1,5 @@ -/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, JohnnySun, Lem Dulfo, Taavi Eomäe, Uwe Hermann +/* Copyright (C) 2015-2018 Andreas Shimokawa, boun, Carsten Pfeiffer, + Daniele Gobbetti, JohnnySun, jonnsoft, Lem Dulfo, Taavi Eomäe, Uwe Hermann This file is part of Gadgetbridge. @@ -231,9 +231,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView public void logMessageContent(byte[] value) { if (value != null) { - for (byte b : value) { - LOG.warn("DATA: " + String.format("0x%2x", b) + " - " + (char) (b & 0xff)); - } + LOG.warn("DATA: " + GB.hexdump(value, 0, value.length)); } } @@ -630,7 +628,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView super.onPause(); stopBTDiscovery(); stopBTLEDiscovery(); - if (GB.supportsBluetoothLE()) { + if (GBApplication.isRunningLollipopOrLater()) { stopNewBTLEDiscovery(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java index 5ab284a7d..a433c1ccf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FwAppInstallerActivity.java @@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.adapter.ItemWithDetailsAdapter; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; @@ -191,7 +192,7 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal } private InstallHandler findInstallHandlerFor(Uri uri) { - for (DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) { + for (DeviceCoordinator coordinator : getAllCoordinatorsConnectedFirst()) { InstallHandler handler = coordinator.findInstallHandler(uri, this); if (handler != null) { return handler; @@ -200,6 +201,29 @@ public class FwAppInstallerActivity extends AbstractGBActivity implements Instal return null; } + private List getAllCoordinatorsConnectedFirst() { + DeviceManager deviceManager = ((GBApplication) getApplicationContext()).getDeviceManager(); + List connectedCoordinators = new ArrayList<>(); + List allCoordinators = DeviceHelper.getInstance().getAllCoordinators(); + List sortedCoordinators = new ArrayList<>(allCoordinators.size()); + + GBDevice connectedDevice = deviceManager.getSelectedDevice(); + if (connectedDevice != null && connectedDevice.isConnected()) { + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(connectedDevice); + if (coordinator != null) { + connectedCoordinators.add(coordinator); + } + } + + sortedCoordinators.addAll(connectedCoordinators); + for (DeviceCoordinator coordinator : allCoordinators) { + if (!connectedCoordinators.contains(coordinator)) { + sortedCoordinators.add(coordinator); + } + } + return sortedCoordinators; + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java index a2f3b0734..b4aaf949f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/HeartRateUtils.java @@ -16,9 +16,14 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + public class HeartRateUtils { public static final int MAX_HEART_RATE_VALUE = 250; - public static final int MIN_HEART_RATE_VALUE = 0; + public static final int MIN_HEART_RATE_VALUE = 10; + /** * The maxiumum gap between two hr measurements in which * we interpolate between the measurements. Otherwise, two @@ -28,7 +33,37 @@ public class HeartRateUtils { */ public static final int MAX_HR_MEASUREMENTS_GAP_MINUTES = 10; - public static boolean isValidHeartRateValue(int value) { - return value > HeartRateUtils.MIN_HEART_RATE_VALUE && value < HeartRateUtils.MAX_HEART_RATE_VALUE; + private int maxHeartRateValue; + private int minHeartRateValue; + + private static final HeartRateUtils instance = new HeartRateUtils(); + + public static HeartRateUtils getInstance() { + return instance; + } + + /** + * Singleton - to access this class use the static #getInstance() + */ + private HeartRateUtils() { + updateCachedHeartRatePreferences(); + } + + public void updateCachedHeartRatePreferences(){ + Prefs prefs = GBApplication.getPrefs(); + maxHeartRateValue = prefs.getInt(GBPrefs.CHART_MAX_HEART_RATE, MAX_HEART_RATE_VALUE); + minHeartRateValue = prefs.getInt(GBPrefs.CHART_MIN_HEART_RATE, MIN_HEART_RATE_VALUE); + } + + public int getMaxHeartRate(){ + return maxHeartRateValue; + } + + public int getMinHeartRate(){ + return minHeartRateValue; + } + + public boolean isValidHeartRateValue(int value) { + return value >= getMinHeartRate() && value <= getMaxHeartRate(); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index dd383d033..078c317ea 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java @@ -1,5 +1,6 @@ /* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer, - Daniele Gobbetti, Felix Konstantin Maurer, Normano64 + Daniele Gobbetti, Felix Konstantin Maurer, José Rebelo, Martin, Normano64, + Pavel Elagin This file is part of Gadgetbridge. @@ -52,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; @@ -60,7 +62,15 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DATEFORMAT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DISPLAY_ITEMS; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ENABLE_TEXT_NOTIFICATIONS; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_BAND_SCREEN_UNLOCK; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_END; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_OFF; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_SCHEDULED; +import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI3_NIGHT_MODE_START; import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_HEIGHT_CM; import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_SLEEP_DURATION; import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_STEPS_GOAL; @@ -84,6 +94,8 @@ public class SettingsActivity extends AbstractSettingsActivity { protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); + Prefs prefs = GBApplication.getPrefs(); + Preference pref = findPreference("notifications_generic"); pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { public boolean onPreferenceClick(Preference preference) { @@ -341,7 +353,7 @@ public class SettingsActivity extends AbstractSettingsActivity { int autoFetchInterval = GBApplication.getPrefs().getInt("auto_fetch_interval_limit", 0); summary = String.format( getApplicationContext().getString(R.string.pref_auto_fetch_limit_fetches_summary), - (int) autoFetchInterval); + autoFetchInterval); pref.setSummary(summary); @@ -360,6 +372,130 @@ public class SettingsActivity extends AbstractSettingsActivity { } }); + final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT); + setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI2_DATEFORMAT); + } + }); + return true; + } + }); + + final Preference miBand2DisplayItems = findPreference(PREF_MI2_DISPLAY_ITEMS); + miBand2DisplayItems.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS); + } + }); + return true; + } + }); + + final Preference miBand3ScreenUnlock = findPreference(PREF_MI3_BAND_SCREEN_UNLOCK); + miBand3ScreenUnlock.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI3_BAND_SCREEN_UNLOCK); + } + }); + return true; + } + }); + + final Preference miBand3DisplayItems = findPreference("miband3_display_items"); + miBand3DisplayItems.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS); + } + }); + return true; + } + }); + + String nightModeState = prefs.getString(MiBandConst.PREF_MI3_NIGHT_MODE, PREF_MI3_NIGHT_MODE_OFF); + boolean nightModeScheduled = nightModeState.equals(PREF_MI3_NIGHT_MODE_SCHEDULED); + + final Preference nightModeStart = findPreference(PREF_MI3_NIGHT_MODE_START); + nightModeStart.setEnabled(nightModeScheduled); + nightModeStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI3_NIGHT_MODE_START); + } + }); + return true; + } + }); + + + final Preference nightModeEnd = findPreference(PREF_MI3_NIGHT_MODE_END); + nightModeEnd.setEnabled(nightModeScheduled); + nightModeEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI3_NIGHT_MODE_END); + } + }); + return true; + } + }); + + + final Preference nightMode = findPreference(PREF_MI3_NIGHT_MODE); + nightMode.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + final boolean scheduled = PREF_MI3_NIGHT_MODE_SCHEDULED.equals(newVal.toString()); + + nightModeStart.setEnabled(scheduled); + nightModeEnd.setEnabled(scheduled); + + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI3_NIGHT_MODE); + } + }); + return true; + } + }); + + final Preference corDisplayItems = findPreference("cor_display_items"); + corDisplayItems.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newVal) { + invokeLater(new Runnable() { + @Override + public void run() { + GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS); + } + }); + return true; + } + }); + // Get all receivers of Media Buttons Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); @@ -488,6 +624,7 @@ public class SettingsActivity extends AbstractSettingsActivity { PREF_USER_WEIGHT_KG, PREF_USER_SLEEP_DURATION, PREF_USER_STEPS_GOAL, + PREF_MI2_ENABLE_TEXT_NOTIFICATIONS, "weather_city", }; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java index 110c9895e..8b5ce649f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/appmanager/AbstractAppManagerFragment.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Lem Dulfo + Gobbetti, Konrad Iturbe, Lem Dulfo This file is part of Gadgetbridge. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index e9fa8f020..b1ae9cc48 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -70,8 +70,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; -import static nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils.isValidHeartRateValue; - /** * A base class fragment to be used with ChartsActivity. The fragment can supply * a title to be displayed in the activity by returning non-null in #getTitle() @@ -443,6 +441,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { List heartrateEntries = hr ? new ArrayList(numEntries) : null; List colors = new ArrayList<>(numEntries); // this is kinda inefficient... int lastHrSampleIndex = -1; + HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); for (int i = 0; i < numEntries; i++) { ActivitySample sample = samples.get(i); @@ -512,7 +511,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment { } activityEntries.add(createLineEntry(value, ts)); } - if (hr && sample.getKind() != ActivityKind.TYPE_NOT_WORN && HeartRateUtils.isValidHeartRateValue(sample.getHeartRate())) { + if (hr && sample.getKind() != ActivityKind.TYPE_NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); heartrateEntries.add(createLineEntry(0, ts - 1)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java index ea53a18b2..f9e3e654c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityAnalysis.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Vebryn + Gobbetti, Pavel Elagin, Vebryn This file is part of Gadgetbridge. @@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import java.util.HashMap; import java.util.List; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -95,6 +94,9 @@ class ActivityAnalysis { } } + amount.setStartDate(sample.getTimestamp()); + amount.setEndDate(sample.getTimestamp()); + previousAmount = amount; previousSample = sample; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java index d12f03f48..c1579891d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivitySleepChartFragment.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, Pavel Elagin This file is part of Gadgetbridge. @@ -27,6 +27,7 @@ import android.view.ViewGroup; import com.github.mikephil.charting.animation.Easing; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.components.LegendEntry; import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; @@ -103,8 +104,8 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { yAxisRight.setDrawLabels(true); yAxisRight.setDrawTopYLabelEntry(true); yAxisRight.setTextColor(CHART_TEXT_COLOR); - yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE); - yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE); + yAxisRight.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate()); + yAxisRight.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate()); // refresh immediately instead of use refreshIfVisible(), for perceived performance refresh(); @@ -177,6 +178,8 @@ public class ActivitySleepChartFragment extends AbstractChartFragment { legendEntries.add(hrEntry); } chart.getLegend().setCustom(legendEntries); + chart.getLegend().setWordWrapEnabled(true); + chart.getLegend().setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java index 53c7563fd..2331a5a22 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java @@ -168,7 +168,7 @@ public class LiveActivityFragment extends AbstractChartFragment { private void addSample(ActivitySample sample) { int heartRate = sample.getHeartRate(); int timestamp = tsTranslation.shorten(sample.getTimestamp()); - if (HeartRateUtils.isValidHeartRateValue(heartRate)) { + if (HeartRateUtils.getInstance().isValidHeartRateValue(heartRate)) { setCurrentHeartRate(heartRate, timestamp); } int steps = sample.getSteps(); @@ -470,8 +470,8 @@ public class LiveActivityFragment extends AbstractChartFragment { yAxisRight.setDrawLabels(true); yAxisRight.setDrawTopYLabelEntry(false); yAxisRight.setTextColor(CHART_TEXT_COLOR); - yAxisRight.setAxisMaximum(HeartRateUtils.MAX_HEART_RATE_VALUE); - yAxisRight.setAxisMinimum(HeartRateUtils.MIN_HEART_RATE_VALUE); + yAxisRight.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate()); + yAxisRight.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate()); mHistorySet = new LineDataSet(new ArrayList(), getString(R.string.live_activity_steps_history)); mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java index 51672e5d8..9cd13a2d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SleepChartFragment.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer, - Daniele Gobbetti + Daniele Gobbetti, Pavel Elagin This file is part of Gadgetbridge. @@ -20,9 +20,11 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import com.github.mikephil.charting.animation.Easing; import com.github.mikephil.charting.charts.Chart; @@ -43,6 +45,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; @@ -62,6 +65,7 @@ public class SleepChartFragment extends AbstractChartFragment { private LineChart mActivityChart; private PieChart mSleepAmountChart; + private TextView mSleepchartInfo; private int mSmartAlarmFrom = -1; private int mSmartAlarmTo = -1; @@ -86,9 +90,25 @@ public class SleepChartFragment extends AbstractChartFragment { List colors = new ArrayList<>(); // int index = 0; long totalSeconds = 0; + + Date startSleep = null; + Date endSleep = null; + for (ActivityAmount amount : amounts.getAmounts()) { if ((amount.getActivityKind() & ActivityKind.TYPE_SLEEP) != 0) { long value = amount.getTotalSeconds(); + if(startSleep == null){ + startSleep = amount.getStartDate(); + } else { + if(startSleep.after(amount.getStartDate())) + startSleep = amount.getStartDate(); + } + if(endSleep == null){ + endSleep = amount.getEndDate(); + } else { + if(endSleep.before(amount.getEndDate())) + endSleep = amount.getEndDate(); + } totalSeconds += value; // entries.add(new PieEntry(value, index++)); entries.add(new PieEntry(value, amount.getName(getActivity()))); @@ -112,7 +132,7 @@ public class SleepChartFragment extends AbstractChartFragment { data.setDataSet(set); //setupLegend(pieChart); - return new MySleepChartsData(totalSleep, data); + return new MySleepChartsData(totalSleep, data, startSleep, endSleep); } @Override @@ -124,6 +144,15 @@ public class SleepChartFragment extends AbstractChartFragment { mActivityChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 mActivityChart.getXAxis().setValueFormatter(mcd.getChartsData().getXValueFormatter()); mActivityChart.setData(mcd.getChartsData().getData()); + + if (mcd.getPieData().getStartSleep() != null && mcd.getPieData().getEndSleep() != null) { + mSleepchartInfo.setText(getContext().getString( + R.string.you_slept, + DateTimeUtils.timeToString(mcd.getPieData().getStartSleep()), + DateTimeUtils.timeToString(mcd.getPieData().getEndSleep()))); + } else { + mSleepchartInfo.setText(getContext().getString(R.string.you_did_not_sleep)); + } } @Override @@ -136,8 +165,9 @@ public class SleepChartFragment extends AbstractChartFragment { Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_sleepchart, container, false); - mActivityChart = (LineChart) rootView.findViewById(R.id.sleepchart); - mSleepAmountChart = (PieChart) rootView.findViewById(R.id.sleepchart_pie_light_deep); + mActivityChart = rootView.findViewById(R.id.sleepchart); + mSleepAmountChart = rootView.findViewById(R.id.sleepchart_pie_light_deep); + mSleepchartInfo = rootView.findViewById(R.id.sleepchart_info); setupActivityChart(); setupSleepAmountChart(); @@ -203,8 +233,8 @@ public class SleepChartFragment extends AbstractChartFragment { yAxisRight.setDrawLabels(true); yAxisRight.setDrawTopYLabelEntry(true); yAxisRight.setTextColor(CHART_TEXT_COLOR); - yAxisRight.setAxisMaxValue(HeartRateUtils.MAX_HEART_RATE_VALUE); - yAxisRight.setAxisMinValue(HeartRateUtils.MIN_HEART_RATE_VALUE); + yAxisRight.setAxisMaxValue(HeartRateUtils.getInstance().getMaxHeartRate()); + yAxisRight.setAxisMinValue(HeartRateUtils.getInstance().getMinHeartRate()); } @Override @@ -246,10 +276,14 @@ public class SleepChartFragment extends AbstractChartFragment { private static class MySleepChartsData extends ChartsData { private String totalSleep; private final PieData pieData; + private @Nullable Date startSleep; + private @Nullable Date endSleep; - public MySleepChartsData(String totalSleep, PieData pieData) { + public MySleepChartsData(String totalSleep, PieData pieData, @Nullable Date startSleep, @Nullable Date endSleep) { this.totalSleep = totalSleep; this.pieData = pieData; + this.startSleep = startSleep; + this.endSleep = endSleep; } public PieData getPieData() { @@ -259,6 +293,16 @@ public class SleepChartFragment extends AbstractChartFragment { public CharSequence getTotalSleep() { return totalSleep; } + + @Nullable + public Date getStartSleep() { + return startSleep; + } + + @Nullable + public Date getEndSleep() { + return endSleep; + } } private static class MyChartsData extends ChartsData { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java index 2ee3c0eb4..a3c44c250 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/WeekSleepChartFragment.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer +/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, Pavel Elagin This file is part of Gadgetbridge. @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.components.AxisBase; +import com.github.mikephil.charting.components.Legend; import com.github.mikephil.charting.components.LegendEntry; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.formatter.IAxisValueFormatter; @@ -132,5 +133,7 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment { chart.getLegend().setCustom(legendEntries); chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); + chart.getLegend().setWordWrapEnabled(true); + chart.getLegend().setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index 12087694e..e9b057f05 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Lem Dulfo + Gobbetti, José Rebelo, Lem Dulfo, maxirnilian This file is part of Gadgetbridge. @@ -22,16 +22,19 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.drawable.GradientDrawable; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; +import android.text.InputType; import android.transition.TransitionManager; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; @@ -40,7 +43,11 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; +import com.jaredrummler.android.colorpicker.ColorPickerDialog; +import com.jaredrummler.android.colorpicker.ColorPickerDialogListener; + import java.util.List; +import java.util.Locale; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -50,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; +import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9CalibrationActivity; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; @@ -108,9 +116,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter 108.0) { + new AlertDialog.Builder(context) + .setTitle(R.string.pref_invalid_frequency_title) + .setMessage(R.string.pref_invalid_frequency_message) + .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + } + }) + .show(); + } else { + device.setExtraInfo("fm_frequency", frequency); + fmFrequencyLabel.setText(String.format(Locale.getDefault(), "%.1f", (float) device.getExtraInfo("fm_frequency"))); + GBApplication.deviceService().onSetFmFrequency(frequency); + } + } + }); + builder.setNegativeButton(context.getResources().getString(R.string.Cancel), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + + builder.show(); + } + }); + + holder.ledColor.setVisibility(View.GONE); + if (device.isInitialized() && device.getExtraInfo("led_color") != null && coordinator.supportsLedColor()) { + holder.ledColor.setVisibility(View.VISIBLE); + final GradientDrawable ledColor = (GradientDrawable) holder.ledColor.getDrawable().mutate(); + ledColor.setColor((int) device.getExtraInfo("led_color")); + holder.ledColor.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ColorPickerDialog.Builder builder = ColorPickerDialog.newBuilder(); + builder.setDialogTitle(R.string.preferences_led_color); + + builder.setColor((int) device.getExtraInfo("led_color")); + if (coordinator.supportsRgbLedColor()) { + builder.setAllowCustom(true); + builder.setShowAlphaSlider(false); + builder.setAllowPresets(true); + } else { + builder.setAllowCustom(false); + builder.setAllowPresets(true); + builder.setShowColorShades(false); + builder.setPresets(coordinator.getColorPresets()); + } + + ColorPickerDialog dialog = builder.create(); + dialog.setColorPickerDialogListener(new ColorPickerDialogListener() { + @Override + public void onColorSelected(int dialogId, int color) { + ledColor.setColor(color); + device.setExtraInfo("led_color", color); + GBApplication.deviceService().onSetLedColor(color); + } + + @Override + public void onDialogDismissed(int dialogId) { + // Nothing to do + } + }); + dialog.show(((Activity) context).getFragmentManager(), "color-picker-dialog"); + } + }); + } + //remove device, hidden under details holder.removeDevice.setOnClickListener(new View.OnClickListener() @@ -356,6 +473,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter. */ +package nodomain.freeyourgadget.gadgetbridge.deviceevents; + +public class GBDeviceEventFmFrequency extends GBDeviceEvent { + public float frequency; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java new file mode 100644 index 000000000..a3c7ca807 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventLEDColor.java @@ -0,0 +1,21 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.deviceevents; + +public class GBDeviceEventLEDColor extends GBDeviceEvent { + public int color; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java index aa5a8f2fd..53d940b77 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventVersionInfo.java @@ -22,4 +22,9 @@ import nodomain.freeyourgadget.gadgetbridge.R; public class GBDeviceEventVersionInfo extends GBDeviceEvent { public String fwVersion = GBApplication.getContext().getString(R.string.n_a); public String hwVersion = GBApplication.getContext().getString(R.string.n_a); + + @Override + public String toString() { + return super.toString() + "fwVersion: " + fwVersion + "; hwVersion: " + hwVersion; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 6acbe6e4c..d6b6795f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2015-2018 Carsten Pfeiffer, Daniele Gobbetti +/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti This file is part of Gadgetbridge. @@ -129,4 +130,23 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { public boolean supportsActivityTracks() { return false; } + + @Override + public boolean supportsMusicInfo() { + return false; + } + + public boolean supportsLedColor() { + return false; + } + + @Override + public boolean supportsRgbLedColor() { + return false; + } + + @Override + public int[] getColorPresets() { + return new int[0]; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 45d4150eb..5364526eb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, JohnnySun, Uwe Hermann + Gobbetti, JohnnySun, José Rebelo, Uwe Hermann This file is part of Gadgetbridge. @@ -242,4 +242,33 @@ public interface DeviceCoordinator { * forecast display. */ boolean supportsWeather(); + + /** + * Indicates whether the device supports being found by vibrating, + * making some sound or lighting up + */ + boolean supportsFindDevice(); + + /** + * Indicates whether the device supports displaying music information + * like artist, title, album, play state etc. + */ + boolean supportsMusicInfo(); + + /** + * Indicates whether the device has an led which supports custom colors + */ + boolean supportsLedColor(); + + /** + * Indicates whether the device's led supports any RGB color, + * or only preset colors + */ + boolean supportsRgbLedColor(); + + /** + * Returns the preset colors supported by the device, if any, in ARGB, with alpha = 255 + */ + @NonNull + int[] getColorPresets(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java index 4acf58aea..5e1fad92a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/EventHandler.java @@ -99,4 +99,12 @@ public interface EventHandler { void onTestNewFunction(); void onSendWeather(WeatherSpec weatherSpec); + + void onSetFmFrequency(float frequency); + + /** + * Set the device's led color. + * @param color the new color, in ARGB, with alpha = 255 + */ + void onSetLedColor(int color); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index e35417449..6a030fa4a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, José Rebelo This file is part of Gadgetbridge. @@ -181,4 +181,24 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { public boolean supportsWeather() { return false; } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public boolean supportsLedColor() { + return false; + } + + @Override + public boolean supportsRgbLedColor() { + return false; + } + + @Override + public int[] getColorPresets() { + return new int[0]; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java index 44499980b..e4f8012e8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java @@ -1,5 +1,5 @@ -/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, João - Paulo Barraca +/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, João Paulo Barraca, José Rebelo This file is part of Gadgetbridge. @@ -103,6 +103,11 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsFindDevice() { + return true; + } + @Override public DeviceType getDeviceType() { return DeviceType.HPLUS; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java index 51eac0345..cd2bc1fe2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiCoordinator.java @@ -263,4 +263,9 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator { public boolean supportsSmartWakeup(GBDevice device) { return false; } + + @Override + public boolean supportsFindDevice() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java similarity index 97% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java index b52a7b736..b522560fc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.devices.miband; +package nodomain.freeyourgadget.gadgetbridge.devices.huami; import java.util.HashMap; import java.util.Map; @@ -23,7 +23,7 @@ import java.util.UUID; import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID; -public class MiBand2Service { +public class HuamiService { public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0")); @@ -50,6 +50,8 @@ public class MiBand2Service { public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_DEVICEEVENT = UUID.fromString("00000010-0000-3512-2118-0009af100700"); + public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER = UUID.fromString("00000020-0000-3512-2118-0009af100700"); + public static final int ALERT_LEVEL_NONE = 0; public static final int ALERT_LEVEL_MESSAGE = 1; public static final int ALERT_LEVEL_PHONE_CALL = 2; @@ -101,7 +103,7 @@ public class MiBand2Service { /** * In some logs it's 0x0... */ - public static final byte AUTH_BYTE = 0x8; + public static final byte AUTH_BYTE = 0x08; // maybe not really activity data, but steps? public static final byte COMMAND_FETCH_DATA = 0x02; @@ -146,6 +148,7 @@ public class MiBand2Service { public static final byte[] DISPLAY_YYY = new byte[] {ENDPOINT_DISPLAY, 0x10, 0x0, 0x1, 0x1 }; public static final byte[] COMMAND_DISTANCE_UNIT_METRIC = new byte[] { ENDPOINT_DISPLAY, 0x03, 0x00, 0x00 }; public static final byte[] COMMAND_DISTANCE_UNIT_IMPERIAL = new byte[] { ENDPOINT_DISPLAY, 0x03, 0x00, 0x01 }; + public static final byte[] COMMAND_SET_LANGUAGE_NEW_TEMPLATE = new byte[]{ENDPOINT_DISPLAY, 0x17, 0x00, 0, 0, 0, 0, 0}; // The third byte controls the threshold, in minutes // The last 8 bytes represent 2 separate time intervals for the inactivity warnings diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java index 36d0ce8ab..bf1406415 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipService.java @@ -19,9 +19,9 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip; import java.util.UUID; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service.DISPLAY_ITEM_BIT_CLOCK; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service.ENDPOINT_DISPLAY; -import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service.ENDPOINT_DISPLAY_ITEMS; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.DISPLAY_ITEM_BIT_CLOCK; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY_ITEMS; public class AmazfitBipService { public static final UUID UUID_CHARACTERISTIC_WEATHER = UUID.fromString("0000000e-0000-3512-2118-0009af100700"); @@ -35,8 +35,6 @@ public class AmazfitBipService { public static final byte[] COMMAND_SET_LANGUAGE_TRADITIONAL_CHINESE = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x01}; public static final byte[] COMMAND_SET_LANGUAGE_ENGLISH = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x02}; public static final byte[] COMMAND_SET_LANGUAGE_SPANISH = new byte[]{ENDPOINT_DISPLAY, 0x13, 0x00, 0x03}; - public static final byte[] COMMAND_SET_LANGUAGE_NEW_TEMPLATE = new byte[]{ENDPOINT_DISPLAY, 0x17, 0x00, 0, 0, 0, 0, 0}; - public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES = 0x05; public static final byte COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS = 0x06; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java index 97d410a26..efbcde180 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorCoordinator.java @@ -68,4 +68,9 @@ public class AmazfitCorCoordinator extends HuamiCoordinator { public boolean supportsWeather() { return true; } + + @Override + public boolean supportsMusicInfo() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorService.java new file mode 100644 index 000000000..0f63c0b08 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor/AmazfitCorService.java @@ -0,0 +1,25 @@ +/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitcor; + +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.DISPLAY_ITEM_BIT_CLOCK; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY_ITEMS; + +public class AmazfitCorService { + public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java index 9fab9909f..c7dc78636 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java @@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; @@ -43,7 +43,7 @@ public class MiBand2Coordinator extends HuamiCoordinator { @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { - if (candidate.supportsService(MiBand2Service.UUID_SERVICE_MIBAND2_SERVICE)) { + if (candidate.supportsService(HuamiService.UUID_SERVICE_MIBAND2_SERVICE)) { return DeviceType.MIBAND2; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2HRXCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2HRXCoordinator.java index f45879b25..9bdf014f0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2HRXCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2HRXCoordinator.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, João Paulo Barraca +/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, João Paulo Barraca This file is part of Gadgetbridge. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java index 91bb24ce2..4d48d0b5c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Coordinator.java @@ -24,12 +24,17 @@ import android.support.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Date; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class MiBand3Coordinator extends HuamiCoordinator { private static final Logger LOG = LoggerFactory.getLogger(MiBand3Coordinator.class); @@ -70,4 +75,23 @@ public class MiBand3Coordinator extends HuamiCoordinator { public boolean supportsWeather() { return true; } + + public static boolean getBandScreenUnlock() { + Prefs prefs = GBApplication.getPrefs(); + return prefs.getBoolean(MiBandConst.PREF_MI3_BAND_SCREEN_UNLOCK, false); + } + + public static String getNightMode() { + Prefs prefs = GBApplication.getPrefs(); + + return prefs.getString(MiBandConst.PREF_MI3_NIGHT_MODE, MiBandConst.PREF_MI3_NIGHT_MODE_OFF); + } + + public static Date getNightModeStart() { + return getTimePreference( MiBandConst.PREF_MI3_NIGHT_MODE_START, "16:00"); + } + + public static Date getNightModeEnd() { + return getTimePreference(MiBandConst.PREF_MI3_NIGHT_MODE_END, "07:00"); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Service.java new file mode 100644 index 000000000..31f1988e5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband3/MiBand3Service.java @@ -0,0 +1,31 @@ +/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ + +package nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3; + +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.DISPLAY_ITEM_BIT_CLOCK; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY; +import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY_ITEMS; + +public class MiBand3Service { + public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00}; + public static final byte[] COMMAND_ENABLE_BAND_SCREEN_UNLOCK = new byte[]{ENDPOINT_DISPLAY, 0x16, 0x00, 0x01}; + public static final byte[] COMMAND_DISABLE_BAND_SCREEN_UNLOCK = new byte[]{ENDPOINT_DISPLAY, 0x16, 0x00, 0x00}; + public static final byte[] COMMAND_NIGHT_MODE_OFF = new byte[]{0x1a, 0x00}; + public static final byte[] COMMAND_NIGHT_MODE_SUNSET = new byte[]{0x1a, 0x02}; + public static final byte[] COMMAND_NIGHT_MODE_SCHEDULED = new byte[]{0x1a, 0x01, 0x10, 0x00, 0x07, 0x00}; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Constants.java new file mode 100644 index 000000000..67b7c2047 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Constants.java @@ -0,0 +1,110 @@ +/* Copyright (C) 2018 Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.id115; + +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; + +import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID; + +public class ID115Constants { + public static final String PREF_WRIST = "id115_wrist"; + public static final String PREF_SCREEN_ORIENTATION = "id115_screen_orientation"; + + public static final UUID UUID_SERVICE_ID115 = UUID.fromString(String.format(BASE_UUID, "0AF0")); + public static final UUID UUID_CHARACTERISTIC_WRITE_NORMAL = UUID.fromString(String.format(BASE_UUID, "0AF6")); + public static final UUID UUID_CHARACTERISTIC_NOTIFY_NORMAL = UUID.fromString(String.format(BASE_UUID, "0AF7")); + public static final UUID UUID_CHARACTERISTIC_WRITE_HEALTH = UUID.fromString(String.format(BASE_UUID, "0AF1")); + public static final UUID UUID_CHARACTERISTIC_NOTIFY_HEALTH = UUID.fromString(String.format(BASE_UUID, "0AF2")); + + public static final byte CMD_ID_WARE_UPDATE = 0x01; + public static final byte CMD_ID_GET_INFO = 0x02; + public static final byte CMD_ID_SETTINGS = 0x03; + public static final byte CMD_ID_BIND_UNBIND = 0x04; + public static final byte CMD_ID_NOTIFY = 0x05; + public static final byte CMD_ID_APP_CONTROL = 0x06; + public static final byte CMD_ID_BLE_CONTROL = 0x07; + public static final byte CMD_ID_HEALTH_DATA = 0x08; + public static final byte CMD_ID_DUMP_STACK = 0x20; + public static final byte CMD_ID_LOG = 0x21; + public static final byte CMD_ID_FACTORY = (byte)0xaa; + public static final byte CMD_ID_DEVICE_RESTART = (byte)0xf0; + + // CMD_ID_SETTINGS + public static final byte CMD_KEY_SET_TIME = 0x01; + public static final byte CMD_KEY_SET_GOAL = 0x03; + public static final byte CMD_KEY_SET_HAND = 0x22; + public static final byte CMD_ARG_LEFT = 0x00; + public static final byte CMD_ARG_RIGHT = 0x01; + public static final byte CMD_KEY_SET_DISPLAY_MODE = 0x2B; + public static final byte CMD_ARG_HORIZONTAL = 0x00; + public static final byte CMD_ARG_VERTICAL = 0x02; + + // CMD_ID_NOTIFY + public static final byte CMD_KEY_NOTIFY_CALL = 0x01; + public static final byte CMD_KEY_NOTIFY_STOP = 0x02; + public static final byte CMD_KEY_NOTIFY_MSG = 0x03; + + // CMD_ID_HEALTH_DATA + public static final byte CMD_KEY_FETCH_ACTIVITY_TODAY = 0x03; + + // CMD_ID_DEVICE_RESTART + public static final byte CMD_KEY_REBOOT = 0x01; + + public static byte getNotificationType(NotificationType type) { + switch (type) { +// case GENERIC_EMAIL: +// return 2; // Icon is not supported + case WECHAT: + return 3; +// case QQ: +// return 4; + case FACEBOOK: + return 6; + case TWITTER: + return 7; + case WHATSAPP: + return 8; + case FACEBOOK_MESSENGER: + return 9; + case INSTAGRAM: + return 10; + case LINKEDIN: + return 11; +// case GENERIC_CALENDAR: +// return 12; // Icon is not supported +// case SKYPE: +// return 13; // Icon is not supported +// case LINE: +// return 17; // Icon is not supported +// case VIBER: +// return 18; // Icon is not supported +// case KAKAO_TALK: +// return 19; // Icon is not supported +// case VK: +// return 16; // Icon is not supported +// case GMAIL: +// return 20; // Icon is not supported +// case OUTLOOK: +// return 21; // Icon is not supported +// case SNAPCHAT: +// return 22; // Icon is not supported + } + return 1; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java new file mode 100644 index 000000000..9f7039378 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java @@ -0,0 +1,157 @@ +/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.id115; + +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 android.os.ParcelUuid; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; +import java.util.Collections; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class ID115Coordinator extends AbstractDeviceCoordinator { + @NonNull + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Collection createBLEScanFilters() { + ParcelUuid service = new ParcelUuid(ID115Constants.UUID_SERVICE_ID115); + ScanFilter filter = new ScanFilter.Builder().setServiceUuid(service).build(); + return Collections.singletonList(filter); + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + if (candidate.supportsService(ID115Constants.UUID_SERVICE_ID115)) { + return DeviceType.ID115; + } + return DeviceType.UNKNOWN; + } + + @Override + public int getBondingStyle(GBDevice deviceCandidate){ + return BONDING_STYLE_NONE; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ID115; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new ID115SampleProvider(device, session); + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return false; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public String getManufacturer() { + return "VeryFit"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115SampleProvider.java new file mode 100644 index 000000000..c7f6bc2e0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115SampleProvider.java @@ -0,0 +1,77 @@ +/* Copyright (C) 2018 Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.id115; + +import android.support.annotation.NonNull; +import android.support.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.ID115ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.ID115ActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class ID115SampleProvider extends AbstractSampleProvider { + public ID115SampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getID115ActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return ID115ActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return ID115ActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return ID115ActivitySampleDao.Properties.DeviceId; + } + + @Override + public int normalizeType(int rawType) { + return rawType; + } + + @Override + public int toRawActivityKind(int activityKind) { + return activityKind; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity; + } + + @Override + public ID115ActivitySample createActivitySample() { + return new ID115ActivitySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java index 4d3e98dbd..04ecdb5ad 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, protomors, Sami Alaoui + Gobbetti, Dougal19, José Rebelo, protomors, Sami Alaoui This file is part of Gadgetbridge. @@ -45,11 +45,16 @@ import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class TeclastH30Coordinator extends AbstractDeviceCoordinator { protected static final Logger LOG = LoggerFactory.getLogger(TeclastH30Coordinator.class); + // e.g. H3-B20F + private Pattern deviceNamePattern = Pattern.compile("^H[13]-[ABCDEF0123456789]{4}$"); + @NonNull @Override @TargetApi(Build.VERSION_CODES.LOLLIPOP) @@ -62,10 +67,20 @@ public class TeclastH30Coordinator extends AbstractDeviceCoordinator { @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { - String name = candidate.getDevice().getName(); - if (name != null && (name.startsWith("TECLAST_H30") || name.startsWith("TECLAST_H10"))) { + if (candidate.supportsService(JYouConstants.UUID_SERVICE_JYOU)) { return DeviceType.TECLASTH30; } + + String name = candidate.getDevice().getName(); + if (name != null) { + if (name.startsWith("TECLAST_H30") || name.startsWith("TECLAST_H10")) { + return DeviceType.TECLASTH30; + } + Matcher deviceNameMatcher = deviceNamePattern.matcher(name); + if (deviceNameMatcher.matches()) { + return DeviceType.TECLASTH30; + } + } return DeviceType.UNKNOWN; } @@ -89,6 +104,11 @@ public class TeclastH30Coordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsFindDevice() { + return true; + } + @Override public DeviceType getDeviceType() { return DeviceType.TECLASTH30; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java index 7b93b6913..cae3c788f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/liveview/LiveviewCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, José Rebelo This file is part of Gadgetbridge. @@ -124,6 +124,11 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsFindDevice() { + return true; + } + @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { // nothing to delete, yet diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java index b9d5ae507..66cb6aa8f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandConst.java @@ -69,6 +69,13 @@ public final class MiBandConst { public static final String PREF_MI2_INACTIVITY_WARNINGS_DND_END = "mi2_inactivity_warnings_dnd_end"; public static final String PREF_MIBAND_SETUP_BT_PAIRING = "mi_setup_bt_pairing"; + public static final String PREF_MI3_BAND_SCREEN_UNLOCK = "mi3_band_screen_unlock"; + public static final String PREF_MI3_NIGHT_MODE = "mi3_night_mode"; + public static final String PREF_MI3_NIGHT_MODE_START = "mi3_night_mode_start"; + public static final String PREF_MI3_NIGHT_MODE_END = "mi3_night_mode_end"; + public static final String PREF_MI3_NIGHT_MODE_OFF = "off"; + public static final String PREF_MI3_NIGHT_MODE_SUNSET = "sunset"; + public static final String PREF_MI3_NIGHT_MODE_SCHEDULED = "scheduled"; public static final String ORIGIN_INCOMING_CALL = "incoming_call"; public static final String ORIGIN_ALARM_CLOCK = "alarm_clock"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java index f9a2063c4..fb30e38fb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Christian - Fischer, Daniele Gobbetti, Szymon Tomasz Stefanek + Fischer, Daniele Gobbetti, José Rebelo, Szymon Tomasz Stefanek This file is part of Gadgetbridge. @@ -176,6 +176,11 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsFindDevice() { + return true; + } + public static boolean hasValidUserInfo() { String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00"; try { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java index 485dae7aa..888fc5b68 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPreferencesActivity.java @@ -109,34 +109,6 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity { } }); - final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT); - setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newVal) { - invokeLater(new Runnable() { - @Override - public void run() { - GBApplication.deviceService().onSendConfiguration(PREF_MI2_DATEFORMAT); - } - }); - return true; - } - }); - - final Preference displayPages = findPreference(PREF_MI2_DISPLAY_ITEMS); - displayPages.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newVal) { - invokeLater(new Runnable() { - @Override - public void run() { - GBApplication.deviceService().onSendConfiguration(PREF_MI2_DISPLAY_ITEMS); - } - }); - return true; - } - }); - final Preference activateDisplayOnLift = findPreference(PREF_ACTIVATE_DISPLAY_ON_LIFT); activateDisplayOnLift.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @Override @@ -440,7 +412,6 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity { prefKeys.add(ActivityUser.PREF_USER_STEPS_GOAL); prefKeys.add(PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR); prefKeys.add(PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS); - prefKeys.add(PREF_MI2_ENABLE_TEXT_NOTIFICATIONS); prefKeys.add(PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD); prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_ALARM_CLOCK)); prefKeys.add(getNotificationPrefKey(VIBRATION_COUNT, ORIGIN_INCOMING_CALL)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java index 373f59aac..b401f24ae 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, protomors + Gobbetti, José Rebelo, protomors This file is part of Gadgetbridge. @@ -152,6 +152,11 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsFindDevice() { + return true; + } + @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { Long deviceId = device.getId(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java index d94a490f7..3791da4d2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebbleCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, José Rebelo This file is part of Gadgetbridge. @@ -160,4 +160,14 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator { public boolean supportsWeather() { return true; } + + @Override + public boolean supportsFindDevice() { + return true; + } + + @Override + public boolean supportsMusicInfo() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java new file mode 100644 index 000000000..47351dd01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi1Coordinator.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Roidmi1Coordinator extends RoidmiCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi1Coordinator.class); + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && name.contains("睿米车载蓝牙播放器")) { + return DeviceType.ROIDMI; + } + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ROIDMI; + } + + @Override + public int[] getColorPresets() { + return RoidmiConst.COLOR_PRESETS; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java new file mode 100644 index 000000000..10c351844 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/Roidmi3Coordinator.java @@ -0,0 +1,58 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.bluetooth.BluetoothDevice; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Roidmi3Coordinator extends RoidmiCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi3Coordinator.class); + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + try { + BluetoothDevice device = candidate.getDevice(); + String name = device.getName(); + + if (name != null && name.contains("Roidmi Music Blue C")) { + LOG.warn("Found a Roidmi 3, but support is disabled."); + return DeviceType.UNKNOWN; // TODO Roidmi 3 is not working atm + } + } catch (Exception ex) { + LOG.error("unable to check device support", ex); + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.ROIDMI3; + } + + @Override + public boolean supportsRgbLedColor() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java new file mode 100644 index 000000000..f7992aa76 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiConst.java @@ -0,0 +1,36 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.graphics.Color; + +public class RoidmiConst { + public static final String ACTION_GET_LED_COLOR = "roidmi_get_led_color"; + public static final String ACTION_GET_FM_FREQUENCY = "roidmi_get_frequency"; + public static final String ACTION_GET_VOLTAGE = "roidmi_get_voltage"; + + public static final int[] COLOR_PRESETS = new int[]{ + Color.rgb(0xFF, 0x00, 0x00), // red + Color.rgb(0x00, 0xFF, 0x00), // green + Color.rgb(0x00, 0x00, 0xFF), // blue + Color.rgb(0xFF, 0xFF, 0x01), // yellow + Color.rgb(0x00, 0xAA, 0xE5), // sky blue + Color.rgb(0xF0, 0x6E, 0xAA), // pink + Color.rgb(0xFF, 0xFF, 0xFF), // white + Color.rgb(0x00, 0x00, 0x00), // black + }; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java new file mode 100644 index 000000000..39d7ce76e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java @@ -0,0 +1,135 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.roidmi; + +import android.app.Activity; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; + +public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiCoordinator.class); + + @Override + public String getManufacturer() { + return "Roidmi"; + } + + @Override + public int getBondingStyle(GBDevice device) { + return BONDING_STYLE_BOND; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return false; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } + + @Override + public boolean supportsLedColor() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java index 55ca064bd..28880648d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/vibratissimo/VibratissimoCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, José Rebelo This file is part of Gadgetbridge. @@ -125,6 +125,11 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsFindDevice() { + return true; + } + @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { // nothing to delete, yet diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9CalibrationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9CalibrationActivity.java new file mode 100644 index 000000000..bda6ee5a9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9CalibrationActivity.java @@ -0,0 +1,123 @@ +/* Copyright (C) 2018 maxirnilian + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.watch9; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.content.LocalBroadcastManager; +import android.view.View; +import android.widget.Button; +import android.widget.NumberPicker; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class Watch9CalibrationActivity extends AbstractGBActivity { + + private static final String STATE_DEVICE = "stateDevice"; + GBDevice device; + + NumberPicker pickerHour, pickerMinute, pickerSecond; + + Handler handler; + Runnable holdCalibration; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_watch9_calibration); + + pickerHour = findViewById(R.id.np_hour); + pickerMinute = findViewById(R.id.np_minute); + pickerSecond = findViewById(R.id.np_second); + + pickerHour.setMinValue(1); + pickerHour.setMaxValue(12); + pickerHour.setValue(12); + pickerMinute.setMinValue(0); + pickerMinute.setMaxValue(59); + pickerMinute.setValue(0); + pickerSecond.setMinValue(0); + pickerSecond.setMaxValue(59); + pickerSecond.setValue(0); + + handler = new Handler(); + holdCalibration = new Runnable() { + @Override + public void run() { + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(new Intent(Watch9Constants.ACTION_CALIBRATION_HOLD)); + handler.postDelayed(this, 10000); + } + }; + + Intent intent = getIntent(); + device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + if (device == null && savedInstanceState != null) { + device = savedInstanceState.getParcelable(STATE_DEVICE); + } + if (device == null) { + finish(); + } + + final Button btCalibrate = findViewById(R.id.watch9_bt_calibrate); + btCalibrate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + btCalibrate.setEnabled(false); + handler.removeCallbacks(holdCalibration); + Intent calibrationData = new Intent(Watch9Constants.ACTION_CALIBRATION_SEND); + calibrationData.putExtra(Watch9Constants.VALUE_CALIBRATION_HOUR, pickerHour.getValue()); + calibrationData.putExtra(Watch9Constants.VALUE_CALIBRATION_MINUTE, pickerMinute.getValue()); + calibrationData.putExtra(Watch9Constants.VALUE_CALIBRATION_SECOND, pickerSecond.getValue()); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibrationData); + finish(); + } + }); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_DEVICE, device); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + device = savedInstanceState.getParcelable(STATE_DEVICE); + } + + @Override + protected void onStart() { + super.onStart(); + Intent calibration = new Intent(Watch9Constants.ACTION_CALIBRATION); + calibration.putExtra(Watch9Constants.ACTION_ENABLE, true); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration); + handler.postDelayed(holdCalibration, 1000); + } + + @Override + protected void onStop() { + super.onStop(); + Intent calibration = new Intent(Watch9Constants.ACTION_CALIBRATION); + calibration.putExtra(Watch9Constants.ACTION_ENABLE, false); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(calibration); + handler.removeCallbacks(holdCalibration); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9Constants.java new file mode 100644 index 000000000..dea5867ae --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9Constants.java @@ -0,0 +1,92 @@ +/* Copyright (C) 2018 maxirnilian + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.watch9; + +import java.util.UUID; + +public final class Watch9Constants { + public static final UUID UUID_SERVICE_WATCH9 = UUID.fromString("0000a800-0000-1000-8000-00805f9b34fb"); + + public static final UUID UUID_UNKNOWN_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + + public static final UUID UUID_CHARACTERISTIC_WRITE = UUID.fromString("0000a801-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_UNKNOWN_2 = UUID.fromString("0000a802-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_UNKNOWN_3 = UUID.fromString("0000a803-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_UNKNOWN_4 = UUID.fromString("0000a804-0000-1000-8000-00805f9b34fb"); + + public static final int NOTIFICATION_CHANNEL_DEFAULT = 128; + public static final int NOTIFICATION_CHANNEL_PHONE_CALL = 1024; + + public static final byte RESPONSE = 0x13; + public static final byte REQUEST = 0x31; + + public static final byte WRITE_VALUE = 0x01; + public static final byte READ_VALUE = 0x02; + public static final byte TASK = 0x04; + public static final byte KEEP_ALIVE = -0x80; + + public static final byte[] CMD_HEADER = new byte[]{0x23, 0x01, 0x00, 0x00, 0x00}; + + // byte[] COMMAND = new byte[]{0x23, 0x01, 0x00, 0x31, 0x00, ... , 0x00} + // | | | | | | └ Checksum + // | | | | | └ Command + value + // | | | | └ Sequence number + // | | | └ Response/Request indicator + // | | └ Value length + // | | + // └-----└ Header + + public static final byte[] CMD_FIRMWARE_INFO = new byte[]{0x01, 0x02}; + public static final byte[] CMD_AUTHORIZATION_TASK = new byte[]{0x01, 0x05}; + public static final byte[] CMD_TIME_SETTINGS = new byte[]{0x01, 0x08}; + public static final byte[] CMD_ALARM_SETTINGS = new byte[]{0x01, 0x0A}; + public static final byte[] CMD_BATTERY_INFO = new byte[]{0x01, 0x14}; + + public static final byte[] CMD_NOTIFICATION_TASK = new byte[]{0x03, 0x01}; + public static final byte[] CMD_NOTIFICATION_SETTINGS = new byte[]{0x03, 0x02}; + public static final byte[] CMD_CALIBRATION_INIT_TASK = new byte[]{0x03, 0x31}; + public static final byte[] CMD_CALIBRATION_TASK = new byte[]{0x03, 0x33, 0x01}; + public static final byte[] CMD_CALIBRATION_KEEP_ALIVE = new byte[]{0x03, 0x34}; + public static final byte[] CMD_DO_NOT_DISTURB_SETTINGS = new byte[]{0x03, 0x61}; + + public static final byte[] CMD_FITNESS_GOAL_SETTINGS = new byte[]{0x10, 0x02}; + + public static final byte[] RESP_AUTHORIZATION_TASK = new byte[]{0x01, 0x01, 0x05}; + public static final byte[] RESP_BUTTON_INDICATOR = new byte[]{0x04, 0x03, 0x11}; + public static final byte[] RESP_ALARM_INDICATOR = new byte[]{-0x80, 0x01, 0x0A}; + + public static final byte[] RESP_FIRMWARE_INFO = new byte[]{0x08, 0x01, 0x02}; + public static final byte[] RESP_TIME_SETTINGS = new byte[]{0x08, 0x01, 0x08}; + public static final byte[] RESP_BATTERY_INFO = new byte[]{0x08, 0x01, 0x14}; + public static final byte[] RESP_NOTIFICATION_SETTINGS = new byte[]{0x08, 0x03, 0x02}; + + public static final String ACTION_ENABLE = "action.watch9.enable"; + + public static final String ACTION_CALIBRATION + = "nodomain.freeyourgadget.gadgetbridge.devices.action.watch9.start_calibration"; + public static final String ACTION_CALIBRATION_SEND + = "nodomain.freeyourgadget.gadgetbridge.devices.action.watch9.send_calibration"; + public static final String ACTION_CALIBRATION_HOLD + = "nodomain.freeyourgadget.gadgetbridge.devices.action.watch9.keep_calibrating"; + public static final String VALUE_CALIBRATION_HOUR + = "value.watch9.calibration_hour"; + public static final String VALUE_CALIBRATION_MINUTE + = "value.watch9.calibration_minute"; + public static final String VALUE_CALIBRATION_SECOND + = "value.watch9.calibration_second"; + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java new file mode 100644 index 000000000..f12bfabb2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java @@ -0,0 +1,164 @@ +/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, maxirnilian, Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.watch9; + +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 android.os.ParcelUuid; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; +import java.util.Collections; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @NonNull + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Collection createBLEScanFilters() { + ParcelUuid watch9Service = new ParcelUuid(Watch9Constants.UUID_SERVICE_WATCH9); + ScanFilter filter = new ScanFilter.Builder().setServiceUuid(watch9Service).build(); + return Collections.singletonList(filter); + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + String macAddress = candidate.getMacAddress().toUpperCase(); + String deviceName = candidate.getName().toUpperCase(); + if (candidate.supportsService(Watch9Constants.UUID_SERVICE_WATCH9)) { + return DeviceType.WATCH9; + } else if (macAddress.startsWith("1C:87:79")) { + return DeviceType.WATCH9; + } else if (deviceName.equals("WATCH 9")) { + return DeviceType.WATCH9; + } + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.WATCH9; + } + + @Override + public int getBondingStyle(GBDevice deviceCandidate) { + return BONDING_STYLE_NONE; + } + + @Nullable + @Override + public Class getPairingActivity() { + return Watch9PairingActivity.class; + } + + @Override + public boolean supportsActivityDataFetching() { + return false; + } + + @Override + public boolean supportsActivityTracking() { + return false; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return null; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return true; + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return false; + } + + @Override + public String getManufacturer() { + return "Lenovo"; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsCalendarEvents() { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public boolean supportsWeather() { + return false; + } + + @Override + public boolean supportsFindDevice() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java new file mode 100644 index 000000000..b7423c29d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java @@ -0,0 +1,129 @@ +/* Copyright (C) 2018 maxirnilian + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.watch9; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import android.widget.TextView; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; +import nodomain.freeyourgadget.gadgetbridge.activities.DiscoveryActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Watch9PairingActivity extends AbstractGBActivity { + private static final Logger LOG = LoggerFactory.getLogger(Watch9PairingActivity.class); + + private static final String STATE_DEVICE_CANDIDATE = "stateDeviceCandidate"; + + private TextView message; + private GBDeviceCandidate deviceCandidate; + + private final BroadcastReceiver mPairingReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (GBDevice.ACTION_DEVICE_CHANGED.equals(intent.getAction())) { + GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + LOG.debug("pairing activity: device changed: " + device); + if (deviceCandidate.getMacAddress().equals(device.getAddress())) { + if (device.isInitialized()) { + pairingFinished(); + } else if (device.isConnecting() || device.isInitializing()) { + LOG.info("still connecting/initializing device..."); + } + } + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_watch9_pairing); + + message = findViewById(R.id.watch9_pair_message); + Intent intent = getIntent(); + deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); + if (deviceCandidate == null && savedInstanceState != null) { + deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); + } + if (deviceCandidate == null) { + Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show(); + startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + finish(); + return; + } + startPairing(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putParcelable(STATE_DEVICE_CANDIDATE, deviceCandidate); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + deviceCandidate = savedInstanceState.getParcelable(STATE_DEVICE_CANDIDATE); + } + + @Override + protected void onDestroy() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + super.onDestroy(); + } + + private void startPairing() { + message.setText(getString(R.string.pairing, deviceCandidate)); + + IntentFilter filter = new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED); + LocalBroadcastManager.getInstance(this).registerReceiver(mPairingReceiver, filter); + + GBApplication.deviceService().disconnect(); + GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); + if (device != null) { + GBApplication.deviceService().connect(device, true); + } else { + GB.toast(this, "Unable to connect, can't recognize the device type: " + deviceCandidate, Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void pairingFinished() { + AndroidUtils.safeUnregisterBroadcastReceiver(LocalBroadcastManager.getInstance(this), mPairingReceiver); + + Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + + finish(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchCoordinator.java index 1cfd7c2e3..207cc3fb2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xwatch/XWatchCoordinator.java @@ -1,5 +1,5 @@ /* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, ladbsoft + Gobbetti, José Rebelo, ladbsoft This file is part of Gadgetbridge. @@ -130,4 +130,9 @@ public class XWatchCoordinator extends AbstractDeviceCoordinator { public boolean supportsWeather() { return false; } + + @Override + public boolean supportsFindDevice() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeConstants.java index d36f0e5f7..bfba33c43 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeConstants.java @@ -1,3 +1,19 @@ +/* Copyright (C) 2018 Kranz, Sebastian Kranz + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.zetime; /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java index 7e59b77be..d0c77dc6b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java @@ -1,3 +1,20 @@ +/* Copyright (C) 2016-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, José Rebelo, Kranz, Sebastian Kranz + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.zetime; import android.app.Activity; @@ -67,6 +84,11 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator { return true; } + @Override + public boolean supportsFindDevice() { + return true; + } + @Override public InstallHandler findInstallHandler(Uri uri, Context context) { return null; @@ -127,6 +149,11 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator { return false; } + @Override + public boolean supportsMusicInfo() { + return true; + } + @Override public int getBondingStyle(GBDevice device) { return BONDING_STYLE_NONE; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java index 8ce05e4a1..a8d768036 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java @@ -1,3 +1,19 @@ +/* Copyright (C) 2018 Sebastian Kranz + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.zetime; import android.support.annotation.NonNull; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java index 323575782..ab8b64914 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/export/GPXExporter.java @@ -131,7 +131,7 @@ public class GPXExporter implements ActivityTrackExporter { ser.attribute(NS_DEFAULT, "lon", formatLocation(location.getLongitude())); ser.attribute(NS_DEFAULT, "lat", formatLocation(location.getLatitude())); ser.startTag(NS_DEFAULT, "ele").text(formatLocation(location.getAltitude())).endTag(NS_DEFAULT, "ele"); - ser.startTag(NS_DEFAULT, "time").text(formatTime(point.getTime())).endTag(NS_DEFAULT, "time"); + ser.startTag(NS_DEFAULT, "time").text(DateTimeUtils.formatIso8601UTC(point.getTime())).endTag(NS_DEFAULT, "time"); String description = point.getDescription(); if (description != null) { ser.startTag(NS_DEFAULT, "desc").text(description).endTag(NS_DEFAULT, "desc"); @@ -151,7 +151,7 @@ public class GPXExporter implements ActivityTrackExporter { } int hr = point.getHeartRate(); - if (!HeartRateUtils.isValidHeartRateValue(hr)) { + if (!HeartRateUtils.getInstance().isValidHeartRateValue(hr)) { if (!includeHeartRateOfNearestSample) { return; } @@ -162,7 +162,7 @@ public class GPXExporter implements ActivityTrackExporter { } hr = closestPointItem.getHeartRate(); - if (!HeartRateUtils.isValidHeartRateValue(hr)) { + if (!HeartRateUtils.getInstance().isValidHeartRateValue(hr)) { return; } } @@ -177,11 +177,12 @@ public class GPXExporter implements ActivityTrackExporter { private @Nullable ActivityPoint findClosestSensibleActivityPoint(Date time, List trackPoints) { ActivityPoint closestPointItem = null; + HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); long lowestDifference = 60 * 2 * 1000; // minimum distance is 2min for (ActivityPoint pointItem : trackPoints) { int hrItem = pointItem.getHeartRate(); - if (HeartRateUtils.isValidHeartRateValue(hrItem)) { + if (heartRateUtilsInstance.isValidHeartRateValue(hrItem)) { Date timeItem = pointItem.getTime(); if (timeItem.after(time) || timeItem.equals(time)) { break; // we assume that the given trackPoints are sorted in time ascending order (oldest first) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmClockReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmClockReceiver.java index c9a3c6831..aafdf94f5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmClockReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/AlarmClockReceiver.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer +/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti This file is part of Gadgetbridge. @@ -40,18 +41,20 @@ public class AlarmClockReceiver extends BroadcastReceiver { /** A public action sent by AlarmService when the alarm has started. */ public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT"; + public static final String GOOGLE_CLOCK_ALARM_ALERT_ACTION = "com.google.android.deskclock.action.ALARM_ALERT"; /** A public action sent by AlarmService when the alarm has stopped for any reason. */ public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE"; + public static final String GOOGLE_CLOCK_ALARM_DONE_ACTION = "com.google.android.deskclock.action.ALARM_DONE"; private int lastId; @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (ALARM_ALERT_ACTION.equals(action)) { + if (ALARM_ALERT_ACTION.equals(action) || GOOGLE_CLOCK_ALARM_ALERT_ACTION.equals(action)) { sendAlarm(true); - } else if (ALARM_DONE_ACTION.equals(action)) { + } else if (ALARM_DONE_ACTION.equals(action) || GOOGLE_CLOCK_ALARM_DONE_ACTION.equals(action)) { sendAlarm(false); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java index 88193d7ef..7224232d7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/MusicPlaybackReceiver.java @@ -76,11 +76,11 @@ public class MusicPlaybackReceiver extends BroadcastReceiver { stateSpec.state = (byte) (((Boolean) incoming) ? MusicStateSpec.STATE_PLAYING : MusicStateSpec.STATE_PAUSED); stateSpec.playRate = (byte) (((Boolean) incoming) ? 100 : 0); } else if (incoming instanceof String && "duration".equals(key)) { - musicSpec.duration = Integer.valueOf((String) incoming) / 1000; + musicSpec.duration = Integer.parseInt((String) incoming) / 1000; } else if (incoming instanceof String && "trackno".equals(key)) { - musicSpec.trackNr = Integer.valueOf((String) incoming); + musicSpec.trackNr = Integer.parseInt((String) incoming); } else if (incoming instanceof String && "totaltrack".equals(key)) { - musicSpec.trackCount = Integer.valueOf((String) incoming); + musicSpec.trackCount = Integer.parseInt((String) incoming); } else if (incoming instanceof Integer && "pos".equals(key)) { stateSpec.position = (Integer) incoming; } else if (incoming instanceof Integer && "repeat".equals(key)) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index f39b64bf7..7ffceff57 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -1,6 +1,6 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Frank Slezak, Hasan Ammar, Julien Pivotto, Kevin Richter, Normano64, - Steffen Liebergeld, Taavi Eomäe, Zhong Jianxin + Gobbetti, Frank Slezak, Hasan Ammar, José Rebelo, Julien Pivotto, Kevin + Richter, Normano64, Steffen Liebergeld, Taavi Eomäe, Zhong Jianxin This file is part of Gadgetbridge. @@ -50,6 +50,7 @@ import android.support.v7.graphics.Palette; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.List; import java.util.Objects; @@ -86,6 +87,8 @@ public class NotificationListener extends NotificationListenerService { private LimitedQueue mActionLookup = new LimitedQueue(16); + private HashMap notificationTimes = new HashMap<>(); + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override @@ -191,6 +194,8 @@ public class NotificationListener extends NotificationListenerService { if (shouldIgnore(sbn)) return; + Prefs prefs = GBApplication.getPrefs(); + switch (GBApplication.getGrantedInterruptionFilter()) { case NotificationManager.INTERRUPTION_FILTER_ALL: break; @@ -265,6 +270,18 @@ public class NotificationListener extends NotificationListenerService { return; } + // Ignore too frequent notifications, according to user preference + long min_timeout = prefs.getInt("notifications_timeout", 0) * 1000; + Long cur_time = System.currentTimeMillis(); + if (notificationTimes.containsKey(source)) { + Long last_time = notificationTimes.get(source); + if (cur_time - last_time < min_timeout) { + LOG.info("Ignoring frequent notification, last one was " + (cur_time - last_time) + "ms ago"); + return; + } + } + notificationTimes.put(source, cur_time); + GBApplication.deviceService().onNotification(notificationSpec); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OmniJawsObserver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OmniJawsObserver.java index 83b21a2e9..a526725de 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OmniJawsObserver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/OmniJawsObserver.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2018 Daniele Gobbetti +/* Copyright (C) 2017-2018 Carsten Pfeiffer, Daniele Gobbetti This file is part of Gadgetbridge. @@ -121,7 +121,7 @@ public class OmniJawsObserver extends ContentObserver { weatherSpec.windSpeed = toKmh(c.getFloat(11)); weatherSpec.windDirection = c.getInt(12); - weatherSpec.timestamp = (int) (Long.valueOf(c.getString(9)) / 1000); + weatherSpec.timestamp = (int) (Long.parseLong(c.getString(9)) / 1000); } else if (i == 1) { weatherSpec.todayMinTemp = toKelvin(c.getFloat(5)); weatherSpec.todayMaxTemp = toKelvin(c.getFloat(6)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java index e5252eda5..e5ae08672 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/PhoneCallReceiver.java @@ -41,9 +41,11 @@ public class PhoneCallReceiver extends BroadcastReceiver { if (intent.getAction().equals("android.intent.action.NEW_OUTGOING_CALL")) { mSavedNumber = intent.getExtras().getString("android.intent.extra.PHONE_NUMBER"); } else { - String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER); - int state = tm.getCallState(); - onCallStateChanged(context, state, number); + if (intent.hasExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)) { + String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER); + int state = tm.getCallState(); + onCallStateChanged(context, state, number); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index 296ae7bb2..2adc2ae71 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -73,11 +74,13 @@ public class GBDevice implements Parcelable { private String mModel; private State mState = State.NOT_CONNECTED; private short mBatteryLevel = BATTERY_UNKNOWN; + private float mBatteryVoltage = BATTERY_UNKNOWN; private short mBatteryThresholdPercent = BATTERY_THRESHOLD_PERCENT; private BatteryState mBatteryState; private short mRssi = RSSI_UNKNOWN; private String mBusyTask; private List mDeviceInfos; + private HashMap mExtraInfos; public GBDevice(String address, String name, DeviceType deviceType) { this(address, null, name, deviceType); @@ -106,6 +109,7 @@ public class GBDevice implements Parcelable { mRssi = (short) in.readInt(); mBusyTask = in.readString(); mDeviceInfos = in.readArrayList(getClass().getClassLoader()); + mExtraInfos = (HashMap) in.readSerializable(); validate(); } @@ -126,6 +130,7 @@ public class GBDevice implements Parcelable { dest.writeInt(mRssi); dest.writeString(mBusyTask); dest.writeList(mDeviceInfos); + dest.writeSerializable(mExtraInfos); } private void validate() { @@ -371,6 +376,33 @@ public class GBDevice implements Parcelable { return mAddress.hashCode() ^ 37; } + + /** + * Returns the extra info value if it is set, null otherwise + * @param key the extra info key + * @return the extra info value if set, null otherwise + */ + public Object getExtraInfo(String key) { + if (mExtraInfos == null) { + return null; + } + + return mExtraInfos.get(key); + } + + /** + * Sets an extra info value, overwriting the current one, if any + * @param key the extra info key + * @param info the extra info value + */ + public void setExtraInfo(String key, Object info) { + if (mExtraInfos == null) { + mExtraInfos = new HashMap<>(); + } + + mExtraInfos.put(key, info); + } + /** * Ranges from 0-100 (percent), or -1 if unknown * @@ -388,6 +420,23 @@ public class GBDevice implements Parcelable { } } + public void setBatteryVoltage(float batteryVoltage) { + if (batteryVoltage >= 0 || batteryVoltage == BATTERY_UNKNOWN) { + mBatteryVoltage = batteryVoltage; + } else { + LOG.error("Battery voltage must be > 0: " + batteryVoltage); + } + } + + /** + * Voltage greater than zero (unit: Volt), or -1 if unknown + * + * @return the battery voltage, or -1 if unknown + */ + public float getBatteryVoltage() { + return mBatteryVoltage; + } + public BatteryState getBatteryState() { return mBatteryState; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index 71155069e..854260788 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -1,5 +1,6 @@ /* Copyright (C) 2015-2018 Alberto, Andreas Shimokawa, Carsten Pfeiffer, - criogenic, Frank Slezak, ivanovlev, Julien Pivotto, Kasha, Steffen Liebergeld + criogenic, dakhnod, Frank Slezak, ivanovlev, Julien Pivotto, Kasha, Steffen + Liebergeld This file is part of Gadgetbridge. @@ -41,9 +42,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; import nodomain.freeyourgadget.gadgetbridge.util.LanguageUtils; +import nodomain.freeyourgadget.gadgetbridge.util.RtlUtils; import static nodomain.freeyourgadget.gadgetbridge.util.JavaExtensions.coalesce; + public class GBDeviceService implements DeviceService { protected final Context mContext; private final Class mServiceClass; @@ -81,6 +84,14 @@ public class GBDeviceService implements DeviceService { } } + if (RtlUtils.rtlSupport()) { + for (String extra : transliterationExtras) { + if (intent.hasExtra(extra)) { + intent.putExtra(extra, RtlUtils.fixRtl(intent.getStringExtra(extra))); + } + } + } + mContext.startService(intent); } @@ -142,7 +153,8 @@ public class GBDeviceService implements DeviceService { .putExtra(EXTRA_NOTIFICATION_ID, notificationSpec.id) .putExtra(EXTRA_NOTIFICATION_TYPE, notificationSpec.type) .putExtra(EXTRA_NOTIFICATION_SOURCENAME, notificationSpec.sourceName) - .putExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR, notificationSpec.pebbleColor); + .putExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR, notificationSpec.pebbleColor) + .putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId); invokeService(intent); } @@ -403,4 +415,18 @@ public class GBDeviceService implements DeviceService { return name; } + + @Override + public void onSetFmFrequency(float frequency) { + Intent intent = createIntent().setAction(ACTION_SET_FM_FREQUENCY) + .putExtra(EXTRA_FM_FREQUENCY, frequency); + invokeService(intent); + } + + @Override + public void onSetLedColor(int color) { + Intent intent = createIntent().setAction(ACTION_SET_LED_COLOR) + .putExtra(EXTRA_LED_COLOR, color); + invokeService(intent); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java index 4c60a316f..3b5aa1548 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivityAmount.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer +/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Pavel Elagin This file is part of Gadgetbridge. @@ -18,6 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.model; import android.content.Context; +import java.util.Date; + import nodomain.freeyourgadget.gadgetbridge.R; public class ActivityAmount { @@ -25,6 +27,8 @@ public class ActivityAmount { private short percent; private long totalSeconds; private long totalSteps; + private Date startDate = null; + private Date endDate = null; public ActivityAmount(int activityKind) { this.activityKind = activityKind; @@ -67,4 +71,21 @@ public class ActivityAmount { } return context.getString(R.string.abstract_chart_fragment_kind_activity); } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(int seconds) { + if(startDate == null) + this.startDate = new Date((long)seconds * 1000); + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(int seconds) { + this.endDate = new Date((long)seconds * 1000); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java index 346612a43..25538d675 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/BatteryState.java @@ -22,5 +22,6 @@ public enum BatteryState { BATTERY_LOW, BATTERY_CHARGING, BATTERY_CHARGING_FULL, - BATTERY_NOT_CHARGING_FULL + BATTERY_NOT_CHARGING_FULL, + NO_BATTERY } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 19b29939c..d15e858db 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -1,5 +1,5 @@ -/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Frank Slezak, - ivanovlev, JohnnySun, Julien Pivotto, Kasha, Steffen Liebergeld +/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, dakhnod, + Frank Slezak, ivanovlev, JohnnySun, Julien Pivotto, Kasha, Steffen Liebergeld This file is part of Gadgetbridge. @@ -65,12 +65,15 @@ public interface DeviceService extends EventHandler { String ACTION_SEND_CONFIGURATION = PREFIX + ".action.send_configuration"; String ACTION_SEND_WEATHER = PREFIX + ".action.send_weather"; String ACTION_TEST_NEW_FUNCTION = PREFIX + ".action.test_new_function"; + String ACTION_SET_FM_FREQUENCY = PREFIX + ".action.set_fm_frequency"; + String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color"; String EXTRA_NOTIFICATION_BODY = "notification_body"; String EXTRA_NOTIFICATION_FLAGS = "notification_flags"; String EXTRA_NOTIFICATION_ID = "notification_id"; String EXTRA_NOTIFICATION_PHONENUMBER = "notification_phonenumber"; String EXTRA_NOTIFICATION_SENDER = "notification_sender"; String EXTRA_NOTIFICATION_SOURCENAME = "notification_sourcename"; + String EXTRA_NOTIFICATION_SOURCEAPPID = "notification_sourceappid"; String EXTRA_NOTIFICATION_SUBJECT = "notification_subject"; String EXTRA_NOTIFICATION_TITLE = "notification_title"; String EXTRA_NOTIFICATION_TYPE = "notification_type"; @@ -105,6 +108,8 @@ public interface DeviceService extends EventHandler { String EXTRA_INTERVAL_SECONDS = "interval_seconds"; String EXTRA_WEATHER = "weather"; String EXTRA_RECORDED_DATA_TYPES = "data_types"; + String EXTRA_FM_FREQUENCY = "fm_frequency"; + String EXTRA_LED_COLOR = "led_color"; /** * Use EXTRA_REALTIME_SAMPLE instead diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 82e07a1af..de073474b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -1,6 +1,6 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, João Paulo Barraca, ladbsoft, protomors, Quallenauge, Sami - Alaoui, tiparega + Gobbetti, João Paulo Barraca, José Rebelo, Kranz, ladbsoft, maxirnilian, + protomors, Quallenauge, Sami Alaoui, tiparega, Vadim Kaushan This file is part of Gadgetbridge. @@ -33,10 +33,10 @@ public enum DeviceType { UNKNOWN(-1, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_unknown), PEBBLE(1, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_pebble), MIBAND(10, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled, R.string.devicetype_miband), - MIBAND2(11, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled, R.string.devicetype_miband2), + MIBAND2(11, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband2), AMAZFITBIP(12, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_bip), AMAZFITCOR(13, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_amazfit_cor), - MIBAND3(14, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled, R.string.devicetype_miband3), + MIBAND3(14, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband3), VIBRATISSIMO(20, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo), LIVEVIEW(30, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_liveview), HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_hplus), @@ -47,6 +47,10 @@ public enum DeviceType { TECLASTH30(60, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_teclast_h30), XWATCH(70, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_xwatch), ZETIME(80, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_mykronoz_zetime), + ID115(90, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_id115), + WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9), + ROIDMI(110, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi), + ROIDMI3(112, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3), TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test); private final int key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java index c0a7d3285..305aae64f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicSpec.java @@ -75,4 +75,16 @@ public class MusicSpec { result = 31 * result + trackNr; return result; } + + @Override + public String toString() { + return "MusicSpec{" + + "artist='" + artist + '\'' + + ", album='" + album + '\'' + + ", track='" + track + '\'' + + ", duration=" + duration + + ", trackCount=" + trackCount + + ", trackNr=" + trackNr + + '}'; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java index 286fdf4cb..b7f4da047 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/MusicStateSpec.java @@ -71,4 +71,15 @@ public class MusicStateSpec { result = 31 * result + (int) repeat; return result; } + + @Override + public String toString() { + return "MusicStateSpec{" + + "state=" + state + + ", position=" + position + + ", playRate=" + playRate + + ", shuffle=" + shuffle + + ", repeat=" + repeat + + '}'; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java index 5ff768946..e48dfe395 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2016-2018 Andreas Shimokawa, Daniele Gobbetti +/* Copyright (C) 2016-2018 Andreas Shimokawa, Daniele Gobbetti, Sebastian + Kranz This file is part of Gadgetbridge. @@ -433,7 +434,7 @@ public class Weather { return 801; case 45: //thundershowers case 47: //isolated thundershowers - return 621; + return 211; case 3200: //not available default: return -1; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index b44657646..7b6331b1d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Taavi Eomäe + Gobbetti, Sebastian Kranz, Taavi Eomäe This file is part of Gadgetbridge. @@ -52,7 +52,9 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; @@ -159,6 +161,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { handleGBDeviceEvent((GBDeviceEventBatteryInfo) deviceEvent); } else if (deviceEvent instanceof GBDeviceEventFindPhone) { handleGBDeviceEvent((GBDeviceEventFindPhone) deviceEvent); + } else if (deviceEvent instanceof GBDeviceEventLEDColor) { + handleGBDeviceEvent((GBDeviceEventLEDColor) deviceEvent); + } else if (deviceEvent instanceof GBDeviceEventFmFrequency) { + handleGBDeviceEvent((GBDeviceEventFmFrequency) deviceEvent); } } @@ -199,7 +205,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { protected void handleGBDeviceEvent(GBDeviceEventVersionInfo infoEvent) { Context context = getContext(); - LOG.info("Got event for VERSION_INFO"); + LOG.info("Got event for VERSION_INFO: " + infoEvent); if (gbDevice == null) { return; } @@ -208,6 +214,26 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { gbDevice.sendDeviceUpdateIntent(context); } + protected void handleGBDeviceEvent(GBDeviceEventLEDColor colorEvent) { + Context context = getContext(); + LOG.info("Got event for LED Color"); + if (gbDevice == null) { + return; + } + gbDevice.setExtraInfo("led_color", colorEvent.color); + gbDevice.sendDeviceUpdateIntent(context); + } + + protected void handleGBDeviceEvent(GBDeviceEventFmFrequency frequencyEvent) { + Context context = getContext(); + LOG.info("Got event for FM Frequency"); + if (gbDevice == null) { + return; + } + gbDevice.setExtraInfo("fm_frequency", frequencyEvent.frequency); + gbDevice.sendDeviceUpdateIntent(context); + } + private void handleGBDeviceEvent(GBDeviceEventAppInfo appInfoEvent) { Context context = getContext(); LOG.info("Got event for APP_INFO"); @@ -328,21 +354,37 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { LOG.info("Got BATTERY_INFO device event"); gbDevice.setBatteryLevel(deviceEvent.level); gbDevice.setBatteryState(deviceEvent.state); + gbDevice.setBatteryVoltage(deviceEvent.voltage); - //show the notification if the battery level is below threshold and only if not connected to charger - if (deviceEvent.level <= gbDevice.getBatteryThresholdPercent() && - (BatteryState.BATTERY_LOW.equals(deviceEvent.state) || - BatteryState.BATTERY_NORMAL.equals(deviceEvent.state)) - ) { - GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)), - deviceEvent.extendedInfoAvailable() ? - context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)) + "\n" + - context.getString(R.string.notif_battery_low_bigtext_last_charge_time, DateFormat.getDateTimeInstance().format(deviceEvent.lastChargeTime.getTime())) + - context.getString(R.string.notif_battery_low_bigtext_number_of_charges, String.valueOf(deviceEvent.numCharges)) - : "" - , context); + if (deviceEvent.level == GBDevice.BATTERY_UNKNOWN) { + // no level available, just "high" or "low" + if (BatteryState.BATTERY_LOW.equals(deviceEvent.state)) { + GB.updateBatteryNotification(context.getString(R.string.notif_battery_low, gbDevice.getName()), + deviceEvent.extendedInfoAvailable() ? + context.getString(R.string.notif_battery_low_extended, gbDevice.getName(), + context.getString(R.string.notif_battery_low_bigtext_last_charge_time, DateFormat.getDateTimeInstance().format(deviceEvent.lastChargeTime.getTime())) + + context.getString(R.string.notif_battery_low_bigtext_number_of_charges, String.valueOf(deviceEvent.numCharges))) + : "" + , context); + } else { + GB.removeBatteryNotification(context); + } } else { - GB.removeBatteryNotification(context); + //show the notification if the battery level is below threshold and only if not connected to charger + if (deviceEvent.level <= gbDevice.getBatteryThresholdPercent() && + (BatteryState.BATTERY_LOW.equals(deviceEvent.state) || + BatteryState.BATTERY_NORMAL.equals(deviceEvent.state)) + ) { + GB.updateBatteryNotification(context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)), + deviceEvent.extendedInfoAvailable() ? + context.getString(R.string.notif_battery_low_percent, gbDevice.getName(), String.valueOf(deviceEvent.level)) + "\n" + + context.getString(R.string.notif_battery_low_bigtext_last_charge_time, DateFormat.getDateTimeInstance().format(deviceEvent.lastChargeTime.getTime())) + + context.getString(R.string.notif_battery_low_bigtext_number_of_charges, String.valueOf(deviceEvent.numCharges)) + : "" + , context); + } else { + GB.removeBatteryNotification(context); + } } gbDevice.sendDeviceUpdateIntent(context); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 3003a9394..b96b663c1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -1,7 +1,7 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, Avamander, Carsten Pfeiffer, - Daniele Gobbetti, Daniel Hauck, Frank Slezak, ivanovlev, João Paulo Barraca, - Julien Pivotto, Kasha, Sergey Trofimov, Steffen Liebergeld, Taavi Eomäe, - Uwe Hermann + dakhnod, Daniele Gobbetti, Daniel Hauck, Frank Slezak, ivanovlev, João Paulo + Barraca, Julien Pivotto, Kasha, Martin, Sergey Trofimov, Steffen Liebergeld, + Taavi Eomäe, Uwe Hermann This file is part of Gadgetbridge. @@ -47,6 +47,7 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver; import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver; @@ -105,7 +106,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SE import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETTIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_ALARMS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_CONSTANT_VIBRATION; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_FM_FREQUENCY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_HEARTRATE_MEASUREMENT_INTERVAL; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SET_LED_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_START; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_STARTAPP; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_TEST_NEW_FUNCTION; @@ -130,7 +133,9 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CAN import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONFIG; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_CONNECT_FIRST_TIME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FIND_START; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_FM_FREQUENCY; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_INTERVAL_SECONDS; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_LED_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ALBUM; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_DURATION; @@ -148,6 +153,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOT import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PEBBLE_COLOR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PHONENUMBER; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SENDER; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SOURCEAPPID; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SOURCENAME; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SUBJECT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_TITLE; @@ -348,6 +354,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere notificationSpec.pebbleColor = (byte) intent.getSerializableExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR); notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0); + notificationSpec.sourceAppId = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCEAPPID); if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) { notificationSpec.id = mRandom.nextInt(); // FIXME: add this in external SMS Receiver? @@ -547,6 +554,18 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } break; } + case ACTION_SET_LED_COLOR: + int color = intent.getIntExtra(EXTRA_LED_COLOR, 0); + if (color != 0) { + mDeviceSupport.onSetLedColor(color); + } + break; + case ACTION_SET_FM_FREQUENCY: + float frequency = intent.getFloatExtra(EXTRA_FM_FREQUENCY, -1); + if (frequency != -1) { + mDeviceSupport.onSetFmFrequency(frequency); + } + break; } return START_STICKY; @@ -637,7 +656,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere mPebbleReceiver = new PebbleReceiver(); registerReceiver(mPebbleReceiver, new IntentFilter("com.getpebble.action.SEND_NOTIFICATION")); } - if (mMusicPlaybackReceiver == null) { + if (mMusicPlaybackReceiver == null && coordinator != null && coordinator.supportsMusicInfo()) { mMusicPlaybackReceiver = new MusicPlaybackReceiver(); IntentFilter filter = new IntentFilter(); for (String action : mMusicActions) { @@ -665,6 +684,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere IntentFilter filter = new IntentFilter(); filter.addAction(AlarmClockReceiver.ALARM_ALERT_ACTION); filter.addAction(AlarmClockReceiver.ALARM_DONE_ACTION); + filter.addAction(AlarmClockReceiver.GOOGLE_CLOCK_ALARM_ALERT_ACTION); + filter.addAction(AlarmClockReceiver.GOOGLE_CLOCK_ALARM_DONE_ACTION); registerReceiver(mAlarmClockReceiver, filter); } if (mCMWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) { @@ -764,6 +785,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere mDeviceSupport.setAutoReconnect(autoReconnect); } } + if (GBPrefs.CHART_MAX_HEART_RATE.equals(key) || GBPrefs.CHART_MIN_HEART_RATE.equals(key)) { + HeartRateUtils.getInstance().updateCachedHeartRatePreferences(); + } } protected boolean hasPrefs() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 33e9f15d5..10962047a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -1,6 +1,6 @@ -/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer, - Daniele Gobbetti, João Paulo Barraca, ladbsoft, protomors, Quallenauge, - Sami Alaoui, Sergey Trofimov, tiparega +/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, João Paulo Barraca, José Rebelo, Kranz, ladbsoft, maxirnilian, + protomors, Quallenauge, Sami Alaoui, Sergey Trofimov, tiparega, Vadim Kaushan This file is part of Gadgetbridge. @@ -29,19 +29,22 @@ import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor.AmazfitCorSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3.MiBand3Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DeviceSupportFactory { @@ -115,7 +118,7 @@ public class DeviceSupportFactory { deviceSupport = new ServiceDeviceSupport(new MiBandSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; case MIBAND2: - deviceSupport = new ServiceDeviceSupport(new MiBand2Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + deviceSupport = new ServiceDeviceSupport(new HuamiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; case MIBAND3: deviceSupport = new ServiceDeviceSupport(new MiBand3Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); @@ -152,9 +155,22 @@ public class DeviceSupportFactory { break; case XWATCH: deviceSupport = new ServiceDeviceSupport(new XWatchSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); - case ZETIME: + break; + case ZETIME: deviceSupport = new ServiceDeviceSupport(new ZeTimeDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); break; + case ID115: + deviceSupport = new ServiceDeviceSupport(new ID115Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case WATCH9: + deviceSupport = new ServiceDeviceSupport(new Watch9DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case ROIDMI: + deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; + case ROIDMI3: + deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + break; } if (deviceSupport != null) { deviceSupport.setContext(gbDevice, mBtAdapter, mContext); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 4c77c5a01..33e49e48a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -374,4 +374,20 @@ public class ServiceDeviceSupport implements DeviceSupport { } delegate.onSendWeather(weatherSpec); } + + @Override + public void onSetFmFrequency(float frequency) { + if (checkBusy("set frequency event")) { + return; + } + delegate.onSetFmFrequency(frequency); + } + + @Override + public void onSetLedColor(int color) { + if (checkBusy("set led color event")) { + return; + } + delegate.onSetLedColor(color); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java index 5724bdfba..efa41e746 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java @@ -81,6 +81,10 @@ public abstract class BtClassicIoThread extends GBDeviceIoThread { public synchronized void write(byte[] bytes) { if (null == bytes) return; + if (mOutStream == null) { + LOG.error("mOutStream is null"); + return; + } LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length)); try { mOutStream.write(bytes); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index e7f157dfe..c65153ba0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -318,4 +318,14 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im profile.onReadRemoteRssi(gatt, rssi, status); } } + + @Override + public void onSetFmFrequency(float frequency) { + + } + + @Override + public void onSetLedColor(int color) { + + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java index 4f2aa823d..46b490089 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/AbstractBleProfile.java @@ -22,6 +22,9 @@ import android.content.Intent; import android.support.v4.content.LocalBroadcastManager; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -44,16 +47,36 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; public abstract class AbstractBleProfile extends AbstractGattCallback { private final T mSupport; + private List listeners = new ArrayList(1); + public AbstractBleProfile(T support) { this.mSupport = support; } + public void addListener(IntentListener listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + } + } + + public boolean removeListener(IntentListener listener) { + return listeners.remove(listener); + } + + protected List getListeners() { + return Collections.unmodifiableList(listeners); + } + /** * All notifications should be sent through this methods to make them testable. * @param intent the intent to broadcast */ protected void notify(Intent intent) { - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + // note: we send synchronously in order to keep the processing order of BLE events + // in BtleQueue and the reception of results. + for (IntentListener listener : listeners) { + listener.notify(intent); + } } /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/IntentListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/IntentListener.java new file mode 100644 index 000000000..cb0df44aa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/profiles/IntentListener.java @@ -0,0 +1,26 @@ +/* Copyright (C) 2018 Carsten Pfeiffer + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles; + +import android.content.Intent; + +/** + * Callback interface for delivering results of ble requests. + */ +public interface IntentListener { + void notify(Intent intent); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java index 183370895..fbb28c469 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/hplus/HPlusSupport.java @@ -23,12 +23,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.net.Uri; -import android.support.v4.content.LocalBroadcastManager; import android.widget.Toast; import org.apache.commons.lang3.ArrayUtils; @@ -63,7 +58,6 @@ 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.profiles.deviceinfo.DeviceInfo; -import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; @@ -79,35 +73,17 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport { private HPlusHandlerThread syncHelper; private DeviceType deviceType = DeviceType.UNKNOWN; - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String s = intent.getAction(); - if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { - handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); - } - } - }; - public HPlusSupport(DeviceType type) { super(LOG); LOG.info("HPlusSupport Instance Created"); deviceType = type; addSupportedService(HPlusConstants.UUID_SERVICE_HP); - - LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); - IntentFilter intentFilter = new IntentFilter(); - - broadcastManager.registerReceiver(mReceiver, intentFilter); } @Override public void dispose() { LOG.info("Dispose"); - LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); - broadcastManager.unregisterReceiver(mReceiver); - close(); super.dispose(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/AbstractMiBand2Operation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiOperation.java similarity index 85% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/AbstractMiBand2Operation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiOperation.java index 4cc5b2768..6ad851019 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/AbstractMiBand2Operation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/AbstractHuamiOperation.java @@ -14,13 +14,14 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.AbstractMiBandOperation; -public abstract class AbstractMiBand2Operation extends AbstractMiBandOperation { - protected AbstractMiBand2Operation(MiBand2Support support) { +public abstract class AbstractHuamiOperation extends AbstractMiBandOperation { + protected AbstractHuamiOperation(HuamiSupport support) { super(support); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java index 9f76582a9..a1d937a15 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiDeviceEvent.java @@ -30,4 +30,5 @@ public class HuamiDeviceEvent { public static final byte BUTTON_PRESSED_LONG = 0x0b; public static final byte TICK_30MIN = 0x0e; // unsure public static final byte FIND_PHONE_STOP = 0x0f; + public static final byte MUSIC_CONTROL = (byte) 0xfe; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java similarity index 76% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/MiBand2Support.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index ab1d4dbe6..757e5996e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/MiBand2Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -34,6 +34,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -54,22 +56,24 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.huami.ActivateDisplayOnLift; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2FWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay; import nodomain.freeyourgadget.gadgetbridge.devices.miband.DoNotDisturb; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; -import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; @@ -84,6 +88,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -97,19 +102,21 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile; import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiBatteryInfo; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.Mi2NotificationStrategy; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.Mi2TextNotificationStrategy; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.actions.StopNotificationAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.InitOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.actions.StopNotificationAction; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.FetchActivityOperation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.FetchSportsSummaryOperation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.InitOperation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.UpdateFirmwareOperation; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -134,7 +141,7 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VI import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue; -public class MiBand2Support extends AbstractBTLEDeviceSupport { +public class HuamiSupport extends AbstractBTLEDeviceSupport { // We introduce key press counter for notification purposes private static int currentButtonActionId = 0; @@ -142,20 +149,21 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { private static long currentButtonPressTime = 0; private static long currentButtonTimerActivationTime = 0; - private static final Logger LOG = LoggerFactory.getLogger(MiBand2Support.class); - private final DeviceInfoProfile deviceInfoProfile; - private final HeartRateProfile heartRateProfile; - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + private static final Logger LOG = LoggerFactory.getLogger(HuamiSupport.class); + private final DeviceInfoProfile deviceInfoProfile; + private final HeartRateProfile heartRateProfile; + private final IntentListener mListener = new IntentListener() { @Override - public void onReceive(Context context, Intent intent) { + public void notify(Intent intent) { String s = intent.getAction(); - if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { + if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(s)) { handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); } } }; - BluetoothGattCharacteristic characteristicHRControlPoint; + private BluetoothGattCharacteristic characteristicHRControlPoint; + protected BluetoothGattCharacteristic characteristicChunked; private boolean needsAuth; private volatile boolean telephoneRinging; @@ -168,11 +176,15 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { private RealtimeSamplesSupport realtimeSamplesSupport; private boolean alarmClockRinging; - public MiBand2Support() { + private boolean isMusicAppStarted = false; + private MusicSpec bufferMusicSpec = null; + private MusicStateSpec bufferMusicStateSpec = null; + + public HuamiSupport() { this(LOG); } - public MiBand2Support(Logger logger) { + public HuamiSupport(Logger logger) { super(logger); addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); @@ -183,25 +195,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE); - addSupportedService(MiBand2Service.UUID_SERVICE_FIRMWARE_SERVICE); + addSupportedService(HuamiService.UUID_SERVICE_FIRMWARE_SERVICE); deviceInfoProfile = new DeviceInfoProfile<>(this); + deviceInfoProfile.addListener(mListener); addSupportedProfile(deviceInfoProfile); heartRateProfile = new HeartRateProfile<>(this); addSupportedProfile(heartRateProfile); - - LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); - intentFilter.addAction(DeviceService.ACTION_MIBAND2_AUTH); - broadcastManager.registerReceiver(mReceiver, intentFilter); - } - - @Override - public void dispose() { - LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); - broadcastManager.unregisterReceiver(mReceiver); - super.dispose(); } @Override @@ -209,8 +209,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { try { boolean authenticate = needsAuth; needsAuth = false; - new InitOperation(authenticate, this, builder).perform(); + byte authFlags = HuamiService.AUTH_BYTE; + if (gbDevice.getType() == DeviceType.MIBAND3) { + authFlags = 0x00; + } + new InitOperation(authenticate, authFlags, this, builder).perform(); characteristicHRControlPoint = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); + characteristicChunked = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER); } catch (IOException e) { GB.toast(getContext(), "Initializing Mi Band 2 failed", Toast.LENGTH_SHORT, GB.ERROR, e); } @@ -219,7 +224,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { /** * Returns the given date/time (calendar) as a byte sequence, suitable for sending to the - * Mi Band 2 (or derivative). The band appears to not handle DST offsets, so we simply add this + * Mi Band 2 (or derivative). The band appears to not handle DST offsets, so we simply add this * to the timezone. * @param calendar * @param precision @@ -246,19 +251,19 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return timestamp; } - public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { + public HuamiSupport setCurrentTimeWithService(TransactionBuilder builder) { GregorianCalendar now = BLETypeConversions.createCalendar(); byte[] bytes = getTimeBytes(now, TimeUnit.SECONDS); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes); return this; } - public MiBand2Support setLowLatency(TransactionBuilder builder) { + public HuamiSupport setLowLatency(TransactionBuilder builder) { // TODO: low latency? return this; } - public MiBand2Support setHighLatency(TransactionBuilder builder) { + public HuamiSupport setHighLatency(TransactionBuilder builder) { // TODO: high latency? return this; } @@ -276,18 +281,18 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // MB2: AVL // TODO: tear down the notifications on quit - public MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) { + public HuamiSupport enableNotifications(TransactionBuilder builder, boolean enable) { builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable); builder.notify(getCharacteristic(GattService.UUID_SERVICE_CURRENT_TIME), enable); // Notify CHARACTERISTIC9 to receive random auth code - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), enable); + builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUTH), enable); return this; } - public MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) { - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), enable); - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable); - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_DEVICEEVENT), enable); + public HuamiSupport enableFurtherNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), enable); + builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable); + builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable); return this; } @@ -303,7 +308,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return super.connect(); } - private MiBand2Support sendDefaultNotification(TransactionBuilder builder, SimpleNotification simpleNotification, short repeat, BtLEAction extraAction) { + private HuamiSupport sendDefaultNotification(TransactionBuilder builder, SimpleNotification simpleNotification, short repeat, BtLEAction extraAction) { LOG.info("Sending notification to MiBand: (" + repeat + " times)"); NotificationStrategy strategy = getNotificationStrategy(); for (short i = 0; i < repeat; i++) { @@ -323,7 +328,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example. * @param builder */ - private MiBand2Support sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { + private HuamiSupport sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { getNotificationStrategy().sendCustomNotification(vibrationProfile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); LOG.info("Sending notification to MiBand"); return this; @@ -348,14 +353,14 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { private static final byte[] startHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 1}; private static final byte[] stopHeartMeasurementContinuous = new byte[]{0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 0}; - private MiBand2Support requestBatteryInfo(TransactionBuilder builder) { + private HuamiSupport requestBatteryInfo(TransactionBuilder builder) { LOG.debug("Requesting Battery Info!"); - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO); + BluetoothGattCharacteristic characteristic = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_6_BATTERY_INFO); builder.read(characteristic); return this; } - public MiBand2Support requestDeviceInfo(TransactionBuilder builder) { + public HuamiSupport requestDeviceInfo(TransactionBuilder builder) { LOG.debug("Requesting Device Info!"); deviceInfoProfile.requestDeviceInfo(builder); return this; @@ -368,16 +373,16 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * @return */ - private MiBand2Support setFitnessGoal(TransactionBuilder transaction) { + private HuamiSupport setFitnessGoal(TransactionBuilder transaction) { LOG.info("Attempting to set Fitness Goal..."); - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_8_USER_SETTINGS); + BluetoothGattCharacteristic characteristic = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_8_USER_SETTINGS); if (characteristic != null) { int fitnessGoal = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, 10000); byte[] bytes = ArrayUtils.addAll( - MiBand2Service.COMMAND_SET_FITNESS_GOAL_START, + HuamiService.COMMAND_SET_FITNESS_GOAL_START, BLETypeConversions.fromUint16(fitnessGoal)); bytes = ArrayUtils.addAll(bytes, - MiBand2Service.COMMAND_SET_FITNESS_GOAL_END); + HuamiService.COMMAND_SET_FITNESS_GOAL_END); transaction.write(characteristic, bytes); } else { LOG.info("Unable to set Fitness Goal"); @@ -392,8 +397,8 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * @return */ - private MiBand2Support setUserInfo(TransactionBuilder transaction) { - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_8_USER_SETTINGS); + private HuamiSupport setUserInfo(TransactionBuilder transaction) { + BluetoothGattCharacteristic characteristic = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_8_USER_SETTINGS); if (characteristic == null) { return this; } @@ -425,7 +430,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // FIXME: Do encoding like in PebbleProtocol, this is ugly byte bytes[] = new byte[]{ - MiBand2Service.COMMAND_SET_USERINFO, + HuamiService.COMMAND_SET_USERINFO, 0, 0, (byte) (birth_year & 0xff), @@ -453,18 +458,18 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * @param builder * @return */ - private MiBand2Support setWearLocation(TransactionBuilder builder) { + private HuamiSupport setWearLocation(TransactionBuilder builder) { LOG.info("Attempting to set wear location..."); - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_8_USER_SETTINGS); + BluetoothGattCharacteristic characteristic = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_8_USER_SETTINGS); if (characteristic != null) { builder.notify(characteristic, true); int location = MiBandCoordinator.getWearLocation(getDevice().getAddress()); switch (location) { case 0: // left hand - builder.write(characteristic, MiBand2Service.WEAR_LOCATION_LEFT_WRIST); + builder.write(characteristic, HuamiService.WEAR_LOCATION_LEFT_WRIST); break; case 1: // right hand - builder.write(characteristic, MiBand2Service.WEAR_LOCATION_RIGHT_WRIST); + builder.write(characteristic, HuamiService.WEAR_LOCATION_RIGHT_WRIST); break; } builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. @@ -512,27 +517,27 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * * @param builder */ - private MiBand2Support setHeartrateSleepSupport(TransactionBuilder builder) { + private HuamiSupport setHeartrateSleepSupport(TransactionBuilder builder) { final boolean enableHrSleepSupport = MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress()); if (characteristicHRControlPoint != null) { builder.notify(characteristicHRControlPoint, true); if (enableHrSleepSupport) { LOG.info("Enabling heartrate sleep support..."); - builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_ENABLE_HR_SLEEP_MEASUREMENT); + builder.write(characteristicHRControlPoint, HuamiService.COMMAND_ENABLE_HR_SLEEP_MEASUREMENT); } else { LOG.info("Disabling heartrate sleep support..."); - builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT); + builder.write(characteristicHRControlPoint, HuamiService.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT); } builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. } return this; } - private MiBand2Support setHeartrateMeasurementInterval(TransactionBuilder builder, int minutes) { + private HuamiSupport setHeartrateMeasurementInterval(TransactionBuilder builder, int minutes) { if (characteristicHRControlPoint != null) { builder.notify(characteristicHRControlPoint, true); LOG.info("Setting heart rate measurement interval to " + minutes + " minutes"); - builder.write(characteristicHRControlPoint, new byte[]{MiBand2Service.COMMAND_SET_PERIODIC_HR_MEASUREMENT_INTERVAL, (byte) minutes}); + builder.write(characteristicHRControlPoint, new byte[]{HuamiService.COMMAND_SET_PERIODIC_HR_MEASUREMENT_INTERVAL, (byte) minutes}); builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. } return this; @@ -608,7 +613,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onSetAlarms(ArrayList alarms) { try { - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION); + BluetoothGattCharacteristic characteristic = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION); TransactionBuilder builder = performInitialized("Set alarm"); boolean anyAlarmEnabled = false; for (Alarm alarm : alarms) { @@ -632,9 +637,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { onAlarmClock(notificationSpec); return; } - int alertLevel = MiBand2Service.ALERT_LEVEL_MESSAGE; + int alertLevel = HuamiService.ALERT_LEVEL_MESSAGE; if (notificationSpec.type == NotificationType.UNKNOWN) { - alertLevel = MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY; + alertLevel = HuamiService.ALERT_LEVEL_VIBRATE_ONLY; } String message = NotificationUtils.getPreferredTextFor(notificationSpec, 40, 40, getContext()).trim(); String origin = notificationSpec.type.getGenericType(); @@ -652,7 +657,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { }; String message = NotificationUtils.getPreferredTextFor(notificationSpec, 40, 40, getContext()); SimpleNotification simpleNotification = new SimpleNotification(message, AlertCategory.HighPriorityAlert, notificationSpec.type); - performPreferredNotification("alarm clock ringing", MiBandConst.ORIGIN_ALARM_CLOCK, simpleNotification, MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY, abortAction); + performPreferredNotification("alarm clock ringing", MiBandConst.ORIGIN_ALARM_CLOCK, simpleNotification, HuamiService.ALERT_LEVEL_VIBRATE_ONLY, abortAction); } @Override @@ -685,7 +690,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { }; String message = NotificationUtils.getPreferredTextFor(callSpec); SimpleNotification simpleNotification = new SimpleNotification(message, AlertCategory.IncomingCall, null); - performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, simpleNotification, MiBand2Service.ALERT_LEVEL_PHONE_CALL, abortAction); + performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, simpleNotification, HuamiService.ALERT_LEVEL_PHONE_CALL, abortAction); } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) { telephoneRinging = false; stopCurrentNotification(); @@ -718,12 +723,113 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onSetMusicState(MusicStateSpec stateSpec) { - // not supported + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); + if (!coordinator.supportsMusicInfo()) { + return; + } + + if (bufferMusicStateSpec != stateSpec) { + bufferMusicStateSpec = stateSpec; + sendMusicStateToDevice(); + } + } @Override public void onSetMusicInfo(MusicSpec musicSpec) { - // not supported + DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); + if (!coordinator.supportsMusicInfo()) { + return; + } + + if (bufferMusicSpec != musicSpec) { + bufferMusicSpec = musicSpec; + if (isMusicAppStarted) { + sendMusicStateToDevice(); + } + } + + } + + + private void sendMusicStateToDevice() { + + + if (characteristicChunked == null) { + return; + } + if (bufferMusicSpec == null || bufferMusicStateSpec == null) { + try { + TransactionBuilder builder = performInitialized("send dummy playback info to enable music controls"); + writeToChunked(builder, 3, new byte[]{1, 0, 1, 0, 0, 0, 1, 0}); + builder.queue(getQueue()); + } catch (IOException e) { + LOG.error("Unable to send dummy music controls"); + } + return; + } + + byte flags = 0x00; + flags |= 0x01; + int length = 8; + if (bufferMusicSpec.track != null && bufferMusicSpec.track.getBytes().length > 0) { + length += bufferMusicSpec.track.getBytes().length + 1; + flags |= 0x02; + } + if (bufferMusicSpec.album != null && bufferMusicSpec.album.getBytes().length > 0) { + length += bufferMusicSpec.album.getBytes().length + 1; + flags |= 0x04; + } + if (bufferMusicSpec.artist != null && bufferMusicSpec.artist.getBytes().length > 0) { + length += bufferMusicSpec.artist.getBytes().length + 1; + flags |= 0x08; + } + + +// LOG.info("Music flags are: " + (flags & 0xff)); + try { + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(flags); + byte state; + switch (bufferMusicStateSpec.state) { + case MusicStateSpec.STATE_PLAYING: + state = 1; + break; + default: + state = 0; + } + + buf.put(state); + buf.put(new byte[]{0x1, 0x0, 0x0, 0x0}); //unknown + buf.put(new byte[]{0x1,0x0}); //show track +// buf.put(new byte[]{0x1,0x1}); //show album + + + if (bufferMusicSpec.track != null && bufferMusicSpec.track.getBytes().length > 0) { + buf.put(bufferMusicSpec.track.getBytes()); + buf.put((byte) 0); + } + if (bufferMusicSpec.album != null && bufferMusicSpec.album.getBytes().length > 0) { + buf.put(bufferMusicSpec.album.getBytes()); + buf.put((byte) 0); + } + if (bufferMusicSpec.artist != null && bufferMusicSpec.artist.getBytes().length > 0) { + buf.put(bufferMusicSpec.artist.getBytes()); + buf.put((byte) 0); + } + + + TransactionBuilder builder = performInitialized("send playback info"); + writeToChunked(builder, 3, buf.array()); + + builder.queue(getQueue()); + } catch (IOException e) { + LOG.error("Unable to send playback state"); + } + +// LOG.info("Sent music: " + bufferMusicSpec.toString() + " " + bufferMusicStateSpec.toString()); + } @Override @@ -737,8 +843,8 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } } - public MiBand2Support sendReboot(TransactionBuilder builder) { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE), new byte[] { MiBand2Service.COMMAND_FIRMWARE_REBOOT}); + public HuamiSupport sendReboot(TransactionBuilder builder) { + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_FIRMWARE), new byte[] { HuamiService.COMMAND_FIRMWARE_REBOOT}); return this; } @@ -817,9 +923,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { try { TransactionBuilder builder = performInitialized(enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications"); if (enable) { - builder.read(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS)); + builder.read(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_7_REALTIME_STEPS)); } - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS), enable); + builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_7_REALTIME_STEPS), enable); builder.queue(getQueue()); enableRealtimeSamplesTimer(enable); } catch (IOException e) { @@ -920,7 +1026,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { LOG.info("Sending " + requiredButtonPressMessage + " with button_id " + currentButtonActionId); this.getContext().getApplicationContext().sendBroadcast(in); if (prefs.getBoolean(MiBandConst.PREF_MIBAND_BUTTON_ACTION_VIBRATE, false)) { - performPreferredNotification(null, null, null, MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY, null); + performPreferredNotification(null, null, null, HuamiService.ALERT_LEVEL_VIBRATE_ONLY, null); } currentButtonActionId = 0; @@ -930,7 +1036,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } public void handleDeviceEvent(byte[] value) { - if (value == null || value.length != 1) { + if (value == null || value.length == 0) { return; } GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); @@ -981,6 +1087,44 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP; evaluateGBDeviceEvent(findPhoneEvent); break; + case HuamiDeviceEvent.MUSIC_CONTROL: + LOG.info("got music control"); + GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl(); + + switch (value[1]) { + case 0: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAY; + break; + case 1: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PAUSE; + break; + case 3: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT; + break; + case 4: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS; + break; + case 5: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP; + break; + case 6: + deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN; + break; + case (byte) 224: + LOG.info("Music app started"); + isMusicAppStarted = true; + sendMusicStateToDevice(); + break; + case (byte) 225: + LOG.info("Music app terminated"); + isMusicAppStarted = false; + break; + default: + LOG.info("unhandled music control event " + value[1]); + return; + } + evaluateGBDeviceEvent(deviceEventMusicControl); + break; default: LOG.warn("unhandled event " + value[0]); } @@ -990,7 +1134,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { try { TransactionBuilder builder = performInitialized("acknowledge find phone"); - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), AmazfitBipService.COMMAND_ACK_FIND_PHONE_IN_PROGRESS); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), AmazfitBipService.COMMAND_ACK_FIND_PHONE_IN_PROGRESS); builder.queue(getQueue()); } catch (Exception ex) { LOG.error("Error sending current weather", ex); @@ -1051,7 +1195,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { super.onCharacteristicChanged(gatt, characteristic); UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) { + if (HuamiService.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS); return true; } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { @@ -1060,14 +1204,14 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } else if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { handleHeartrate(characteristic.getValue()); return true; - } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + } else if (HuamiService.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { LOG.info("AUTHENTICATION?? " + characteristicUUID); logMessageContent(characteristic.getValue()); return true; - } else if (MiBand2Service.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) { + } else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) { handleDeviceEvent(characteristic.getValue()); return true; - } else if (MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { + } else if (HuamiService.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); return true; } else { @@ -1087,16 +1231,16 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) { handleDeviceName(characteristic.getValue(), status); return true; - } else if (MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) { + } else if (HuamiService.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), status); return true; } else if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { logHeartrate(characteristic.getValue(), status); return true; - } else if (MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { + } else if (HuamiService.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); return true; - } else if (MiBand2Service.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) { + } else if (HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT.equals(characteristicUUID)) { handleDeviceEvent(characteristic.getValue()); return true; } else { @@ -1111,7 +1255,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + if (HuamiService.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { LOG.info("KEY AES SEND"); logMessageContent(characteristic.getValue()); return true; @@ -1282,8 +1426,10 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { LOG.warn("Device info: " + info); versionCmd.hwVersion = info.getHardwareRevision(); -// versionCmd.fwVersion = info.getFirmwareRevision(); // always null - versionCmd.fwVersion = info.getSoftwareRevision(); + versionCmd.fwVersion = info.getFirmwareRevision(); + if (versionCmd.fwVersion == null) { + versionCmd.fwVersion = info.getSoftwareRevision(); + } if (versionCmd.fwVersion != null && versionCmd.fwVersion.length() > 0 && versionCmd.fwVersion.charAt(0) == 'V') { versionCmd.fwVersion = versionCmd.fwVersion.substring(1); } @@ -1305,8 +1451,8 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * Fetch the events from the android device calendars and set the alarms on the miband. * @param builder */ - private MiBand2Support sendCalendarEvents(TransactionBuilder builder) { - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION); + private HuamiSupport sendCalendarEvents(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristic = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION); Prefs prefs = GBApplication.getPrefs(); int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); @@ -1395,55 +1541,55 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } - private MiBand2Support setDateDisplay(TransactionBuilder builder) { + private HuamiSupport setDateDisplay(TransactionBuilder builder) { DateTimeDisplay dateTimeDisplay = HuamiCoordinator.getDateDisplay(getContext()); LOG.info("Setting date display to " + dateTimeDisplay); switch (dateTimeDisplay) { case TIME: - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.DATEFORMAT_TIME); break; case DATE_TIME: - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_DATE_TIME); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.DATEFORMAT_DATE_TIME); break; } return this; } - private MiBand2Support setTimeFormat(TransactionBuilder builder) { + private HuamiSupport setTimeFormat(TransactionBuilder builder) { boolean is24Format = DateFormat.is24HourFormat(getContext()); LOG.info("Setting 24h time format to " + is24Format); if (is24Format) { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME_24_HOURS); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.DATEFORMAT_TIME_24_HOURS); } else { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME_12_HOURS); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.DATEFORMAT_TIME_12_HOURS); } return this; } - private MiBand2Support setGoalNotification(TransactionBuilder builder) { + private HuamiSupport setGoalNotification(TransactionBuilder builder) { boolean enable = HuamiCoordinator.getGoalNotification(); LOG.info("Setting goal notification to " + enable); if (enable) { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_GOAL_NOTIFICATION); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENABLE_GOAL_NOTIFICATION); } else { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_GOAL_NOTIFICATION); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_GOAL_NOTIFICATION); } return this; } - private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) { + private HuamiSupport setActivateDisplayOnLiftWrist(TransactionBuilder builder) { ActivateDisplayOnLift displayOnLift = HuamiCoordinator.getActivateDisplayOnLiftWrist(getContext()); LOG.info("Setting activate display on lift wrist to " + displayOnLift); switch (displayOnLift) { case ON: - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST); break; case OFF: - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST); break; case SCHEDULED: - byte[] cmd = MiBand2Service.COMMAND_SCHEDULE_DISPLAY_ON_LIFT_WRIST.clone(); + byte[] cmd = HuamiService.COMMAND_SCHEDULE_DISPLAY_ON_LIFT_WRIST.clone(); Calendar calendar = GregorianCalendar.getInstance(); @@ -1457,81 +1603,81 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { cmd[6] = (byte) calendar.get(Calendar.HOUR_OF_DAY); cmd[7] = (byte) calendar.get(Calendar.MINUTE); - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), cmd); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), cmd); } return this; } - protected MiBand2Support setDisplayItems(TransactionBuilder builder) { + protected HuamiSupport setDisplayItems(TransactionBuilder builder) { Set pages = HuamiCoordinator.getDisplayItems(); LOG.info("Setting display items to " + (pages == null ? "none" : pages)); - byte[] data = MiBand2Service.COMMAND_CHANGE_SCREENS.clone(); + byte[] data = HuamiService.COMMAND_CHANGE_SCREENS.clone(); if (pages != null) { if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_STEPS)) { - data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_STEPS; + data[HuamiService.SCREEN_CHANGE_BYTE] |= HuamiService.DISPLAY_ITEM_BIT_STEPS; } if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_DISTANCE)) { - data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_DISTANCE; + data[HuamiService.SCREEN_CHANGE_BYTE] |= HuamiService.DISPLAY_ITEM_BIT_DISTANCE; } if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_CALORIES)) { - data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_CALORIES; + data[HuamiService.SCREEN_CHANGE_BYTE] |= HuamiService.DISPLAY_ITEM_BIT_CALORIES; } if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_HEART_RATE)) { - data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_HEART_RATE; + data[HuamiService.SCREEN_CHANGE_BYTE] |= HuamiService.DISPLAY_ITEM_BIT_HEART_RATE; } if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_BATTERY)) { - data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_BATTERY; + data[HuamiService.SCREEN_CHANGE_BYTE] |= HuamiService.DISPLAY_ITEM_BIT_BATTERY; } } - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), data); return this; } - private MiBand2Support setRotateWristToSwitchInfo(TransactionBuilder builder) { + private HuamiSupport setRotateWristToSwitchInfo(TransactionBuilder builder) { boolean enable = HuamiCoordinator.getRotateWristToSwitchInfo(); LOG.info("Setting rotate wrist to cycle info to " + enable); if (enable) { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_ROTATE_WRIST_TO_SWITCH_INFO); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENABLE_ROTATE_WRIST_TO_SWITCH_INFO); } else { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_ROTATE_WRIST_TO_SWITCH_INFO); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_ROTATE_WRIST_TO_SWITCH_INFO); } return this; } - private MiBand2Support setDisplayCaller(TransactionBuilder builder) { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_DISPLAY_CALLER); + private HuamiSupport setDisplayCaller(TransactionBuilder builder) { + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENABLE_DISPLAY_CALLER); return this; } - private MiBand2Support setDoNotDisturb(TransactionBuilder builder) { + private HuamiSupport setDoNotDisturb(TransactionBuilder builder) { DoNotDisturb doNotDisturb = HuamiCoordinator.getDoNotDisturb(getContext()); LOG.info("Setting do not disturb to " + doNotDisturb); switch (doNotDisturb) { case OFF: - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DO_NOT_DISTURB_OFF); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DO_NOT_DISTURB_OFF); break; case AUTOMATIC: - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DO_NOT_DISTURB_AUTOMATIC); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DO_NOT_DISTURB_AUTOMATIC); break; case SCHEDULED: - byte[] data = MiBand2Service.COMMAND_DO_NOT_DISTURB_SCHEDULED.clone(); + byte[] data = HuamiService.COMMAND_DO_NOT_DISTURB_SCHEDULED.clone(); Calendar calendar = GregorianCalendar.getInstance(); Date start = HuamiCoordinator.getDoNotDisturbStart(); calendar.setTime(start); - data[MiBand2Service.DND_BYTE_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.DND_BYTE_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.DND_BYTE_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.DND_BYTE_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE); Date end = HuamiCoordinator.getDoNotDisturbEnd(); calendar.setTime(end); - data[MiBand2Service.DND_BYTE_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.DND_BYTE_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.DND_BYTE_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.DND_BYTE_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), data); break; } @@ -1539,15 +1685,15 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return this; } - private MiBand2Support setInactivityWarnings(TransactionBuilder builder) { + private HuamiSupport setInactivityWarnings(TransactionBuilder builder) { boolean enable = HuamiCoordinator.getInactivityWarnings(); LOG.info("Setting inactivity warnings to " + enable); if (enable) { - byte[] data = MiBand2Service.COMMAND_ENABLE_INACTIVITY_WARNINGS.clone(); + byte[] data = HuamiService.COMMAND_ENABLE_INACTIVITY_WARNINGS.clone(); int threshold = HuamiCoordinator.getInactivityWarningsThreshold(); - data[MiBand2Service.INACTIVITY_WARNINGS_THRESHOLD] = (byte) threshold; + data[HuamiService.INACTIVITY_WARNINGS_THRESHOLD] = (byte) threshold; Calendar calendar = GregorianCalendar.getInstance(); @@ -1560,50 +1706,78 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // The first interval always starts when the warnings interval starts calendar.setTime(intervalStart); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_1_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_1_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE); if(enableDnd) { // The first interval ends when the dnd interval starts calendar.setTime(dndStart); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); // The second interval starts when the dnd interval ends calendar.setTime(dndEnd); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_2_START_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_2_START_MINUTES] = (byte) calendar.get(Calendar.MINUTE); // ... and it ends when the warnings interval ends calendar.setTime(intervalEnd); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_2_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_2_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_2_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); } else { // No Dnd, use the first interval calendar.setTime(intervalEnd); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); - data[MiBand2Service.INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + data[HuamiService.INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES] = (byte) calendar.get(Calendar.MINUTE); } - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), data); } else { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_INACTIVITY_WARNINGS); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_INACTIVITY_WARNINGS); } return this; } - private MiBand2Support setDistanceUnit(TransactionBuilder builder) { + private HuamiSupport setDistanceUnit(TransactionBuilder builder) { MiBandConst.DistanceUnit unit = HuamiCoordinator.getDistanceUnit(); LOG.info("Setting distance unit to " + unit); if (unit == MiBandConst.DistanceUnit.METRIC) { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISTANCE_UNIT_METRIC); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISTANCE_UNIT_METRIC); } else { - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISTANCE_UNIT_IMPERIAL); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISTANCE_UNIT_IMPERIAL); } return this; } + protected void writeToChunked(TransactionBuilder builder, int type, byte[] data) { + final int MAX_CHUNKLENGTH = 17; + int remaining = data.length; + byte count = 0; + while (remaining > 0) { + int copybytes = Math.min(remaining, MAX_CHUNKLENGTH); + byte[] chunk = new byte[copybytes + 3]; + + byte flags = 0; + if (remaining <= MAX_CHUNKLENGTH) { + flags |= 0x80; // last chunk + if (count == 0) { + flags |= 0x40; // weird but true + } + } else if (count > 0) { + flags |= 0x40; // consecutive chunk + } + + chunk[0] = 0; + chunk[1] = (byte) (flags | type); + chunk[2] = (byte) (count & 0xff); + + System.arraycopy(data, count++ * MAX_CHUNKLENGTH, chunk, 3, copybytes); + builder.write(characteristicChunked, chunk); + remaining -= copybytes; + } + } + public void phase2Initialize(TransactionBuilder builder) { LOG.info("phase2Initialize..."); requestBatteryInfo(builder); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/actions/StopNotificationAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/actions/StopNotificationAction.java similarity index 89% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/actions/StopNotificationAction.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/actions/StopNotificationAction.java index 97e41aaef..7bcb4c38d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/actions/StopNotificationAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/actions/StopNotificationAction.java @@ -14,12 +14,12 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.actions; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.actions; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; public abstract class StopNotificationAction extends AbortTransactionAction { @@ -34,7 +34,7 @@ public abstract class StopNotificationAction extends AbortTransactionAction { public boolean run(BluetoothGatt gatt) { if (!super.run(gatt)) { // send a signal to stop the vibration - alertLevelCharacteristic.setValue(new byte[]{MiBand2Service.ALERT_LEVEL_NONE}); + alertLevelCharacteristic.setValue(new byte[]{HuamiService.ALERT_LEVEL_NONE}); gatt.writeCharacteristic(alertLevelCharacteristic); return false; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/ActivityDetailsParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/ActivityDetailsParser.java index eaec8cc8e..b39f09c78 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/ActivityDetailsParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/ActivityDetailsParser.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, AndrewH, Carsten Pfeiffer +/* Copyright (C) 2017-2018 Andreas Shimokawa, AndrewH, Carsten Pfeiffer, + szilardx This file is part of Gadgetbridge. @@ -21,7 +22,10 @@ import org.slf4j.LoggerFactory; import java.math.BigDecimal; import java.math.RoundingMode; +import java.util.ArrayList; import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; @@ -44,7 +48,7 @@ public class ActivityDetailsParser { public static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0); private final BaseActivitySummary summary; private final ActivityTrack activityTrack; -// private final int version; + // private final int version; private final Date baseDate; private long baseLongitude; private long baseLatitude; @@ -126,9 +130,49 @@ public class ActivityDetailsParser { throw new GBException("Error parsing activity details: " + ex.getMessage(), ex); } + fixupMissingTimestamps(activityTrack); + return activityTrack; } + private void fixupMissingTimestamps(ActivityTrack activityTrack) { + try { + int pointer = 0; + List activityPointList = activityTrack.getTrackPoints(); + Date gpsStartTime = null; + List entriesToFixUp = new ArrayList<>(); + while (pointer < activityPointList.size() - 1) { + ActivityPoint activityPoint = activityPointList.get(pointer); + if (activityPoint.getLocation() == null) { + pointer++; + continue; + } + if (activityPoint.getTime().equals(activityPointList.get(pointer + 1).getTime())) { + entriesToFixUp.add(activityPoint); + } else { + // found the first activity point with a proper timestamp + entriesToFixUp.add(activityPoint); + gpsStartTime = activityPointList.get(pointer + 1).getTime(); + break; + } + pointer++; + } + if (gpsStartTime != null) { + // now adjust those entries without a timestamp + long differenceInSec = TimeUnit.SECONDS.convert(Math.abs(gpsStartTime.getTime() - baseDate.getTime()), TimeUnit.MILLISECONDS); + + double multiplier = (double) differenceInSec / (double) (entriesToFixUp.size()); + + for (int j = 0; j < entriesToFixUp.size(); j++) { + long timeOffsetSeconds = Math.round(j * multiplier); + entriesToFixUp.get(j).setTime(makeAbsolute(timeOffsetSeconds)); + } + } + } catch (Exception ex) { + LOG.warn("Error cleaning activity details", ex); + } + } + private int consumeGPSAndUpdateBaseLocation(byte[] bytes, int offset, long timeOffset) { int i = 0; int longitudeDelta = BLETypeConversions.toInt16(bytes[offset + i++], bytes[offset + i++]); @@ -144,7 +188,7 @@ public class ActivityDetailsParser { convertHuamiValueToDecimalDegrees(baseLatitude), baseAltitude); - ActivityPoint ap = getActivityPointFor(timeOffset); + ActivityPoint ap = getActivityPointFor(timeOffset, coordinate); ap.setLocation(coordinate); add(ap); @@ -197,6 +241,19 @@ public class ActivityDetailsParser { return new ActivityPoint(time); } + private ActivityPoint getActivityPointFor(long timeOffsetSeconds, GPSCoordinate gpsCoordinate) { + Date time = makeAbsolute(timeOffsetSeconds); + if (lastActivityPoint != null) { + if (lastActivityPoint.getTime().equals(time)) { + if (lastActivityPoint.getLocation() != null && !lastActivityPoint.getLocation().equals(gpsCoordinate)) { + return new ActivityPoint(time); + } + return lastActivityPoint; + } + } + return new ActivityPoint(time); + } + private Date makeAbsolute(long timeOffsetSeconds) { return new Date(baseDate.getTime() + timeOffsetSeconds * 1000); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipFirmwareInfo.java index 7457f22c2..17ee1b594 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipFirmwareInfo.java @@ -93,6 +93,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo { crcToVersion.put(56670, "0.1.1.41"); crcToVersion.put(58736, "0.1.1.45"); crcToVersion.put(2602, "1.0.2.00"); + crcToVersion.put(36157, "1.1.2.05"); // resources crcToVersion.put(12586, "0.0.8.74"); @@ -115,6 +116,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo { crcToVersion.put(21109, "0.1.1.41"); crcToVersion.put(23073, "0.1.1.45"); crcToVersion.put(59245, "1.0.2.00"); + crcToVersion.put(20591, "1.1.2.05"); // gps crcToVersion.put(61520, "9367,8f79a91,0,0,"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java index 8ae653ac5..58d9270d3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipSupport.java @@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.net.Uri; import org.slf4j.Logger; @@ -34,10 +36,10 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -50,17 +52,17 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWrit import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.operations.AmazfitBipFetchLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiIcon; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.operations.AmazfitBipFetchLogsOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.FetchActivityOperation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.FetchSportsSummaryOperation; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import nodomain.freeyourgadget.gadgetbridge.util.Version; -public class AmazfitBipSupport extends MiBand2Support { +public class AmazfitBipSupport extends HuamiSupport { private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipSupport.class); @@ -92,11 +94,8 @@ public class AmazfitBipSupport extends MiBand2Support { try { TransactionBuilder builder = performInitialized("new notification"); - AlertNotificationProfile profile = new AlertNotificationProfile(this); - profile.setMaxLength(230); byte customIconId = HuamiIcon.mapToIconId(notificationSpec.type); - AlertCategory alertCategory = AlertCategory.CustomHuami; // The SMS icon for AlertCategory.SMS is unique and not available as iconId @@ -108,8 +107,54 @@ public class AmazfitBipSupport extends MiBand2Support { alertCategory = AlertCategory.Email; } - NewAlert alert = new NewAlert(alertCategory, 1, message, customIconId); - profile.newAlert(builder, alert); + int maxLength = 230; + if (characteristicChunked != null) { + int prefixlength = 2; + + // We also need a (fake) source name for Mi Band 3 for SMS/EMAIL, else the message is not displayed + byte[] appSuffix = "\0 \0".getBytes(); + int suffixlength = appSuffix.length; + + if (alertCategory == AlertCategory.CustomHuami) { + String appName; + prefixlength = 3; + final PackageManager pm = getContext().getPackageManager(); + ApplicationInfo ai = null; + try { + ai = pm.getApplicationInfo(notificationSpec.sourceAppId, 0); + } catch (PackageManager.NameNotFoundException ignored) { + } + + if (ai != null) { + appName = "\0" + pm.getApplicationLabel(ai) + "\0"; + } else { + appName = "\0" + "UNKNOWN" + "\0"; + } + appSuffix = appName.getBytes(); + suffixlength = appSuffix.length; + } + + byte[] rawmessage = message.getBytes(); + int length = Math.min(rawmessage.length, maxLength - prefixlength); + + byte[] command = new byte[length + prefixlength + suffixlength]; + + command[0] = (byte) alertCategory.getId(); + command[1] = 1; + if (alertCategory == AlertCategory.CustomHuami) { + command[2] = customIconId; + } + + System.arraycopy(rawmessage, 0, command, prefixlength, length); + System.arraycopy(appSuffix, 0, command, prefixlength + length, appSuffix.length); + + writeToChunked(builder, 0, command); + } else { + AlertNotificationProfile profile = new AlertNotificationProfile(this); + NewAlert alert = new NewAlert(alertCategory, 1, message, customIconId); + profile.setMaxLength(maxLength); + profile.newAlert(builder, alert); + } builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to send notification to Amazfit Bip", ex); @@ -131,9 +176,6 @@ public class AmazfitBipSupport extends MiBand2Support { @Override protected AmazfitBipSupport setDisplayItems(TransactionBuilder builder) { - if (gbDevice.getType() != DeviceType.AMAZFITBIP) { - return this; // Disable for Cor for now - } if (gbDevice.getFirmwareVersion() == null) { LOG.warn("Device not initialized yet, won't set menu items"); return this; @@ -185,7 +227,7 @@ public class AmazfitBipSupport extends MiBand2Support { shortcut_alipay = true; } } - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), command); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command); setShortcuts(builder, shortcut_weather, shortcut_alipay); return this; @@ -201,7 +243,7 @@ public class AmazfitBipSupport extends MiBand2Support { (byte) ((alipay && weather) ? 0x81 : 0x01), 0x01, }; - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), command); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command); } @Override @@ -241,13 +283,18 @@ public class AmazfitBipSupport extends MiBand2Support { buf.put((byte) 0); } - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + if (characteristicChunked != null) { + writeToChunked(builder, 1, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + builder.queue(getQueue()); } catch (Exception ex) { LOG.error("Error sending current weather", ex); } - if (gbDevice.getType() == DeviceType.AMAZFITBIP) { + if (gbDevice.getType() != DeviceType.AMAZFITCOR) { try { TransactionBuilder builder; builder = performInitialized("Sending air quality index"); @@ -266,7 +313,13 @@ public class AmazfitBipSupport extends MiBand2Support { buf.put(aqiString.getBytes()); buf.put((byte) 0); } - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + + if (characteristicChunked != null) { + writeToChunked(builder, 1, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Error sending air quality"); @@ -310,7 +363,6 @@ public class AmazfitBipSupport extends MiBand2Support { for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) { condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(forecast.conditionCode); - buf.put(condition); buf.put(condition); buf.put((byte) (forecast.maxTemp - 273)); @@ -321,7 +373,12 @@ public class AmazfitBipSupport extends MiBand2Support { } } - builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + if (characteristicChunked != null) { + writeToChunked(builder, 1, buf.array()); + } else { + builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array()); + } + builder.queue(getQueue()); } catch (Exception ex) { LOG.error("Error sending weather forecast", ex); @@ -372,7 +429,7 @@ public class AmazfitBipSupport extends MiBand2Support { boolean handled = super.onCharacteristicChanged(gatt, characteristic); if (!handled) { UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION.equals(characteristicUUID)) { + if (HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION.equals(characteristicUUID)) { return handleConfigurationInfo(characteristic.getValue()); } } @@ -395,11 +452,11 @@ public class AmazfitBipSupport extends MiBand2Support { // this probably does more than only getting the GPS version... private AmazfitBipSupport requestGPSVersion(TransactionBuilder builder) { LOG.info("Requesting GPS version"); - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), AmazfitBipService.COMMAND_REQUEST_GPS_VERSION); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), AmazfitBipService.COMMAND_REQUEST_GPS_VERSION); return this; } - private AmazfitBipSupport setLanguage(TransactionBuilder builder) { + protected AmazfitBipSupport setLanguage(TransactionBuilder builder) { String language = Locale.getDefault().getLanguage(); String country = Locale.getDefault().getCountry(); @@ -427,6 +484,10 @@ public class AmazfitBipSupport extends MiBand2Support { command_old = AmazfitBipService.COMMAND_SET_LANGUAGE_SPANISH; localeString = "es_ES"; break; + case 4: + command_old = AmazfitBipService.COMMAND_SET_LANGUAGE_ENGLISH; + localeString = "ru_RU"; + break; default: switch (language) { case "zh": @@ -442,19 +503,24 @@ public class AmazfitBipSupport extends MiBand2Support { command_old = AmazfitBipService.COMMAND_SET_LANGUAGE_SPANISH; localeString = "es_ES"; break; + case "ru": + command_old = AmazfitBipService.COMMAND_SET_LANGUAGE_ENGLISH; + localeString = "ru_RU"; + break; default: command_old = AmazfitBipService.COMMAND_SET_LANGUAGE_ENGLISH; localeString = "en_US"; break; } } - command_new = AmazfitBipService.COMMAND_SET_LANGUAGE_NEW_TEMPLATE; + command_new = HuamiService.COMMAND_SET_LANGUAGE_NEW_TEMPLATE.clone(); System.arraycopy(localeString.getBytes(), 0, command_new, 3, localeString.getBytes().length); - builder.add(new ConditionalWriteAction(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION)) { + builder.add(new ConditionalWriteAction(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION)) { @Override protected byte[] checkCondition() { - if (gbDevice.getType() == DeviceType.MIBAND3 || (gbDevice.getType() == DeviceType.AMAZFITBIP && new Version(gbDevice.getFirmwareVersion()).compareTo(new Version("0.1.0.77")) >= 0)) { + if ((gbDevice.getType() == DeviceType.AMAZFITBIP && new Version(gbDevice.getFirmwareVersion()).compareTo(new Version("0.1.0.77")) >= 0) || + (gbDevice.getType() == DeviceType.AMAZFITCOR && new Version(gbDevice.getFirmwareVersion()).compareTo(new Version("1.0.7.23")) >= 0)) { return command_new; } else { return command_old; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipTextNotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipTextNotificationStrategy.java index 60547c9ce..e62961720 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipTextNotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipTextNotificationStrategy.java @@ -26,14 +26,14 @@ 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.devices.common.SimpleNotification; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.Mi2TextNotificationStrategy; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; // This class in no longer in use except for incoming calls class AmazfitBipTextNotificationStrategy extends Mi2TextNotificationStrategy { - AmazfitBipTextNotificationStrategy(MiBand2Support support) { + AmazfitBipTextNotificationStrategy(HuamiSupport support) { super(support); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java index 0aa62184f..bbaeb2c85 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/operations/AmazfitBipFetchLogsOperation.java @@ -33,12 +33,12 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations.AbstractFetchOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -75,12 +75,12 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation { GregorianCalendar sinceWhen = BLETypeConversions.createCalendar(); sinceWhen.add(Calendar.DAY_OF_MONTH, -10); builder.write(characteristicFetch, BLETypeConversions.join(new byte[]{ - MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, + HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_DEBUGLOGS}, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply builder.notify(characteristicActivityData, true); - builder.write(characteristicFetch, new byte[]{MiBand2Service.COMMAND_FETCH_DATA}); + builder.write(characteristicFetch, new byte[]{HuamiService.COMMAND_FETCH_DATA}); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorFirmwareInfo.java index b7395e791..bf171699f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorFirmwareInfo.java @@ -42,6 +42,7 @@ public class AmazfitCorFirmwareInfo extends HuamiFirmwareInfo { crcToVersion.put(54213, "1.0.6.76"); crcToVersion.put(9458, "1.0.7.52"); crcToVersion.put(51575, "1.0.7.88"); + crcToVersion.put(6346, "1.2.5.00"); // resources crcToVersion.put(46341, "RES 1.0.5.60"); @@ -49,6 +50,7 @@ public class AmazfitCorFirmwareInfo extends HuamiFirmwareInfo { crcToVersion.put(64977, "RES 1.0.6.76"); crcToVersion.put(60501, "RES 1.0.7.52-71"); crcToVersion.put(31263, "RES 1.0.7.77-91"); + crcToVersion.put(20920, "RES 1.2.5.00-65"); // font crcToVersion.put(61054, "8"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java index 60164c544..a3d614624 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitcor/AmazfitCorSupport.java @@ -19,14 +19,67 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor; import android.content.Context; import android.net.Uri; -import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.Set; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitcor.AmazfitCorFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitcor.AmazfitCorService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class AmazfitCorSupport extends AmazfitBipSupport { + private static final Logger LOG = LoggerFactory.getLogger(AmazfitCorSupport.class); + + @Override + protected AmazfitCorSupport setDisplayItems(TransactionBuilder builder) { + + Prefs prefs = GBApplication.getPrefs(); + Set pages = prefs.getStringSet("cor_display_items", null); + LOG.info("Setting display items to " + (pages == null ? "none" : pages)); + byte[] command = AmazfitCorService.COMMAND_CHANGE_SCREENS.clone(); + + if (pages != null) { + if (pages.contains("status")) { + command[1] |= 0x02; + } + if (pages.contains("notifications")) { + command[1] |= 0x04; + } + if (pages.contains("activity")) { + command[1] |= 0x08; + } + if (pages.contains("weather")) { + command[1] |= 0x10; + } + if (pages.contains("alarm")) { + command[1] |= 0x20; + } + if (pages.contains("timer")) { + command[1] |= 0x40; + } + if (pages.contains("settings")) { + command[1] |= 0x80; + } + if (pages.contains("alipay")) { + command[2] |= 0x01; + } + if (pages.contains("music")) { + command[2] |= 0x02; + } + } + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command); + + return this; + } + @Override public HuamiFWHelper createFWHelper(Uri uri, Context context) throws IOException { return new AmazfitCorFWHelper(uri, context); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2NotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2NotificationStrategy.java index 880ad8eaa..7628a17a0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2NotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2NotificationStrategy.java @@ -24,13 +24,14 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.V2NotificationStrategy; -public class Mi2NotificationStrategy extends V2NotificationStrategy { +public class Mi2NotificationStrategy extends V2NotificationStrategy { private final BluetoothGattCharacteristic alertLevelCharacteristic; - public Mi2NotificationStrategy(MiBand2Support support) { + public Mi2NotificationStrategy(HuamiSupport support) { super(support); alertLevelCharacteristic = support.getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java index b69ea329b..292aeebb8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/Mi2TextNotificationStrategy.java @@ -30,12 +30,13 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotificat import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.OverflowStrategy; import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiIcon; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class Mi2TextNotificationStrategy extends Mi2NotificationStrategy { private final BluetoothGattCharacteristic newAlertCharacteristic; - public Mi2TextNotificationStrategy(MiBand2Support support) { + public Mi2TextNotificationStrategy(HuamiSupport support) { super(support); newAlertCharacteristic = support.getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_NEW_ALERT); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3FirmwareInfo.java index fcfda8127..a821a74f7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3FirmwareInfo.java @@ -43,8 +43,22 @@ public class MiBand3FirmwareInfo extends HuamiFirmwareInfo { static { // firmware + crcToVersion.put(55852, "1.2.0.8"); + crcToVersion.put(14899, "1.3.0.4"); + crcToVersion.put(20651, "1.3.0.8"); + crcToVersion.put(60781, "1.4.0.12"); + crcToVersion.put(30045, "1.5.0.2"); + crcToVersion.put(38254, "1.5.0.7"); + crcToVersion.put(46985, "1.5.0.11"); // resources + crcToVersion.put(54724, "1.2.0.8"); + crcToVersion.put(52589, "1.3.0.4"); + crcToVersion.put(34642, "1.3.0.8"); + crcToVersion.put(25278, "1.4.0.12-1.5.0.11"); + + // font + crcToVersion.put(19775, "1"); } public MiBand3FirmwareInfo(byte[] bytes) { @@ -60,7 +74,7 @@ public class MiBand3FirmwareInfo extends HuamiFirmwareInfo { return HuamiFirmwareType.INVALID; } if (ArrayUtils.startsWith(bytes, RES_HEADER)) { - if (bytes.length > 100000) { // don't know how to distinguish from Bip/Cor .res + if (bytes.length > 150000) { // don't know how to distinguish from Bip/Cor .res return HuamiFirmwareType.INVALID; } return HuamiFirmwareType.RES; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java index 930977c9f..70192943f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband3/MiBand3Support.java @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer +/* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, José Rebelo This file is part of Gadgetbridge. @@ -18,15 +18,180 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3; import android.content.Context; import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.Set; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3FWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Service; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class MiBand3Support extends AmazfitBipSupport { + private static final Logger LOG = LoggerFactory.getLogger(MiBand3Support.class); + + @Override + protected MiBand3Support setDisplayItems(TransactionBuilder builder) { + Prefs prefs = GBApplication.getPrefs(); + Set pages = prefs.getStringSet("miband3_display_items", null); + LOG.info("Setting display items to " + (pages == null ? "none" : pages)); + byte[] command = MiBand3Service.COMMAND_CHANGE_SCREENS.clone(); + + byte pos = 1; + if (pages != null) { + if (pages.contains("notifications")) { + command[1] |= 0x02; + command[4] = pos++; + } + if (pages.contains("weather")) { + command[1] |= 0x04; + command[5] = pos++; + } + if (pages.contains("more")) { + command[1] |= 0x10; + command[7] = pos++; + } + if (pages.contains("status")) { + command[1] |= 0x20; + command[8] = pos++; + } + if (pages.contains("heart_rate")) { + command[1] |= 0x40; + command[9] = pos++; + } + } + + for (int i = 4; i <= 9; i++) { + if (command[i] == 0) { + command[i] = pos++; + } + } + + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command); + + return this; + } + + @Override + public void onSendConfiguration(String config) { + TransactionBuilder builder; + try { + builder = performInitialized("Sending configuration for option: " + config); + switch (config) { + case MiBandConst.PREF_MI3_BAND_SCREEN_UNLOCK: + setBandScreenUnlock(builder); + break; + case MiBandConst.PREF_MI3_NIGHT_MODE: + case MiBandConst.PREF_MI3_NIGHT_MODE_START: + case MiBandConst.PREF_MI3_NIGHT_MODE_END: + setNightMode(builder); + break; + default: + super.onSendConfiguration(config); + return; + } + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e); + } + } + + @Override + protected MiBand3Support setLanguage(TransactionBuilder builder) { + String localeString = GBApplication.getPrefs().getString("miband3_language", "auto"); + + if (localeString.equals("auto")) { + String language = Locale.getDefault().getLanguage(); + String country = Locale.getDefault().getCountry(); + + if (country == null) { + // sometimes country is null, no idea why, guess it. + country = language; + } + localeString = language + "_" + country.toUpperCase(); + } + LOG.info("Setting device to locale: " + localeString); + byte[] command_new = HuamiService.COMMAND_SET_LANGUAGE_NEW_TEMPLATE.clone(); + System.arraycopy(localeString.getBytes(), 0, command_new, 3, localeString.getBytes().length); + + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command_new); + + return this; + } + + private MiBand3Support setBandScreenUnlock(TransactionBuilder builder) { + boolean enable = MiBand3Coordinator.getBandScreenUnlock(); + LOG.info("Setting band screen unlock to " + enable); + + if (enable) { + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand3Service.COMMAND_ENABLE_BAND_SCREEN_UNLOCK); + } else { + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand3Service.COMMAND_DISABLE_BAND_SCREEN_UNLOCK); + } + + return this; + } + + private MiBand3Support setNightMode(TransactionBuilder builder) { + String nightMode = MiBand3Coordinator.getNightMode(); + LOG.info("Setting night mode to " + nightMode); + + switch (nightMode) { + case MiBandConst.PREF_MI3_NIGHT_MODE_SUNSET: + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand3Service.COMMAND_NIGHT_MODE_SUNSET); + break; + case MiBandConst.PREF_MI3_NIGHT_MODE_OFF: + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand3Service.COMMAND_NIGHT_MODE_OFF); + break; + case MiBandConst.PREF_MI3_NIGHT_MODE_SCHEDULED: + byte[] cmd = MiBand3Service.COMMAND_NIGHT_MODE_SCHEDULED.clone(); + + Calendar calendar = GregorianCalendar.getInstance(); + + Date start = MiBand3Coordinator.getNightModeStart(); + calendar.setTime(start); + cmd[2] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + cmd[3] = (byte) calendar.get(Calendar.MINUTE); + + Date end = MiBand3Coordinator.getNightModeEnd(); + calendar.setTime(end); + cmd[4] = (byte) calendar.get(Calendar.HOUR_OF_DAY); + cmd[5] = (byte) calendar.get(Calendar.MINUTE); + + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), cmd); + break; + default: + LOG.error("Invalid night mode: " + nightMode); + break; + } + + return this; + } + + @Override + public void phase2Initialize(TransactionBuilder builder) { + super.phase2Initialize(builder); + LOG.info("phase2Initialize..."); + setBandScreenUnlock(builder); + setNightMode(builder); + } + @Override public HuamiFWHelper createFWHelper(Uri uri, Context context) throws IOException { return new MiBand3FWHelper(uri, context); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/AbstractFetchOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java similarity index 89% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/AbstractFetchOperation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java index 1d0b5e72c..ab3bab6b8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/AbstractFetchOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/AbstractFetchOperation.java @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -36,12 +36,12 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.AbstractMiBand2Operation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiOperation; import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -49,7 +49,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB; * An operation that fetches activity data. For every fetch, a new operation must * be created, i.e. an operation may not be reused for multiple fetches. */ -public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { +public abstract class AbstractFetchOperation extends AbstractHuamiOperation { private static final Logger LOG = LoggerFactory.getLogger(AbstractFetchOperation.class); protected byte lastPacketCounter; @@ -59,7 +59,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { protected Calendar startTimestamp; protected int expectedDataLength; - public AbstractFetchOperation(MiBand2Support support) { + public AbstractFetchOperation(HuamiSupport support) { super(support); } @@ -87,10 +87,10 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { } fetchCount++; - characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA); + characteristicActivityData = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_DATA); builder.notify(characteristicActivityData, false); - characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4); + characteristicFetch = getCharacteristic(HuamiService.UUID_UNKNOWN_CHARACTERISTIC4); builder.notify(characteristicFetch, true); startFetching(builder); @@ -105,10 +105,10 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) { + if (HuamiService.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) { handleActivityNotif(characteristic.getValue()); return true; - } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { + } else if (HuamiService.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { handleActivityMetadata(characteristic.getValue()); return true; } else { @@ -140,7 +140,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { protected void handleActivityMetadata(byte[] value) { if (value.length == 15) { // first two bytes are whether our request was accepted - if (ArrayUtils.equals(value, MiBand2Service.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { + if (ArrayUtils.equals(value, HuamiService.RESPONSE_ACTIVITY_DATA_START_DATE_SUCCESS, 0)) { // the third byte (0x01 on success) = ? // the 4th - 7th bytes epresent the number of bytes/packets to expect, excluding the counter bytes expectedDataLength = BLETypeConversions.toUint32(Arrays.copyOfRange(value, 3, 7)); @@ -157,7 +157,7 @@ public abstract class AbstractFetchOperation extends AbstractMiBand2Operation { handleActivityFetchFinish(false); } } else if (value.length == 3) { - if (Arrays.equals(MiBand2Service.RESPONSE_FINISH_SUCCESS, value)) { + if (Arrays.equals(HuamiService.RESPONSE_FINISH_SUCCESS, value)) { handleActivityFetchFinish(true); } else { LOG.warn("Unexpected activity metadata: " + Logging.formatBytes(value)); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java similarity index 95% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchActivityOperation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java index c68b178da..650bc2406 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchActivityOperation.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.text.format.DateUtils; import android.widget.Toast; @@ -33,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -42,7 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -55,7 +55,7 @@ public class FetchActivityOperation extends AbstractFetchOperation { private List samples = new ArrayList<>(60*24); // 1day per default - public FetchActivityOperation(MiBand2Support support) { + public FetchActivityOperation(HuamiSupport support) { super(support); setName("fetching activity data"); } @@ -69,10 +69,10 @@ public class FetchActivityOperation extends AbstractFetchOperation { @Override protected void startFetching(TransactionBuilder builder) { GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); - builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, MiBand2Service.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); + builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, HuamiService.COMMAND_ACTIVITY_DATA_TYPE_ACTIVTY }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply builder.notify(characteristicActivityData, true); - builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA}); + builder.write(characteristicFetch, new byte[] { HuamiService.COMMAND_FETCH_DATA}); } protected void handleActivityFetchFinish(boolean success) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchSportsDetailsOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java similarity index 85% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchSportsDetailsOperation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java index 721e4867a..6de70b5e6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchSportsDetailsOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsDetailsOperation.java @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.support.annotation.NonNull; import android.widget.Toast; @@ -30,18 +30,20 @@ import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter; import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.ActivityDetailsParser; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; @@ -57,7 +59,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { private ByteArrayOutputStream buffer; - public FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary, @NonNull MiBand2Support support, @NonNull String lastSyncTimeKey) { + public FetchSportsDetailsOperation(@NonNull BaseActivitySummary summary, @NonNull HuamiSupport support, @NonNull String lastSyncTimeKey) { super(support); setName("fetching sport details"); this.summary = summary; @@ -71,12 +73,12 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { - MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, + HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_DETAILS}, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply builder.notify(characteristicActivityData, true); - builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA }); + builder.write(characteristicFetch, new byte[] { HuamiService.COMMAND_FETCH_DATA }); } @Override @@ -99,7 +101,22 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation { try { ActivityTrack track = parser.parse(buffer.toByteArray()); ActivityTrackExporter exporter = createExporter(); - String fileName = FileUtils.makeValidFileName("gadgetbridge-track-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx"); + String trackType = "track"; + switch (summary.getActivityKind()) { + case ActivityKind.TYPE_CYCLING: + trackType = getContext().getString(R.string.activity_type_biking); + break; + case ActivityKind.TYPE_RUNNING: + trackType = getContext().getString(R.string.activity_type_running); + break; + case ActivityKind.TYPE_WALKING: + trackType = getContext().getString(R.string.activity_type_walking); + break; + case ActivityKind.TYPE_SWIMMING: + trackType = getContext().getString(R.string.activity_type_swimming); + break; + } + String fileName = FileUtils.makeValidFileName("gadgetbridge-"+trackType.toLowerCase()+"-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx"); File targetFile = new File(FileUtils.getExternalFilesDir(), fileName); try { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java similarity index 96% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchSportsSummaryOperation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java index fb2d6bad1..0d26eca17 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/FetchSportsSummaryOperation.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -35,8 +35,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -46,7 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.BipActivityType; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; /** @@ -58,7 +58,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { private ByteArrayOutputStream buffer = new ByteArrayOutputStream(140); - public FetchSportsSummaryOperation(MiBand2Support support) { + public FetchSportsSummaryOperation(HuamiSupport support) { super(support); setName("fetching sport summaries"); } @@ -68,12 +68,12 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { LOG.info("start" + getName()); GregorianCalendar sinceWhen = getLastSuccessfulSyncTime(); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { - MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, + HuamiService.COMMAND_ACTIVITY_DATA_START_DATE, AmazfitBipService.COMMAND_ACTIVITY_DATA_TYPE_SPORTS_SUMMARIES}, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); builder.add(new WaitAction(1000)); // TODO: actually wait for the success-reply builder.notify(characteristicActivityData, true); - builder.write(characteristicFetch, new byte[] { MiBand2Service.COMMAND_FETCH_DATA }); + builder.write(characteristicFetch, new byte[] { HuamiService.COMMAND_FETCH_DATA }); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java similarity index 80% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/InitOperation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java index 93204f441..a7d64a135 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/InitOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -36,23 +36,25 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class InitOperation extends AbstractBTLEOperation { +public class InitOperation extends AbstractBTLEOperation { private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class); private final TransactionBuilder builder; private final boolean needsAuth; + private final byte authFlags; - public InitOperation(boolean needsAuth, MiBand2Support support, TransactionBuilder builder) { + public InitOperation(boolean needsAuth, byte authFlags, HuamiSupport support, TransactionBuilder builder) { super(support); this.needsAuth = needsAuth; + this.authFlags = authFlags; this.builder = builder; builder.setGattCallback(this); } @@ -63,17 +65,17 @@ public class InitOperation extends AbstractBTLEOperation { if (needsAuth) { builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); // write key to miband2 - byte[] sendKey = org.apache.commons.lang3.ArrayUtils.addAll(new byte[]{MiBand2Service.AUTH_SEND_KEY, MiBand2Service.AUTH_BYTE}, getSecretKey()); - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), sendKey); + byte[] sendKey = org.apache.commons.lang3.ArrayUtils.addAll(new byte[]{HuamiService.AUTH_SEND_KEY, authFlags}, getSecretKey()); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUTH), sendKey); } else { builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); // get random auth number - builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), requestAuthNumber()); + builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUTH), requestAuthNumber()); } } private byte[] requestAuthNumber() { - return new byte[]{MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}; + return new byte[]{HuamiService.AUTH_REQUEST_RANDOM_AUTH_NUMBER, authFlags}; } private byte[] getSecretKey() { @@ -89,31 +91,31 @@ public class InitOperation extends AbstractBTLEOperation { public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + if (HuamiService.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { try { byte[] value = characteristic.getValue(); getSupport().logMessageContent(value); - if (value[0] == MiBand2Service.AUTH_RESPONSE && - value[1] == MiBand2Service.AUTH_SEND_KEY && - value[2] == MiBand2Service.AUTH_SUCCESS) { + if (value[0] == HuamiService.AUTH_RESPONSE && + value[1] == HuamiService.AUTH_SEND_KEY && + value[2] == HuamiService.AUTH_SUCCESS) { TransactionBuilder builder = createTransactionBuilder("Sending the secret key to the band"); builder.write(characteristic, requestAuthNumber()); getSupport().performImmediately(builder); - } else if (value[0] == MiBand2Service.AUTH_RESPONSE && - value[1] == MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER && - value[2] == MiBand2Service.AUTH_SUCCESS) { + } else if (value[0] == HuamiService.AUTH_RESPONSE && + value[1] == HuamiService.AUTH_REQUEST_RANDOM_AUTH_NUMBER && + value[2] == HuamiService.AUTH_SUCCESS) { // md5?? byte[] eValue = handleAESAuth(value, getSecretKey()); byte[] responseValue = org.apache.commons.lang3.ArrayUtils.addAll( - new byte[]{MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}, eValue); + new byte[]{HuamiService.AUTH_SEND_ENCRYPTED_AUTH_NUMBER, authFlags}, eValue); TransactionBuilder builder = createTransactionBuilder("Sending the encrypted random key to the band"); builder.write(characteristic, responseValue); getSupport().setCurrentTimeWithService(builder); getSupport().performImmediately(builder); - } else if (value[0] == MiBand2Service.AUTH_RESPONSE && - value[1] == MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER && - value[2] == MiBand2Service.AUTH_SUCCESS) { + } else if (value[0] == HuamiService.AUTH_RESPONSE && + value[1] == HuamiService.AUTH_SEND_ENCRYPTED_AUTH_NUMBER && + value[2] == HuamiService.AUTH_SUCCESS) { TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2"); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); getSupport().requestDeviceInfo(builder); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/UpdateFirmwareOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/UpdateFirmwareOperation.java similarity index 90% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/UpdateFirmwareOperation.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/UpdateFirmwareOperation.java index a711c1ee0..08ffaff62 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband2/operations/UpdateFirmwareOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/UpdateFirmwareOperation.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.operations; +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -33,19 +33,19 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; -import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareInfo; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.AbstractMiBand2Operation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband2.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -public class UpdateFirmwareOperation extends AbstractMiBand2Operation { +public class UpdateFirmwareOperation extends AbstractHuamiOperation { private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class); protected final Uri uri; @@ -54,11 +54,11 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { protected final Prefs prefs = GBApplication.getPrefs(); protected HuamiFirmwareInfo firmwareInfo; - public UpdateFirmwareOperation(Uri uri, MiBand2Support support) { + public UpdateFirmwareOperation(Uri uri, HuamiSupport support) { super(support); this.uri = uri; - fwCControlChar = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE); - fwCDataChar = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE_DATA); + fwCControlChar = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_FIRMWARE); + fwCDataChar = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_FIRMWARE_DATA); } @Override @@ -132,20 +132,20 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { getSupport().logMessageContent(value); return; } - boolean success = value[2] == MiBand2Service.SUCCESS; + boolean success = value[2] == HuamiService.SUCCESS; - if (value[0] == MiBand2Service.RESPONSE && success) { + if (value[0] == HuamiService.RESPONSE && success) { try { switch (value[1]) { - case MiBand2Service.COMMAND_FIRMWARE_INIT: { + case HuamiService.COMMAND_FIRMWARE_INIT: { sendFirmwareData(getFirmwareInfo()); break; } - case MiBand2Service.COMMAND_FIRMWARE_START_DATA: { + case HuamiService.COMMAND_FIRMWARE_START_DATA: { sendChecksum(getFirmwareInfo()); break; } - case MiBand2Service.COMMAND_FIRMWARE_CHECKSUM: { + case HuamiService.COMMAND_FIRMWARE_CHECKSUM: { if (getFirmwareInfo().getFirmwareType() == HuamiFirmwareType.FIRMWARE) { TransactionBuilder builder = performInitialized("reboot"); getSupport().sendReboot(builder); @@ -156,7 +156,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { } break; } - case MiBand2Service.COMMAND_FIRMWARE_REBOOT: { + case HuamiService.COMMAND_FIRMWARE_REBOOT: { LOG.info("Reboot command successfully sent."); GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_update_complete), false, 100, getContext()); // getSupport().onReboot(); @@ -202,7 +202,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { } byte[] bytes = new byte[arraySize]; int i = 0; - bytes[i++] = MiBand2Service.COMMAND_FIRMWARE_INIT; + bytes[i++] = HuamiService.COMMAND_FIRMWARE_INIT; bytes[i++] = sizeBytes[0]; bytes[i++] = sizeBytes[1]; bytes[i++] = sizeBytes[2]; @@ -243,7 +243,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { if (prefs.getBoolean("mi_low_latency_fw_update", true)) { getSupport().setLowLatency(builder); } - builder.write(fwCControlChar, new byte[] { MiBand2Service.COMMAND_FIRMWARE_START_DATA }); + builder.write(fwCControlChar, new byte[] { HuamiService.COMMAND_FIRMWARE_START_DATA }); for (int i = 0; i < packets; i++) { byte[] fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength); @@ -253,7 +253,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { int progressPercent = (int) ((((float) firmwareProgress) / len) * 100); if ((i > 0) && (i % 100 == 0)) { - builder.write(fwCControlChar, new byte[]{MiBand2Service.COMMAND_FIRMWARE_UPDATE_SYNC}); + builder.write(fwCControlChar, new byte[]{HuamiService.COMMAND_FIRMWARE_UPDATE_SYNC}); builder.add(new SetProgressAction(getContext().getString(R.string.updatefirmwareoperation_update_in_progress), true, progressPercent, getContext())); } } @@ -264,7 +264,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { firmwareProgress = len; } - builder.write(fwCControlChar, new byte[]{MiBand2Service.COMMAND_FIRMWARE_UPDATE_SYNC}); + builder.write(fwCControlChar, new byte[]{HuamiService.COMMAND_FIRMWARE_UPDATE_SYNC}); builder.queue(getQueue()); } catch (IOException ex) { @@ -281,7 +281,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation { int crc16 = firmwareInfo.getCrc16(); byte[] bytes = BLETypeConversions.fromUint16(crc16); builder.write(fwCControlChar, new byte[] { - MiBand2Service.COMMAND_FIRMWARE_CHECKSUM, + HuamiService.COMMAND_FIRMWARE_CHECKSUM, bytes[0], bytes[1], }); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/AbstractID115Operation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/AbstractID115Operation.java new file mode 100644 index 000000000..7a564e847 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/AbstractID115Operation.java @@ -0,0 +1,96 @@ +/* Copyright (C) 2018 Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.id115; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Constants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public abstract class AbstractID115Operation extends AbstractBTLEOperation { + protected BluetoothGattCharacteristic controlCharacteristic = null; + protected BluetoothGattCharacteristic notifyCharacteristic = null; + + protected AbstractID115Operation(ID115Support support) { + super(support); + + if (isHealthOperation()) { + controlCharacteristic = getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_WRITE_HEALTH); + notifyCharacteristic = getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_NOTIFY_HEALTH); + } else { + controlCharacteristic = getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_WRITE_NORMAL); + notifyCharacteristic = getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_NOTIFY_NORMAL); + } + } + + @Override + protected void prePerform() throws IOException { + super.prePerform(); + getDevice().setBusyTask("AbstractID115Operation starting..."); // mark as busy quickly to avoid interruptions from the outside + TransactionBuilder builder = performInitialized("disabling some notifications"); + enableNotifications(builder, true); + builder.queue(getQueue()); + } + + @Override + protected void operationFinished() { + operationStatus = OperationStatus.FINISHED; + if (getDevice() != null && getDevice().isConnected()) { + unsetBusy(); + try { + TransactionBuilder builder = performInitialized("reenabling disabled notifications"); + enableNotifications(builder, false); + builder.setGattCallback(null); // unset ourselves from being the queue's gatt callback + builder.queue(getQueue()); + } catch (IOException ex) { + GB.toast(getContext(), "Error enabling ID115 notifications, you may need to connect and disconnect", Toast.LENGTH_LONG, GB.ERROR, ex); + } + } + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (notifyCharacteristic.getUuid().equals(characteristicUUID)) { + handleResponse(characteristic.getValue()); + return true; + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + void enableNotifications(TransactionBuilder builder, boolean enable) { + if (isHealthOperation()) { + builder.notify(getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_NOTIFY_HEALTH), enable); + } else { + builder.notify(getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_NOTIFY_NORMAL), enable); + } + } + + abstract boolean isHealthOperation(); + + abstract void handleResponse(byte[] data); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/FetchActivityOperation.java new file mode 100644 index 000000000..9b4c97818 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/FetchActivityOperation.java @@ -0,0 +1,176 @@ +/* Copyright (C) 2018 Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.id115; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Constants; +import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.ID115ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class FetchActivityOperation extends AbstractID115Operation { + private static final Logger LOG = LoggerFactory.getLogger(FetchActivityOperation.class); + private byte expectedCmd; + private byte expectedSeq; + private ArrayList packets; + + protected FetchActivityOperation(ID115Support support) { + super(support); + } + + @Override + boolean isHealthOperation() { + return true; + } + + @Override + protected void doPerform() throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(ID115Constants.CMD_ID_HEALTH_DATA); + outputStream.write(ID115Constants.CMD_KEY_FETCH_ACTIVITY_TODAY); + outputStream.write(0x01); + outputStream.write(0x00); + outputStream.write(0x00); + byte cmd[] = outputStream.toByteArray(); + + expectedCmd = ID115Constants.CMD_KEY_FETCH_ACTIVITY_TODAY; + expectedSeq = 1; + packets = new ArrayList<>(); + + TransactionBuilder builder = performInitialized("send activity fetch request"); + builder.write(controlCharacteristic, cmd); + builder.queue(getQueue()); + } + + @Override + void handleResponse(byte[] data) { + if (!isOperationRunning()) { + LOG.error("ignoring notification because operation is not running. Data length: " + data.length); + getSupport().logMessageContent(data); + return; + } + + if (data.length < 4) { + LOG.warn("short GATT response"); + return; + } + + if (data[0] == ID115Constants.CMD_ID_HEALTH_DATA) { + if (data[1] == (byte)0xEE) { + LOG.info("Activity data transfer has finished."); + parseAndStore(); + operationFinished(); + } else { + if ((data[1] != expectedCmd) || (data[2] != expectedSeq)) { + GB.toast(getContext(), "Error fetching ID115 activity data, you may need to connect and disconnect", Toast.LENGTH_LONG, GB.ERROR); + operationFinished(); + return; + } + expectedSeq += 1; + + byte payload[] = new byte[data.length - 4]; + System.arraycopy(data, 4, payload, 0, payload.length); + packets.add(payload); + } + } + } + + void parseAndStore() { + if (packets.size() <= 1) { + return; + } + + byte[] header = packets.get(0); + int year = ((header[1] & 0xFF) << 8) | (header[0] & 0xFF); + int month = header[2] & 0xFF; + int day = header[3] & 0xFF; + int sampleDurationMinutes = header[6] & 0xFF; + Calendar calendar = new GregorianCalendar(year, month - 1, day); + int ts = (int)(calendar.getTimeInMillis() / 1000); + int dt = sampleDurationMinutes * 60; + + ArrayList samples = new ArrayList<>(); + + for (int i = 2; i < packets.size(); i++) { + byte[] packet = packets.get(i); + for (int j = 0; j <= packet.length - 5; j += 5) { + byte[] sampleData = new byte[5]; + System.arraycopy(packet, j, sampleData, 0, sampleData.length); + + ID115ActivitySample sample = parseSample(sampleData); + if (sample != null) { + sample.setTimestamp(ts); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + samples.add(sample); + } + ts += dt; + } + } + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + ID115ActivitySample[] sampleArray = samples.toArray(new ID115ActivitySample[0]); + long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); + for (ID115ActivitySample sample: sampleArray) { + sample.setUserId(userId); + sample.setDeviceId(deviceId); + } + + ID115SampleProvider provider = new ID115SampleProvider(getDevice(), dbHandler.getDaoSession()); + provider.addGBActivitySamples(sampleArray); + } catch (Exception ex) { + GB.toast(getContext(), "Error saving activity data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + ID115ActivitySample parseSample(byte[] data) { + int d01 = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF); + int d12 = ((data[2] & 0xFF) << 8) | (data[1] & 0xFF); + int d23 = ((data[3] & 0xFF) << 8) | (data[2] & 0xFF); + int d34 = ((data[4] & 0xFF) << 8) | (data[3] & 0xFF); + int stepCount = (d01 >> 2) & 0xFFF; + int activeTime = (d12 >> 6) & 0xF; + int calories = (d23 >> 2) & 0x3FF; + int distance = (d34 >> 4); + + if (stepCount == 0) { + return null; + } + + ID115ActivitySample sample = new ID115ActivitySample(); + sample.setSteps(stepCount); + sample.setActiveTimeMinutes(activeTime); + sample.setCaloriesBurnt(calories); + sample.setDistanceMeters(distance); + return sample; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/ID115Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/ID115Support.java new file mode 100644 index 000000000..92c93d4ae --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/ID115Support.java @@ -0,0 +1,358 @@ +/* Copyright (C) 2018 Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.id115; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.net.Uri; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.TimeZone; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Constants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.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.actions.SetDeviceStateAction; + +public class ID115Support extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(ID115Support.class); + + public BluetoothGattCharacteristic normalWriteCharacteristic = null; + public BluetoothGattCharacteristic healthWriteCharacteristic = null; + + public ID115Support() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(ID115Constants.UUID_SERVICE_ID115); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + normalWriteCharacteristic = getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_WRITE_NORMAL); + healthWriteCharacteristic = getCharacteristic(ID115Constants.UUID_CHARACTERISTIC_WRITE_HEALTH); + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + setTime(builder) + .setWrist(builder) + .setScreenOrientation(builder) + .setGoal(builder) + .setInitialized(builder); + + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); + + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + try { + new SendNotificationOperation(this, notificationSpec).perform(); + } catch (IOException ex) { + LOG.error("Unable to send ID115 notification", ex); + } + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized("time"); + setTime(builder); + performConnected(builder.getTransaction()); + } catch(IOException e) { + LOG.warn("Unable to send current time", e); + } + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onSetCallState(CallSpec callSpec) { + if (callSpec.command == CallSpec.CALL_INCOMING) { + try { + new SendNotificationOperation(this, callSpec).perform(); + } catch (IOException ex) { + LOG.error("Unable to send ID115 notification", ex); + } + } else { + sendStopCallNotification(); + } + } + + @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) { + try { + new FetchActivityOperation(this).perform(); + } catch (IOException ex) { + LOG.error("Unable to fetch ID115 activity data", ex); + } + } + + @Override + public void onReboot() { + try { + getQueue().clear(); + + TransactionBuilder builder = performInitialized("reboot"); + builder.write(normalWriteCharacteristic, new byte[] { + ID115Constants.CMD_ID_DEVICE_RESTART, ID115Constants.CMD_KEY_REBOOT + }); + performConnected(builder.getTransaction()); + } catch(Exception e) { + } + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + private void setInitialized(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + } + + ID115Support setTime(TransactionBuilder builder) { + Calendar c = Calendar.getInstance(TimeZone.getDefault()); + + int day = c.get(Calendar.DAY_OF_WEEK); + + byte dayOfWeek; + if (day == Calendar.SUNDAY) { + dayOfWeek = 6; + } else { + dayOfWeek = (byte)(day - 2); + } + + int year = c.get(Calendar.YEAR); + builder.write(normalWriteCharacteristic, new byte[] { + ID115Constants.CMD_ID_SETTINGS, ID115Constants.CMD_KEY_SET_TIME, + (byte)(year & 0xff), + (byte)(year >> 8), + (byte)(1 + c.get(Calendar.MONTH)), + (byte)c.get(Calendar.DAY_OF_MONTH), + (byte)c.get(Calendar.HOUR_OF_DAY), + (byte)c.get(Calendar.MINUTE), + (byte)c.get(Calendar.SECOND), + dayOfWeek + }); + return this; + } + + ID115Support setWrist(TransactionBuilder builder) { + String value = GBApplication.getPrefs().getString(ID115Constants.PREF_WRIST, + "left"); + + byte wrist; + if (value.equals("left")) { + wrist = ID115Constants.CMD_ARG_LEFT; + } else { + wrist = ID115Constants.CMD_ARG_RIGHT; + } + + builder.write(normalWriteCharacteristic, new byte[] { + ID115Constants.CMD_ID_SETTINGS, ID115Constants.CMD_KEY_SET_HAND, + wrist + }); + return this; + } + + ID115Support setScreenOrientation(TransactionBuilder builder) { + String value = GBApplication.getPrefs().getString(ID115Constants.PREF_SCREEN_ORIENTATION, + "horizontal"); + + byte orientation; + if (value.equals("horizontal")) { + orientation = ID115Constants.CMD_ARG_HORIZONTAL; + } else { + orientation = ID115Constants.CMD_ARG_VERTICAL; + } + + builder.write(normalWriteCharacteristic, new byte[] { + ID115Constants.CMD_ID_SETTINGS, ID115Constants.CMD_KEY_SET_DISPLAY_MODE, + orientation + }); + return this; + } + + private ID115Support setGoal(TransactionBuilder transaction) { + ActivityUser activityUser = new ActivityUser(); + int value = activityUser.getStepsGoal(); + + transaction.write(normalWriteCharacteristic, new byte[]{ + ID115Constants.CMD_ID_SETTINGS, + ID115Constants.CMD_KEY_SET_GOAL, + 0, + (byte) (value & 0xff), + (byte) ((value >> 8) & 0xff), + (byte) ((value >> 16) & 0xff), + (byte) ((value >> 24) & 0xff), + 0, 0 + }); + return this; + } + + void sendStopCallNotification() { + try { + TransactionBuilder builder = performInitialized("stop_call_notification"); + builder.write(normalWriteCharacteristic, new byte[] { + ID115Constants.CMD_ID_NOTIFY, + ID115Constants.CMD_KEY_NOTIFY_STOP, + 1 + }); + performConnected(builder.getTransaction()); + } catch(IOException e) { + LOG.warn("Unable to stop call notification", e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/SendNotificationOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/SendNotificationOperation.java new file mode 100644 index 000000000..bd9740f95 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/id115/SendNotificationOperation.java @@ -0,0 +1,209 @@ +/* Copyright (C) 2018 Vadim Kaushan + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.id115; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Constants; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class SendNotificationOperation extends AbstractID115Operation { + private static final Logger LOG = LoggerFactory.getLogger(SendNotificationOperation.class); + + byte[] currentNotificationBuffer; + int currentNotificationSize; + int currentNotificationIndex; + byte currentNotificationType; + + SendNotificationOperation(ID115Support support, NotificationSpec notificationSpec) + { + super(support); + + String phone = ""; + if (notificationSpec.phoneNumber != null) { + phone = notificationSpec.phoneNumber; + } + + String title = ""; + if (notificationSpec.sender != null) { + title = notificationSpec.sender; + } else if (notificationSpec.title != null) { + title = notificationSpec.title; + } else if (notificationSpec.subject != null) { + title = notificationSpec.subject; + } + + String text = ""; + if (notificationSpec.body != null) { + text = notificationSpec.body; + } + + currentNotificationBuffer = encodeMessageNotification(notificationSpec.type, title, phone, text); + currentNotificationSize = (currentNotificationBuffer.length + 15) / 16; + currentNotificationType = ID115Constants.CMD_KEY_NOTIFY_MSG; + } + + SendNotificationOperation(ID115Support support, CallSpec callSpec) + { + super(support); + + String number = ""; + if (callSpec.number != null) { + number = callSpec.number; + } + + String name = ""; + if (callSpec.name != null) { + name = callSpec.name; + } + + currentNotificationBuffer = encodeCallNotification(name, number); + currentNotificationSize = (currentNotificationBuffer.length + 15) / 16; + currentNotificationType = ID115Constants.CMD_KEY_NOTIFY_CALL; + } + + @Override + boolean isHealthOperation() { + return false; + } + + @Override + protected void doPerform() throws IOException { + sendNotificationChunk(1); + } + + void sendNotificationChunk(int chunkIndex) throws IOException { + currentNotificationIndex = chunkIndex; + + int offset = (chunkIndex - 1) * 16; + int tailSize = currentNotificationBuffer.length - offset; + int chunkSize = (tailSize > 16)? 16 : tailSize; + + byte raw[] = new byte[16]; + System.arraycopy(currentNotificationBuffer, offset, raw, 0, chunkSize); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(ID115Constants.CMD_ID_NOTIFY); + outputStream.write(currentNotificationType); + outputStream.write((byte)currentNotificationSize); + outputStream.write((byte)currentNotificationIndex); + outputStream.write(raw); + byte cmd[] = outputStream.toByteArray(); + + TransactionBuilder builder = performInitialized("send notification chunk"); + builder.write(controlCharacteristic, cmd); + builder.queue(getQueue()); + } + + void handleResponse(byte[] data) { + if (!isOperationRunning()) { + LOG.error("ignoring notification because operation is not running. Data length: " + data.length); + getSupport().logMessageContent(data); + return; + } + + if (data.length < 2) { + LOG.warn("short GATT response"); + return; + } + if (data[0] == ID115Constants.CMD_ID_NOTIFY) { + if (data.length < 4) { + LOG.warn("short GATT response for NOTIFY"); + return; + } + if (data[1] == currentNotificationType) { + if (data[3] == currentNotificationIndex) { + if (currentNotificationIndex != currentNotificationSize) { + try { + sendNotificationChunk(currentNotificationIndex + 1); + } catch (IOException ex) { + GB.toast(getContext(), "Error sending ID115 notification, you may need to connect and disconnect", Toast.LENGTH_LONG, GB.ERROR, ex); + } + } else { + LOG.info("Notification transfer has finished."); + operationFinished(); + } + } + } + } + } + + byte[] encodeCallNotification(String name, String phone) { + if (name.length() > 20) { + name = name.substring(0, 20); + } + if (phone.length() > 20) { + phone = phone.substring(0, 20); + } + + byte[] name_bytes = name.getBytes(StandardCharsets.UTF_8); + byte[] phone_bytes = phone.getBytes(StandardCharsets.UTF_8); + + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write((byte) phone_bytes.length); + outputStream.write((byte) name_bytes.length); + outputStream.write(phone_bytes); + outputStream.write(name_bytes); + return outputStream.toByteArray(); + } catch (IOException e) { + return null; + } + } + + byte[] encodeMessageNotification(NotificationType type, String title, String phone, String text) { + if (title.length() > 20) { + title = title.substring(0, 20); + } + if (phone.length() > 20) { + phone = phone.substring(0, 20); + } + if (text.length() > 20) { + text = text.substring(0, 20); + } + byte[] title_bytes = title.getBytes(StandardCharsets.UTF_8); + byte[] phone_bytes = phone.getBytes(StandardCharsets.UTF_8); + byte[] text_bytes = text.getBytes(StandardCharsets.UTF_8); + + byte nativeType = ID115Constants.getNotificationType(type); + + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(nativeType); + outputStream.write((byte) text_bytes.length); + outputStream.write((byte) phone_bytes.length); + outputStream.write((byte) title_bytes.length); + outputStream.write(phone_bytes); + outputStream.write(title_bytes); + outputStream.write(text_bytes); + return outputStream.toByteArray(); + } catch (IOException e) { + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java index d09a2a3cd..b2d4117b1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/no1f1/No1F1Support.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, Daniele Gobbetti, protomors +/* Copyright (C) 2017-2018 Andreas Shimokawa, Daniele Gobbetti, Pavel + Elagin, protomors This file is part of Gadgetbridge. @@ -89,6 +90,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { builder.setGattCallback(this); builder.notify(measureCharacteristic, true); + setTime(builder); sendSettings(builder); builder.write(ctrlCharacteristic, new byte[]{No1F1Constants.CMD_FIRMWARE_VERSION}); @@ -173,18 +175,8 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { public void onSetTime() { try { TransactionBuilder builder = performInitialized("setTime"); - Calendar c = GregorianCalendar.getInstance(); - byte[] datetimeBytes = new byte[]{ - No1F1Constants.CMD_DATETIME, - (byte) ((c.get(Calendar.YEAR) / 256) & 0xff), - (byte) (c.get(Calendar.YEAR) % 256), - (byte) (c.get(Calendar.MONTH) + 1), - (byte) c.get(Calendar.DAY_OF_MONTH), - (byte) c.get(Calendar.HOUR_OF_DAY), - (byte) c.get(Calendar.MINUTE), - (byte) c.get(Calendar.SECOND) - }; - builder.write(ctrlCharacteristic, datetimeBytes); + setTime(builder); + performConnected(builder.getTransaction()); } catch (IOException e) { GB.toast(getContext(), "Error setting time: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } @@ -396,6 +388,21 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { return true; } + private void setTime(TransactionBuilder transaction) { + Calendar c = GregorianCalendar.getInstance(); + byte[] datetimeBytes = new byte[]{ + No1F1Constants.CMD_DATETIME, + (byte) ((c.get(Calendar.YEAR) / 256) & 0xff), + (byte) (c.get(Calendar.YEAR) % 256), + (byte) (c.get(Calendar.MONTH) + 1), + (byte) c.get(Calendar.DAY_OF_MONTH), + (byte) c.get(Calendar.HOUR_OF_DAY), + (byte) c.get(Calendar.MINUTE), + (byte) c.get(Calendar.SECOND) + }; + transaction.write(ctrlCharacteristic, datetimeBytes); + } + /** * Set display settings (time format and measurement system) * @@ -522,7 +529,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { byte[] msg; // send header - bytes = header.toString().getBytes("EUC-JP"); + bytes = header.getBytes("EUC-JP"); length = min(bytes.length, 18); msg = new byte[length + 2]; msg[0] = No1F1Constants.CMD_NOTIFICATION; @@ -531,7 +538,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { builder.write(ctrlCharacteristic, msg); // send body - bytes = header.toString().getBytes("EUC-JP"); + bytes = header.getBytes("EUC-JP"); length = min(bytes.length, 18); msg = new byte[length + 2]; msg[0] = No1F1Constants.CMD_NOTIFICATION; @@ -598,7 +605,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport { if (data[0] == No1F1Constants.CMD_FETCH_STEPS) { samples.get(i).setRawKind(ActivityKind.TYPE_ACTIVITY); samples.get(i).setRawIntensity(samples.get(i).getSteps()); - } else if (data[0] == No1F1Constants.CMD_FETCH_STEPS) { + } else if (data[0] == No1F1Constants.CMD_FETCH_SLEEP) { if (samples.get(i).getRawIntensity() < 7) samples.get(i).setRawKind(ActivityKind.TYPE_DEEP_SLEEP); else diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java index 96a375823..c1b647599 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/webview/GBWebClient.java @@ -1,5 +1,5 @@ /* Copyright (C) 2017-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti + Gobbetti, Pavel Elagin This file is part of Gadgetbridge. @@ -136,10 +136,10 @@ public class GBWebClient extends WebViewClient { headers.put("Access-Control-Allow-Origin", "*"); return new WebResourceResponse("text/html", "utf-8", 200, "OK", headers, - new ByteArrayInputStream("1".toString().getBytes()) + new ByteArrayInputStream("1".getBytes()) ); } else { - return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream("1".toString().getBytes())); + return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream("1".getBytes())); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java new file mode 100644 index 000000000..196559e35 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi1Protocol.java @@ -0,0 +1,150 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Roidmi1Protocol extends RoidmiProtocol { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi1Protocol.class); + + public Roidmi1Protocol(GBDevice device) { + super(device); + } + + private static final byte[] PACKET_HEADER = new byte[]{(byte) 0xaa, 0x55}; + private static final byte[] PACKET_TRAILER = new byte[]{(byte) 0xc3, 0x3c}; + private static final byte COMMAND_SET_FREQUENCY = 0x10; + private static final byte COMMAND_GET_FREQUENCY = (byte) 0x80; + private static final byte COMMAND_SET_COLOR = 0x11; + private static final byte COMMAND_GET_COLOR = (byte) 0x81; + + private static final int PACKET_MIN_LENGTH = 6; + + private static final int LED_COLOR_RED = 1; + private static final int LED_COLOR_GREEN = 2; + private static final int LED_COLOR_BLUE = 3; + private static final int LED_COLOR_YELLOW = 4; // not official + private static final int LED_COLOR_SKY_BLUE = 5; + private static final int LED_COLOR_PINK = 6; // not official + private static final int LED_COLOR_WHITE = 7; // not official + private static final int LED_COLOR_OFF = 8; + + // Other commands: + // App periodically sends aa5502018588c33c and receives aa5506018515111804cec33c + private static final byte[] COMMAND_PERIODIC = new byte[]{(byte) 0xaa, 0x55, 0x02, 0x01, (byte) 0x85, (byte) 0x88, (byte) 0xc3, 0x3c}; + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + if (responseData.length <= PACKET_MIN_LENGTH) { + LOG.info("Response too small"); + return null; + } + + for (int i = 0; i < packetHeader().length; i++) { + if (responseData[i] != packetHeader()[i]) { + LOG.info("Invalid response header"); + return null; + } + } + + for (int i = 0; i < packetTrailer().length; i++) { + if (responseData[responseData.length - packetTrailer().length + i] != packetTrailer()[i]) { + LOG.info("Invalid response trailer"); + return null; + } + } + + if (calcChecksum(responseData) != responseData[responseData.length - packetTrailer().length - 1]) { + LOG.info("Invalid response checksum"); + return null; + } + + switch (responseData[3]) { + case COMMAND_GET_COLOR: + int color = responseData[5]; + LOG.debug("Got color: " + color); + GBDeviceEventLEDColor evColor = new GBDeviceEventLEDColor(); + evColor.color = RoidmiConst.COLOR_PRESETS[color - 1]; + return new GBDeviceEvent[]{evColor}; + case COMMAND_GET_FREQUENCY: + String frequencyHex = GB.hexdump(responseData, 4, 2); + float frequency = Float.valueOf(frequencyHex) / 10.0f; + LOG.debug("Got frequency: " + frequency); + GBDeviceEventFmFrequency evFrequency = new GBDeviceEventFmFrequency(); + evFrequency.frequency = frequency; + return new GBDeviceEvent[]{evFrequency}; + default: + LOG.error("Unrecognized response type 0x" + GB.hexdump(responseData, packetHeader().length, 1)); + return null; + } + } + + @Override + public byte[] encodeLedColor(int color) { + int[] presets = RoidmiConst.COLOR_PRESETS; + int color_id = -1; + for (int i = 0; i < presets.length; i++) { + if (presets[i] == color) { + color_id = (i + 1) & 255; + break; + } + } + + if (color_id < 0 || color_id > 8) + throw new IllegalArgumentException("color must belong to RoidmiConst.COLOR_PRESETS"); + + return encodeCommand(COMMAND_SET_COLOR, (byte) 0, (byte) color_id); + } + + @Override + public byte[] encodeFmFrequency(float frequency) { + if (frequency < 87.5 || frequency > 108.0) + throw new IllegalArgumentException("Frequency must be >= 87.5 and <= 180.0"); + + byte[] freq = frequencyToBytes(frequency); + + return encodeCommand(COMMAND_SET_FREQUENCY, freq[0], freq[1]); + } + + public byte[] encodeGetLedColor() { + return encodeCommand(COMMAND_GET_COLOR, (byte) 0, (byte) 0); + } + + public byte[] encodeGetFmFrequency() { + return encodeCommand(COMMAND_GET_FREQUENCY, (byte) 0, (byte) 0); + } + + public byte[] encodeGetVoltage() { + return null; + } + + public byte[] packetHeader() { + return PACKET_HEADER; + } + + public byte[] packetTrailer() { + return PACKET_TRAILER; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java new file mode 100644 index 000000000..a4d6412e7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/Roidmi3Protocol.java @@ -0,0 +1,154 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Roidmi3Protocol extends RoidmiProtocol { + private static final Logger LOG = LoggerFactory.getLogger(Roidmi3Protocol.class); + + public Roidmi3Protocol(GBDevice device) { + super(device); + } + + // Commands below need to be wrapped in a packet + + private static final byte[] COMMAND_GET_COLOR = new byte[]{0x02, (byte) 0x81}; + private static final byte[] COMMAND_GET_FREQUENCY = new byte[]{0x05, (byte) 0x81}; + private static final byte[] COMMAND_GET_VOLTAGE = new byte[]{0x06, (byte) 0x81}; + + private static final byte[] COMMAND_SET_COLOR = new byte[]{0x02, 0x01, 0x00, 0x00, 0x00}; + private static final byte[] COMMAND_SET_FREQUENCY = new byte[]{0x05, (byte) 0x81, 0x09, 0x64}; + private static final byte[] COMMAND_DENOISE_ON = new byte[]{0x05, 0x06, 0x12}; + private static final byte[] COMMAND_DENOISE_OFF = new byte[]{0x05, 0x06, 0x00}; + + private static final byte RESPONSE_COLOR = 0x02; + private static final byte RESPONSE_FREQUENCY = 0x05; + private static final byte RESPONSE_VOLTAGE = 0x06; + // Next response byte is always 0x81, followed by the value + + private static final int PACKET_MIN_LENGTH = 4; + + @Override + public GBDeviceEvent[] decodeResponse(byte[] res) { + if (res.length <= PACKET_MIN_LENGTH) { + LOG.info("Response too small"); + return null; + } + + if (calcChecksum(res) != res[res.length - 2]) { + LOG.info("Invalid response checksum"); + return null; + } + + if (res[0] + 2 != res.length) { + LOG.info("Packet length doesn't match"); + return null; + } + + if (res[1] != (byte) 0x81) { + LOG.error("Unrecognized response" + GB.hexdump(res, 0, res.length)); + return null; + } + + if (res[1] == RESPONSE_VOLTAGE) { + String voltageHex = GB.hexdump(res, 3, 2); + float voltage = Float.valueOf(voltageHex) / 10.0f; + LOG.debug("Got voltage: " + voltage); + GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo(); + evBattery.voltage = voltage; + return new GBDeviceEvent[]{evBattery}; + } else if (res[1] == RESPONSE_COLOR) { + LOG.debug("Got color: " + GB.hexdump(res, 3, 3)); + int color = res[3] << 16 | res[4] << 8 | res[4]; + GBDeviceEventLEDColor evColor = new GBDeviceEventLEDColor(); + evColor.color = color; + return new GBDeviceEvent[]{evColor}; + } else if (res[1] == RESPONSE_FREQUENCY) { + String frequencyHex = GB.hexdump(res, 3, 2); + float frequency = Float.valueOf(frequencyHex) / 10.0f; + LOG.debug("Got frequency: " + frequency); + GBDeviceEventFmFrequency evFrequency = new GBDeviceEventFmFrequency(); + evFrequency.frequency = frequency; + return new GBDeviceEvent[]{evFrequency}; + } else { + LOG.error("Unrecognized response" + GB.hexdump(res, 0, res.length)); + return null; + } + } + + @Override + public byte[] encodeLedColor(int color) { + byte[] cmd = COMMAND_SET_COLOR.clone(); + + cmd[2] = (byte) color; + cmd[3] = (byte) (color >> 8); + cmd[4] = (byte) (color >> 16); + + return encodeCommand(cmd); + } + + @Override + public byte[] encodeFmFrequency(float frequency) { + if (frequency < 87.5 || frequency > 108.0) + throw new IllegalArgumentException("Frequency must be >= 87.5 and <= 180.0"); + + byte[] cmd = COMMAND_SET_FREQUENCY.clone(); + byte[] freq = frequencyToBytes(frequency); + cmd[2] = freq[0]; + cmd[3] = freq[1]; + + return encodeCommand(cmd); + } + + @Override + public byte[] encodeGetLedColor() { + return encodeCommand(COMMAND_GET_COLOR); + } + + @Override + public byte[] encodeGetFmFrequency() { + return encodeCommand(COMMAND_GET_FREQUENCY); + } + + @Override + public byte[] packetHeader() { + return new byte[0]; + } + + @Override + public byte[] packetTrailer() { + return new byte[0]; + } + + public byte[] encodeGetVoltage() { + return COMMAND_GET_VOLTAGE; + } + + public byte[] encodeDenoise(boolean enabled) { + byte[] cmd = enabled ? COMMAND_DENOISE_ON : COMMAND_DENOISE_OFF; + return encodeCommand(cmd); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java new file mode 100644 index 000000000..012f0c28d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiIoThread.java @@ -0,0 +1,70 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class RoidmiIoThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiIoThread.class); + + private final byte[] HEADER; + private final byte[] TRAILER; + + public RoidmiIoThread(GBDevice gbDevice, Context context, RoidmiProtocol roidmiProtocol, RoidmiSupport roidmiSupport, BluetoothAdapter roidmiBtAdapter) { + super(gbDevice, context, roidmiProtocol, roidmiSupport, roidmiBtAdapter); + + HEADER = roidmiProtocol.packetHeader(); + TRAILER = roidmiProtocol.packetTrailer(); + } + + @Override + protected byte[] parseIncoming(InputStream inputStream) throws IOException { + ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); + + boolean finished = false; + byte[] incoming = new byte[1]; + + while (!finished) { + inputStream.read(incoming); + msgStream.write(incoming); + + byte[] arr = msgStream.toByteArray(); + if (arr.length > HEADER.length) { + int expectedLength = HEADER.length + TRAILER.length + arr[HEADER.length] + 2; + if (arr.length == expectedLength) { + finished = true; + } + } + } + + byte[] msgArray = msgStream.toByteArray(); + LOG.debug("Packet: " + GB.hexdump(msgArray, 0, msgArray.length)); + return msgArray; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java new file mode 100644 index 000000000..07f809905 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiProtocol.java @@ -0,0 +1,93 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public abstract class RoidmiProtocol extends GBDeviceProtocol { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiProtocol.class); + + // Packet structure: HEADER N_PARAMS PARAM_1 ... PARAM_N CHECKSUM TRAILER + + public RoidmiProtocol(GBDevice device) { + super(device); + } + + @Override + public abstract GBDeviceEvent[] decodeResponse(byte[] responseData); + + @Override + public abstract byte[] encodeLedColor(int color); + + @Override + public abstract byte[] encodeFmFrequency(float frequency); + + public abstract byte[] encodeGetLedColor(); + + public abstract byte[] encodeGetFmFrequency(); + + public abstract byte[] encodeGetVoltage(); + + public abstract byte[] packetHeader(); + + public abstract byte[] packetTrailer(); + + public byte[] encodeCommand(byte... params) { + byte[] cmd = new byte[packetHeader().length + packetTrailer().length + params.length + 2]; + + for (int i = 0; i < packetHeader().length; i++) + cmd[i] = packetHeader()[i]; + for (int i = 0; i < packetTrailer().length; i++) + cmd[cmd.length - packetTrailer().length + i] = packetTrailer()[i]; + + cmd[packetHeader().length] = (byte) params.length; + for (int i = 0; i < params.length; i++) { + cmd[packetHeader().length + 1 + i] = params[i]; + } + cmd[cmd.length - packetTrailer().length - 1] = calcChecksum(cmd); + + return cmd; + } + + public byte calcChecksum(byte[] packet) { + int chk = 0; + for (int i = packetHeader().length; i < packet.length - packetTrailer().length - 1; i++) { + chk += packet[i] & 255; + } + return (byte) chk; + } + + public byte[] frequencyToBytes(float frequency) { + byte[] res = new byte[2]; + String format = String.format(Locale.getDefault(), "%04d", (int) (10.0f * frequency)); + try { + res[0] = (byte) (Integer.parseInt(format.substring(0, 2), 16) & 255); + res[1] = (byte) (Integer.parseInt(format.substring(2), 16) & 255); + } catch (Exception e) { + LOG.error(e.getMessage()); + } + + return res; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java new file mode 100644 index 000000000..7736387a0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/roidmi/RoidmiSupport.java @@ -0,0 +1,171 @@ +/* Copyright (C) 2018 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi; + +import android.net.Uri; +import android.os.Handler; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.RoidmiConst; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class RoidmiSupport extends AbstractSerialDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(RoidmiSupport.class); + + private final Handler handler = new Handler(); + private int infoRequestTries = 0; + private final Runnable infosRunnable = new Runnable() { + public void run() { + infoRequestTries += 1; + + try { + boolean infoMissing = false; + + if (getDevice().getExtraInfo("led_color") == null) { + infoMissing = true; + onSendConfiguration(RoidmiConst.ACTION_GET_LED_COLOR); + } + + if (getDevice().getExtraInfo("fm_frequency") == null) { + infoMissing = true; + + onSendConfiguration(RoidmiConst.ACTION_GET_FM_FREQUENCY); + } + + if (getDevice().getType() == DeviceType.ROIDMI3) { + if (getDevice().getBatteryVoltage() == -1) { + infoMissing = true; + + onSendConfiguration(RoidmiConst.ACTION_GET_VOLTAGE); + } + } + + if (infoMissing) { + if (infoRequestTries < 6) { + requestDeviceInfos(500 + infoRequestTries * 120); + } else { + LOG.error("Failed to get Roidmi infos after 6 tries"); + } + } + } catch (Exception e) { + LOG.error("Failed to get Roidmi infos", e); + } + } + }; + + private void requestDeviceInfos(int delayMillis) { + handler.postDelayed(infosRunnable, delayMillis); + } + + @Override + public boolean connect() { + getDeviceIOThread().start(); + + requestDeviceInfos(1500); + + return true; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + if (getDevice().getType() == DeviceType.ROIDMI) { + return new Roidmi1Protocol(getDevice()); + } else if (getDevice().getType() == DeviceType.ROIDMI3) { + return new Roidmi3Protocol(getDevice()); + } + + LOG.error("Unsupported device type with key = " + getDevice().getType().getKey()); + return null; + } + + @Override + public void onSendConfiguration(final String config) { + LOG.debug("onSendConfiguration " + config); + + RoidmiIoThread roidmiIoThread = getDeviceIOThread(); + RoidmiProtocol roidmiProtocol = (RoidmiProtocol) getDeviceProtocol(); + + switch (config) { + case RoidmiConst.ACTION_GET_LED_COLOR: + roidmiIoThread.write(roidmiProtocol.encodeGetLedColor()); + break; + case RoidmiConst.ACTION_GET_FM_FREQUENCY: + roidmiIoThread.write(roidmiProtocol.encodeGetFmFrequency()); + break; + case RoidmiConst.ACTION_GET_VOLTAGE: + roidmiIoThread.write(roidmiProtocol.encodeGetVoltage()); + break; + default: + LOG.error("Invalid Roidmi configuration " + config); + break; + } + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new RoidmiIoThread(getDevice(), getContext(), (RoidmiProtocol) getDeviceProtocol(), RoidmiSupport.this, getBluetoothAdapter()); + } + + @Override + public synchronized RoidmiIoThread getDeviceIOThread() { + return (RoidmiIoThread) super.getDeviceIOThread(); + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void onInstallApp(Uri uri) { + // Nothing to do + } + + @Override + public void onAppConfiguration(UUID uuid, String config, Integer id) { + // Nothing to do + } + + @Override + public void onHeartRateTest() { + // Nothing to do + } + + @Override + public void onSetConstantVibration(int intensity) { + // Nothing to do + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + // Nothing to do + } + + @Override + public void onSetAlarms(ArrayList alarms) { + // Nothing to do + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java index caebf7a1d..10acfbf73 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vibratissimo/VibratissimoSupport.java @@ -18,12 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.net.Uri; -import android.support.v4.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSuppo import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; @@ -56,9 +53,9 @@ public class VibratissimoSupport extends AbstractBTLEDeviceSupport { private final BatteryInfoProfile batteryInfoProfile; private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + private final IntentListener mListener = new IntentListener() { @Override - public void onReceive(Context context, Intent intent) { + public void notify(Intent intent) { String s = intent.getAction(); if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); @@ -77,15 +74,11 @@ public class VibratissimoSupport extends AbstractBTLEDeviceSupport { addSupportedService(UUID.fromString("00001523-1212-efde-1523-785feabcd123")); deviceInfoProfile = new DeviceInfoProfile<>(this); + deviceInfoProfile.addListener(mListener); batteryInfoProfile = new BatteryInfoProfile<>(this); + batteryInfoProfile.addListener(mListener); addSupportedProfile(deviceInfoProfile); addSupportedProfile(batteryInfoProfile); - - LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BatteryInfoProfile.ACTION_BATTERY_INFO); - intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); - broadcastManager.registerReceiver(mReceiver, intentFilter); } private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) { @@ -93,13 +86,6 @@ public class VibratissimoSupport extends AbstractBTLEDeviceSupport { handleGBDeviceEvent(batteryCmd); } - @Override - public void dispose() { - LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); - broadcastManager.unregisterReceiver(mReceiver); - super.dispose(); - } - @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/Watch9DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/Watch9DeviceSupport.java new file mode 100644 index 000000000..dab60a938 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/Watch9DeviceSupport.java @@ -0,0 +1,621 @@ +/* Copyright (C) 2018 maxirnilian + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.watch9; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.support.annotation.IntRange; +import android.support.v4.content.LocalBroadcastManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +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.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.operations.InitOperation; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; + +public class Watch9DeviceSupport extends AbstractBTLEDeviceSupport { + + private boolean needsAuth; + private int sequenceNumber = 0; + private boolean isCalibrationActive = false; + + private byte ACK_CALIBRATION = 0; + + private final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); + private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + + private static final Logger LOG = LoggerFactory.getLogger(Watch9DeviceSupport.class); + + private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String broadcastAction = intent.getAction(); + switch (broadcastAction) { + case Watch9Constants.ACTION_CALIBRATION: + enableCalibration(intent.getBooleanExtra(Watch9Constants.ACTION_ENABLE, false)); + break; + case Watch9Constants.ACTION_CALIBRATION_SEND: + int hour = intent.getIntExtra(Watch9Constants.VALUE_CALIBRATION_HOUR, -1); + int minute = intent.getIntExtra(Watch9Constants.VALUE_CALIBRATION_MINUTE, -1); + int second = intent.getIntExtra(Watch9Constants.VALUE_CALIBRATION_SECOND, -1); + if (hour != -1 && minute != -1 && second != -1) { + sendCalibrationData(hour, minute, second); + } + break; + case Watch9Constants.ACTION_CALIBRATION_HOLD: + holdCalibration(); + break; + } + } + }; + + public Watch9DeviceSupport() { + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(Watch9Constants.UUID_SERVICE_WATCH9); + + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Watch9Constants.ACTION_CALIBRATION); + intentFilter.addAction(Watch9Constants.ACTION_CALIBRATION_SEND); + intentFilter.addAction(Watch9Constants.ACTION_CALIBRATION_HOLD); + broadcastManager.registerReceiver(broadcastReceiver, intentFilter); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + try { + boolean auth = needsAuth; + needsAuth = false; + new InitOperation(auth, this, builder).perform(); + } catch (IOException e) { + e.printStackTrace(); + } + + return builder; + } + + @Override + public boolean connectFirstTime() { + needsAuth = true; + return super.connect(); + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + sendNotification(Watch9Constants.NOTIFICATION_CHANNEL_DEFAULT, false); + } + + private void sendNotification(int notificationChannel, boolean isStopNotification) { + try { + TransactionBuilder builder = performInitialized("showNotification"); + byte[] command = Watch9Constants.CMD_NOTIFICATION_TASK; + command[1] = (byte) (isStopNotification ? 0x04 : 0x01); + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(command, + Watch9Constants.TASK, + Conversion.toByteArr32(notificationChannel))); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to send notification", e); + } + } + + private Watch9DeviceSupport enableNotificationChannels(TransactionBuilder builder) { + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_NOTIFICATION_SETTINGS, + Watch9Constants.WRITE_VALUE, + new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF})); + + return this; + } + + public Watch9DeviceSupport authorizationRequest(TransactionBuilder builder, boolean firstConnect) { + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_AUTHORIZATION_TASK, + Watch9Constants.TASK, + new byte[]{(byte) (firstConnect ? 0x00 : 0x01)})); //possibly not the correct meaning + + return this; + } + + private Watch9DeviceSupport enableDoNotDisturb(TransactionBuilder builder, boolean active) { + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_DO_NOT_DISTURB_SETTINGS, + Watch9Constants.WRITE_VALUE, + new byte[]{(byte) (active ? 0x01 : 0x00)})); + + return this; + } + + private void enableCalibration(boolean enable) { + try { + TransactionBuilder builder = performInitialized("enableCalibration"); + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_CALIBRATION_INIT_TASK, + Watch9Constants.TASK, + new byte[]{(byte) (enable ? 0x01 : 0x00)})); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to start/stop calibration mode", e); + } + } + + private void holdCalibration() { + try { + TransactionBuilder builder = performInitialized("holdCalibration"); + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_CALIBRATION_KEEP_ALIVE, + Watch9Constants.KEEP_ALIVE)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to keep calibration mode alive", e); + } + } + + private void sendCalibrationData(@IntRange(from=0,to=23)int hour, @IntRange(from=0,to=59)int minute, @IntRange(from=0,to=59)int second) { + try { + isCalibrationActive = true; + TransactionBuilder builder = performInitialized("calibrate"); + int handsPosition = ((hour % 12) * 60 + minute) * 60 + second; + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_CALIBRATION_TASK, + Watch9Constants.TASK, + Conversion.toByteArr16(handsPosition))); + performImmediately(builder); + } catch (IOException e) { + isCalibrationActive = false; + LOG.warn("Unable to send calibration data", e); + } + } + + private void getTime() { + try { + TransactionBuilder builder = performInitialized("getTime"); + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_TIME_SETTINGS, + Watch9Constants.READ_VALUE)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to get device time", e); + } + } + + private void handleTime(byte[] time) { + GregorianCalendar now = BLETypeConversions.createCalendar(); + GregorianCalendar nowDevice = BLETypeConversions.createCalendar(); + int year = (nowDevice.get(Calendar.YEAR) / 100) * 100 + Conversion.fromBcd8(time[8]); + nowDevice.set(year, + Conversion.fromBcd8(time[9]) - 1, + Conversion.fromBcd8(time[10]), + Conversion.fromBcd8(time[11]), + Conversion.fromBcd8(time[12]), + Conversion.fromBcd8(time[13])); + nowDevice.set(Calendar.DAY_OF_WEEK, Conversion.fromBcd8(time[16]) + 1); + + long timeDiff = (Math.abs(now.getTimeInMillis() - nowDevice.getTimeInMillis())) / 1000; + if (10 < timeDiff && timeDiff < 120) { + enableCalibration(true); + setTime(BLETypeConversions.createCalendar()); + enableCalibration(false); + } + } + + private void setTime(Calendar calendar) { + try { + TransactionBuilder builder = performInitialized("setTime"); + int timezoneOffsetMinutes = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000); + int timezoneOffsetIndustrialMinutes = Math.round((Math.abs(timezoneOffsetMinutes) % 60) * 100f / 60f); + byte[] time = new byte[]{Conversion.toBcd8(calendar.get(Calendar.YEAR) % 100), + Conversion.toBcd8(calendar.get(Calendar.MONTH) + 1), + Conversion.toBcd8(calendar.get(Calendar.DAY_OF_MONTH)), + Conversion.toBcd8(calendar.get(Calendar.HOUR_OF_DAY)), + Conversion.toBcd8(calendar.get(Calendar.MINUTE)), + Conversion.toBcd8(calendar.get(Calendar.SECOND)), + (byte) (timezoneOffsetMinutes / 60), + (byte) timezoneOffsetIndustrialMinutes, + (byte) (calendar.get(Calendar.DAY_OF_WEEK) - 1) + }; + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_TIME_SETTINGS, + Watch9Constants.WRITE_VALUE, + time)); + performImmediately(builder); + } catch (IOException e) { + LOG.warn("Unable to set time", e); + } + } + + public Watch9DeviceSupport getFirmwareVersion(TransactionBuilder builder) { + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_FIRMWARE_INFO, + Watch9Constants.READ_VALUE)); + + return this; + } + + private Watch9DeviceSupport getBatteryState(TransactionBuilder builder) { + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_BATTERY_INFO, + Watch9Constants.READ_VALUE)); + + return this; + } + + private Watch9DeviceSupport setFitnessGoal(TransactionBuilder builder) { + int fitnessGoal = new ActivityUser().getStepsGoal(); + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_FITNESS_GOAL_SETTINGS, + Watch9Constants.WRITE_VALUE, + Conversion.toByteArr16(fitnessGoal))); + + return this; + } + + public Watch9DeviceSupport initialize(TransactionBuilder builder) { + getFirmwareVersion(builder) + .getBatteryState(builder) + .enableNotificationChannels(builder) + .enableDoNotDisturb(builder, false) + .setFitnessGoal(builder); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + builder.setGattCallback(this); + + return this; + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onSetTime() { + getTime(); + } + + @Override + public void onSetAlarms(ArrayList alarms) { + try { + TransactionBuilder builder = performInitialized("setAlarms"); + for (Alarm alarm : alarms) { + setAlarm(alarm, alarm.getIndex() + 1, builder); + } + builder.queue(getQueue()); + } catch (IOException e) { + LOG.warn("Unable to set alarms", e); + } + } + + // No useful use case at the moment, used to clear alarm slots for testing. + private void deleteAlarm(TransactionBuilder builder, int index) { + if (0 < index && index < 4) { + byte[] alarmValue = new byte[]{(byte) index, 0x00, 0x00, 0x00, 0x00, 0x00}; + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_ALARM_SETTINGS, + Watch9Constants.WRITE_VALUE, + alarmValue)); + } + } + + private void setAlarm(Alarm alarm, int index, TransactionBuilder builder) { + // Shift the GB internal repetition mask to match the device specific one. + byte repetitionMask = (byte) ((alarm.getRepetitionMask() << 1) | (alarm.isRepetitive() ? 0x80 : 0x00)); + repetitionMask |= (alarm.getRepetition(Alarm.ALARM_SUN) ? 0x01 : 0x00); + if (0 < index && index < 4) { + byte[] alarmValue = new byte[]{(byte) index, + Conversion.toBcd8(alarm.getAlarmCal().get(Calendar.HOUR_OF_DAY)), + Conversion.toBcd8(alarm.getAlarmCal().get(Calendar.MINUTE)), + repetitionMask, + (byte) (alarm.isEnabled() ? 0x01 : 0x00), + 0x00 // TODO: Unknown + }; + builder.write(getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE), + buildCommand(Watch9Constants.CMD_ALARM_SETTINGS, + Watch9Constants.WRITE_VALUE, + alarmValue)); + } + } + + @Override + public void onSetCallState(CallSpec callSpec) { + switch (callSpec.command) { + case CallSpec.CALL_INCOMING: + sendNotification(Watch9Constants.NOTIFICATION_CHANNEL_PHONE_CALL, false); + break; + case CallSpec.CALL_START: + case CallSpec.CALL_END: + sendNotification(Watch9Constants.NOTIFICATION_CHANNEL_PHONE_CALL, true); + break; + default: + break; + } + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onFetchRecordedData(int dataTypes) { + + } + + @Override + public void onReboot() { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onSendConfiguration(String config) { + TransactionBuilder builder; + try { + builder = performInitialized("sendConfig: " + config); + switch (config) { + case ActivityUser.PREF_USER_STEPS_GOAL: + setFitnessGoal(builder); + break; + } + builder.queue(getQueue()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + if (Watch9Constants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) { + byte[] value = characteristic.getValue(); + if (ArrayUtils.equals(value, Watch9Constants.RESP_FIRMWARE_INFO, 5)) { + handleFirmwareInfo(value); + } else if (ArrayUtils.equals(value, Watch9Constants.RESP_BATTERY_INFO, 5)) { + handleBatteryState(value); + } else if (ArrayUtils.equals(value, Watch9Constants.RESP_TIME_SETTINGS, 5)) { + handleTime(value); + } else if (ArrayUtils.equals(value, Watch9Constants.RESP_BUTTON_INDICATOR, 5)) { + LOG.info("Unhandled action: Button pressed"); + } else if (ArrayUtils.equals(value, Watch9Constants.RESP_ALARM_INDICATOR, 5)) { + LOG.info("Alarm active: id=" + value[8]); + } else if (isCalibrationActive && value.length == 7 && value[4] == ACK_CALIBRATION) { + setTime(BLETypeConversions.createCalendar()); + isCalibrationActive = false; + } + + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + + return false; + } + + private byte[] buildCommand(byte[] command, byte action) { + return buildCommand(command, action, null); + } + + private byte[] buildCommand(byte[] command, byte action, byte[] value) { + if (Arrays.equals(command, Watch9Constants.CMD_CALIBRATION_TASK)) { + ACK_CALIBRATION = (byte) sequenceNumber; + } + command = BLETypeConversions.join(command, value); + byte[] result = new byte[7 + command.length]; + System.arraycopy(Watch9Constants.CMD_HEADER, 0, result, 0, 5); + System.arraycopy(command, 0, result, 6, command.length); + result[2] = (byte) (command.length + 1); + result[3] = Watch9Constants.REQUEST; + result[4] = (byte) sequenceNumber++; + result[5] = action; + result[result.length - 1] = calculateChecksum(result); + + return result; + } + + private byte calculateChecksum(byte[] bytes) { + byte checksum = 0x00; + for (int i = 0; i < bytes.length - 1; i++) { + checksum += (bytes[i] ^ i) & 0xFF; + } + return (byte) (checksum & 0xFF); + } + + private void handleFirmwareInfo(byte[] value) { + versionInfo.fwVersion = String.format(Locale.US,"%d.%d.%d", value[8], value[9], value[10]); + handleGBDeviceEvent(versionInfo); + } + + private void handleBatteryState(byte[] value) { + batteryInfo.state = value[9] == 1 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_LOW; + batteryInfo.level = GBDevice.BATTERY_UNKNOWN; + handleGBDeviceEvent(batteryInfo); + } + + @Override + public void dispose() { + LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); + broadcastManager.unregisterReceiver(broadcastReceiver); + super.dispose(); + } + + private static class Conversion { + static byte toBcd8(@IntRange(from = 0, to = 99) int value) { + int high = (value / 10) << 4; + int low = value % 10; + return (byte) (high | low); + } + + static int fromBcd8(byte value) { + int high = ((value & 0xF0) >> 4) * 10; + int low = value & 0x0F; + return high + low; + } + + static byte[] toByteArr16(int value) { + return new byte[]{(byte) (value >> 8), (byte) value}; + } + + static byte[] toByteArr32(int value) { + return new byte[]{(byte) (value >> 24), + (byte) (value >> 16), + (byte) (value >> 8), + (byte) value}; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/operations/InitOperation.java new file mode 100644 index 000000000..f26ac5553 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/watch9/operations/InitOperation.java @@ -0,0 +1,93 @@ +/* Copyright (C) 2018 maxirnilian + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9Constants; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class InitOperation extends AbstractBTLEOperation{ + + private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class); + + private final TransactionBuilder builder; + private final boolean needsAuth; + private final BluetoothGattCharacteristic cmdCharacteristic = getCharacteristic(Watch9Constants.UUID_CHARACTERISTIC_WRITE); + + public InitOperation(boolean needsAuth, Watch9DeviceSupport support, TransactionBuilder builder) { + super(support); + this.needsAuth = needsAuth; + this.builder = builder; + builder.setGattCallback(this); + } + + @Override + protected void doPerform() throws IOException { + builder.notify(cmdCharacteristic, true); + if (needsAuth) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + getSupport().authorizationRequest(builder, needsAuth); + } else { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getSupport().initialize(builder); + getSupport().performImmediately(builder); + } + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (Watch9Constants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID) && needsAuth) { + try { + byte[] value = characteristic.getValue(); + getSupport().logMessageContent(value); + if (ArrayUtils.equals(value, Watch9Constants.RESP_AUTHORIZATION_TASK, 5) && value[8] == 0x01) { + TransactionBuilder builder = getSupport().createTransactionBuilder("authInit"); + builder.setGattCallback(this); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getSupport().initialize(builder).performImmediately(builder); + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } catch (Exception e) { + GB.toast(getContext(), "Error authenticating Watch 9", Toast.LENGTH_LONG, GB.ERROR, e); + } + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/zetime/ZeTimeDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/zetime/ZeTimeDeviceSupport.java index d309c8f96..fcd65f586 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/zetime/ZeTimeDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/zetime/ZeTimeDeviceSupport.java @@ -1,3 +1,20 @@ +/* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer, + Julien Pivotto, Kranz, Sebastian Kranz, Steffen Liebergeld + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.zetime; import android.bluetooth.BluetoothGatt; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java index e73527eab..c9050af91 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/AbstractSerialDeviceSupport.java @@ -250,4 +250,16 @@ public abstract class AbstractSerialDeviceSupport extends AbstractDeviceSupport byte[] bytes = gbDeviceProtocol.encodeSendWeather(weatherSpec); sendToDevice(bytes); } + + @Override + public void onSetFmFrequency(float frequency) { + byte[] bytes = gbDeviceProtocol.encodeFmFrequency(frequency); + sendToDevice(bytes); + } + + @Override + public void onSetLedColor(int color) { + byte[] bytes = gbDeviceProtocol.encodeLedColor(color); + sendToDevice(bytes); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java index 5070ce8e6..f29b1c23b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/serial/GBDeviceProtocol.java @@ -133,4 +133,12 @@ public abstract class GBDeviceProtocol { public byte[] encodeSendWeather(WeatherSpec weatherSpec) { return null; } + + public byte[] encodeLedColor(int color) { + return null; + } + + public byte[] encodeFmFrequency(float frequency) { + return null; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java index 7bf8b9cfb..ddbbb100c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/AndroidUtils.java @@ -181,7 +181,7 @@ public class AndroidUtils { return id.replaceFirst("raw:", ""); } uri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + Uri.parse("content://downloads/public_downloads"), Long.parseLong(id)); } } else if ("com.android.providers.media.documents".equals(uri.getAuthority())) { final String docId = DocumentsContract.getDocumentId(uri); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BengaliLanguageUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BengaliLanguageUtils.java new file mode 100644 index 000000000..8c91eb7b9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/BengaliLanguageUtils.java @@ -0,0 +1,242 @@ +/* Copyright (C) 2017-2018 Aniruddha Adhikary + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.util; + +import java.util.HashMap; +import java.util.regex.*; + +// What's the reason to extending LanguageUtils? +// Just doing it because already done in the previous code. +public class BengaliLanguageUtils extends LanguageUtils { + // Composite Letters. + private final static HashMap composites = new HashMap() { + { + put("ক্ষ", "kkh"); + put("ঞ্চ", "NC"); + put("ঞ্ছ", "NCh"); + put("ঞ্জ", "Ng"); + put("জ্ঞ", "gg"); + put("ঞ্ঝ", "Ngh"); + put("্র", "r"); + put("্ল", "l"); + put("ষ্ম", "SSh"); + put("র্", "r"); + put("্য", "y"); + put("্ব", "w"); + } + }; + // Vowels Only + private final static HashMap vowelsAndHasants = new HashMap() { + { + put("আ", "aa"); + put("অ", "a"); + put("ই", "i"); + put("ঈ", "ii"); + put("উ", "u"); + put("ঊ", "uu"); + put("ঋ", "ri"); + put("এ", "e"); + put("ঐ", "oi"); + put("ও", "o"); + put("ঔ", "ou"); + put("া", "aa"); + put("ি", "i"); + put("ী", "ii"); + put("ু", "u"); + put("ূ", "uu"); + put("ৃ", "r"); + put("ে", "e"); + put("ো", "o"); + put("ৈ", "oi"); + put("ৗ", "ou"); + put("ৌ", "ou"); + put("ং", "ng"); + put("ঃ", "h"); + put("।", "."); + } + }; + + // Single Character Letters. + private final static HashMap letters = new HashMap() { + { + put("আ", "aa"); + put("অ", "a"); + put("ই", "i"); + put("ঈ", "ii"); + put("উ", "u"); + put("ঊ", "uu"); + put("ঋ", "ri"); + put("এ", "e"); + put("ঐ", "oi"); + put("ও", "o"); + put("ঔ", "ou"); + put("ক", "k"); + put("খ", "kh"); + put("গ", "g"); + put("ঘ", "gh"); + put("ঙ", "ng"); + put("চ", "ch"); + put("ছ", "chh"); + put("জ", "j"); + put("ঝ", "jh"); + put("ঞ", "Ng"); + put("ট", "T"); + put("ঠ", "Th"); + put("ড", "D"); + put("ঢ", "Dh"); + put("ণ", "N"); + put("ত", "t"); + put("থ", "th"); + put("দ", "d"); + put("ধ", "dh"); + put("ন", "n"); + put("প", "p"); + put("ফ", "ph"); + put("ব", "b"); + put("ভ", "bh"); + put("ম", "m"); + put("য", "J"); + put("র", "r"); + put("ল", "l"); + put("শ", "sh"); + put("ষ", "Sh"); + put("স", "s"); + put("হ", "h"); + put("ড়", "rh"); + put("ঢ়", "rH"); + put("য়", "y"); + put("ৎ", "t"); + put("০", "0"); + put("১", "1"); + put("২", "2"); + put("৩", "3"); + put("৪", "4"); + put("৫", "5"); + put("৬", "6"); + put("৭", "7"); + put("৮", "8"); + put("৯", "9"); + put("া", "aa"); + put("ি", "i"); + put("ী", "ii"); + put("ু", "u"); + put("ূ", "uu"); + put("ৃ", "r"); + put("ে", "e"); + put("ো", "o"); + put("ৈ", "oi"); + put("ৗ", "ou"); + put("ৌ", "ou"); + put("ং", "ng"); + put("ঃ", "h"); + put("ঁ", "nN"); + put("।", "."); + } + }; + + // The regex to extract Bengali characters in nested groups. + private final static String pattern = "(র্){0,1}(([অ-হড়-য়])(্([অ-মশ-হড়-য়]))*)((‍){0,1}(্([য-ল]))){0,1}([া-ৌ]){0,1}|([্ঁঃংৎ০-৯।])| "; + private final static Pattern bengaliRegex = Pattern.compile(pattern); + + private static String getVal(String key) { + if (key != null) { + String comp = composites.get(key); + if (comp != null) { + return comp; + } + String sl = letters.get(key); + if (sl != null) { + return letters.get(key); + } + } + return null; + } + + public static String transliterate(String txt) { + if (txt.isEmpty()) { + return txt; + } + + Matcher m = bengaliRegex.matcher(txt); + StringBuffer sb = new StringBuffer(); + while (m.find()) { + String appendableString = ""; + String reff = m.group(1); + if (reff != null) { + appendableString = appendableString + "rr"; + } + // This is a filter-down approach. First considering larger groups, + // If found any match breaks their. Else go to the next step. + // Helpful to solve some corner-cases. + String mainPart = getVal(m.group(2)); + if (mainPart != null) { + appendableString = appendableString + mainPart; + } else { + String firstPart = getVal(m.group(3)); + if (firstPart != null) { + appendableString = appendableString + firstPart; + } + int g = 4; + while (g < 6) { + String part = getVal(m.group(g)); + if (part != null) { + appendableString = appendableString + part; + break; + } + g = g + 1; + } + } + int g = 6; + while (g < 10) { + String key = getVal(m.group(g)); + if (key != null) { + appendableString = appendableString + key; + break; + } + g = g + 1; + } + String kaar = m.group(10); + if (kaar != null) { + String kaarStr = letters.get(kaar); + if (kaarStr != null) { + appendableString = appendableString + kaarStr; + } + } else if (appendableString.length() > 0 && !vowelsAndHasants.containsKey(m.group(0))) { + // Adding 'a' like ITRANS if no vowel is present. + // TODO: Have to add it dynamically using Bengali grammer rules. + appendableString = appendableString + "a"; + } + String singleton = m.group(11); + if (singleton != null) { + String singleStr = letters.get(singleton); + if (singleStr != null) { + appendableString = appendableString + singleStr; + } + } + String others = m.group(0); + if (others != null) { + + if (appendableString.length() <= 0) { + appendableString = appendableString + others; + } + } + m.appendReplacement(sb, appendableString); + } + m.appendTail(sb); + return sb.toString(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java index b9a577ce8..1f5017094 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DateTimeUtils.java @@ -1,5 +1,5 @@ /* Copyright (C) 2015-2018 Andreas Shimokawa, AndrewH, Carsten Pfeiffer, - Daniele Gobbetti + Daniele Gobbetti, Pavel Elagin This file is part of Gadgetbridge. @@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; public class DateTimeUtils { private static SimpleDateFormat DAY_STORAGE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US); + private static SimpleDateFormat HOURS_MINUTES_FORMAT = new SimpleDateFormat("HH:mm", Locale.US); public static SimpleDateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US){ //see https://github.com/Freeyourgadget/Gadgetbridge/issues/1076#issuecomment-383834116 and https://stackoverflow.com/a/30221245 @@ -51,7 +52,12 @@ public class DateTimeUtils { @Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) { StringBuffer rfcFormat = super.format(date, toAppendTo, pos); - return rfcFormat.insert(rfcFormat.length() - 2, ":"); + if (this.getTimeZone().equals(TimeZone.getTimeZone("UTC"))) { + rfcFormat.setLength(rfcFormat.length()-5); + return rfcFormat.append("Z"); + } else { + return rfcFormat.insert(rfcFormat.length() - 2, ":"); + } } }; //no public access, we have to workaround Android bugs @@ -64,6 +70,17 @@ public class DateTimeUtils { if(GBApplication.isRunningNougatOrLater()){ return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US).format(date); } + ISO_8601_FORMAT.setTimeZone(TimeZone.getDefault()); + return ISO_8601_FORMAT.format(date); + } + + public static String formatIso8601UTC(Date date) { + if(GBApplication.isRunningNougatOrLater()){ + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US); + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + return sdf.format(date); + } + ISO_8601_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); return ISO_8601_FORMAT.format(date); } @@ -115,6 +132,10 @@ public class DateTimeUtils { return DAY_STORAGE_FORMAT.parse(day); } + public static String timeToString(Date date) { + return HOURS_MINUTES_FORMAT.format(date); + } + public static Date todayUTC() { Calendar cal = getCalendarUTC(); return cal.getTime(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index feab94917..cc1fc17cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -1,6 +1,6 @@ /* Copyright (C) 2015-2018 0nse, Andreas Shimokawa, Carsten Pfeiffer, - Daniele Gobbetti, João Paulo Barraca, ladbsoft, protomors, Quallenauge, - Sami Alaoui, tiparega + Daniele Gobbetti, João Paulo Barraca, Kranz, ladbsoft, maxirnilian, + protomors, Quallenauge, Sami Alaoui, tiparega, Vadim Kaushan This file is part of Gadgetbridge. @@ -50,13 +50,17 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitcor.AmazfitCorC import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2HRXCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -213,6 +217,10 @@ public class DeviceHelper { result.add(new TeclastH30Coordinator()); result.add(new XWatchCoordinator()); result.add(new ZeTimeCoordinator()); + result.add(new ID115Coordinator()); + result.add(new Watch9DeviceCoordinator()); + result.add(new Roidmi1Coordinator()); + result.add(new Roidmi3Coordinator()); return result; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java index 9ec1ad2da..6500a5b43 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/FileUtils.java @@ -318,6 +318,6 @@ public class FileUtils { * @return the valid file name */ public static String makeValidFileName(String name) { - return name.replaceAll("\0/:\\r\\n\\\\", "_"); + return name.replaceAll("[\0/:\\r\\n\\\\]", "_"); } } \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java index 1bd889bb1..1089280de 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -32,12 +32,17 @@ public class GBPrefs { private static final boolean AUTO_START_DEFAULT = true; private static final String BG_JS_ENABLED = "pebble_enable_background_javascript"; private static final boolean BG_JS_ENABLED_DEFAULT = false; + public static final String RTL_SUPPORT = "rtl"; + public static final String RTL_CONTEXTUAL_ARABIC = "contextualArabic"; public static boolean AUTO_RECONNECT_DEFAULT = true; public static final String USER_NAME = "mi_user_alias"; public static final String USER_NAME_DEFAULT = "gadgetbridge-user"; private static final String USER_BIRTHDAY = ""; + public static final String CHART_MAX_HEART_RATE = "chart_max_heart_rate"; + public static final String CHART_MIN_HEART_RATE = "chart_min_heart_rate"; + private final Prefs mPrefs; public GBPrefs(Prefs prefs) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LanguageUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LanguageUtils.java index 21246c436..c97ff7258 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LanguageUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LanguageUtils.java @@ -1,5 +1,6 @@ -/* Copyright (C) 2017-2018 Andreas Shimokawa, Daniele Gobbetti, ivanovlev, - lazarosfs, McSym28, Ted Stein, Yaron Shahrabani +/* Copyright (C) 2017-2018 Andreas Shimokawa, Aniruddha Adhikary, Daniele + Gobbetti, ivanovlev, kalaee, lazarosfs, McSym28, Ted Stein, Thomas, Yaron + Shahrabani This file is part of Gadgetbridge. @@ -26,12 +27,18 @@ import java.util.Map; import nodomain.freeyourgadget.gadgetbridge.GBApplication; public class LanguageUtils { + //transliteration map with english equivalent for unsupported chars private static Map transliterateMap = new HashMap(){ { //extended ASCII characters - put('æ', "ae"); put('œ', "oe"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\""); - + put('œ', "oe"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\""); + + // Scandinavian characters + put('Æ',"Ae"); put('æ',"ae"); + put('Ø',"Oe"); put('ø',"oe"); + put('Å',"Aa"); put('å',"aa"); + //german characters put('ä',"ae"); put('ö',"oe"); put('ü',"ue"); put('Ä',"Ae"); put('Ö',"Oe"); put('Ü',"Üe"); @@ -71,13 +78,16 @@ public class LanguageUtils { put('و', "w"); put('ي', "y"); put('ى', "a"); put('ﺓ', ""); put('آ', "2"); put('ئ', "2"); put('إ', "2"); put('ؤ', "2"); put('أ', "2"); put('ء', "2"); - // Farsi - put('پ', "p"); put('چ', "ch"); put('ڜ', "ch"); put('ڤ', "v"); put('ڥ', "v"); - put('ڨ', "g"); put('گ', "g"); put('ݣ', "g"); + // Persian(Farsi) + put('پ', "p"); put('چ', "ch"); put('ژ', "zh"); put('ک', "k"); put('گ', "g"); put('ی', "y"); + put('؟', "?"); put('٪', "%"); put('؛', ";"); put('،', ","); // Polish put('Ł', "L"); put('ł', "l"); + //Lithuanian + put('ą', "a"); put('č', "c"); put('ę', "e"); put('ė', "e"); put('į', "i"); put('š', "s"); put('ų', "u"); put('ū', "u"); put('ž', "z"); + //TODO: these must be configurable. If someone wants to transliterate cyrillic it does not mean his device has no German umlauts //all or nothing is really bad here } @@ -111,7 +121,9 @@ public class LanguageUtils { message.append(transliterate(c)); } - return flattenToAscii(message.toString()); + String messageString = BengaliLanguageUtils.transliterate(message.toString()); + + return flattenToAscii(messageString); } /** diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RtlUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RtlUtils.java new file mode 100644 index 000000000..ae14ab888 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/RtlUtils.java @@ -0,0 +1,594 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; + +public class RtlUtils { + + /** + * Checks the status of right-to-left option + * @return true if right-to-left option is On, and false, if Off or not exist + */ + public static boolean rtlSupport() + { + return GBApplication.getPrefs().getBoolean(GBPrefs.RTL_SUPPORT, false); + } + + public enum characterType{ + ltr, + rtl, + rtl_arabic, + punctuation, + lineEnd, + space, + } + + public static characterType getCharacterType(Character c){ + characterType type; + switch (Character.getDirectionality(c)) { + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + type = characterType.rtl; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + type = characterType.rtl_arabic; + break; + case Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR: + case Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR: + case Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR: + case Character.DIRECTIONALITY_OTHER_NEUTRALS: + type = characterType.punctuation; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + case Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR: + type = characterType.lineEnd; + break; + case Character.DIRECTIONALITY_WHITESPACE: + type = characterType.space; + break; + default: + type = characterType.ltr; + } + + return type; + } + + /** + * Checks the status of right-to-left option + * @return true if right-to-left option is On, and false, if Off or not exist + */ + public static boolean contextualSupport() + { + return GBApplication.getPrefs().getBoolean(GBPrefs.RTL_CONTEXTUAL_ARABIC, false); + } + + //map with brackets chars to change there direction + private static Map directionSignsMap = new HashMap(){ + { + put('(', ')'); put(')', '('); put('[', ']'); put(']', '['); put('{','}'); put('}','{'); + + + } + }; + + /** + * @return true if the char is in the rtl range, otherwise false + */ + public static boolean isHebrew(Character c){ + + return getCharacterType(c) == characterType.rtl; + } + + /** + * @return true if the char is in the rtl range, otherwise false + */ + public static boolean isArabic(Character c){ + + return getCharacterType(c) == characterType.rtl_arabic; + } + + /** + * @return true if the char is in the rtl range, otherwise false + */ + public static boolean isLtr(Character c){ + + return getCharacterType(c) == characterType.ltr; + } + + /** + * @return true if the char is in the rtl range, otherwise false + */ + public static boolean isRtl(Character c){ + + return (getCharacterType(c) == characterType.rtl) || (getCharacterType(c) == characterType.rtl_arabic); + } + + /** + * @return true if the char is in the punctuations range, otherwise false + */ + public static boolean isPunctuations(Character c){ + + return getCharacterType(c) == characterType.punctuation; + } + + + /** + * @return true if the char is in the end of word list, otherwise false + */ + public static boolean isSpaceSign(Character c){ + + return getCharacterType(c) == characterType.space; + } + + /** + * @return true if the char is in the end of word list, otherwise false + */ + public static boolean isEndLineSign(Character c){ + + return getCharacterType(c) == characterType.lineEnd; + } + + //map from Arabian characters to their contextual form in the beginning of the word + private static Map contextualArabicIsolated = new HashMap(){ + { + put('ا', '\uFE8D'); + put('ب', '\uFE8F'); + put('ت', '\uFE95'); + put('ث', '\uFE99'); + put('ج', '\uFE9D'); + put('ح', '\uFEA1'); + put('خ', '\uFEA5'); + put('د', '\uFEA9'); + put('ذ', '\uFEAB'); + put('ر', '\uFEAD'); + put('ز', '\uFEAF'); + put('س', '\uFEB1'); + put('ش', '\uFEB5'); + put('ص', '\uFEB9'); + put('ض', '\uFEBD'); + put('ط', '\uFEC1'); + put('ظ', '\uFEC5'); + put('ع', '\uFEC9'); + put('غ', '\uFECD'); + put('ف', '\uFED1'); + put('ق', '\uFED5'); + put('ك', '\uFED9'); + put('ل', '\uFEDD'); + put('م', '\uFEE1'); + put('ن', '\uFEE5'); + put('ه', '\uFEE9'); + put('و', '\uFEED'); + put('ي', '\uFEF1'); + put('آ', '\uFE81'); + put('ة', '\uFE93'); + put('ى', '\uFEEF'); + put('ئ', '\uFE89'); + put('إ', '\uFE87'); + put('أ', '\uFE83'); + put('ء', '\uFE80'); + put('ؤ', '\uFE85'); + put((char)('ل' + 'آ'), '\uFEF5'); + put((char)('ل' + 'أ'), '\uFEF7'); + put((char)('ل' + 'إ'), '\uFEF9'); + put((char)('ل' + 'ا'), '\uFEFB'); + //Farsi + put('گ', '\uFB92'); + put('ک', '\uFB8E'); + put('چ', '\uFB7A'); + put('پ', '\uFB56'); + put('ژ', '\uFB8A'); + put('ی', '\uFBFC'); + + } + }; + + //map from Arabian characters to their contextual form in the beginning of the word + private static Map contextualArabicBeginning = new HashMap(){ + { + put('ب', '\uFE91'); + put('ت', '\uFE97'); + put('ث', '\uFE9B'); + put('ج', '\uFE9F'); + put('ح', '\uFEA3'); + put('خ', '\uFEA7'); + put('س', '\uFEB3'); + put('ش', '\uFEB7'); + put('ص', '\uFEBB'); + put('ض', '\uFEBF'); + put('ط', '\uFEC3'); + put('ظ', '\uFEC7'); + put('ع', '\uFECB'); + put('غ', '\uFECF'); + put('ف', '\uFED3'); + put('ق', '\uFED7'); + put('ك', '\uFEDB'); + put('ل', '\uFEDF'); + put('م', '\uFEE3'); + put('ن', '\uFEE7'); + put('ه', '\uFEEB'); + put('ي', '\uFEF3'); + put('ئ', '\uFE8B'); + //Farsi + put('گ', '\uFB94'); + put('ک', '\uFB90'); + put('چ', '\uFB7C'); + put('پ', '\uFB58'); + put('ی', '\uFBFE'); + } + }; + + //map from Arabian characters to their contextual form in the middle of the word + private static Map contextualArabicMiddle = new HashMap(){ + { + put('ب', '\uFE92'); + put('ت', '\uFE98'); + put('ث', '\uFE9C'); + put('ج', '\uFEA0'); + put('ح', '\uFEA4'); + put('خ', '\uFEA8'); + put('س', '\uFEB4'); + put('ش', '\uFEB8'); + put('ص', '\uFEBC'); + put('ض', '\uFEC0'); + put('ط', '\uFEC4'); + put('ظ', '\uFEC8'); + put('ع', '\uFECC'); + put('غ', '\uFED0'); + put('ف', '\uFED4'); + put('ق', '\uFED8'); + put('ك', '\uFEDC'); + put('ل', '\uFEE0'); + put('م', '\uFEE4'); + put('ن', '\uFEE8'); + put('ه', '\uFEEC'); + put('ي', '\uFEF4'); + put('ئ', '\uFE8C'); + //Farsi + put('گ', '\uFB95'); + put('ک', '\uFB91'); + put('چ', '\uFB7D'); + put('پ', '\uFB59'); + put('ی', '\uFBFF'); + } + }; + + //map from Arabian characters to their contextual form in the end of the word + private static Map contextualArabicEnd = new HashMap(){ + { + put('ا', '\uFE8E'); + put('ب', '\uFE90'); + put('ت', '\uFE96'); + put('ث', '\uFE9A'); + put('ج', '\uFE9E'); + put('ح', '\uFEA2'); + put('خ', '\uFEA6'); + put('د', '\uFEAA'); + put('ذ', '\uFEAC'); + put('ر', '\uFEAE'); + put('ز', '\uFEB0'); + put('س', '\uFEB2'); + put('ش', '\uFEB6'); + put('ص', '\uFEBA'); + put('ض', '\uFEBE'); + put('ط', '\uFEC2'); + put('ظ', '\uFEC6'); + put('ع', '\uFECA'); + put('غ', '\uFECE'); + put('ف', '\uFED2'); + put('ق', '\uFED6'); + put('ك', '\uFEDA'); + put('ل', '\uFEDE'); + put('م', '\uFEE2'); + put('ن', '\uFEE6'); + put('ه', '\uFEEA'); + put('و', '\uFEEE'); + put('ي', '\uFEF2'); + put('آ', '\uFE82'); + put('ة', '\uFE94'); + put('ى', '\uFEF0'); + put('ئ', '\uFE8A'); + put('إ', '\uFE88'); + put('أ', '\uFE84'); + put('ؤ', '\uFE86'); + put((char)('ل' + 'آ'), '\uFEF6'); + put((char)('ل' + 'أ'), '\uFEF8'); + put((char)('ل' + 'إ'), '\uFEFA'); + put((char)('ل' + 'ا'), '\uFEFC'); + //Farsi + put('گ', '\uFB93'); + put('ک', '\uFB8F'); + put('چ', '\uFB7B'); + put('پ', '\uFB57'); + put('ژ', '\uFB8B'); + put('ی', '\uFBFD'); + } + }; + public enum contextualState{ + isolate, + begin, + middle, + end + } + + public static boolean exceptionAfterLam(Character c){ + switch (c){ + case '\u0622': + case '\u0623': + case '\u0625': + case '\u0627': + return true; + default: + return false; + + } + } + + /** + * This function return the contextual form of Arabic characters in a given state + * @param c - the character to convert + * @param state - the character state: beginning, middle, end or isolated + * @return the contextual character + */ + public static Character getContextualSymbol(Character c, contextualState state) { + Character newChar; + switch (state){ + case begin: + newChar = contextualArabicBeginning.get(c); + break; + case middle: + newChar = contextualArabicMiddle.get(c); + break; + case end: + newChar = contextualArabicEnd.get(c); + break; + case isolate: + default: + newChar = contextualArabicIsolated.get(c);; + } + if (newChar != null){ + return newChar; + } else{ + return c; + } + } + + /** + * This function return the contextual state of a given character, depend of the previous + * character state and the next charachter. + * @param prevState - previous character state or isolated if none + * @param curChar - the current character + * @param nextChar - the next character or null if none + * @return the current character contextual state + */ + public static contextualState getCharContextualState(contextualState prevState, Character curChar, Character nextChar) { + contextualState curState; + if ((prevState == contextualState.isolate || prevState == contextualState.end) && + contextualArabicBeginning.containsKey(curChar) && + contextualArabicEnd.containsKey(nextChar)){ + + curState = contextualState.begin; + + } else if ((prevState == contextualState.begin || prevState == contextualState.middle) && + contextualArabicEnd.containsKey(curChar)){ + + if (contextualArabicMiddle.containsKey(curChar) && contextualArabicEnd.containsKey(nextChar)){ + curState = contextualState.middle; + }else{ + curState = contextualState.end; + } + }else{ + curState = contextualState.isolate; + } + return curState; + } + + /** + * this function convert given string to it's contextual form + * @param s - the given string + * @return the contextual form of the string + */ + public static String convertToContextual(String s){ + if (s == null || s.isEmpty() || s.length() == 1){ + return s; + } + + int length = s.length(); + StringBuilder newWord = new StringBuilder(length); + + Character curChar, nextChar = s.charAt(0); + contextualState prevState = contextualState.isolate; + contextualState curState = contextualState.isolate; + + for (int i = 0; i < length - 1; i++){ + curChar = nextChar; + nextChar = s.charAt(i + 1); + + if (curChar == 'ل' && exceptionAfterLam(nextChar)){ + i++; + curChar = (char)(nextChar + curChar); + if (i < length - 1) { + nextChar = s.charAt(i + 1); + }else{ + nextChar = curChar; + prevState = curState; + break; + } + + } + + curState = getCharContextualState(prevState, curChar, nextChar); + newWord.append(getContextualSymbol(curChar, curState)); + prevState = curState; + + + } + curState = getCharContextualState(prevState, nextChar, null); + newWord.append(getContextualSymbol(nextChar, curState)); + + return newWord.toString(); + } + + + /** + * The function get a string and reverse it. + * in case of end-of-word sign, it will leave it at the end. + * in case of sign with direction like brackets, it will change the direction. + * @param s - the string to reverse + * @return reversed string + */ + public static String reverse(String s) { + int j = s.length(); + int isEndLine = 0; + char[] newWord = new char[j]; + + if (j == 0) { + return s; + } + + for (int i = 0; i < s.length() - isEndLine; i++) { + if (directionSignsMap.containsKey(s.charAt(i))) { + newWord[--j] = directionSignsMap.get(s.charAt(i)); + } else { + newWord[--j] = s.charAt(i); + } + } + + return new String(newWord); + } + + public static String fixWhitespace(String s){ + int length = s.length(); + + if (length > 0 && isSpaceSign(s.charAt(length - 1))){ + return s.charAt(length - 1) + s.substring(0, length - 1); + } else { + return s; + } + } + + /** + * The function get a string and fix the rtl words. + * since simple reverse puts the beginning of the text at the end, the text should have been from bottom to top. + * To avoid that, we save the text in lines (line max size can be change in the settings) + * @param s - the string to fix. + * @return a fix string. + */ + public static String fixRtl(String s) { + if (s == null || s.isEmpty()){ + return s; + } + Log.d("ROIGR", "before: |" + org.apache.commons.lang3.StringEscapeUtils.escapeJava(s) + "|"); + + int length = s.length(); + String oldString = s.substring(0, length); + String newString = ""; + List lines = new ArrayList<>(); + char[] newWord = new char[length]; + int line_max_size = GBApplication.getPrefs().getInt("rtl_max_line_length", 18); + + int startPos = 0; + int endPos = 0; + characterType CurRtlType = isRtl(oldString.charAt(0))? characterType.rtl : characterType.ltr; + characterType PhraseRtlType = CurRtlType; + + Character c; +// String word = "", phrase = "", line = ""; + StringBuilder word = new StringBuilder(); + StringBuilder phrase = new StringBuilder(); + StringBuilder line = new StringBuilder(); + String phraseString = ""; + boolean addCharToWord = false; + for (int i = 0; i < length; i++) { + c = oldString.charAt(i); + addCharToWord = false; + Log.d("ROIGR", "char: " + c + " :" + Character.getDirectionality(c)); +// Log.d("ROIGR", "hex : " + (int)c); + + if (isLtr(c)){ + CurRtlType = characterType.ltr; + } else if (isRtl(c)) { + CurRtlType = characterType.rtl; + } + + if ((CurRtlType == PhraseRtlType) && !(isSpaceSign(c) || isEndLineSign(c))){ + Log.d("ROIGR", "add: " + c + " to: " + word); + word.append(c); + addCharToWord = true; + if (i < length - 1) { + continue; + } + } + + + + do { + if ((line.length() + phrase.length() + word.length() < line_max_size) || + (line.length() == 0 && word.length() > line_max_size)) { + if (isSpaceSign(c)) { + word.append(c); + addCharToWord = true; + } + + phrase.append(word); + word.setLength(0); + + if (isSpaceSign(c)) { + break; + } + } + + + phraseString = phrase.toString(); + Log.d("ROIGR", "phrase: |" + phraseString + "|"); + if (PhraseRtlType == characterType.rtl) { + if (contextualSupport()) { + phraseString = convertToContextual(phraseString); + } + phraseString = reverse(phraseString); + } + + line.insert(0, fixWhitespace(phraseString)); + Log.d("ROIGR", "line now: |" + line + "|"); + phrase.setLength(0); + + if (word.length() > 0){ + line.append('\n'); + } else if (isEndLineSign(c)) { + line.append(c); + } else if (!addCharToWord) { + word.append(c); + if (i == length - 1){ + addCharToWord = true; + continue; + } + PhraseRtlType = PhraseRtlType == characterType.rtl ? characterType.ltr : characterType.rtl; + break; + } + + lines.add(line.toString()); + Log.d("ROIGR", "line: |" + line + "|"); + line.setLength(0); + + if (word.length() == 0){ + break; + } + + } while (true); + + } + + lines.add(line.toString()); + + newString = TextUtils.join("", lines); + + Log.d("ROIGR", "after : |" + org.apache.commons.lang3.StringEscapeUtils.escapeJava(newString) + "|"); + + return newString; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java index 641773a4c..e6c3aca5c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java @@ -20,6 +20,8 @@ import android.support.annotation.NonNull; public class StringUtils { + + @NonNull public static String truncate(String s, int maxLength){ if (s == null) { diff --git a/app/src/main/java/ru/gelin/android/weather/notification/ParcelableWeather2.java b/app/src/main/java/ru/gelin/android/weather/notification/ParcelableWeather2.java index b58560c73..89dda36a7 100644 --- a/app/src/main/java/ru/gelin/android/weather/notification/ParcelableWeather2.java +++ b/app/src/main/java/ru/gelin/android/weather/notification/ParcelableWeather2.java @@ -1,4 +1,5 @@ -/* Copyright (C) 2015-2018 Andreas Shimokawa, Daniele Gobbetti, Taavi Eomäe +/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele + Gobbetti, Taavi Eomäe This file is part of Gadgetbridge. @@ -124,7 +125,7 @@ public class ParcelableWeather2 implements Parcelable { } catch (JSONException e) { LOG.error("error while construction JSON", e); } - LOG.debug("Forecast JSON for Webview: " + reconstructedOWMForecast.toString()); + LOG.debug("Forecast JSON for Webview: " + reconstructedOWMForecast); } } diff --git a/app/src/main/res/drawable-hdpi/ic_device_miband2.png b/app/src/main/res/drawable-hdpi/ic_device_miband2.png new file mode 100644 index 000000000..8cc9cc357 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_miband2.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_miband2_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_miband2_disabled.png new file mode 100644 index 000000000..2307aa024 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_miband2_disabled.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_roidmi.png b/app/src/main/res/drawable-hdpi/ic_device_roidmi.png new file mode 100644 index 000000000..84b60d631 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..a7c66947b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_miband2.png b/app/src/main/res/drawable-mdpi/ic_device_miband2.png new file mode 100644 index 000000000..c56be18d8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_miband2.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_miband2_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_miband2_disabled.png new file mode 100644 index 000000000..0ea803d73 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_miband2_disabled.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_roidmi.png b/app/src/main/res/drawable-mdpi/ic_device_roidmi.png new file mode 100644 index 000000000..8e3af1efb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..f6e6526f5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable-v21/splash.xml b/app/src/main/res/drawable-v21/splash.xml new file mode 100644 index 000000000..10bcca000 --- /dev/null +++ b/app/src/main/res/drawable-v21/splash.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/ic_device_miband2.png b/app/src/main/res/drawable-xhdpi/ic_device_miband2.png new file mode 100644 index 000000000..297969cd4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_miband2.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_miband2_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_miband2_disabled.png new file mode 100644 index 000000000..2b1f455c8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_miband2_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png b/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png new file mode 100644 index 000000000..fdd34e5bc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..be31b8025 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_miband2.png b/app/src/main/res/drawable-xxhdpi/ic_device_miband2.png new file mode 100644 index 000000000..a4a684e1c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_miband2.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_miband2_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_miband2_disabled.png new file mode 100644 index 000000000..a6f88c666 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_miband2_disabled.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png new file mode 100644 index 000000000..34fec7f48 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png new file mode 100644 index 000000000..1c67417d0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_device_roidmi_disabled.png differ diff --git a/app/src/main/res/drawable/gadgetbridge_img.png b/app/src/main/res/drawable/gadgetbridge_img.png deleted file mode 100644 index 176a54e0f..000000000 Binary files a/app/src/main/res/drawable/gadgetbridge_img.png and /dev/null differ diff --git a/app/src/main/res/drawable/gadgetbridge_img.xml b/app/src/main/res/drawable/gadgetbridge_img.xml new file mode 100644 index 000000000..25dec7b26 --- /dev/null +++ b/app/src/main/res/drawable/gadgetbridge_img.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_led_color.xml b/app/src/main/res/drawable/ic_led_color.xml new file mode 100644 index 000000000..8c0a801e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_led_color.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_radio.xml b/app/src/main/res/drawable/ic_radio.xml new file mode 100644 index 000000000..d14122596 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/level_list_device.xml b/app/src/main/res/drawable/level_list_device.xml deleted file mode 100644 index 4f4fcd9f8..000000000 --- a/app/src/main/res/drawable/level_list_device.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/splash.xml b/app/src/main/res/drawable/splash.xml new file mode 100644 index 000000000..b0916a7f0 --- /dev/null +++ b/app/src/main/res/drawable/splash.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout-land/fragment_sleepchart.xml b/app/src/main/res/layout-land/fragment_sleepchart.xml index 97b877baf..48d0bfd7d 100644 --- a/app/src/main/res/layout-land/fragment_sleepchart.xml +++ b/app/src/main/res/layout-land/fragment_sleepchart.xml @@ -1,22 +1,35 @@ + android:orientation="vertical"> - - + android:layout_weight="90" /> - + + + + + - android:layout_weight="20" /> diff --git a/app/src/main/res/layout/activity_controlcenterv2_content_main.xml b/app/src/main/res/layout/activity_controlcenterv2_content_main.xml index 0c4c48949..08bd36fe5 100644 --- a/app/src/main/res/layout/activity_controlcenterv2_content_main.xml +++ b/app/src/main/res/layout/activity_controlcenterv2_content_main.xml @@ -15,7 +15,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:src="@drawable/gadgetbridge_img" /> + android:alpha="0.1" + android:tint="?attr/textColorPrimary" + app:srcCompat="@drawable/gadgetbridge_img" /> +