1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 03:16:07 +02:00

Merge branch 'master' into master

This commit is contained in:
Andreas Shimokawa 2017-08-27 00:44:00 +02:00 committed by GitHub
commit 1dbc22c235
163 changed files with 4677 additions and 1043 deletions

View File

@ -1,6 +1,7 @@
#### Before opening an issue please confirm the following:
- [ ] I have read the [wiki](https://github.com/Freeyourgadget/Gadgetbridge/wiki), and I didn't find a solution to my problem / an answer to my question.
- [ ] I have searched the [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues), and I didn't find a solution to my problem / an answer to my question.
- [ ] If you upload an image or other content, please make sure you have read and understood the [github policies and terms of services](https://help.github.com/articles/github-terms-of-service/#1-responsibility-for-user-generated-content)
#### Your issue is:
*In case of a bug, do not forget to attach logs!*

View File

@ -1,11 +1,55 @@
### Changelog
###Version 0.19.1
#### Version 0.20.2 (next)
* Amazfit Bip: Various fixes regarding weather, add condition string support for FW 0.0.8.74
* Amazfit Bip: enable caller display in later firmwares
* Amazfit Bip: initial firmware update support (EXPERIMENTAL, AT YOUR OWN RISK)
#### Version 0.20.1
* Amazfit Bip: Support icons and text body for notifications
* Mi Band: Fix setting smart alarms
#### Version 0.20.0
* Inital Amazfit Bip support (WIP)
* Various theming fixes
* Add workaround for blacklist not properly persisting
* Handle resetting language to default properly
* Pebble: Pass booleans from Javascript Appmessage correctly
* Pebble: Make local configuration pages work on most recent webview implementation
* Pebble: Allow to blacklist calendars
* Add Greek and German transliteration support
* Various visual improvements to charts
#### Version 0.19.4
* Replace or relicense CC-NC licensed icons to satisfy F-Droid
* Mi Band 2: Make infos to display on the Band configurable
* Mi Band 2: Support wrist rotation to switch info setting
* Mi Band 2: Support goal notification setting
* Mi Band 2: Support do not disturb setting
* Mi Band 2: Support inactivity warning setting
#### Version 0.19.3
* Pebble: Fix crash when calendar access permission has been denied
* Pebble: Fix wrong timestamps with Morpheuz running on Firmware >=3
* Mi Band 2: Improve reliability when fetching activity data
* HPlus: Fix intensity calculation without continuous connectivity
* HPlus: Fix Unicode handling
* HPlus: Initial not work detection
* Fix memory leak
* Only show Realtime Chart on devices supporting it
#### Version 0.19.2
* Pebble: Fix recurring calendar events only appearing once per week
* HPlus: Fix crash when receiving calls without phone number
* HPlus: Detect unicode support on Zeband Plus
* No longer quit Gadgetbridge when bluetooth gets turned off
#### Version 0.19.1
* Fix crash at startup
* HPlus: Improve reconnection to device
* Improve transliteration
###Version 0.19.0
#### Version 0.19.0
* Pebble: allow calendar sync with Timeline (Title, Location, Description)
* Pebble: display calendar icon for reminders from AOSP Calendar
* HPlus: try to fix latin characters showing as random Chinese text
@ -13,7 +57,7 @@
* Improve generic notification reliability by trying to restart the notification listener when stale/crashed
* Other small bugfixes
###Version 0.18.5
#### Version 0.18.5
* Applied some material design guidelines to Charts and (pebble) app management
* Changed colours: deep sleep is now dark blue, light sleep is now light blue
* Support for exporting and importing of preferences in addition to the database
@ -24,24 +68,24 @@
* HPlus: users can now decide whether they want to pair the device or not, hopefully fixing some connection problems (#642)
* HPlus: display battery state and warn on low battery
###Version 0.18.4
#### Version 0.18.4
* Mi Band 2: Display realtime steps in Live Activity
* Mi Band: Attempt to recognize Mi Band model with hwVersion = 8
* Alarms activity improvements and fixes
* Make Buttons in the main activity easier to hit
###Version 0.18.3
#### Version 0.18.3
* Fix bug that caused the same value in weekly charts for every day on Android 6 and older
###Version 0.18.2
#### Version 0.18.2
* Mi Band 2: Fix crash on "chat" or "social network" text notification (#603)
###Version 0.18.1
#### Version 0.18.1
* Pebble: Fix Firmware insstallation on Pebble Time Round (broken since 0.16.0)
* Start VibrationActivity when using "find device" button with Vibratissimo
* Support material fork of K9
###Version 0.18.0
#### Version 0.18.0
* All new GUI for the control center
* Add Portuguese pt_PT and pt_BR translations
* Add Czech translation

View File

@ -1,21 +1,27 @@
The following artwork is licensed under the following licenses
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0):
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
ic_launcher.png
ic_device_pebble.png
ic_device_miband.png
ic_device_lovetoy.png
ic_device_hplus.png
ic_device_default.png
ic_activitytracker.png
ic_watchface.png
ic_languagepack.png
ic_firmware.png
ic_watchapp.png
ic_systemapp.png
icon.png (fastlane metadata directories)
featureGraphic.png (fastlane metadata directories)
(All of the above by xphnx)
Creative Commons Attribution-NonCommercial-ShareAlike (CC BY-NC-SA):
ic_launcher.png (by Joseph Kim)
Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0):
"GET IT ON F-Droid" button by Laura Kalbag. Source: https://ind.ie/about/blog/f-droid-button/
Creative Commons Attribution 3.0 Unported license (CC BY-3.0):
ic_notification_battery_low.png by Picol.org. Source: https://commons.wikimedia.org/wiki/File:Battery_1_Picol_icon.svg
Creative Commons Attribution 3.0 United States (CC BY-3.0 US):
ic_donate by Peter van Driel https://thenounproject.com/term/donate/239009/

View File

@ -2,8 +2,13 @@ Gadgetbridge
============
Gadgetbridge is an Android (4.4+) application which will allow you to use your
Pebble or Mi Band 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.
Pebble, Mi Band, Amazfit Bit 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.
[Homepage](https://gadgetbridge.org)
[Blog](https://blog.gadgetbridge.org)
[![Build](https://travis-ci.org/Freeyourgadget/Gadgetbridge.svg?branch=master)](https://travis-ci.org/Freeyourgadget/Gadgetbridge)
@ -11,16 +16,17 @@ need to create an account and transmit any of your data to the vendor's servers.
[![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)
[List of changes](CHANGELOG.md)
[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 section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 (add the device from within Gadgetbridge!) [Wiki section about pebble](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Pebble), most parts apply to Pebble 2 as well
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki section about mi band](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Mi-Band), some parts apply to mi band 2 as well
* Vibratissimo (experimental)
* Liveview
* Amazfit Bip (WIP) [Wiki section about the Amazfit Bip](https://github.com/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* HPlus Devices (e.g. ZeBand) [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* Liveview
* Vibratissimo (experimental)
## Features (Pebble)
@ -141,7 +147,7 @@ Contributions are welcome, be it feedback, bugreports, documentation, translatio
on any of the open [issues](https://github.com/Freeyourgadget/Gadgetbridge/issues?q=is%3Aopen+is%3Aissue);
just leave a comment that you're working on one to avoid duplicated work.
Translations can be contributed via https://www.transifex.com/projects/p/gadgetbridge/resource/strings/ or manually.
Translations can be contributed via https://hosted.weblate.org/projects/freeyourgadget/gadgetbridge/
## Do you have further questions or feedback?

View File

@ -26,8 +26,8 @@ android {
targetSdkVersion 25
// note: always bump BOTH versionCode and versionName!
versionName "0.19.1"
versionCode 94
versionName "0.20.2"
versionCode 100
vectorDrawables.useSupportLibrary = true
}
buildTypes {

View File

@ -63,6 +63,10 @@
android:name=".activities.AppBlacklistActivity"
android:label="@string/title_activity_appblacklist"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.CalBlacklistActivity"
android:label="@string/title_activity_calblacklist"
android:parentActivityName=".activities.SettingsActivity" />
<activity
android:name=".activities.FwAppInstallerActivity"
android:theme="@style/GadgetbridgeTheme.NoActionBar"
@ -110,6 +114,26 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\.res" />
<data android:pathPattern="/.*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\.gps" />
<data android:pathPattern="/.*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />
@ -183,6 +207,26 @@
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.ft" />
<data android:pathPattern="/.*\\.res" />
<data android:pathPattern="/.*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.res" />
<data android:pathPattern="/.*\\.gps" />
<data android:pathPattern="/.*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.gps" />
<data android:pathPattern="/.*\\.pbw" />
<data android:pathPattern="/.*\\..*\\.pbw" />
<data android:pathPattern="/.*\\..*\\..*\\.pbw" />

View File

@ -9,6 +9,18 @@
<script type="text/javascript">
</script>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
iframe {
display: block;
width: 100%;
height: 100%;
border: none;
}
body {
-webkit-user-select: none;
-moz-user-select: none;

View File

@ -75,6 +75,13 @@ function showStep(desiredStep) {
}
}
function hideSteps() {
var steps = document.getElementsByClassName("step");
for (var i = 0; i < steps.length; i ++) {
steps[i].style.display = 'none';
}
}
function gbPebble() {
this.configurationURL = null;
this.configurationValues = null;
@ -142,7 +149,18 @@ function gbPebble() {
self.configurationURL = new Uri(url).addQueryParam("return_to", "gadgetbridge://"+UUID+"?config=true&json=");
} else {
//TODO: add custom return_to
location.href = url;
var iframe = document.getElementsByTagName('iframe')[0];
var oldbody = document.getElementsByTagName("body")[0];
if (iframe === undefined && oldbody !== undefined) {
iframe = document.createElement("iframe");
oldbody.parentNode.replaceChild(iframe,oldbody);
} else {
hideSteps();
document.documentElement.appendChild(iframe);
}
iframe.src = url;
}
}
@ -155,11 +173,15 @@ function gbPebble() {
try {
self.configurationValues = JSON.stringify(dict);
document.getElementById("jsondata").innerHTML=self.configurationValues;
return callbackAck;
if (callbackAck != undefined) {
callbackAck();
}
}
catch (e) {
GBjs.gbLog("sendAppMessage failed");
return callbackNack;
if (callbackNack != undefined) {
callbackNack();
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -24,6 +24,7 @@ import android.app.NotificationManager.Policy;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
@ -40,6 +41,7 @@ import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
@ -88,6 +90,7 @@ public class GBApplication extends Application {
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change";
private static GBApplication app;
@ -102,6 +105,7 @@ public class GBApplication extends Application {
}
}
};
private static Locale language;
private DeviceManager deviceManager;
@ -157,9 +161,12 @@ public class GBApplication extends Application {
setupExceptionHandler();
deviceManager = new DeviceManager(this);
String language = prefs.getString("language", "default");
setLanguage(language);
deviceService = createDeviceService();
loadBlackList();
loadAppsBlackList();
loadCalendarsBlackList();
if (isRunningMarshmallowOrLater()) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
@ -335,47 +342,107 @@ public class GBApplication extends Application {
return NotificationManager.INTERRUPTION_FILTER_ALL;
}
private static HashSet<String> blacklist = null;
private static HashSet<String> apps_blacklist = null;
public static boolean isBlacklisted(String packageName) {
return blacklist != null && blacklist.contains(packageName);
public static boolean appIsBlacklisted(String packageName) {
if (apps_blacklist == null) {
GB.log("appIsBlacklisted: apps_blacklist is null!", GB.INFO, null);
}
return apps_blacklist != null && apps_blacklist.contains(packageName);
}
public static void setBlackList(Set<String> packageNames) {
public static void setAppsBlackList(Set<String> packageNames) {
if (packageNames == null) {
blacklist = new HashSet<>();
GB.log("Set null apps_blacklist", GB.INFO, null);
apps_blacklist = new HashSet<>();
} else {
blacklist = new HashSet<>(packageNames);
apps_blacklist = new HashSet<>(packageNames);
}
saveBlackList();
GB.log("New apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
saveAppsBlackList();
}
private static void loadBlackList() {
blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (blacklist == null) {
blacklist = new HashSet<>();
private static void loadAppsBlackList() {
GB.log("Loading apps_blacklist", GB.INFO, null);
apps_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (apps_blacklist == null) {
apps_blacklist = new HashSet<>();
}
GB.log("Loaded apps_blacklist has " + apps_blacklist.size() + " entries", GB.INFO, null);
}
private static void saveBlackList() {
private static void saveAppsBlackList() {
GB.log("Saving apps_blacklist with " + apps_blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (blacklist.isEmpty()) {
if (apps_blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
} else {
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, blacklist);
Prefs.putStringSet(editor, GBPrefs.PACKAGE_BLACKLIST, apps_blacklist);
}
editor.apply();
}
public static void addToBlacklist(String packageName) {
if (blacklist.add(packageName)) {
saveBlackList();
public static void addAppToBlacklist(String packageName) {
if (apps_blacklist.add(packageName)) {
saveAppsBlackList();
}
}
public static synchronized void removeFromBlacklist(String packageName) {
blacklist.remove(packageName);
saveBlackList();
public static synchronized void removeFromAppsBlacklist(String packageName) {
GB.log("Removing from apps_blacklist: " + packageName, GB.INFO, null);
apps_blacklist.remove(packageName);
saveAppsBlackList();
}
private static HashSet<String> calendars_blacklist = null;
public static boolean calendarIsBlacklisted(String calendarDisplayName) {
if (calendars_blacklist == null) {
GB.log("calendarIsBlacklisted: calendars_blacklist is null!", GB.INFO, null);
}
return calendars_blacklist != null && calendars_blacklist.contains(calendarDisplayName);
}
public static void setCalendarsBlackList(Set<String> calendarNames) {
if (calendarNames == null) {
GB.log("Set null apps_blacklist", GB.INFO, null);
calendars_blacklist = new HashSet<>();
} else {
calendars_blacklist = new HashSet<>(calendarNames);
}
GB.log("New calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
saveCalendarsBlackList();
}
public static void addCalendarToBlacklist(String calendarDisplayName) {
if (calendars_blacklist.add(calendarDisplayName)) {
saveCalendarsBlackList();
}
}
public static void removeFromCalendarBlacklist(String calendarDisplayName) {
calendars_blacklist.remove(calendarDisplayName);
saveCalendarsBlackList();
}
private static void loadCalendarsBlackList() {
GB.log("Loading calendars_blacklist", GB.INFO, null);
calendars_blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
if (calendars_blacklist == null) {
calendars_blacklist = new HashSet<>();
}
GB.log("Loaded calendars_blacklist has " + calendars_blacklist.size() + " entries", GB.INFO, null);
}
private static void saveCalendarsBlackList() {
GB.log("Saving calendars_blacklist with " + calendars_blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (calendars_blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.CALENDAR_BLACKLIST, null);
} else {
Prefs.putStringSet(editor, GBPrefs.CALENDAR_BLACKLIST, calendars_blacklist);
}
editor.apply();
}
/**
@ -459,6 +526,23 @@ public class GBApplication extends Application {
editor.apply();
}
public static void setLanguage(String lang) {
if (lang.equals("default")) {
language = Resources.getSystem().getConfiguration().locale;
} else {
language = new Locale(lang);
}
Configuration config = new Configuration();
config.setLocale(language);
// FIXME: I have no idea what I am doing
context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
Intent intent = new Intent();
intent.setAction(ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public static LimitedQueue getIDSenderLookup() {
return mIDSenderLookup;
}
@ -496,4 +580,8 @@ public class GBApplication extends Application {
public static GBApplication app() {
return app;
}
public static Locale getLanguage() {
return language;
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.

View File

@ -17,20 +17,28 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.text.InputType;
import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
/**
* A settings activity with support for preferences directly displaying their value.
@ -42,6 +50,22 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
private static final Logger LOG = LoggerFactory.getLogger(AbstractSettingsActivity.class);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage());
break;
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
@ -110,6 +134,12 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
} else {
setTheme(R.style.GadgetbridgeTheme);
}
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
super.onCreate(savedInstanceState);
}
@ -127,6 +157,12 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
/**
* Subclasses should reimplement this to return the keys of those
* preferences which should print its values as a summary below the
@ -178,4 +214,8 @@ public abstract class AbstractSettingsActivity extends AppCompatPreferenceActivi
}
return super.onOptionsItemSelected(item);
}
private void setLanguage(Locale language) {
AndroidUtils.setLanguage(this, language);
}
}

View File

@ -17,13 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
@ -32,7 +27,6 @@ import android.view.MenuItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter;
@ -42,16 +36,6 @@ public class AppBlacklistActivity extends GBActivity {
private AppBlacklistAdapter appBlacklistAdapter;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(GBApplication.ACTION_QUIT)) {
finish();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -78,10 +62,6 @@ public class AppBlacklistActivity extends GBActivity {
return true;
}
});
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
}
@Override
@ -93,10 +73,4 @@ public class AppBlacklistActivity extends GBActivity {
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -0,0 +1,152 @@
/* Copyright (C) 2017 Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CalendarContract;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NavUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class CalBlacklistActivity extends GBActivity {
private final String[] EVENT_PROJECTION = new String[]{
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.CALENDAR_COLOR
};
private ArrayList<Calendar> calendarsArrayList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_calblacklist);
ListView calListView = (ListView) findViewById(R.id.calListView);
final Uri uri = CalendarContract.Calendars.CONTENT_URI;
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) {
GB.toast(this, "Calendar permission not granted. Nothing to do.", Toast.LENGTH_SHORT, GB.WARN);
return;
}
try (Cursor cur = getContentResolver().query(uri, EVENT_PROJECTION, null, null, null)) {
calendarsArrayList = new ArrayList<>();
while (cur != null && cur.moveToNext()) {
calendarsArrayList.add(new Calendar(cur.getString(0), cur.getInt(1)));
}
}
ArrayAdapter<Calendar> calAdapter = new CalendarListAdapter(this, calendarsArrayList);
calListView.setAdapter(calAdapter);
calListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int i, long id) {
Calendar item = calendarsArrayList.get(i);
CheckBox selected = (CheckBox) view.findViewById(R.id.item_checkbox);
toggleEntry(view);
if (selected.isChecked()) {
GBApplication.addCalendarToBlacklist(item.displayName);
} else {
GBApplication.removeFromCalendarBlacklist(item.displayName);
}
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void toggleEntry(View view) {
TextView name = (TextView) view.findViewById(R.id.calendar_name);
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
name.setPaintFlags(name.getPaintFlags() ^ Paint.STRIKE_THRU_TEXT_FLAG);
checked.toggle();
}
class Calendar {
private final String displayName;
private final int color;
public Calendar(String displayName, int color) {
this.displayName = displayName;
this.color = color;
}
}
private class CalendarListAdapter extends ArrayAdapter<Calendar> {
CalendarListAdapter(@NonNull Context context, @NonNull List<Calendar> calendars) {
super(context, 0, calendars);
}
@NonNull
@Override
public View getView(int position, @Nullable View view, @NonNull ViewGroup parent) {
Calendar item = getItem(position);
if (view == null) {
LayoutInflater inflater = (LayoutInflater) super.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = inflater.inflate(R.layout.item_cal_blacklist, parent, false);
}
View color = view.findViewById(R.id.calendar_color);
TextView name = (TextView) view.findViewById(R.id.calendar_name);
CheckBox checked = (CheckBox) view.findViewById(R.id.item_checkbox);
if (GBApplication.calendarIsBlacklisted(item.displayName) && !checked.isChecked()) {
toggleEntry(view);
}
color.setBackgroundColor(item.color);
name.setText(item.displayName);
return view;
}
}
}

View File

@ -25,6 +25,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -49,6 +50,7 @@ import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.cketti.library.changelog.ChangeLog;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -56,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -80,6 +83,9 @@ public class ControlCenterv2 extends AppCompatActivity
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage());
break;
case GBApplication.ACTION_QUIT:
finish();
break;
@ -170,6 +176,7 @@ public class ControlCenterv2 extends AppCompatActivity
registerForContextMenu(deviceListView);
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
@ -189,7 +196,7 @@ public class ControlCenterv2 extends AppCompatActivity
checkAndRequestPermissions();
}
ChangeLog cl = new ChangeLog(this);
ChangeLog cl = createChangeLog();
if (cl.isFirstRun()) {
cl.getLogDialog().show();
}
@ -203,6 +210,13 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
@Override
protected void onDestroy() {
unregisterForContextMenu(deviceListView);
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
@ -235,8 +249,13 @@ public class ControlCenterv2 extends AppCompatActivity
case R.id.action_quit:
GBApplication.quit();
return true;
case R.id.donation_link:
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
return true;
case R.id.external_changelog:
ChangeLog cl = new ChangeLog(this);
ChangeLog cl = createChangeLog();
cl.getFullLogDialog().show();
return true;
}
@ -244,6 +263,14 @@ public class ControlCenterv2 extends AppCompatActivity
return true;
}
private ChangeLog createChangeLog() {
String css = ChangeLog.DEFAULT_CSS;
css += "body { "
+ "color: " + AndroidUtils.getTextColorHex(getBaseContext()) + "; "
+ "background-color: " + AndroidUtils.getBackgroundColorHex(getBaseContext()) + ";" +
"}";
return new ChangeLog(this, css);
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivity.class));
}
@ -288,4 +315,7 @@ public class ControlCenterv2 extends AppCompatActivity
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[wantedPermissions.size()]), 0);
}
private void setLanguage(Locale language) {
AndroidUtils.setLanguage(this, language);
}
}

View File

@ -19,7 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Bundle;
@ -62,9 +61,6 @@ public class DbManagementActivity extends GBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_db_management);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
dbPath = (TextView) findViewById(R.id.activity_db_management_path);
dbPath.setText(getExternalPath());

View File

@ -78,10 +78,6 @@ public class DebugActivity extends GBActivity {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case GBApplication.ACTION_QUIT: {
finish();
break;
}
case ACTION_REPLY: {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
CharSequence reply = remoteInput.getCharSequence(EXTRA_REPLY);
@ -104,7 +100,6 @@ public class DebugActivity extends GBActivity {
setContentView(R.layout.activity_debug);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
filter.addAction(ACTION_REPLY);
filter.addAction(DeviceService.ACTION_HEARTRATE_MEASUREMENT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);

View File

@ -221,9 +221,6 @@ public class ExternalPebbleJSActivity extends GBActivity {
if (passKey) {
Object obj = in.get(inKey);
if (obj instanceof Boolean) {
obj = ((Boolean) obj) ? "true" : "false";
}
out.put(outKey, obj);
} else {
GB.toast("Discarded key " + inKey + ", not found in the local configuration and is not an integer key.", Toast.LENGTH_SHORT, GB.WARN);

View File

@ -97,9 +97,7 @@ public class FwAppInstallerActivity extends AppCompatActivity implements Install
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (GBApplication.ACTION_QUIT.equals(action)) {
finish();
} else if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (device != null) {
refreshBusyState(device);
@ -181,7 +179,6 @@ public class FwAppInstallerActivity extends AppCompatActivity implements Install
detailsListView.setAdapter(mDetailsItemAdapter);
setInstallEnabled(false);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
filter.addAction(GB.ACTION_DISPLAY_MESSAGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);

View File

@ -17,44 +17,64 @@
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.LocaleList;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class GBActivity extends AppCompatActivity {
private void setLanguage(String language) {
Locale locale;
if (language.equals("default")) {
locale = Locale.getDefault();
} else {
locale = new Locale(language);
}
Configuration config = new Configuration();
config.locale = locale;
// FIXME: I have no idea what I am doing
getApplicationContext().getResources().updateConfiguration(config, getApplicationContext().getResources().getDisplayMetrics());
getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_LANGUAGE_CHANGE:
setLanguage(GBApplication.getLanguage());
break;
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
private void setLanguage(Locale language) {
AndroidUtils.setLanguage(this, language);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
if (GBApplication.isDarkThemeEnabled()) {
setTheme(R.style.GadgetbridgeThemeDark);
} else {
setTheme(R.style.GadgetbridgeTheme);
}
Prefs prefs = GBApplication.getPrefs();
String language = prefs.getString("language", "default");
setLanguage(language);
super.onCreate(savedInstanceState);
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -97,6 +97,15 @@ public class SettingsActivity extends AbstractSettingsActivity {
}
});
pref = findPreference("pref_key_blacklist_calendars");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, CalBlacklistActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pebble_emu_addr");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -143,6 +152,25 @@ public class SettingsActivity extends AbstractSettingsActivity {
});
pref = findPreference("language");
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
String newLang = newVal.toString();
try {
GBApplication.setLanguage(newLang);
} catch (Exception ex) {
GB.toast(getApplicationContext(),
"Error setting language: " + ex.getLocalizedMessage(),
Toast.LENGTH_LONG,
GB.ERROR,
ex);
}
return true;
}
});
if (!GBApplication.isRunningMarshmallowOrLater()) {
pref = findPreference("notification_filter");
PreferenceCategory category = (PreferenceCategory) findPreference("pref_key_notifications");

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Andreas Shimokawa
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -16,12 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
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.SeekBar;
import org.slf4j.Logger;
@ -33,17 +28,6 @@ import nodomain.freeyourgadget.gadgetbridge.R;
public class VibrationActivity extends GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(VibrationActivity.class);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case GBApplication.ACTION_QUIT: {
finish();
break;
}
}
}
};
private SeekBar seekBar;
@Override
@ -51,11 +35,6 @@ public class VibrationActivity extends GBActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_vibration);
IntentFilter filter = new IntentFilter();
filter.addAction(GBApplication.ACTION_QUIT);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
registerReceiver(mReceiver, filter);
seekBar = (SeekBar) findViewById(R.id.vibration_seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
@ -77,11 +56,4 @@ public class VibrationActivity extends GBActivity {
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}
}

View File

@ -18,17 +18,13 @@
package nodomain.freeyourgadget.gadgetbridge.activities.appmanager;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.ViewPager;
import android.view.MenuItem;
import android.view.View;
@ -45,7 +41,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
@ -61,18 +56,6 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
private GBDevice mGBDevice = null;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_QUIT:
finish();
break;
}
}
};
public GBDevice getGBDevice() {
return mGBDevice;
}
@ -104,11 +87,6 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
}
});
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
// Set up the ViewPager with the sections adapter.
ViewPager viewPager = (ViewPager) findViewById(R.id.appmanager_pager);
if (viewPager != null) {
@ -232,10 +210,4 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
}
}
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
}

View File

@ -487,7 +487,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
}
activityEntries.add(createBarEntry(value, ts));
if (hr && isValidHeartRateValue(sample.getHeartRate())) {
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 60*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1));
heartrateEntries.add(createLineEntry(0, ts - 1));
}
@ -530,7 +530,7 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
List<IBarDataSet> list = new ArrayList<>();
list.add(activitySet);
BarData barData = new BarData(list);
barData.setBarWidth(100f);
barData.setBarWidth(200f);
// barData.setGroupSpace(0);
combinedData.setData(barData);
@ -595,10 +595,10 @@ public abstract class AbstractChartFragment extends AbstractGBFragment {
protected LineDataSet createHeartrateSet(List<Entry> values, String label) {
LineDataSet set1 = new LineDataSet(values, label);
set1.setLineWidth(0.8f);
set1.setLineWidth(2.2f);
set1.setColor(HEARTRATE_COLOR);
// set1.setDrawCubic(true);
set1.setMode(LineDataSet.Mode.CUBIC_BEZIER);
set1.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
set1.setCubicIntensity(0.1f);
set1.setDrawCircles(false);
// set1.setCircleRadius(2f);

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
Gobbetti, Vebryn
This file is part of Gadgetbridge.
@ -17,7 +17,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
@ -25,6 +30,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class ActivityAnalysis {
private static final Logger LOG = LoggerFactory.getLogger(ActivityAnalysis.class);
// store raw steps and duration
protected HashMap<Integer, Long> stats = new HashMap<Integer, Long>();
// max speed determined from samples
private int maxSpeed = 0;
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
@ -53,7 +65,7 @@ class ActivityAnalysis {
int steps = sample.getSteps();
if (steps > 0) {
amount.addSteps(sample.getSteps());
amount.addSteps(steps);
}
if (previousSample != null) {
@ -65,6 +77,22 @@ class ActivityAnalysis {
previousAmount.addSeconds(sharedTimeDifference);
amount.addSeconds(sharedTimeDifference);
}
// add time
if (steps > 0 && sample.getKind() == ActivityKind.TYPE_ACTIVITY) {
if (steps > maxSpeed) {
maxSpeed = steps;
}
if (!stats.containsKey(steps)) {
LOG.info("Adding: " + steps);
stats.put(steps, timeDifference);
} else {
long time = stats.get(steps);
LOG.info("Updating: " + steps + " " + timeDifference + time);
stats.put(steps, timeDifference + time);
}
}
}
previousAmount = amount;

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
Gobbetti, Vebryn
This file is part of Gadgetbridge.
@ -102,9 +102,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case GBApplication.ACTION_QUIT:
finish();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
refreshBusyState(dev);
@ -136,7 +133,6 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
initDates();
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
@ -333,6 +329,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return new WeekStepsChartFragment();
case 4:
return new LiveActivityFragment();
case 5:
return new SpeedZonesFragment();
}
return null;
@ -340,7 +338,11 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
@Override
public int getCount() {
// Show 5 total pages.
// Show 5 or 6 total pages.
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
if (coordinator.supportsRealtimeData()) {
return 6;
}
return 5;
}
@ -357,6 +359,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return getString(R.string.weekstepschart_steps_a_week);
case 4:
return getString(R.string.liveactivity_live_activity);
case 5:
return getString(R.string.stats_title);
}
return super.getPageTitle(position);
}

View File

@ -0,0 +1,170 @@
/* Copyright (C) 2015-2017 0nse, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, Vebryn
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.HorizontalBarChart;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
public class SpeedZonesFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(SpeedZonesFragment.class);
private HorizontalBarChart mStatsChart;
@Override
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
List<? extends ActivitySample> samples = getSamples(db, device);
MySpeedZonesData mySpeedZonesData = refreshStats(samples);
return new MyChartsData(mySpeedZonesData);
}
private MySpeedZonesData refreshStats(List<? extends ActivitySample> samples) {
ActivityAnalysis analysis = new ActivityAnalysis();
analysis.calculateActivityAmounts(samples);
BarData data = new BarData();
data.setValueTextColor(CHART_TEXT_COLOR);
List<BarEntry> entries = new ArrayList<>();
ActivityUser user = new ActivityUser();
/*double distanceFactorCm;
if (user.getGender() == user.GENDER_MALE){
distanceFactorCm = user.getHeightCm() * user.GENDER_MALE_DISTANCE_FACTOR / 1000;
} else {
distanceFactorCm = user.getHeightCm() * user.GENDER_FEMALE_DISTANCE_FACTOR / 1000;
}*/
for (Map.Entry<Integer, Long> entry : analysis.stats.entrySet()) {
entries.add(new BarEntry(entry.getKey(), entry.getValue() / 60));
}
BarDataSet set = new BarDataSet(entries, "");
set.setValueTextColor(CHART_TEXT_COLOR);
set.setColors(getColorFor(ActivityKind.TYPE_ACTIVITY));
//set.setDrawValues(false);
//data.setBarWidth(0.1f);
data.addDataSet(set);
return new MySpeedZonesData(data);
}
@Override
protected void updateChartsnUIThread(ChartsData chartsData) {
MyChartsData mcd = (MyChartsData) chartsData;
mStatsChart.setData(mcd.getChartsData().getBarData());
}
@Override
public String getTitle() {
return getString(R.string.stats_title);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_statschart, container, false);
mStatsChart = (HorizontalBarChart) rootView.findViewById(R.id.statschart);
setupStatsChart();
// refresh immediately instead of use refreshIfVisible(), for perceived performance
refresh();
return rootView;
}
private void setupStatsChart() {
mStatsChart.setBackgroundColor(BACKGROUND_COLOR);
mStatsChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mStatsChart.setNoDataText("");
mStatsChart.getLegend().setEnabled(false);
mStatsChart.setTouchEnabled(false);
mStatsChart.getDescription().setText("");
XAxis x = mStatsChart.getXAxis();
x.setTextColor(CHART_TEXT_COLOR);
YAxis yr = mStatsChart.getAxisRight();
yr.setTextColor(CHART_TEXT_COLOR);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
@Override
protected void setupLegend(Chart chart) {
// no legend here, it is all about the steps here
chart.getLegend().setEnabled(false);
}
@Override
protected void renderCharts() {
mStatsChart.invalidate();
}
private static class MySpeedZonesData extends ChartsData {
private final BarData barData;
MySpeedZonesData(BarData barData) {
this.barData = barData;
}
BarData getBarData() {
return barData;
}
}
private static class MyChartsData extends ChartsData {
private final MySpeedZonesData chartsData;
MyChartsData(MySpeedZonesData chartsData) {
this.chartsData = chartsData;
}
MySpeedZonesData getChartsData() {
return chartsData;
}
}
}

View File

@ -62,7 +62,7 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapte
if (name == null) {
name = ai.packageName;
}
if (GBApplication.isBlacklisted(ai.packageName)) {
if (GBApplication.appIsBlacklisted(ai.packageName)) {
// sort blacklisted first by prefixing with a '!'
name = "!" + name;
}
@ -94,7 +94,7 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapte
holder.deviceAppNameLabel.setText(mNameMap.get(appInfo));
holder.deviceImageView.setImageDrawable(appInfo.loadIcon(mPm));
holder.checkbox.setChecked(GBApplication.isBlacklisted(appInfo.packageName));
holder.checkbox.setChecked(GBApplication.appIsBlacklisted(appInfo.packageName));
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
@ -102,9 +102,9 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter<AppBlacklistAdapte
CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox));
checkBox.toggle();
if (checkBox.isChecked()) {
GBApplication.addToBlacklist(appInfo.packageName);
GBApplication.addAppToBlacklist(appInfo.packageName);
} else {
GBApplication.removeFromBlacklist(appInfo.packageName);
GBApplication.removeFromAppsBlacklist(appInfo.packageName);
}
}
});

View File

@ -189,6 +189,7 @@ public class GBDeviceAdapterv2 extends RecyclerView.Adapter<GBDeviceAdapterv2.Vi
public void onClick(View v) {
Intent startIntent;
startIntent = new Intent(context, ConfigureAlarms.class);
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
context.startActivity(startIntent);
}
}

View File

@ -201,8 +201,6 @@ public interface DeviceCoordinator {
*/
boolean supportsHeartRateMeasurement(GBDevice device);
int getTapString();
/**
* Returns the readable name of the manufacturer.
*/
@ -234,4 +232,10 @@ public interface DeviceCoordinator {
*/
boolean supportsCalendarEvents();
/**
* Indicates whether the device supports getting a stream of live data.
* This can be live HR, steps etc.
*/
boolean supportsRealtimeData();
}

View File

@ -157,11 +157,6 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public int getTapString() {
return 0;
}
@Override
public String getManufacturer() {
return "unknown";
@ -181,4 +176,9 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
}

View File

@ -0,0 +1,60 @@
/* Copyright (C) 2017 Andreas Shimokawa, João Paulo Barraca
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class AmazfitBipCooordinator extends MiBand2Coordinator {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipCooordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.AMAZFITBIP;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit Bip Watch")) {
return DeviceType.AMAZFITBIP;
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
AmazfitBipFWInstallHandler handler = new AmazfitBipFWInstallHandler(uri, context);
return handler.isValid() ? handler : null;
}
}

View File

@ -0,0 +1,90 @@
/* Copyright (C) 2016-2017 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.AmazfitBipFirmwareInfo;
public class AmazfitBipFWHelper extends AbstractMiBandFWHelper {
public AmazfitBipFWHelper(Uri uri, Context context) throws IOException {
super(uri, context);
}
private AmazfitBipFirmwareInfo firmwareInfo;
@Override
public String format(int version) {
return AmazfitBipFirmwareInfo.toVersion(version);
}
@Override
public int getFirmwareVersion() {
return firmwareInfo.getFirmwareVersion();
}
@Override
public int getFirmware2Version() {
return 0;
}
@Override
public String getHumanFirmwareVersion2() {
return "";
}
@Override
protected int[] getWhitelistedFirmwareVersions() {
return AmazfitBipFirmwareInfo.getWhitelistedVersions();
}
@Override
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
return firmwareInfo.isGenerallyCompatibleWith(device);
}
@Override
public boolean isSingleFirmware() {
return true;
}
@NonNull
@Override
protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) {
firmwareInfo = new AmazfitBipFirmwareInfo(wholeFirmwareBytes);
if (!firmwareInfo.isHeaderValid()) {
throw new IllegalArgumentException("Not a an Amazifit Bip firmware");
}
}
@Override
public void checkValid() throws IllegalArgumentException {
firmwareInfo.checkValid();
}
public AmazfitBipFirmwareInfo getFirmwareInfo() {
return firmwareInfo;
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2015-2017 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import android.content.Context;
import android.net.Uri;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
class AmazfitBipFWInstallHandler extends AbstractMiBandFWInstallHandler {
AmazfitBipFWInstallHandler(Uri uri, Context context) {
super(uri, context);
}
@Override
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
return new AmazfitBipFWHelper(uri, context);
}
@Override
protected boolean isSupportedDeviceType(GBDevice device) {
return device.getType() == DeviceType.AMAZFITBIP;
}
}

View File

@ -0,0 +1,97 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
public class AmazfitBipIcon {
// icons which are unsure which app they are for are suffixed with _NN
public static final int CHAT = 0;
public static final int PENGUIN_1 = 1;
public static final int MI_CHAT_2 = 2;
public static final int FACEBOOK = 3;
public static final int TWITTER = 4;
public static final int MI_APP_5 = 5;
public static final int SNAPCHAT = 6;
public static final int WHATSAPP = 7;
public static final int RED_WHITE_FIRE_8 = 8;
public static final int CHINESE_9 = 9;
public static final int ALARM_CLOCK = 10;
public static final int APP_11 = 11;
public static final int CAMERA_12 = 12;
public static final int CHAT_BLUE_13 = 13;
public static final int COW_14 = 14;
public static final int CHINESE_15 = 15;
public static final int CHINESE_16 = 16;
public static final int STAR_17 = 17;
public static final int APP_18 = 18;
public static final int CHINESE_19 = 19;
public static final int CHINESE_20 = 20;
public static final int CALENDAR = 21;
public static final int FACEBOOK_MESSENGER = 22;
public static final int WHATSAPP_CALL_23 = 23;
public static final int LINE = 24;
public static final int TELEGRAM = 25;
public static final int KAKAOTALK = 26;
public static final int SKYPE = 27;
public static final int VKONTAKTE = 28;
public static final int POKEMONGO = 29;
public static final int HANGOUTS = 30;
public static final int MI_31 = 31;
public static final int CHINESE_32 = 32;
public static final int CHINESE_33 = 33;
public static final int EMAIL = 34;
public static final int WEATHER = 35;
public static final int HR_WARNING_36 = 36;
public static int mapToIconId(NotificationType type) {
switch (type) {
case UNKNOWN:
return APP_11;
case CONVERSATIONS:
return CHAT;
case GENERIC_EMAIL:
return EMAIL;
case GENERIC_NAVIGATION:
return APP_11;
case GENERIC_SMS:
return CHAT;
case GENERIC_CALENDAR:
return CALENDAR;
case FACEBOOK:
return FACEBOOK;
case FACEBOOK_MESSENGER:
return FACEBOOK_MESSENGER;
case RIOT:
return CHAT;
case SIGNAL:
return CHAT_BLUE_13;
case TWITTER:
return TWITTER;
case TELEGRAM:
return TELEGRAM;
case WHATSAPP:
return WHATSAPP;
case GENERIC_ALARM_CLOCK:
return ALARM_CLOCK;
}
return APP_11;
}
}

View File

@ -0,0 +1,24 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
import java.util.UUID;
public class AmazfitBipService {
public static final UUID UUID_CHARACTERISTIC_WEATHER = UUID.fromString("0000000e-0000-3512-2118-0009af100700");
}

View File

@ -0,0 +1,160 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip;
public class AmazfitBipWeatherConditions {
public static final byte CLEAR_SKY = 0;
public static final byte SCATTERED_CLOUDS = 1;
public static final byte CLOUDY = 2;
public static final byte RAIN_WITH_SUN = 3;
public static final byte THUNDERSTORM = 4;
public static final byte HAIL = 5;
public static final byte RAIN_AND_SNOW = 6;
public static final byte LIGHT_RAIN = 7;
public static final byte MEDIUM_RAIN = 8;
public static final byte HEAVY_RAIN = 9;
public static final byte EXTREME_RAIN = 10;
public static final byte SUPER_EXTREME_RAIN = 11;
public static final byte TORRENTIAL_RAIN = 12;
public static final byte SNOW_AND_SUN = 13;
public static final byte LIGHT_SNOW = 14;
public static final byte MEDIUM_SNOW = 15;
public static final byte HEAVY_SNOW = 16;
public static final byte EXTREME_SNOW = 17;
public static final byte MIST = 18;
public static final byte DRIZZLE = 19;
public static final byte WIND_AND_RAIN = 20;
// 21- various types of rain
public static byte mapToAmazfitBipWeatherCode(int openWeatherMapCondition) {
// openweathermap.org conditions:
// http://openweathermap.org/weather-conditions
switch (openWeatherMapCondition) {
//Group 2xx: Thunderstorm
case 200: //thunderstorm with light rain: //11d
case 201: //thunderstorm with rain: //11d
case 202: //thunderstorm with heavy rain: //11d
case 210: //light thunderstorm:: //11d
case 211: //thunderstorm: //11d
case 230: //thunderstorm with light drizzle: //11d
case 231: //thunderstorm with drizzle: //11d
case 232: //thunderstorm with heavy drizzle: //11d
case 212: //heavy thunderstorm: //11d
case 221: //ragged thunderstorm: //11d
return THUNDERSTORM;
//Group 3xx: Drizzle
case 300: //light intensity drizzle: //09d
case 301: //drizzle: //09d
case 302: //heavy intensity drizzle: //09d
case 310: //light intensity drizzle rain: //09d
case 311: //drizzle rain: //09d
case 312: //heavy intensity drizzle rain: //09d
case 313: //shower rain and drizzle: //09d
case 314: //heavy shower rain and drizzle: //09d
case 321: //shower drizzle: //09d
return DRIZZLE;
//Group 5xx: Rain
case 500: //light rain: //10d
return LIGHT_RAIN;
case 501: //moderate rain: //10d
return MEDIUM_RAIN;
case 502: //heavy intensity rain: //10d
return HEAVY_RAIN;
case 503: //very heavy rain: //10d
return EXTREME_RAIN;
case 504: //extreme rain: //10d
return TORRENTIAL_RAIN;
case 511: //freezing rain: //13d
return MEDIUM_RAIN;
case 520: //light intensity shower rain: //09d
return LIGHT_RAIN;
case 521: //shower rain: //09d
return MEDIUM_RAIN;
case 522: //heavy intensity shower rain: //09d
return HEAVY_RAIN;
case 531: //ragged shower rain: //09d
return MEDIUM_RAIN;
//Group 6xx: Snow
case 600: //light snow: //[[file:13d.png]]
return LIGHT_SNOW;
case 601: //snow: //[[file:13d.png]]
return MEDIUM_SNOW;
case 602: //heavy snow: //[[file:13d.png]]
return HEAVY_SNOW;
case 611: //sleet: //[[file:13d.png]]
case 612: //shower sleet: //[[file:13d.png]]
case 615: //light rain and snow: //[[file:13d.png]]
case 616: //rain and snow: //[[file:13d.png]]
case 620: //light shower snow: //[[file:13d.png]]
case 621: //shower snow: //[[file:13d.png]]
case 622: //heavy shower snow: //[[file:13d.png]]
return RAIN_AND_SNOW;
//Group 7xx: Atmosphere
case 701: //mist: //[[file:50d.png]]
case 711: //smoke: //[[file:50d.png]]
case 721: //haze: //[[file:50d.png]]
case 731: //sandcase dust whirls: //[[file:50d.png]]
case 741: //fog: //[[file:50d.png]]
case 751: //sand: //[[file:50d.png]]
case 761: //dust: //[[file:50d.png]]
case 762: //volcanic ash: //[[file:50d.png]]
case 771: //squalls: //[[file:50d.png]]
return MIST;
case 781: //tornado: //[[file:50d.png]]
case 900: //tornado
return WIND_AND_RAIN;
//Group 800: Clear
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
return CLEAR_SKY;
//Group 80x: Clouds
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
return SCATTERED_CLOUDS;
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
return CLOUDY;
//Group 90x: Extreme
case 901: //tropical storm
return WIND_AND_RAIN;
case 903: //cold
case 904: //hot
case 905: //windy
return 0;
case 906: //hail
return HAIL;
//Group 9xx: Additional
case 951: //calm
case 952: //light breeze
case 953: //gentle breeze
case 954: //moderate breeze
case 955: //fresh breeze
case 956: //strong breeze
case 957: //high windcase near gale
case 958: //gale
case 959: //severe gale
case 960: //storm
case 961: //violent storm
case 902: //hurricane
case 962: //hurricane
default:
return 0;
}
}
}

View File

@ -79,7 +79,6 @@ public final class HPlusConstants {
public static final byte ARG_FINDME_ON = 0x01;
public static final byte ARG_FINDME_OFF = 0x02;
public static final byte CMD_GET_VERSION = 0x17;
public static final byte CMD_SET_END = 0x4f;
public static final byte CMD_SET_INCOMING_CALL_NUMBER = 0x23;
public static final byte CMD_SET_ALLDAY_HRM = 0x35;
@ -89,7 +88,8 @@ public final class HPlusConstants {
public static final byte CMD_SET_SIT_INTERVAL = 0x51;
public static final byte CMD_SET_HEARTRATE_STATE = 0x32;
//Actions to device
//GET messages
public static final byte CMD_GET_VERSION = 0x17;
public static final byte CMD_GET_ACTIVE_DAY = 0x27;
public static final byte CMD_GET_DAY_DATA = 0x15;
public static final byte CMD_GET_SLEEP = 0x19;
@ -122,7 +122,7 @@ public final class HPlusConstants {
public static final byte DATA_SLEEP = 0x1A;
public static final byte DATA_VERSION = 0x18;
public static final byte DATA_VERSION1 = 0x2E;
public static final byte DATA_DAY_UNKNOWN = 0x52; //To be defined
public static final byte DATA_UNKNOWN = 0x4d;
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";

View File

@ -92,6 +92,11 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HPLUS;
@ -147,11 +152,6 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Zeblaze";
@ -289,6 +289,6 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}
public static boolean getUnicodeSupport(String address){
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE, false));
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, false));
}
}

View File

@ -21,6 +21,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.hplus;
*/
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.Calendar;
import java.util.Collections;
@ -141,6 +142,58 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
List<HPlusHealthActivityOverlay> overlayRecords = qb.build().list();
//Apply Overlays
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
//Create fake events to improve activity counters if there are no events around the overlay
//timestamp boundaries
//Insert one before, one at the beginning, one at the end, and one 1s after.
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom() - 1, timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo(), timestamp_to), overlay.getDeviceId(), overlay.getUserId());
}
Collections.sort(samples, new Comparator<HPlusHealthActivitySample>() {
public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) {
return one.getTimestamp() - other.getTimestamp();
}
});
//Apply Overlays
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
long nonSleepTimeEnd = 0;
for (HPlusHealthActivitySample sample : samples) {
if (sample.getRawKind() == ActivityKind.TYPE_NOT_WORN)
continue;
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
if (overlay.getRawKind() == ActivityKind.TYPE_NOT_WORN || overlay.getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP || overlay.getRawKind() == ActivityKind.TYPE_DEEP_SLEEP) {
if (sample.getRawKind() == HPlusDataRecord.TYPE_DAY_SLOT && sample.getSteps() > 0){
nonSleepTimeEnd = sample.getTimestamp() + 10 * 60; // 10 minutes
continue;
}else if(sample.getRawKind() == HPlusDataRecord.TYPE_REALTIME && sample.getTimestamp() <= nonSleepTimeEnd){
continue;
}
if (overlay.getRawKind() == ActivityKind.TYPE_NOT_WORN)
sample.setHeartRate(0);
if (sample.getRawKind() != ActivityKind.TYPE_NOT_WORN)
sample.setRawKind(overlay.getRawKind());
sample.setRawIntensity(10);
}
}
}
}
//Fix Step counters
//Todays sample steps will come from the Day Slots messages
//Historical steps will be provided by Day Summaries messages
//This will allow both week and current day results to be consistent
@ -180,35 +233,8 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
if(lastSample != null)
lastSample.setSteps(Math.max(stepsTodayCount, stepsTodayMax));
for (HPlusHealthActivityOverlay overlay : overlayRecords) {
//Create fake events to improve activity counters if there are no events around the overlay
//timestamp boundaries
//Insert one before, one at the beginning, one at the end, and one 1s after.
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom() - 1, timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.max(overlay.getTimestampFrom(), timestamp_from), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo() - 1, timestamp_to - 1), overlay.getDeviceId(), overlay.getUserId());
insertVirtualItem(samples, Math.min(overlay.getTimestampTo(), timestamp_to), overlay.getDeviceId(), overlay.getUserId());
for (HPlusHealthActivitySample sample : samples) {
if (sample.getTimestamp() >= overlay.getTimestampFrom() && sample.getTimestamp() < overlay.getTimestampTo()) {
if(overlay.getRawKind() == ActivityKind.TYPE_LIGHT_SLEEP || overlay.getRawKind() == ActivityKind.TYPE_DEEP_SLEEP)
sample.setRawIntensity(10);
sample.setRawKind(overlay.getRawKind());
}
}
}
detachFromSession();
Collections.sort(samples, new Comparator<HPlusHealthActivitySample>() {
public int compare(HPlusHealthActivitySample one, HPlusHealthActivitySample other) {
return one.getTimestamp() - other.getTimestamp();
}
});
return samples;
}

View File

@ -99,12 +99,6 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public int getTapString() {
//TODO: changeme
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Sony Ericsson";
@ -125,6 +119,11 @@ public class LiveviewCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet

View File

@ -82,7 +82,7 @@ public abstract class AbstractMiBandFWInstallHandler implements InstallHandler {
}
GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion()));
fwItem.setIcon(R.drawable.ic_device_miband);
fwItem.setIcon(device.getType().getIcon());
if (!helper.isFirmwareGenerallyCompatibleWith(device)) {
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2017 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
public enum DoNotDisturb {
OFF,
AUTOMATIC,
SCHEDULED
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Carsten Pfeiffer, José Rebelo
This file is part of Gadgetbridge.
@ -28,8 +28,12 @@ import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -117,6 +121,92 @@ public class MiBand2Coordinator extends MiBandCoordinator {
return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
}
public static Set<String> getDisplayItems() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getStringSet(MiBandConst.PREF_MI2_DISPLAY_ITEMS, null);
}
public static boolean getGoalNotification() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_GOAL_NOTIFICATION, false);
}
public static boolean getRotateWristToSwitchInfo() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO, false);
}
public static boolean getInactivityWarnings() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS, false);
}
public static int getInactivityWarningsThreshold() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getInt(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD, 60);
}
public static boolean getInactivityWarningsDnd() {
Prefs prefs = GBApplication.getPrefs();
return prefs.getBoolean(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND, false);
}
public static Date getInactivityWarningsStart() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_START, "06:00");
}
public static Date getInactivityWarningsEnd() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_END, "22:00");
}
public static Date getInactivityWarningsDndStart() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_START, "12:00");
}
public static Date getInactivityWarningsDndEnd() {
return getTimePreference(MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_END, "14:00");
}
public static Date getDoNotDisturbStart() {
return getTimePreference(MiBandConst.PREF_MI2_DO_NOT_DISTURB_START, "01:00");
}
public static Date getDoNotDisturbEnd() {
return getTimePreference(MiBandConst.PREF_MI2_DO_NOT_DISTURB_END, "06:00");
}
public static Date getTimePreference(String key, String defaultValue) {
Prefs prefs = GBApplication.getPrefs();
String time = prefs.getString(key, defaultValue);
DateFormat df = new SimpleDateFormat("HH:mm");
try {
return df.parse(time);
} catch(Exception e) {
LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage());
}
return new Date();
}
public static DoNotDisturb getDoNotDisturb(Context context) {
Prefs prefs = GBApplication.getPrefs();
String dndOff = context.getString(R.string.p_off);
String dndAutomatic = context.getString(R.string.p_automatic);
String dndScheduled = context.getString(R.string.p_scheduled);
String pref = prefs.getString(MiBandConst.PREF_MI2_DO_NOT_DISTURB, dndOff);
if (dndAutomatic.equals(pref)) {
return DoNotDisturb.AUTOMATIC;
} else if (dndScheduled.equals(pref)) {
return DoNotDisturb.SCHEDULED;
}
return DoNotDisturb.OFF;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context);

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, JohnnySun, Uwe Hermann
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, JohnnySun,
José Rebelo, Uwe Hermann
This file is part of Gadgetbridge.
@ -53,7 +54,6 @@ public class MiBand2Service {
public static final int ALERT_LEVEL_MESSAGE = 1;
public static final int ALERT_LEVEL_PHONE_CALL = 2;
public static final int ALERT_LEVEL_VIBRATE_ONLY = 3;
public static final int ALERT_LEVEL_CUSTOM = 0xfa; // followed by another uin8 to select the actual icon
// set metric distance
// set 12 hour time mode
@ -145,6 +145,19 @@ public class MiBand2Service {
public static final byte ICON_HIGH_PRIORITY = 0x7;
public static byte ENDPOINT_DISPLAY_ITEMS = 0x0a;
public static byte DISPLAY_ITEM_BIT_CLOCK = 0x01;
public static byte DISPLAY_ITEM_BIT_STEPS = 0x02;
public static byte DISPLAY_ITEM_BIT_DISTANCE = 0x04;
public static byte DISPLAY_ITEM_BIT_CALORIES= 0x08;
public static byte DISPLAY_ITEM_BIT_HEART_RATE = 0x10;
public static byte DISPLAY_ITEM_BIT_BATTERY = 0x20;
// Second byte must be a bitwise OR combination of the above
// The clock can't be disabled
public static int SCREEN_CHANGE_BYTE = 1;
public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05};
public static byte ENDPOINT_DISPLAY = 0x06;
@ -154,9 +167,42 @@ public class MiBand2Service {
public static final byte[] DATEFORMAT_TIME_24_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x1 };
public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_GOAL_NOTIFICATION = new byte[]{ENDPOINT_DISPLAY, 0x06, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_GOAL_NOTIFICATION = new byte[]{ENDPOINT_DISPLAY, 0x06, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_ROTATE_WRIST_TO_SWITCH_INFO = new byte[]{ENDPOINT_DISPLAY, 0x0d, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_ROTATE_WRIST_TO_SWITCH_INFO = new byte[]{ENDPOINT_DISPLAY, 0x0d, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_DISPLAY_CALLER = new byte[]{ENDPOINT_DISPLAY, 0x10, 0x00, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_DISPLAY_CALLER = new byte[]{ENDPOINT_DISPLAY, 0x10, 0x00, 0x00, 0x00};
public static final byte[] DISPLAY_XXX = new byte[] {ENDPOINT_DISPLAY, 0x03, 0x0, 0x0 };
public static final byte[] DISPLAY_YYY = new byte[] {ENDPOINT_DISPLAY, 0x10, 0x0, 0x1, 0x1 };
// The third byte controls the threshold, in minutes
// The last 8 bytes represent 2 separate time intervals for the inactivity warnings
// If there is no do not disturb interval, the last 4 bytes (the second interval) are 0
// and only the first interval of the command is used
public static int INACTIVITY_WARNINGS_THRESHOLD = 2;
public static int INACTIVITY_WARNINGS_INTERVAL_1_START_HOURS = 4;
public static int INACTIVITY_WARNINGS_INTERVAL_1_START_MINUTES = 5;
public static int INACTIVITY_WARNINGS_INTERVAL_1_END_HOURS = 6;
public static int INACTIVITY_WARNINGS_INTERVAL_1_END_MINUTES = 7;
public static int INACTIVITY_WARNINGS_INTERVAL_2_START_HOURS = 8;
public static int INACTIVITY_WARNINGS_INTERVAL_2_START_MINUTES = 9;
public static int INACTIVITY_WARNINGS_INTERVAL_2_END_HOURS = 10;
public static int INACTIVITY_WARNINGS_INTERVAL_2_END_MINUTES = 11;
public static final byte[] COMMAND_ENABLE_INACTIVITY_WARNINGS = new byte[] { 0x08, 0x01, 0x3c, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static final byte[] COMMAND_DISABLE_INACTIVITY_WARNINGS = new byte[] { 0x08, 0x00, 0x3c, 0x00, 0x04, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00 };
public static byte ENDPOINT_DND = 0x09;
public static final byte[] COMMAND_DO_NOT_DISTURB_AUTOMATIC = new byte[] { ENDPOINT_DND, (byte) 0x83 };
public static final byte[] COMMAND_DO_NOT_DISTURB_OFF = new byte[] { ENDPOINT_DND, (byte) 0x82 };
public static final byte[] COMMAND_DO_NOT_DISTURB_SCHEDULED = new byte[] { ENDPOINT_DND, (byte) 0x81, 0x01, 0x00, 0x06, 0x00 };
// The 4 last bytes set the start and end time in 24h format
public static byte DND_BYTE_START_HOURS = 2;
public static byte DND_BYTE_START_MINUTES = 3;
public static byte DND_BYTE_END_HOURS = 4;
public static byte DND_BYTE_END_MINUTES = 5;
public static final byte RESPONSE = 0x10;
public static final byte SUCCESS = 0x01;

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 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.
@ -35,8 +35,30 @@ public final class MiBandConst {
public static final String PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION = "mi_hr_sleep_detection";
public static final String PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS = "mi_device_time_offset_hours";
public static final String PREF_MI2_DATEFORMAT = "mi2_dateformat";
public static final String PREF_MI2_GOAL_NOTIFICATION = "mi2_goal_notification";
public static final String PREF_MI2_DISPLAY_ITEMS = "mi2_display_items";
public static final String PREF_MI2_DISPLAY_ITEM_CLOCK = "clock";
public static final String PREF_MI2_DISPLAY_ITEM_STEPS = "steps";
public static final String PREF_MI2_DISPLAY_ITEM_DISTANCE = "distance";
public static final String PREF_MI2_DISPLAY_ITEM_CALORIES = "calories";
public static final String PREF_MI2_DISPLAY_ITEM_HEART_RATE = "heart_rate";
public static final String PREF_MI2_DISPLAY_ITEM_BATTERY = "battery";
public static final String PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT = "mi2_activate_display_on_lift_wrist";
public static final String PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO = "mi2_rotate_wrist_to_switch_info";
public static final String PREF_MI2_ENABLE_TEXT_NOTIFICATIONS = "mi2_enable_text_notifications";
public static final String PREF_MI2_DO_NOT_DISTURB = "mi2_do_not_disturb";
public static final String PREF_MI2_DO_NOT_DISTURB_OFF = "off";
public static final String PREF_MI2_DO_NOT_DISTURB_AUTOMATIC = "automatic";
public static final String PREF_MI2_DO_NOT_DISTURB_SCHEDULED = "scheduled";
public static final String PREF_MI2_DO_NOT_DISTURB_START = "mi2_do_not_disturb_start";
public static final String PREF_MI2_DO_NOT_DISTURB_END = "mi2_do_not_disturb_end";
public static final String PREF_MI2_INACTIVITY_WARNINGS = "mi2_inactivity_warnings";
public static final String PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD = "mi2_inactivity_warnings_threshold";
public static final String PREF_MI2_INACTIVITY_WARNINGS_START = "mi2_inactivity_warnings_start";
public static final String PREF_MI2_INACTIVITY_WARNINGS_END = "mi2_inactivity_warnings_end";
public static final String PREF_MI2_INACTIVITY_WARNINGS_DND = "mi2_inactivity_warnings_dnd";
public static final String PREF_MI2_INACTIVITY_WARNINGS_DND_START = "mi2_inactivity_warnings_dnd_start";
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";

View File

@ -151,11 +151,6 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return true;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_activity;
}
@Override
public String getManufacturer() {
return "Xiaomi";
@ -176,6 +171,11 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
public static boolean hasValidUserInfo() {
String dummyMacAddress = MiBandService.MAC_ADDRESS_FILTER_1_1A + ":00:00:00";
try {

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2015-2017 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.
@ -34,12 +34,28 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_ALARM_CLOCK;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.ORIGIN_INCOMING_CALL;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT;
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_DO_NOT_DISTURB;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_SCHEDULED;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_DO_NOT_DISTURB_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ENABLE_TEXT_NOTIFICATIONS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_GOAL_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_ADDRESS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR;
@ -57,6 +73,8 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
addTryListeners();
Prefs prefs = GBApplication.getPrefs();
final Preference enableHeartrateSleepSupport = findPreference(PREF_MIBAND_USE_HR_FOR_SLEEP_DETECTION);
enableHeartrateSleepSupport.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -66,6 +84,20 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
}
});
final Preference goalNotification = findPreference(PREF_MI2_GOAL_NOTIFICATION);
goalNotification.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_GOAL_NOTIFICATION);
}
});
return true;
}
});
final Preference setDateFormat = findPreference(PREF_MI2_DATEFORMAT);
setDateFormat.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -80,6 +112,20 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
}
});
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_MI2_ACTIVATE_DISPLAY_ON_LIFT);
activateDisplayOnLift.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -94,6 +140,170 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
}
});
final Preference rotateWristCycleInfo = findPreference(PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO);
rotateWristCycleInfo.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO);
}
});
return true;
}
});
final Preference inactivityWarnings = findPreference(PREF_MI2_INACTIVITY_WARNINGS);
inactivityWarnings.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS);
}
});
return true;
}
});
final Preference inactivityWarningsThreshold = findPreference(PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD);
inactivityWarningsThreshold.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD);
}
});
return true;
}
});
final Preference inactivityWarningsStart = findPreference(PREF_MI2_INACTIVITY_WARNINGS_START);
inactivityWarningsStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_START);
}
});
return true;
}
});
final Preference inactivityWarningsEnd = findPreference(PREF_MI2_INACTIVITY_WARNINGS_END);
inactivityWarningsEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_END);
}
});
return true;
}
});
final Preference inactivityWarningsDnd = findPreference(PREF_MI2_INACTIVITY_WARNINGS_DND);
inactivityWarningsDnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_DND);
}
});
return true;
}
});
final Preference inactivityWarningsDndStart = findPreference(PREF_MI2_INACTIVITY_WARNINGS_DND_START);
inactivityWarningsDndStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_DND_START);
}
});
return true;
}
});
final Preference inactivityWarningsDndEnd = findPreference(PREF_MI2_INACTIVITY_WARNINGS_DND_END);
inactivityWarningsDndEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_INACTIVITY_WARNINGS_DND_END);
}
});
return true;
}
});
String doNotDisturbState = prefs.getString(MiBandConst.PREF_MI2_DO_NOT_DISTURB, PREF_MI2_DO_NOT_DISTURB_OFF);
boolean doNotDisturbScheduled = doNotDisturbState.equals(PREF_MI2_DO_NOT_DISTURB_SCHEDULED);
final Preference doNotDisturbStart = findPreference(PREF_MI2_DO_NOT_DISTURB_START);
doNotDisturbStart.setEnabled(doNotDisturbScheduled);
doNotDisturbStart.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DO_NOT_DISTURB_START);
}
});
return true;
}
});
final Preference doNotDisturbEnd = findPreference(PREF_MI2_DO_NOT_DISTURB_END);
doNotDisturbEnd.setEnabled(doNotDisturbScheduled);
doNotDisturbEnd.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DO_NOT_DISTURB_END);
}
});
return true;
}
});
final Preference doNotDisturb = findPreference(PREF_MI2_DO_NOT_DISTURB);
doNotDisturb.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newVal) {
final boolean scheduled = PREF_MI2_DO_NOT_DISTURB_SCHEDULED.equals(newVal.toString());
doNotDisturbStart.setEnabled(scheduled);
doNotDisturbEnd.setEnabled(scheduled);
invokeLater(new Runnable() {
@Override
public void run() {
GBApplication.deviceService().onSendConfiguration(PREF_MI2_DO_NOT_DISTURB);
}
});
return true;
}
});
final Preference fitnessGoal = findPreference(ActivityUser.PREF_USER_STEPS_GOAL);
fitnessGoal.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
@ -165,6 +375,7 @@ public class MiBandPreferencesActivity extends AbstractSettingsActivity {
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));

View File

@ -137,11 +137,6 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
return PebbleUtils.hasHRM(device.getModel());
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_app_mananger;
}
@Override
public String getManufacturer() {
return "Pebble";
@ -161,4 +156,9 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
public boolean supportsCalendarEvents() {
return true;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
}

View File

@ -37,11 +37,13 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
public class PebbleHealthSampleProvider extends AbstractSampleProvider<PebbleHealthActivitySample> {
public static final int TYPE_LIGHT_SLEEP = 1;
public static final int TYPE_DEEP_SLEEP = 2;
public static final int TYPE_LIGHT_NAP = 3; //probably
public static final int TYPE_DEEP_NAP = 4; //probably
public static final int TYPE_WALK = 5; //probably
public static final int TYPE_LIGHT_NAP = 3;
public static final int TYPE_DEEP_NAP = 4;
public static final int TYPE_WALK = 5;
public static final int TYPE_RUN = 6;
public static final int TYPE_ACTIVITY = -1;
protected final float movementDivisor = 8000f;
public PebbleHealthSampleProvider(GBDevice device, DaoSession session) {
@ -114,6 +116,8 @@ public class PebbleHealthSampleProvider extends AbstractSampleProvider<PebbleHea
case TYPE_LIGHT_SLEEP:
return ActivityKind.TYPE_LIGHT_SLEEP;
case TYPE_ACTIVITY:
case TYPE_WALK:
case TYPE_RUN:
return ActivityKind.TYPE_ACTIVITY;
default:
return ActivityKind.TYPE_UNKNOWN;

View File

@ -100,11 +100,6 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public int getTapString() {
return R.string.tap_connected_device_for_vibration;
}
@Override
public String getManufacturer() {
return "Amor AG";
@ -125,6 +120,11 @@ public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false; // hmmm well, it has a temperature sensor :D
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
// nothing to delete, yet

View File

@ -34,7 +34,6 @@ import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncState;

View File

@ -48,6 +48,12 @@ public class MusicPlaybackReceiver extends BroadcastReceiver {
MusicStateSpec stateSpec = new MusicStateSpec(lastStateSpec);
Bundle incomingBundle = intent.getExtras();
if (incomingBundle == null) {
LOG.warn("Not processing incoming null bundle.");
return;
}
for (String key : incomingBundle.keySet()) {
Object incoming = incomingBundle.get(key);
if (incoming instanceof String && "artist".equals(key)) {

View File

@ -18,7 +18,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
@ -30,17 +29,19 @@ import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.RemoteInput;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -76,7 +77,7 @@ public class NotificationListener extends NotificationListenerService {
private LimitedQueue mActionLookup = new LimitedQueue(16);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("NewApi")
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
@ -102,7 +103,7 @@ public class NotificationListener extends NotificationListenerService {
} else {
// ACTION_MUTE
LOG.info("going to mute " + sbn.getPackageName());
GBApplication.addToBlacklist(sbn.getPackageName());
GBApplication.addAppToBlacklist(sbn.getPackageName());
}
}
}
@ -177,16 +178,8 @@ public class NotificationListener extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
/*
* return early if DeviceCommunicationService is not running,
* else the service would get started every time we get a notification.
* unfortunately we cannot enable/disable NotificationListener at runtime like we do with
* broadcast receivers because it seems to invalidate the permissions that are
* necessary for NotificationListenerService
*/
if (!isServiceRunning()) {
if (shouldIgnore(sbn))
return;
}
switch (GBApplication.getGrantedInterruptionFilter()) {
case NotificationManager.INTERRUPTION_FILTER_ALL:
@ -201,53 +194,8 @@ public class NotificationListener extends NotificationListenerService {
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if (handleMediaSessionNotification(notification))
return;
Prefs prefs = GBApplication.getPrefs();
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
if (powermanager.isScreenOn()) {
// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
return;
}
}
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags);
return;
}
/* do not display messages from "android"
* This includes keyboard selection message, usb connection messages, etc
* Hope it does not filter out too much, we will see...
*/
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
LOG.info("Not forwarding notification, is a system event");
return;
}
if (source.equals("com.moez.QKSMS") ||
source.equals("com.android.mms") ||
source.equals("com.sonyericsson.conversations") ||
source.equals("com.android.messaging") ||
source.equals("org.smssecure.smssecure")) {
if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) {
return;
}
}
if (GBApplication.isBlacklisted(source)) {
LOG.info("Not forwarding notification, application is blacklisted");
return;
}
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
// determinate Source App Name ("Label")
PackageManager pm = getPackageManager();
@ -266,10 +214,6 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = AppNotificationType.getInstance().get(source);
if (source.startsWith("com.fsck.k9")) {
// we dont want group summaries at all for k9
if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) {
return;
}
preferBigText = true;
}
@ -277,10 +221,9 @@ public class NotificationListener extends NotificationListenerService {
notificationSpec.type = NotificationType.UNKNOWN;
}
LOG.info("Processing notification from source " + source + " with flags: " + notification.flags);
LOG.info("Processing notification " + notificationSpec.id + " from source " + source + " with flags: " + notification.flags);
dissectNotificationTo(notification, notificationSpec, preferBigText);
notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better
// ignore Gadgetbridge's very own notifications, except for those from the debug screen
if (getApplicationContext().getPackageName().equals(source)) {
@ -292,11 +235,6 @@ public class NotificationListener extends NotificationListenerService {
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
List<NotificationCompat.Action> actions = wearableExtender.getActions();
if (actions.isEmpty() && (notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) { //this could cause #395 to come back
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags);
return;
}
for (NotificationCompat.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) {
LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag());
@ -306,11 +244,17 @@ public class NotificationListener extends NotificationListenerService {
}
}
if ((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) == 0 && NotificationCompat.isGroupSummary(notification)) { //this could cause #395 to come back
LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags);
return;
}
GBApplication.deviceService().onNotification(notificationSpec);
}
private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, boolean preferBigText) {
Bundle extras = notification.extras;
Bundle extras = NotificationCompat.getExtras(notification);
//dumpExtras(extras);
@ -321,9 +265,9 @@ public class NotificationListener extends NotificationListenerService {
CharSequence contentCS = null;
if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) {
contentCS = extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
contentCS = extras.getCharSequence(NotificationCompat.EXTRA_BIG_TEXT);
} else if (extras.containsKey(Notification.EXTRA_TEXT)) {
contentCS = extras.getCharSequence(Notification.EXTRA_TEXT);
contentCS = extras.getCharSequence(NotificationCompat.EXTRA_TEXT);
}
if (contentCS != null) {
notificationSpec.body = contentCS.toString();
@ -344,31 +288,18 @@ public class NotificationListener extends NotificationListenerService {
/**
* Try to handle media session notifications that tell info about the current play state.
*
* @param notification The notification to handle.
* @param mediaSession The mediasession to handle.
* @return true if notification was handled, false otherwise
*/
public boolean handleMediaSessionNotification(Notification notification) {
// this code requires Android 5.0 or newer
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}
public boolean handleMediaSessionNotification(MediaSessionCompat.Token mediaSession) {
MusicSpec musicSpec = new MusicSpec();
MusicStateSpec stateSpec = new MusicStateSpec();
Bundle extras = notification.extras;
if (extras == null)
return false;
if (extras.get(Notification.EXTRA_MEDIA_SESSION) == null)
return false;
MediaController c;
MediaControllerCompat c;
try {
c = new MediaController(getApplicationContext(), (MediaSession.Token) extras.get(Notification.EXTRA_MEDIA_SESSION));
c = new MediaControllerCompat(getApplicationContext(), mediaSession);
PlaybackState s = c.getPlaybackState();
PlaybackStateCompat s = c.getPlaybackState();
stateSpec.position = (int) (s.getPosition() / 1000);
stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed());
stateSpec.repeat = 1;
@ -388,57 +319,41 @@ public class NotificationListener extends NotificationListenerService {
break;
}
MediaMetadata d = c.getMetadata();
MediaMetadataCompat d = c.getMetadata();
if (d == null)
return false;
if (d.containsKey(MediaMetadata.METADATA_KEY_ARTIST))
musicSpec.artist = d.getString(MediaMetadata.METADATA_KEY_ARTIST);
musicSpec.artist = d.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
if (d.containsKey(MediaMetadata.METADATA_KEY_ALBUM))
musicSpec.album = d.getString(MediaMetadata.METADATA_KEY_ALBUM);
musicSpec.album = d.getString(MediaMetadataCompat.METADATA_KEY_ALBUM);
if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE))
musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE);
musicSpec.track = d.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION))
musicSpec.duration = (int) d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000;
musicSpec.duration = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_DURATION) / 1000;
if (d.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS))
musicSpec.trackCount = (int) d.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS);
musicSpec.trackCount = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS);
if (d.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER))
musicSpec.trackNr = (int) d.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER);
musicSpec.trackNr = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER);
// finally, tell the device about it
GBApplication.deviceService().onSetMusicInfo(musicSpec);
GBApplication.deviceService().onSetMusicState(stateSpec);
return true;
} catch (NullPointerException e) {
} catch (NullPointerException | RemoteException e) {
return false;
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
//FIXME: deduplicate code
if (!isServiceRunning() || sbn == null) {
if (shouldIgnore(sbn))
return;
}
String source = sbn.getPackageName();
Notification notification = sbn.getNotification();
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
return;
}
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
return;
}
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("autoremove_notifications", false)) {
LOG.info("notification removed, will ask device to delete it");
GBApplication.deviceService().onDeleteNotification((int) sbn.getPostTime()); //FIMXE: a truly unique id would be better
GBApplication.deviceService().onDeleteNotification(sbn.getPackageName().hashCode() * 31 + sbn.getId());
}
}
@ -451,4 +366,88 @@ public class NotificationListener extends NotificationListenerService {
LOG.debug(String.format("Notification extra: %s %s (%s)", key, value.toString(), value.getClass().getName()));
}
}
private boolean shouldIgnore(StatusBarNotification sbn) {
/*
* return early if DeviceCommunicationService is not running,
* else the service would get started every time we get a notification.
* unfortunately we cannot enable/disable NotificationListener at runtime like we do with
* broadcast receivers because it seems to invalidate the permissions that are
* necessary for NotificationListenerService
*/
if (!isServiceRunning() || sbn == null) {
return true;
}
if (shouldIgnoreSource(sbn.getPackageName()))
return true;
if (shouldIgnoreNotification(sbn.getNotification()))
return true;
return false;
}
private boolean shouldIgnoreSource(String source) {
Prefs prefs = GBApplication.getPrefs();
/* do not display messages from "android"
* This includes keyboard selection message, usb connection messages, etc
* Hope it does not filter out too much, we will see...
*/
if (source.equals("android") ||
source.equals("com.android.systemui") ||
source.equals("com.android.dialer") ||
source.equals("com.cyanogenmod.eleven")) {
LOG.info("Ignoring notification, is a system event");
return true;
}
if (source.equals("com.moez.QKSMS") ||
source.equals("com.android.mms") ||
source.equals("com.sonyericsson.conversations") ||
source.equals("com.android.messaging") ||
source.equals("org.smssecure.smssecure")) {
if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) {
return true;
}
}
if (GBApplication.appIsBlacklisted(source)) {
LOG.info("Ignoring notification, application is blacklisted");
return true;
}
return false;
}
private boolean shouldIgnoreNotification(Notification notification) {
MediaSessionCompat.Token mediaSession = NotificationCompat.getMediaSession(notification);
//try to handle media session notifications
if (mediaSession != null && handleMediaSessionNotification(mediaSession))
return true;
//ignore notifications marked as LocalOnly https://developer.android.com/reference/android/app/Notification.html#FLAG_LOCAL_ONLY
if (NotificationCompat.getLocalOnly(notification))
return true;
Prefs prefs = GBApplication.getPrefs();
if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) {
PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE);
if (powermanager.isScreenOn()) {
// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this");
return true;
}
}
if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) {
// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags);
return true;
}
return false;
}
}

View File

@ -23,7 +23,6 @@ import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract.Instances;
import android.text.format.Time;
import android.util.Log;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,6 +33,8 @@ import java.util.GregorianCalendar;
import java.util.List;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class CalendarEvents {
private static final Logger LOG = LoggerFactory.getLogger(CalendarEvents.class);
@ -102,7 +103,11 @@ public class CalendarEvents {
evtCursor.getString(7),
!evtCursor.getString(8).equals("0")
);
if (!GBApplication.calendarIsBlacklisted(calEvent.getCalName())) {
calendarEventList.add(calEvent);
} else {
LOG.debug("calendar " + calEvent.getCalName() + " skipped because it's blacklisted");
}
}
return true;
}

View File

@ -28,15 +28,16 @@ import nodomain.freeyourgadget.gadgetbridge.R;
* and may not be changed.
*/
public enum DeviceType {
UNKNOWN(-1, R.drawable.ic_launcher, R.drawable.ic_device_default_disabled),
UNKNOWN(-1, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled),
PEBBLE(1, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled),
MIBAND(10, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled),
MIBAND2(11, R.drawable.ic_device_miband, R.drawable.ic_device_miband_disabled),
AMAZFITBIP(12, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled),
VIBRATISSIMO(20, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled),
LIVEVIEW(30, R.drawable.ic_launcher, R.drawable.ic_device_default_disabled),
LIVEVIEW(30, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled),
HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled),
MAKIBESF68(41, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled),
TEST(1000, R.drawable.ic_launcher, R.drawable.ic_device_default_disabled);
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled);
private final int key;
@DrawableRes

View File

@ -18,6 +18,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.app.Service;
@ -27,9 +28,11 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.widget.Toast;
@ -584,6 +587,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
if (enable && initialized && coordinator != null && coordinator.supportsCalendarEvents()) {
if (mCalendarReceiver == null && getPrefs().getBoolean("enable_calendar_sync", true)) {
if (!(GBApplication.isRunningMarshmallowOrLater() && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)) {
IntentFilter calendarIntentFilter = new IntentFilter();
calendarIntentFilter.addAction("android.intent.action.PROVIDER_CHANGED");
calendarIntentFilter.addDataScheme("content");
@ -591,6 +595,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mCalendarReceiver = new CalendarReceiver(mGBDevice);
registerReceiver(mCalendarReceiver, calendarIntentFilter);
}
}
if (mAlarmReceiver == null) {
mAlarmReceiver = new AlarmReceiver();
registerReceiver(mAlarmReceiver, new IntentFilter("DAILY_ALARM"));

View File

@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
@ -109,6 +110,9 @@ public class DeviceSupportFactory {
case MIBAND2:
deviceSupport = new ServiceDeviceSupport(new MiBand2Support(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case AMAZFITBIP:
deviceSupport = new ServiceDeviceSupport(new AmazfitBipSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case VIBRATISSIMO:
deviceSupport = new ServiceDeviceSupport(new VibratissimoSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;

View File

@ -0,0 +1,202 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btclassic;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.ParcelUuid;
import android.support.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class BtClassicIoThread extends GBDeviceIoThread {
private static final Logger LOG = LoggerFactory.getLogger(BtClassicIoThread.class);
private final GBDeviceProtocol mProtocol;
private final AbstractSerialDeviceSupport mDeviceSupport;
private BluetoothAdapter mBtAdapter = null;
private BluetoothSocket mBtSocket = null;
private InputStream mInStream = null;
private OutputStream mOutStream = null;
private boolean mQuit = false;
@Override
public void quit() {
mQuit = true;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
}
}
private boolean mIsConnected = false;
public BtClassicIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol deviceProtocol, AbstractSerialDeviceSupport deviceSupport, BluetoothAdapter btAdapter) {
super(gbDevice, context);
mProtocol = deviceProtocol;
mDeviceSupport = deviceSupport;
mBtAdapter = btAdapter;
}
@Override
public synchronized void write(byte[] bytes) {
if (null == bytes)
return;
LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length));
try {
mOutStream.write(bytes);
mOutStream.flush();
} catch (IOException e) {
LOG.error("Error writing.", e);
}
}
@Override
public void run() {
mIsConnected = connect();
if (!mIsConnected) {
setUpdateState(GBDevice.State.NOT_CONNECTED);
return;
}
mQuit = false;
while (!mQuit) {
LOG.info("Ready for a new message exchange.");
try {
GBDeviceEvent deviceEvents[] = mProtocol.decodeResponse(parseIncoming(mInStream));
if (deviceEvents == null) {
LOG.info("unhandled message");
} else {
for (GBDeviceEvent deviceEvent : deviceEvents) {
if (deviceEvent == null) {
continue;
}
mDeviceSupport.evaluateGBDeviceEvent(deviceEvent);
}
}
} catch (SocketTimeoutException ignore) {
LOG.debug("socket timeout, we can't help but ignore this");
} catch (IOException e) {
LOG.info(e.getMessage());
mIsConnected = false;
mBtSocket = null;
mInStream = null;
mOutStream = null;
LOG.info("Bluetooth socket closed, will quit IO Thread");
break;
}
}
mIsConnected = false;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
mBtSocket = null;
}
setUpdateState(GBDevice.State.NOT_CONNECTED);
}
@Override
protected boolean connect() {
GBDevice.State originalState = gbDevice.getState();
setUpdateState(GBDevice.State.CONNECTING);
try {
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress());
ParcelUuid uuids[] = btDevice.getUuids();
if (uuids == null) {
LOG.warn("Device provided no UUIDs to connect to, giving up: " + gbDevice);
return false;
}
for (ParcelUuid uuid : uuids) {
LOG.info("found service UUID " + uuid);
}
mBtSocket = btDevice.createRfcommSocketToServiceRecord(getUuidToConnect(uuids));
mBtSocket.connect();
mInStream = mBtSocket.getInputStream();
mOutStream = mBtSocket.getOutputStream();
setUpdateState(GBDevice.State.CONNECTED);
} catch (IOException e) {
LOG.error("Server socket cannot be started.");
//LOG.error(e.getMessage());
setUpdateState(originalState);
mInStream = null;
mOutStream = null;
mBtSocket = null;
return false;
}
write(mProtocol.encodeSetTime());
setUpdateState(GBDevice.State.INITIALIZED);
return true;
}
/**
* Returns the uuid to connect to.
* Default implementation returns the first of the given uuids that were
* read from the remote device.
* @param uuids
* @return
*/
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return uuids[0].getUuid();
}
protected void setUpdateState(GBDevice.State state) {
gbDevice.setState(state);
gbDevice.sendDeviceUpdateIntent(getContext());
}
/**
* Returns an incoming message for consuming by the GBDeviceProtocol
* @return
* @throws IOException
* @param inStream
*/
protected abstract byte[] parseIncoming(InputStream inStream) throws IOException;
}

View File

@ -1,3 +1,19 @@
/* Copyright (C) 2015-2017 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 <http://www.gnu.org/licenses/>. */
/*
* Copyright (C) 2013 The Android Open Source Project
*

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Uwe Hermann
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer, Uwe Hermann
This file is part of Gadgetbridge.
@ -37,7 +37,8 @@ public enum AlertCategory {
// 10-250 reserved for future use
// 251-255 defined by service specification
Any(255),
Custom(-1);
Custom(-1),
CustomMiBand2(-6);
private final int id;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -33,12 +33,16 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> extends AbstractBleProfile<T> {
private static final Logger LOG = LoggerFactory.getLogger(AlertNotificationProfile.class);
private static final int MAX_MSG_LENGTH = 18;
private int maxLength = 18; // Mi2-ism?
public AlertNotificationProfile(T support) {
super(support);
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
public void configure(TransactionBuilder builder, AlertNotificationControl control) {
BluetoothGattCharacteristic characteristic = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_NOTIFICATION_CONTROL_POINT);
if (characteristic != null) {
@ -57,21 +61,21 @@ public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> exten
BluetoothGattCharacteristic characteristic = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_NEW_ALERT);
if (characteristic != null) {
String message = StringUtils.ensureNotNull(alert.getMessage());
if (message.length() > MAX_MSG_LENGTH && strategy == OverflowStrategy.TRUNCATE) {
message = StringUtils.truncate(message, MAX_MSG_LENGTH);
if (message.length() > maxLength && strategy == OverflowStrategy.TRUNCATE) {
message = StringUtils.truncate(message, maxLength);
}
int numChunks = message.length() / MAX_MSG_LENGTH;
if (message.length() % MAX_MSG_LENGTH > 0) {
int numChunks = message.length() / maxLength;
if (message.length() % maxLength > 0) {
numChunks++;
}
try {
boolean hasAlerted = false;
for (int i = 0; i < numChunks; i++) {
int offset = i * MAX_MSG_LENGTH;
int offset = i * maxLength;
int restLength = message.length() - offset;
message = message.substring(offset, offset + Math.min(MAX_MSG_LENGTH, restLength));
message = message.substring(offset, offset + Math.min(maxLength, restLength));
if (hasAlerted && message.length() == 0) {
// no need to do it again when there is no text content
break;
@ -91,10 +95,17 @@ public class AlertNotificationProfile<T extends AbstractBTLEDeviceSupport> exten
}
}
public void newAlert(TransactionBuilder builder, NewAlert alert) {
newAlert(builder, alert, OverflowStrategy.TRUNCATE);
}
protected byte[] getAlertMessage(NewAlert alert, String message, int chunk) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream(100);
stream.write(BLETypeConversions.fromUint8(alert.getCategory().getId()));
stream.write(BLETypeConversions.fromUint8(alert.getNumAlerts()));
if (alert.getCategory() == AlertCategory.CustomMiBand2) {
stream.write(BLETypeConversions.fromUint8(alert.getCustomIcon()));
}
if (message.length() > 0) {
stream.write(BLETypeConversions.toUtf8s(message));

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -16,6 +16,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification;
import android.icu.util.IslamicCalendar;
/**
* https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.new_alert.xml&u=org.bluetooth.characteristic.new_alert.xml
*
@ -47,6 +49,7 @@ public class NewAlert {
private final AlertCategory category;
private final int numAlerts;
private final String message;
private int customIcon = -1;
public NewAlert(AlertCategory category, int /*uint8*/ numAlerts, String /*utf8s*/ message) {
this.category = category;
@ -54,6 +57,13 @@ public class NewAlert {
this.message = message;
}
public NewAlert(AlertCategory category, int /*uint8*/ numAlerts, String /*utf8s*/ message, int customIcon) {
this.category = category;
this.numAlerts = numAlerts;
this.message = message;
this.customIcon = customIcon;
}
public AlertCategory getCategory() {
return category;
}
@ -65,4 +75,8 @@ public class NewAlert {
public String getMessage() {
return message;
}
public int getCustomIcon() {
return customIcon;
}
}

View File

@ -0,0 +1,80 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.FirmwareType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
public class AmazfitBipFirmwareInfo extends Mi2FirmwareInfo {
// total crap maybe
private static final byte[] GPS_HEADER = new byte[]{
(byte) 0xcb, 0x51, (byte) 0xc1, 0x30, 0x41, (byte) 0x9e, 0x5e, (byte) 0xd3,
0x51, 0x35, (byte) 0xdf, 0x66, (byte) 0xed, (byte) 0xd9, 0x5f, (byte) 0xa7
};
// guessed - at least it is the same accross current versions and different from other devices
private static final byte[] FW_HEADER = new byte[]{
0x68, 0x46, 0x70, 0x47, 0x68, 0x46, 0x70, 0x47,
0x68, 0x46, 0x70, 0x47, 0x68, 0x46, 0x70, 0x47
};
private static final int FW_HEADER_OFFSET = 0x9330;
private static final byte[] RES_HEADER = new byte[]{ // HMRES resources file (*.res)
0x48, 0x4d, 0x52, 0x45, 0x53
};
static {
// firmware
crcToVersion.put(25257, "0.0.8.74");
// resources
crcToVersion.put(12586, "RES 0.0.8.74");
// gps
crcToVersion.put(61520, "GPS 0.0.8.xx");
}
public AmazfitBipFirmwareInfo(byte[] bytes) {
super(bytes);
}
@Override
protected FirmwareType determineFirmwareType(byte[] bytes) {
if (ArrayUtils.startsWith(bytes, RES_HEADER)) {
return FirmwareType.RES;
}
if (ArrayUtils.startsWith(bytes, GPS_HEADER)) {
return FirmwareType.GPS;
}
if (ArrayUtils.equals(bytes, FW_HEADER, FW_HEADER_OFFSET)) {
// TODO: this is certainly not a correct validation, but it works for now
return FirmwareType.FIRMWARE;
}
return FirmwareType.INVALID;
}
@Override
public boolean isGenerallyCompatibleWith(GBDevice device) {
return isHeaderValid() && device.getType() == DeviceType.AMAZFITBIP;
}
}

View File

@ -0,0 +1,184 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
import android.net.Uri;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.SimpleTimeZone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipIcon;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipService;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipWeatherConditions;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
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.amazfitbip.operations.AmazfitBipUpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public class AmazfitBipSupport extends MiBand2Support {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipSupport.class);
@Override
public NotificationStrategy getNotificationStrategy() {
return new AmazfitBipTextNotificationStrategy(this);
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) {
onAlarmClock(notificationSpec);
return;
}
String senderOrTiltle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
String message = StringUtils.truncate(senderOrTiltle, 32) + "\0";
if (notificationSpec.subject != null) {
message += StringUtils.truncate(notificationSpec.subject, 128) + "\n\n";
}
if (notificationSpec.body != null) {
message += StringUtils.truncate(notificationSpec.body, 128);
}
try {
TransactionBuilder builder = performInitialized("new notification");
AlertNotificationProfile<?> profile = new AlertNotificationProfile(this);
profile.setMaxLength(255); // TODO: find out real limit, certainly it is more than 18 which is default
int customIconId = AmazfitBipIcon.mapToIconId(notificationSpec.type);
AlertCategory alertCategory = AlertCategory.CustomMiBand2;
// The SMS icon for AlertCategory.SMS is unique and not available as iconId
if (notificationSpec.type == NotificationType.GENERIC_SMS) {
alertCategory = AlertCategory.SMS;
}
NewAlert alert = new NewAlert(alertCategory, 1, message, customIconId);
profile.newAlert(builder, alert);
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Unable to send notification to Amazfit Bip", ex);
}
}
@Override
public void onFindDevice(boolean start) {
CallSpec callSpec = new CallSpec();
callSpec.command = start ? CallSpec.CALL_INCOMING : CallSpec.CALL_END;
callSpec.name = "Gadgetbridge";
onSetCallState(callSpec);
}
@Override
public void handleButtonPressed(byte[] value) {
if (value == null || value.length != 1) {
return;
}
GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl();
if (value[0] == 0x07) {
callCmd.event = GBDeviceEventCallControl.Event.REJECT;
} else if (value[0] == 0x09) {
callCmd.event = GBDeviceEventCallControl.Event.ACCEPT;
} else {
return;
}
evaluateGBDeviceEvent(callCmd);
}
@Override
public void onInstallApp(Uri uri) {
try {
new AmazfitBipUpdateFirmwareOperation(uri, this).perform();
} catch (IOException ex) {
GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
try {
TransactionBuilder builder = performInitialized("Sending weather forecast");
Version version = new Version(gbDevice.getFirmwareVersion());
boolean supportsConditionString = false;
if (version.compareTo(new Version("0.0.8.74")) >= 0) {
supportsConditionString = true;
}
final byte NR_DAYS = 2;
int bytesPerDay = 4;
int conditionsLength = 0;
if (supportsConditionString) {
bytesPerDay = 5;
conditionsLength = weatherSpec.currentCondition.getBytes().length;
}
int length = 7 + bytesPerDay * NR_DAYS + conditionsLength;
ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 1);
buf.putInt(weatherSpec.timestamp);
int tz_offset_hours = SimpleTimeZone.getDefault().getOffset(weatherSpec.timestamp * 1000L) / (1000 * 60 * 60);
buf.put((byte) (tz_offset_hours * 4));
buf.put(NR_DAYS);
byte condition = AmazfitBipWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
buf.put(condition);
buf.put(condition);
buf.put((byte) (weatherSpec.todayMaxTemp - 273));
buf.put((byte) (weatherSpec.todayMinTemp - 273));
if (supportsConditionString) {
buf.put(weatherSpec.currentCondition.getBytes());
buf.put((byte) 0); //
}
condition = AmazfitBipWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.tomorrowConditionCode);
buf.put(condition);
buf.put(condition);
buf.put((byte) (weatherSpec.tomorrowMaxTemp - 273));
buf.put((byte) (weatherSpec.tomorrowMinTemp - 273));
if (supportsConditionString) {
buf.put((byte) 0); // not yet in weatherspec
}
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
builder.queue(getQueue());
} catch (IOException ignore) {
}
}
}

View File

@ -0,0 +1,67 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip;
import android.support.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
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.btle.profiles.alertnotification.OverflowStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2TextNotificationStrategy;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.MiBand2Support;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
// This class in no longer in use except for incoming calls
class AmazfitBipTextNotificationStrategy extends Mi2TextNotificationStrategy {
AmazfitBipTextNotificationStrategy(MiBand2Support support) {
super(support);
}
@Override
protected void sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, BtLEAction extraAction, TransactionBuilder builder) {
if (simpleNotification != null && !StringUtils.isEmpty(simpleNotification.getMessage())) {
sendAlert(simpleNotification, builder);
}
}
@Override
protected void sendAlert(@NonNull SimpleNotification simpleNotification, TransactionBuilder builder) {
AlertNotificationProfile<?> profile = new AlertNotificationProfile<>(getSupport());
profile.setMaxLength(255); // TODO: find out real limit, certainly it is more than 18 which is default
AlertCategory category = simpleNotification.getAlertCategory();
switch (simpleNotification.getAlertCategory()) {
// only these are confirmed working so far on Amazfit Bip
case Email:
case IncomingCall:
case SMS:
break;
// default to SMS for non working categories
default:
category = AlertCategory.SMS;
}
NewAlert alert = new NewAlert(category, 1, simpleNotification.getMessage());
profile.newAlert(builder, alert, OverflowStrategy.TRUNCATE);
}
}

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2017 Andreas Shimokawa
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.operations;
import android.net.Uri;
import android.widget.Toast;
import java.io.IOException;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipFWHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AmazfitBipUpdateFirmwareOperation extends UpdateFirmwareOperation {
public AmazfitBipUpdateFirmwareOperation(Uri uri, AmazfitBipSupport support) {
super(uri, support);
}
@Override
protected void doPerform() throws IOException {
AmazfitBipFWHelper mFwHelper = new AmazfitBipFWHelper(uri, getContext());
firmwareInfo = mFwHelper.getFirmwareInfo();
if (!firmwareInfo.isGenerallyCompatibleWith(getDevice())) {
throw new IOException("Firmware is not compatible with the given device: " + getDevice().getAddress());
}
if (!sendFwInfo()) {
displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR);
done();
}
//the firmware will be sent by the notification listener if the band confirms that the metadata are ok.
}
}

View File

@ -39,23 +39,23 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
/**
* Number of steps
*/
public int steps;
public int steps = ActivitySample.NOT_MEASURED;
/**
* Number of seconds without activity (TBC)
*/
public int secondsInactive;
public int secondsInactive = ActivitySample.NOT_MEASURED;
/**
* Average Heart Rate in Beats Per Minute
*/
public int heartRate;
public int heartRate = ActivitySample.NOT_MEASURED;
private int age = 0;
/**
* Raw intensity calculated from calories
*/
public int intensity;
public int intensity = ActivitySample.NOT_MEASURED;
public HPlusDataRecordDaySlot(byte[] data, int age) {
super(data, TYPE_DAY_SLOT);
@ -85,6 +85,8 @@ public class HPlusDataRecordDaySlot extends HPlusDataRecord {
timestamp = (int) (slotTime.getTimeInMillis() / 1000L);
this.age = age;
intensity = (int) ((100*heartRate)/(208-0.7*age));
}
public String toString(){

View File

@ -24,6 +24,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.hplus;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -80,19 +81,24 @@ class HPlusDataRecordRealtime extends HPlusDataRecord {
int y = (data[8] & 0xFF) * 256 + (data[7] & 0xFF);
battery = data[9];
calories = x + y; // KCal
heartRate = data[11] & 0xFF; // BPM
activeTime = (data[14] & 0xFF * 256) + (data[13] & 0xFF);
if (battery == 255) {
battery = ActivitySample.NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_WORN;
} else {
heartRate = data[11] & 0xFF; // BPM
if (heartRate == 255) {
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
}
else {
} else {
intensity = (int) ((100 * heartRate) / (208 - 0.7 * age));
activityKind = ActivityKind.TYPE_UNKNOWN;
activityKind = HPlusDataRecord.TYPE_REALTIME;
}
}
}

View File

@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusHealthSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.HPlusHealthActivityOverlay;
@ -247,7 +248,6 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
if (mSlotsInitialSync) {
//If the slot is in the future, actually it is from the previous day
//Subtract a day of seconds
if (record.slot > nowSlot) {
@ -286,6 +286,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
});
List<Integer> notWornSlots = new ArrayList<>();
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
List<HPlusHealthActivitySample> samples = new ArrayList<>();
@ -300,16 +302,55 @@ class HPlusHandlerThread extends GBDeviceIoThread {
sample.setRawHPlusHealthData(storedRecord.getRawData());
sample.setSteps(storedRecord.steps);
sample.setRawIntensity(storedRecord.intensity);
sample.setHeartRate(storedRecord.heartRate);
sample.setRawKind(storedRecord.type);
sample.setRawIntensity(record.intensity);
sample.setProvider(provider);
samples.add(sample);
if (HPlusCoordinator.getAllDayHR(gbDevice.getAddress()) == HPlusConstants.ARG_HEARTRATE_ALLDAY_ON && storedRecord.heartRate == ActivitySample.NOT_MEASURED && storedRecord.steps <= 0) {
notWornSlots.add(sample.getTimestamp());
notWornSlots.add(sample.getTimestamp() + 10 * 60);
}
}
provider.getSampleDao().insertOrReplaceInTx(samples);
mDaySlotRecords.clear();
//Create an overlay with unused slots
if (notWornSlots.size() > 0) {
DaoSession session = dbHandler.getDaoSession();
Long userId = DBHelper.getUser(session).getId();
Long deviceId = DBHelper.getDevice(getDevice(), session).getId();
HPlusHealthActivityOverlayDao overlayDao = session.getHPlusHealthActivityOverlayDao();
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
int firstSlotTimestamp = notWornSlots.get(0);
int lastSlotTimestamp = notWornSlots.get(0);
int i = 1;
for (Integer timestamp : notWornSlots) {
//If it is the last of the samples or of the interruption period
if (timestamp - lastSlotTimestamp > 10 * 60) {
overlayList.add(new HPlusHealthActivityOverlay(firstSlotTimestamp, lastSlotTimestamp, ActivityKind.TYPE_NOT_WORN, deviceId, userId, null));
firstSlotTimestamp = timestamp;
}
lastSlotTimestamp = timestamp;
}
if (firstSlotTimestamp != lastSlotTimestamp)
overlayList.add(new HPlusHealthActivityOverlay(firstSlotTimestamp, lastSlotTimestamp, ActivityKind.TYPE_NOT_WORN, deviceId, userId, null));
overlayDao.insertOrReplaceInTx(overlayList);
}
} catch (GBException ex) {
LOG.info((ex.getMessage()));
} catch (Exception ex) {
@ -402,17 +443,6 @@ class HPlusHandlerThread extends GBDeviceIoThread {
getDevice().setBatteryLevel(record.battery);
//Skip when measuring heart rate
//Calories and Distance are updated and these values will be lost.
//Because a message with a valid Heart Rate will be provided, this loss very limited
if(record.heartRate == ActivityKind.TYPE_NOT_MEASURED) {
getDevice().setFirmwareVersion2("---");
getDevice().sendDeviceUpdateIntent(getContext());
}else {
getDevice().setFirmwareVersion2("" + record.heartRate);
getDevice().sendDeviceUpdateIntent(getContext());
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
HPlusHealthSampleProvider provider = new HPlusHealthSampleProvider(getDevice(), dbHandler.getDaoSession());
@ -506,7 +536,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
int hwMinor = data[1] & 0xFF;
getDevice().setFirmwareVersion2(hwMajor + "." + hwMinor);
mHPlusSupport.setUnicodeSupport((data[3] != 0));
mHPlusSupport.setUnicodeSupport(data[3] != 0);
} else {
major = data[2] & 0xFF;
minor = data[1] & 0xFF;

View File

@ -1,5 +1,5 @@
/* Copyright (C) 2016-2017 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
ivanovlev, João Paulo Barraca
ivanovlev, João Paulo Barraca, Pavel Motyrev
This file is part of Gadgetbridge.
@ -808,6 +808,8 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
private byte[] encodeStringToDevice(String s) {
List<Byte> outBytes = new ArrayList<Byte>();
Boolean unicode = HPlusCoordinator.getUnicodeSupport(this.gbDevice.getAddress());
LOG.info("Encode String: Unicode=" + unicode);
for (int i = 0; i < s.length(); i++) {
Character c = s.charAt(i);
@ -817,13 +819,14 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
cs = HPlusConstants.transliterateMap.get(c);
} else {
try {
if(HPlusCoordinator.getUnicodeSupport(this.gbDevice.getAddress()))
if(unicode)
cs = c.toString().getBytes("Unicode");
else
cs = c.toString().getBytes("GB2312");
} catch (UnsupportedEncodingException e) {
//Fallback. Result string may be strange, but better than nothing
cs = c.toString().getBytes();
LOG.error("Could not convert String to Bytes: " + e.getMessage());
}
}
for (int j = 0; j < cs.length; j++)
@ -884,7 +887,7 @@ public class HPlusSupport extends AbstractBTLEDeviceSupport {
String DEVINFO_STEP = getContext().getString(R.string.chart_steps) + ": ";
String DEVINFO_DISTANCE = getContext().getString(R.string.distance) + ": ";
String DEVINFO_CALORY = getContext().getString(R.string.calories) + ": ";
String DEVINFO_HEART = getContext().getString(R.string.charts_legend_heartrate);
String DEVINFO_HEART = getContext().getString(R.string.charts_legend_heartrate) + ": ";
String info = "";
if (record.steps > 0) {

View File

@ -17,10 +17,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.liveview;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.ParcelUuid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -28,158 +25,25 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class LiveviewIoThread extends GBDeviceIoThread {
public class LiveviewIoThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(LiveviewIoThread.class);
private static final UUID SERIAL = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private final LiveviewProtocol mLiveviewProtocol;
private final LiveviewSupport mLiveviewSupport;
private BluetoothAdapter mBtAdapter = null;
private BluetoothSocket mBtSocket = null;
private InputStream mInStream = null;
private OutputStream mOutStream = null;
private boolean mQuit = false;
@Override
public void quit() {
mQuit = true;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
}
}
private boolean mIsConnected = false;
public LiveviewIoThread(GBDevice gbDevice, Context context, GBDeviceProtocol lvProtocol, LiveviewSupport lvSupport, BluetoothAdapter lvBtAdapter) {
super(gbDevice, context);
mLiveviewProtocol = (LiveviewProtocol) lvProtocol;
mBtAdapter = lvBtAdapter;
mLiveviewSupport = lvSupport;
super(gbDevice, context, lvProtocol, lvSupport, lvBtAdapter);
}
@Override
public synchronized void write(byte[] bytes) {
if (null == bytes)
return;
LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length));
try {
mOutStream.write(bytes);
mOutStream.flush();
} catch (IOException e) {
LOG.error("Error writing.", e);
}
}
@Override
public void run() {
mIsConnected = connect();
if (!mIsConnected) {
setUpdateState(GBDevice.State.NOT_CONNECTED);
return;
}
mQuit = false;
while (!mQuit) {
LOG.info("Ready for a new message exchange.");
try {
GBDeviceEvent deviceEvents[] = mLiveviewProtocol.decodeResponse(parseIncoming());
if (deviceEvents == null) {
LOG.info("unhandled message");
} else {
for (GBDeviceEvent deviceEvent : deviceEvents) {
if (deviceEvent == null) {
continue;
}
mLiveviewSupport.evaluateGBDeviceEvent(deviceEvent);
}
}
} catch (SocketTimeoutException ignore) {
LOG.debug("socket timeout, we can't help but ignore this");
} catch (IOException e) {
LOG.info(e.getMessage());
mIsConnected = false;
mBtSocket = null;
mInStream = null;
mOutStream = null;
LOG.info("Bluetooth socket closed, will quit IO Thread");
break;
}
}
mIsConnected = false;
if (mBtSocket != null) {
try {
mBtSocket.close();
} catch (IOException e) {
LOG.error(e.getMessage());
}
mBtSocket = null;
}
setUpdateState(GBDevice.State.NOT_CONNECTED);
}
@Override
protected boolean connect() {
GBDevice.State originalState = gbDevice.getState();
setUpdateState(GBDevice.State.CONNECTING);
try {
BluetoothDevice btDevice = mBtAdapter.getRemoteDevice(gbDevice.getAddress());
ParcelUuid uuids[] = btDevice.getUuids();
if (uuids == null) {
return false;
}
for (ParcelUuid uuid : uuids) {
LOG.info("found service UUID " + uuid);
}
mBtSocket = btDevice.createRfcommSocketToServiceRecord(uuids[0].getUuid());
mBtSocket.connect();
mInStream = mBtSocket.getInputStream();
mOutStream = mBtSocket.getOutputStream();
setUpdateState(GBDevice.State.CONNECTED);
} catch (IOException e) {
LOG.error("Server socket cannot be started.");
//LOG.error(e.getMessage());
setUpdateState(originalState);
mInStream = null;
mOutStream = null;
mBtSocket = null;
return false;
}
write(mLiveviewProtocol.encodeSetTime());
setUpdateState(GBDevice.State.INITIALIZED);
return true;
}
private void setUpdateState(GBDevice.State state) {
gbDevice.setState(state);
gbDevice.sendDeviceUpdateIntent(getContext());
}
private byte[] parseIncoming() throws IOException {
protected byte[] parseIncoming(InputStream inputStream) throws IOException {
ByteArrayOutputStream msgStream = new ByteArrayOutputStream();
boolean finished = false;
@ -187,7 +51,7 @@ public class LiveviewIoThread extends GBDeviceIoThread {
byte[] incoming = new byte[1];
while (!finished) {
mInStream.read(incoming);
inputStream.read(incoming);
msgStream.write(incoming);
switch (state) {

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, atkyritsis, Carsten Pfeiffer,
Christian Fischer, Daniele Gobbetti, JohnnySun, Julien Pivotto, Kasha,
Sergey Trofimov, Steffen Liebergeld
Christian Fischer, Daniele Gobbetti, freezed-or-frozen, JohnnySun, Julien
Pivotto, Kasha, Sergey Trofimov, Steffen Liebergeld
This file is part of Gadgetbridge.
@ -1270,8 +1270,26 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
}
private void handleSensorData(byte[] value) {
int counter=0, step=0, axis1=0, axis2=0, axis3 =0;
/**
* Analyse and decode sensor data from ADXL362 accelerometer
* @param value to decode
* @return nothing
*
* Each axis raw value is 16bits long and look like : ttssvvvvvvvvvvvv
* tt : 2 bits for the type of data (00=x, 01=y, 10=z, 11=temperature)
* ss : sign of the value
* vvvvvvvvvvvv : accelerometer value encoded using two complements
*
* TODO: Because each accelerometer is different, all values should be calibrated with :
* a scale factor
* an offset factor
*/
private static void handleSensorData(byte[] value) {
int counter=0, step=0;
double xAxis=0.0, yAxis=0.0, zAxis=0.0;
double scale_factor = 1000.0;
double gravity = 9.81;
if ((value.length - 2) % 6 != 0) {
LOG.warn("GOT UNEXPECTED SENSOR DATA WITH LENGTH: " + value.length);
for (byte b : value) {
@ -1282,11 +1300,46 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport {
counter = (value[0] & 0xff) | ((value[1] & 0xff) << 8);
for (int idx = 0; idx < ((value.length - 2) / 6); idx++) {
step = idx * 6;
axis1 = (value[step+2] & 0xff) | ((value[step+3] & 0xff) << 8);
axis2 = (value[step+4] & 0xff) | ((value[step+5] & 0xff) << 8);
axis3 = (value[step+6] & 0xff) | ((value[step+7] & 0xff) << 8);
// Analyse X-axis data
int xAxisRawValue = (value[step+2] & 0xff) | ((value[step+3] & 0xff) << 8);
int xAxisSign = (value[step+3] & 0x30) >> 4;
int xAxisType = (value[step+3] & 0xc0) >> 6;
if (xAxisSign == 0) {
xAxis = xAxisRawValue & 0xfff;
}
else {
xAxis = (xAxisRawValue & 0xfff) - 4097;
}
xAxis = (xAxis*1.0 / scale_factor) * gravity;
// Analyse Y-axis data
int yAxisRawValue = (value[step+4] & 0xff) | ((value[step+5] & 0xff) << 8);
int yAxisSign = (value[step+5] & 0x30) >> 4;
int yAxisType = (value[step+5] & 0xc0) >> 6;
if (yAxisSign == 0) {
yAxis = yAxisRawValue & 0xfff;
}
else {
yAxis = (yAxisRawValue & 0xfff) - 4097;
}
yAxis = (yAxis / scale_factor) * gravity;
// Analyse Z-axis data
int zAxisRawValue = (value[step+6] & 0xff) | ((value[step+7] & 0xff) << 8);
int zAxisSign = (value[step+7] & 0x30) >> 4;
int zAxisType = (value[step+7] & 0xc0) >> 6;
if (zAxisSign == 0) {
zAxis = zAxisRawValue & 0xfff;
}
else {
zAxis = (zAxisRawValue & 0xfff) - 4097;
}
zAxis = (zAxis / scale_factor) * gravity;
// Print results in log
LOG.info("READ SENSOR DATA VALUES: counter:"+counter+" step:"+step+" x-axis:"+ String.format("%.03f",xAxis)+" y-axis:"+String.format("%.03f",yAxis)+" z-axis:"+String.format("%.03f",zAxis)+";");
}
LOG.info("READ SENSOR DATA VALUES: counter:"+counter+" step:"+step+" axis1:"+axis1+" axis2:"+axis2+" axis3:"+axis3+";");
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -19,8 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
public enum FirmwareType {
FIRMWARE((byte) 0),
FONT((byte) 1),
UNKNOWN1((byte) 2),
UNKNOWN2((byte) 3),
RES((byte) 2),
GPS((byte) 3),
INVALID(Byte.MIN_VALUE);
private final byte value;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -53,7 +53,7 @@ public class Mi2FirmwareInfo {
0x4b
};
private static Map<Integer,String> crcToVersion = new HashMap<>();
protected static Map<Integer,String> crcToVersion = new HashMap<>();
static {
// firmware
crcToVersion.put(41899, "1.0.0.39");
@ -89,7 +89,7 @@ public class Mi2FirmwareInfo {
firmwareType = determineFirmwareType(bytes);
}
private FirmwareType determineFirmwareType(byte[] bytes) {
protected FirmwareType determineFirmwareType(byte[] bytes) {
if (ArrayUtils.startsWith(bytes, FT_HEADER)) {
return FirmwareType.FONT;
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Carsten Pfeiffer
/* Copyright (C) 2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -18,7 +18,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
import android.bluetooth.BluetoothGattCharacteristic;
import android.support.annotation.NonNull;
import android.util.Log;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
@ -67,11 +66,11 @@ public class Mi2TextNotificationStrategy extends Mi2NotificationStrategy {
if (simpleNotification != null) {
switch (simpleNotification.getAlertCategory()) {
case Email:
return new byte[] { BLETypeConversions.fromUint8(MiBand2Service.ALERT_LEVEL_MESSAGE), BLETypeConversions.fromUint8(numAlerts)};
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.Email.getId()), BLETypeConversions.fromUint8(numAlerts)};
case InstantMessage:
return new byte[] { BLETypeConversions.fromUint8(MiBand2Service.ALERT_LEVEL_CUSTOM), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_CHAT};
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.CustomMiBand2.getId()), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_CHAT};
case News:
return new byte[] { BLETypeConversions.fromUint8(MiBand2Service.ALERT_LEVEL_CUSTOM), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_PENGUIN};
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.CustomMiBand2.getId()), BLETypeConversions.fromUint8(numAlerts), MiBand2Service.ICON_PENGUIN};
}
}
return new byte[] { BLETypeConversions.fromUint8(AlertCategory.SMS.getId()), BLETypeConversions.fromUint8(numAlerts)};

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, JohnnySun, Julien Pivotto, Kasha, Sergey Trofimov,
Steffen Liebergeld
Fischer, Daniele Gobbetti, JohnnySun, José Rebelo, Julien Pivotto, Kasha,
Sergey Trofimov, Steffen Liebergeld
This file is part of Gadgetbridge.
@ -36,8 +36,10 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@ -50,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInf
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.DoNotDisturb;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service;
@ -298,7 +301,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return this;
}
private NotificationStrategy getNotificationStrategy() {
public NotificationStrategy getNotificationStrategy() {
String firmwareVersion = getDevice().getFirmwareVersion();
if (firmwareVersion != null) {
Version ver = new Version(firmwareVersion);
@ -432,7 +435,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
}
}
private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
protected void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
try {
TransactionBuilder builder = performInitialized(task);
Prefs prefs = GBApplication.getPrefs();
@ -526,7 +529,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
performPreferredNotification(origin + " received", origin, simpleNotification, alertLevel, null);
}
private void onAlarmClock(NotificationSpec notificationSpec) {
protected void onAlarmClock(NotificationSpec notificationSpec) {
alarmClockRinging = true;
AbortTransactionAction abortAction = new StopNotificationAction(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) {
@Override
@ -805,7 +808,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return false;
}
private void handleButtonPressed(byte[] value) {
public void handleButtonPressed(byte[] value) {
LOG.info("Button pressed");
logMessageContent(value);
}
@ -1075,12 +1078,35 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
case MiBandConst.PREF_MI2_DATEFORMAT:
setDateDisplay(builder);
break;
case MiBandConst.PREF_MI2_GOAL_NOTIFICATION:
setGoalNotification(builder);
break;
case MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT:
setActivateDisplayOnLiftWrist(builder);
break;
case MiBandConst.PREF_MI2_DISPLAY_ITEMS:
setDisplayItems(builder);
break;
case MiBandConst.PREF_MI2_ROTATE_WRIST_TO_SWITCH_INFO:
setRotateWristToSwitchInfo(builder);
break;
case ActivityUser.PREF_USER_STEPS_GOAL:
setFitnessGoal(builder);
break;
case MiBandConst.PREF_MI2_DO_NOT_DISTURB:
case MiBandConst.PREF_MI2_DO_NOT_DISTURB_START:
case MiBandConst.PREF_MI2_DO_NOT_DISTURB_END:
setDoNotDisturb(builder);
break;
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_THRESHOLD:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_START:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_END:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_START:
case MiBandConst.PREF_MI2_INACTIVITY_WARNINGS_DND_END:
setInactivityWarnings(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
@ -1128,6 +1154,17 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return this;
}
private MiBand2Support setGoalNotification(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getGoalNotification();
LOG.info("Setting goal notification to " + enable);
if (enable) {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_GOAL_NOTIFICATION);
} else {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_GOAL_NOTIFICATION);
}
return this;
}
private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist();
LOG.info("Setting activate display on lift wrist to " + enable);
@ -1139,6 +1176,137 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
return this;
}
private MiBand2Support setDisplayItems(TransactionBuilder builder) {
Set<String> pages = MiBand2Coordinator.getDisplayItems();
LOG.info("Setting display items to " + (pages == null ? "none" : pages));
byte[] data = MiBand2Service.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;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_DISTANCE)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_DISTANCE;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_CALORIES)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.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;
}
if (pages.contains(MiBandConst.PREF_MI2_DISPLAY_ITEM_BATTERY)) {
data[MiBand2Service.SCREEN_CHANGE_BYTE] |= MiBand2Service.DISPLAY_ITEM_BIT_BATTERY;
}
}
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
return this;
}
private MiBand2Support setRotateWristToSwitchInfo(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.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);
} else {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.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);
return this;
}
private MiBand2Support setDoNotDisturb(TransactionBuilder builder) {
DoNotDisturb doNotDisturb = MiBand2Coordinator.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);
break;
case AUTOMATIC:
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DO_NOT_DISTURB_AUTOMATIC);
break;
case SCHEDULED:
byte[] data = MiBand2Service.COMMAND_DO_NOT_DISTURB_SCHEDULED.clone();
Calendar calendar = GregorianCalendar.getInstance();
Date start = MiBand2Coordinator.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);
Date end = MiBand2Coordinator.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);
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
break;
}
return this;
}
private MiBand2Support setInactivityWarnings(TransactionBuilder builder) {
boolean enable = MiBand2Coordinator.getInactivityWarnings();
LOG.info("Setting inactivity warnings to " + enable);
if (enable) {
byte[] data = MiBand2Service.COMMAND_ENABLE_INACTIVITY_WARNINGS.clone();
int threshold = MiBand2Coordinator.getInactivityWarningsThreshold();
data[MiBand2Service.INACTIVITY_WARNINGS_THRESHOLD] = (byte) threshold;
Calendar calendar = GregorianCalendar.getInstance();
boolean enableDnd = MiBand2Coordinator.getInactivityWarningsDnd();
Date intervalStart = MiBand2Coordinator.getInactivityWarningsStart();
Date intervalEnd = MiBand2Coordinator.getInactivityWarningsEnd();
Date dndStart = MiBand2Coordinator.getInactivityWarningsDndStart();
Date dndEnd = MiBand2Coordinator.getInactivityWarningsDndEnd();
// 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);
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);
// 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);
// ... 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);
} 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);
}
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), data);
} else {
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_INACTIVITY_WARNINGS);
}
return this;
}
public void phase2Initialize(TransactionBuilder builder) {
LOG.info("phase2Initialize...");
enableFurtherNotifications(builder, true);
@ -1147,7 +1315,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
setTimeFormat(builder);
setWearLocation(builder);
setFitnessGoal(builder);
setDisplayItems(builder);
setDoNotDisturb(builder);
setRotateWristToSwitchInfo(builder);
setActivateDisplayOnLiftWrist(builder);
setDisplayCaller(builder);
setGoalNotification(builder);
setInactivityWarnings(builder);
setHeartrateSleepSupport(builder);
}
}

View File

@ -20,6 +20,8 @@ import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.v4.util.TimeUtils;
import android.text.format.DateUtils;
import android.widget.Toast;
import org.slf4j.Logger;
@ -66,8 +68,9 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
private List<MiBandActivitySample> samples = new ArrayList<>(60*24); // 1day per default
private byte lastPacketCounter = -1;
private byte lastPacketCounter;
private Calendar startTimestamp;
private int fetchCount;
public FetchActivityOperation(MiBand2Support support) {
super(support);
@ -83,12 +86,25 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
@Override
protected void doPerform() throws IOException {
startFetching();
}
private void startFetching() throws IOException {
samples.clear();
lastPacketCounter = -1;
TransactionBuilder builder = performInitialized("fetching activity data");
getSupport().setLowLatency(builder);
if (fetchCount == 0) {
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
}
fetchCount++;
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
builder.notify(characteristicActivityData, false);
BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4);
builder.notify(characteristicFetch, true);
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
GregorianCalendar sinceWhen = getLastSuccessfulSyncTime();
builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)));
@ -136,13 +152,40 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
}
private void handleActivityFetchFinish() {
LOG.info("Fetching activity data has finished.");
saveSamples();
LOG.info("Fetching activity data has finished round " + fetchCount);
GregorianCalendar lastSyncTimestamp = saveSamples();
if (lastSyncTimestamp != null && needsAnotherFetch(lastSyncTimestamp)) {
try {
startFetching();
return;
} catch (IOException ex) {
LOG.error("Error starting another round of fetching activity data", ex);
}
}
operationFinished();
unsetBusy();
}
private void saveSamples() {
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
if (fetchCount > 5) {
LOG.warn("Already jave 5 fetch rounds, not doing another one.");
return false;
}
if (DateUtils.isToday(lastSyncTimestamp.getTimeInMillis())) {
LOG.info("Hopefully no further fetch needed, last synced timestamp is from today.");
return false;
}
if (lastSyncTimestamp.getTimeInMillis() > System.currentTimeMillis()) {
LOG.warn("Not doing another fetch since last synced timestamp is in the future: " + DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return false;
}
LOG.info("Doing another fetch since last sync timestamp is still too old: " + DateTimeUtils.formatDateTime(lastSyncTimestamp.getTime()));
return true;
}
private GregorianCalendar saveSamples() {
if (samples.size() > 0) {
// save all the samples that we got
try (DBHandler handler = GBApplication.acquireDB()) {
@ -168,6 +211,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
saveLastSyncTimestamp(timestamp);
LOG.info("Mi2 activity data: last sample timestamp: " + DateTimeUtils.formatDateTime(timestamp.getTime()));
return timestamp;
} catch (Exception ex) {
GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR);
@ -175,6 +219,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
samples.clear();
}
}
return null;
}
/**

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Andreas Shimokawa, Carsten Pfeiffer
This file is part of Gadgetbridge.
@ -48,11 +48,11 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class);
private final Uri uri;
private final BluetoothGattCharacteristic fwCControlChar;
private final BluetoothGattCharacteristic fwCDataChar;
final Prefs prefs = GBApplication.getPrefs();
private Mi2FirmwareInfo firmwareInfo;
protected final Uri uri;
protected final BluetoothGattCharacteristic fwCControlChar;
protected final BluetoothGattCharacteristic fwCDataChar;
protected final Prefs prefs = GBApplication.getPrefs();
protected Mi2FirmwareInfo firmwareInfo;
public UpdateFirmwareOperation(Uri uri, MiBand2Support support) {
super(support);
@ -82,7 +82,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
//the firmware will be sent by the notification listener if the band confirms that the metadata are ok.
}
private void done() {
protected void done() {
LOG.info("Operation done.");
operationFinished();
unsetBusy();
@ -163,7 +163,7 @@ public class UpdateFirmwareOperation extends AbstractMiBand2Operation {
done();
}
}
private void displayMessage(Context context, String message, int duration, int severity) {
protected void displayMessage(Context context, String message, int duration, int severity) {
getSupport().handleGBDeviceEvent(new GBDeviceEventDisplayMessage(message, duration, severity));
}

View File

@ -28,7 +28,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
@ -158,9 +157,11 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
int version = (int) pair.second;
LOG.info("got version: " + ((float) version / 10.0f));
ctrl_message |= CTRL_VERSION_DONE;
} else if (pair.first.equals(keyBase)) {// fix timestamp
TimeZone tz = SimpleTimeZone.getDefault();
recording_base_timestamp = (int) pair.second - (tz.getOffset(System.currentTimeMillis())) / 1000;
} else if (pair.first.equals(keyBase)) {
recording_base_timestamp = (int) pair.second;
if (mPebbleProtocol.mFwMajor < 3) {
recording_base_timestamp -= SimpleTimeZone.getDefault().getOffset(recording_base_timestamp * 1000L) / 1000;
}
LOG.info("got base: " + recording_base_timestamp);
ctrl_message |= CTRL_SET_LAST_SENT | CTRL_DO_NEXT;
} else if (pair.first.equals(keyAutoReset)) {

View File

@ -156,7 +156,7 @@ class PebbleIoThread extends GBDeviceIoThread {
}
}
} catch (IOException e) {
e.printStackTrace();
LOG.warn("error while connecting: " + e.getMessage(), e);
gbDevice.setState(originalState);
gbDevice.sendDeviceUpdateIntent(getContext());

View File

@ -87,6 +87,8 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
byteArray[i] = ((Integer) jsonArray.get(i)).byteValue();
}
object = byteArray;
} else if (object instanceof Boolean) {
object = (short) (((Boolean) object) ? 1 : 0);
}
pairs.add(new Pair<>(Integer.parseInt(keyStr), object));
}

View File

@ -164,7 +164,14 @@ class PebbleGATTClient extends BluetoothGattCallback {
if (doPairing) {
BluetoothGattCharacteristic characteristic = gatt.getService(SERVICE_UUID).getCharacteristic(PAIRING_TRIGGER_CHARACTERISTIC);
if ((characteristic.getProperties() & PROPERTY_WRITE) != 0) {
characteristic.setValue(new byte[]{1});
LOG.info("This seems to be a >=4.0 FW Pebble, writing to pairing trigger");
// flags:
// 0 - always 1
// 1 - unknown
// 2 - always 0
// 3 - unknown, set on kitkat (seems to help to get a "better" pairing)
// 4 - unknown, set on some phones
characteristic.setValue(new byte[]{9});
gatt.writeCharacteristic(characteristic);
} else {
LOG.info("This seems to be some <4.0 FW Pebble, reading pairing trigger");

View File

@ -39,6 +39,7 @@ public class GBCallControlReceiver extends BroadcastReceiver {
GBDeviceEventCallControl.Event callCmd = GBDeviceEventCallControl.Event.values()[intent.getIntExtra("event", 0)];
switch (callCmd) {
case END:
case REJECT:
case START:
try {
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
@ -46,7 +47,7 @@ public class GBCallControlReceiver extends BroadcastReceiver {
Method method = clazz.getDeclaredMethod("getITelephony");
method.setAccessible(true);
ITelephony telephonyService = (ITelephony) method.invoke(telephonyManager);
if (callCmd == GBDeviceEventCallControl.Event.END) {
if (callCmd == GBDeviceEventCallControl.Event.END || callCmd == GBDeviceEventCallControl.Event.REJECT) {
telephonyService.endCall();
} else {
telephonyService.answerRingingCall();

View File

@ -16,12 +16,20 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Color;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
public class AndroidUtils {
public static ParcelUuid[] toParcelUUids(Parcelable[] uuids) {
if (uuids == null) {
@ -61,4 +69,48 @@ public class AndroidUtils {
return false;
}
}
public static void setLanguage(Activity activity, Locale language) {
Configuration config = new Configuration();
config.setLocale(language);
// FIXME: I have no idea what I am doing
activity.getBaseContext().getResources().updateConfiguration(config, activity.getBaseContext().getResources().getDisplayMetrics());
activity.recreate();
}
/**
* Returns the theme dependent text color as a css-style hex string.
* @param context the context to access the colour
*/
public static String getTextColorHex(Context context) {
int color;
if (GBApplication.isDarkThemeEnabled()) {
color = context.getResources().getColor(R.color.primarytext_dark);
} else {
color = context.getResources().getColor(R.color.primarytext_light);
}
return colorToHex(color);
}
/**
* Returns the theme dependent background color as a css-style hex string.
* @param context the context to access the colour
*/
public static String getBackgroundColorHex(Context context) {
int color;
if (GBApplication.isDarkThemeEnabled()) {
color = context.getResources().getColor(R.color.cardview_dark_background);
} else {
color = context.getResources().getColor(R.color.cardview_light_background);
}
return colorToHex(color);
}
private static String colorToHex(int color) {
return "#"
+ Integer.toHexString(Color.red(color))
+ Integer.toHexString(Color.green(color))
+ Integer.toHexString(Color.blue(color));
}
}

View File

@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.UnknownDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.amazfitbip.AmazfitBipCooordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.MakibesF68Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
@ -184,6 +185,7 @@ public class DeviceHelper {
private List<DeviceCoordinator> createCoordinators() {
List<DeviceCoordinator> result = new ArrayList<>();
result.add(new AmazfitBipCooordinator()); // Note: AmazfitBip must come before MiBand2 because detection is hacky, atm
result.add(new MiBand2Coordinator()); // Note: MiBand2 must come before MiBand because detection is hacky, atm
result.add(new MiBandCoordinator());
result.add(new PebbleCoordinator());

View File

@ -76,7 +76,7 @@ public class GB {
.setContentIntent(pendingIntent)
.setOngoing(true);
if (GBApplication.isRunningLollipopOrLater()) {
builder.setVisibility(Notification.VISIBILITY_PUBLIC);
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
}
if (GBApplication.minimizeNotification()) {
builder.setPriority(Notification.PRIORITY_MIN);
@ -268,7 +268,7 @@ public class GB {
notificationIntent, 0);
NotificationCompat.Builder nb = new NotificationCompat.Builder(context)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(text)
.setContentIntent(pendingIntent)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2017 Carsten Pfeiffer
/* Copyright (C) 2016-2017 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -21,6 +21,7 @@ import java.util.Date;
public class GBPrefs {
public static final String PACKAGE_BLACKLIST = "package_blacklist";
public static final String CALENDAR_BLACKLIST = "calendar_blacklist";
public static final String AUTO_RECONNECT = "general_autocreconnect";
private static final String AUTO_START = "general_autostartonboot";
private static final boolean AUTO_START_DEFAULT = true;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Alberto, Carsten Pfeiffer
/* Copyright (C) 2017 Alberto, Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
@ -17,6 +17,12 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.SharedPreferences;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
@ -27,12 +33,6 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import android.content.SharedPreferences;
import android.util.Xml;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class ImportExportSharedPreferences {
@ -125,12 +125,19 @@ public class ImportExportSharedPreferences {
editor.putString(key, text);
} else if (HASHSET.equals(name)) {
if (key.equals(GBPrefs.PACKAGE_BLACKLIST)) {
Set<String> blacklist = new HashSet<>();
Set<String> apps_blacklist = new HashSet<>();
text=text.replace("[","").replace("]","");
for (int z=0;z<text.split(",").length;z++){
blacklist.add(text.split(",")[z].trim());
apps_blacklist.add(text.split(",")[z].trim());
}
GBApplication.setBlackList(blacklist);
GBApplication.setAppsBlackList(apps_blacklist);
} else if (key.equals(GBPrefs.CALENDAR_BLACKLIST)) { //TODO: untested
Set<String> calendars_blacklist = new HashSet<>();
text = text.replace("[", "").replace("]", "");
for (int z = 0; z < text.split(",").length; z++) {
calendars_blacklist.add(text.split(",")[z].trim());
}
GBApplication.setCalendarsBlackList(calendars_blacklist);
}
} else if (!PREFERENCES.equals(name)) {
throw new Exception("Unkown type " + name);

View File

@ -1,4 +1,5 @@
/* Copyright (C) 2017 ivanovlev, Yaron Shahrabani
/* Copyright (C) 2017 Andreas Shimokawa, ivanovlev, lazarosfs, Yaron
Shahrabani
This file is part of Gadgetbridge.
@ -28,7 +29,12 @@ public class LanguageUtils {
private static Map<Character, String> transliterateMap = new HashMap<Character, String>(){
{
//extended ASCII characters
put('æ', "ae"); put('œ', "oe"); put('ß', "B"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\"");
put('æ', "ae"); put('œ', "oe"); put('ª', "a"); put('º', "o"); put('«',"\""); put('»',"\"");
//german characters
put('ä',"ae"); put('ö',"oe"); put('ü',"ue");
put('Ä',"Ae"); put('Ö',"Oe"); put('Ü',"Üe");
put('ß',"ss"); put('ẞ',"SS");
//russian chars
put('а', "a"); put('б', "b"); put('в', "v"); put('г', "g"); put('д', "d"); put('е', "e"); put('ё', "jo"); put('ж', "zh");
@ -42,7 +48,19 @@ public class LanguageUtils {
put('ט', "t"); put('י', "y"); put('כ', "c"); put('ל', "l"); put('מ', "m"); put('נ', "n"); put('ס', "s"); put('ע', "'");
put('פ', "p"); put('צ', "ts"); put('ק', "k"); put('ר', "r"); put('ש', "sh"); put('ת', "th"); put('ף', "f"); put('ץ', "ts");
put('ך', "ch");put('ם', "m");put('ן', "n");
//continue for other languages...
// greek chars
put('α',"a");put('ά',"a");put('β',"v");put('γ',"g");put('δ',"d");put('ε',"e");put('έ',"e");put('ζ',"z");put('η',"i");
put('ή',"i");put('θ',"th");put('ι',"i");put('ί',"i");put('ϊ',"i");put('ΐ',"i");put('κ',"k");put('λ',"l");put('μ',"m");
put('ν',"n");put('ξ',"ks");put('ο',"o");put('ό',"o");put('π',"p");put('ρ',"r");put('σ',"s");put('ς',"s");put('τ',"t");
put('υ',"y");put('ύ',"y");put('ϋ',"y");put('ΰ',"y");put('φ',"f");put('χ',"ch");put('ψ',"ps");put('ω',"o");put('ώ',"o");
put('Α',"A");put('Ά',"A");put('Β',"B");put('Γ',"G");put('Δ',"D");put('Ε',"E");put('Έ',"E");put('Ζ',"Z");put('Η',"I");
put('Ή',"I");put('Θ',"TH");put('Ι',"I");put('Ί',"I");put('Ϊ',"I");put('Κ',"K");put('Λ',"L");put('Μ',"M");put('Ν',"N");
put('Ξ',"KS");put('Ο',"O");put('Ό',"O");put('Π',"P");put('Ρ',"R");put('Σ',"S");put('Τ',"T");put('Υ',"Y");put('Ύ',"Y");
put('Ϋ',"Y");put('Φ',"F");put('Χ',"CH");put('Ψ',"PS");put('Ω',"O");put('Ώ',"O");
//TODO: these must be configurabe. If someone wants to transliterate cyrillic it does not mean his device has no German umlauts
//all or nothing is really bad here
}
};

View File

@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.SharedPreferences;
import android.util.Log;
import java.util.HashSet;
import java.util.Set;
/**
@ -162,4 +163,16 @@ public class Prefs {
public SharedPreferences getPreferences() {
return preferences;
}
/**
* Ugly workaround for Set<String> preferences not consistently applying.
* @param editor
* @param preference
* @param value
*/
public static void putStringSet(SharedPreferences.Editor editor, String preference, HashSet<String> value) {
editor.putStringSet(preference, null);
editor.commit();
editor.putStringSet(preference, new HashSet<>(value));
}
}

View File

@ -0,0 +1,116 @@
/* Copyright (C) 2017 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.DialogPreference;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TimePicker;
public class TimePreference extends DialogPreference {
private int hour = 0;
private int minute = 0;
private TimePicker picker = null;
public TimePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected View onCreateDialogView() {
picker = new TimePicker(getContext());
picker.setIs24HourView(DateFormat.is24HourFormat(getContext()));
picker.setPadding(0, 50, 0, 50);
return picker;
}
@Override
protected void onBindDialogView(View v) {
super.onBindDialogView(v);
picker.setCurrentHour(hour);
picker.setCurrentMinute(minute);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
hour = picker.getCurrentHour();
minute = picker.getCurrentMinute();
String time = getTime24h();
if (callChangeListener(time)) {
persistString(time);
updateSummary();
}
}
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getString(index);
}
@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
String time;
if (restoreValue) {
if (defaultValue == null) {
time = getPersistedString("00:00");
} else {
time = getPersistedString(defaultValue.toString());
}
} else {
time = defaultValue.toString();
}
String[] pieces = time.split(":");
hour = Integer.parseInt(pieces[0]);
minute = Integer.parseInt(pieces[1]);
updateSummary();
}
public void updateSummary() {
if (DateFormat.is24HourFormat(getContext()))
setSummary(getTime24h());
else
setSummary(getTime12h());
}
public String getTime24h() {
return String.format("%02d", hour) + ":" + String.format("%02d", minute);
}
public String getTime12h() {
String suffix = hour < 12 ? " AM" : " PM";
int h = hour > 12 ? hour - 12 : hour;
return String.valueOf(h) + ":" + String.format("%02d", minute) + suffix;
}
}

View File

@ -28,6 +28,8 @@ public class Version implements Comparable<Version> {
public Version(String version) {
if(version == null)
throw new IllegalArgumentException("Version can not be null");
version = version.trim();
if (!version.matches("[0-9]+(\\.[0-9]+)*"))
throw new IllegalArgumentException("Invalid version format");
this.version = version;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

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