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

Merge branch 'master' into background-javascript

This commit is contained in:
Andreas Shimokawa 2017-07-15 23:07:46 +02:00
commit eb7e635cdc
53 changed files with 1649 additions and 449 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,5 +1,15 @@
### Changelog
#### 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

View File

@ -1,6 +1,6 @@
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_device_pebble.png
ic_device_miband.png
ic_activitytracker.png

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 or Mi Band or HPlus device 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,16 @@ 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
* HPlus Devices (e.g. ZeBand) [Wiki section about this device](https://github.com/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* Liveview
* Vibratissimo (experimental)
## Features (Pebble)

View File

@ -26,8 +26,8 @@ android {
targetSdkVersion 25
// note: always bump BOTH versionCode and versionName!
versionName "0.19.2"
versionCode 95
versionName "0.19.3"
versionCode 96
vectorDrawables.useSupportLibrary = true
}
buildTypes {

View File

@ -350,26 +350,34 @@ public class GBApplication extends Application {
private static HashSet<String> blacklist = null;
public static boolean isBlacklisted(String packageName) {
if (blacklist == null) {
GB.log("isBlacklisted: blacklisti is null!", GB.INFO, null);
}
return blacklist != null && blacklist.contains(packageName);
}
public static void setBlackList(Set<String> packageNames) {
if (packageNames == null) {
GB.log("Set null blacklist", GB.INFO, null);
blacklist = new HashSet<>();
} else {
blacklist = new HashSet<>(packageNames);
}
GB.log("New blacklist has " + blacklist.size() + " entries", GB.INFO, null);
saveBlackList();
}
private static void loadBlackList() {
GB.log("Loading blacklist", GB.INFO, null);
blacklist = (HashSet<String>) sharedPrefs.getStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
if (blacklist == null) {
blacklist = new HashSet<>();
}
GB.log("Loaded blacklist has " + blacklist.size() + " entries", GB.INFO, null);
}
private static void saveBlackList() {
GB.log("Saving blacklist with " + blacklist.size() + " entries", GB.INFO, null);
SharedPreferences.Editor editor = sharedPrefs.edit();
if (blacklist.isEmpty()) {
editor.putStringSet(GBPrefs.PACKAGE_BLACKLIST, null);
@ -386,6 +394,7 @@ public class GBApplication extends Application {
}
public static synchronized void removeFromBlacklist(String packageName) {
GB.log("Removing from blacklist: " + packageName, GB.INFO, null);
blacklist.remove(packageName);
saveBlackList();
}

View File

@ -203,6 +203,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);

View File

@ -17,7 +17,9 @@
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 nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
@ -25,6 +27,20 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
class ActivityAnalysis {
// store raw steps and duration
protected HashMap<Integer, Long> stats = new HashMap<Integer, Long>();
// normalize steps
protected HashMap<Float, Float> statsQuantified = new HashMap<Float, Float>();
// store maxSpeed / resolution
protected float maxSpeedQuantifier;
// store an average of round precision
protected float roundPrecision = 0f;
// max speed determined from samples
private int maxSpeed = 0;
// number of bars on stats chart
private int resolution = 5;
ActivityAmounts calculateActivityAmounts(List<? extends ActivitySample> samples) {
ActivityAmount deepSleep = new ActivityAmount(ActivityKind.TYPE_DEEP_SLEEP);
ActivityAmount lightSleep = new ActivityAmount(ActivityKind.TYPE_LIGHT_SLEEP);
@ -53,7 +69,7 @@ class ActivityAnalysis {
int steps = sample.getSteps();
if (steps > 0) {
amount.addSteps(sample.getSteps());
amount.addSteps(steps);
}
if (previousSample != null) {
@ -65,12 +81,56 @@ 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)) {
//System.out.println("Adding: " + steps);
stats.put(steps, timeDifference);
} else {
long time = stats.get(steps);
//System.out.println("Updating: " + steps + " " + timeDifference + time);
stats.put(steps, timeDifference + time);
}
}
}
previousAmount = amount;
previousSample = sample;
}
maxSpeedQuantifier = maxSpeed / resolution;
for (Map.Entry<Integer, Long> entry : stats.entrySet()) {
// 0.1 precision
//float keyQuantified = Math.round(entry.getKey() / maxSpeedQuantifier * 10f) / 10f;
// 1 precision
float keyQuantified = entry.getKey() / maxSpeedQuantifier;
float keyQuantifiedRounded = Math.round(entry.getKey() / maxSpeedQuantifier);
float keyQuantifiedPrecision = keyQuantifiedRounded - keyQuantified;
roundPrecision = (roundPrecision + Math.abs(keyQuantifiedPrecision)) / 2;
//System.out.println("Precision: " + roundPrecision);
// no scale
//keyQuantified = entry.getKey();
// scaling to minutes
float timeMinutes = entry.getValue() / 60;
if (!statsQuantified.containsKey(keyQuantifiedRounded)) {
//System.out.println("Adding: " + keyQuantified + "/" + timeMinutes);
statsQuantified.put(keyQuantifiedRounded, timeMinutes);
} else {
float previousTime = statsQuantified.get(keyQuantifiedRounded);
//System.out.println("Updating: " + keyQuantified + "/" + (timeMinutes + previousTime));
statsQuantified.put(keyQuantifiedRounded, (timeMinutes + previousTime));
}
}
ActivityAmounts result = new ActivityAmounts();
if (deepSleep.getTotalSeconds() > 0) {
result.addAmount(deepSleep);

View File

@ -333,6 +333,8 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
return new WeekStepsChartFragment();
case 4:
return new LiveActivityFragment();
case 5:
return new StatsChartFragment();
}
return null;
@ -340,8 +342,12 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
@Override
public int getCount() {
// Show 5 total pages.
return 5;
// Show 4 or 5 total pages. (always hide speed zones)
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(mGBDevice);
if (coordinator.supportsRealtimeData()) {
return 5;
}
return 4;
}
@Override
@ -357,6 +363,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,193 @@
/* 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.LegendEntry;
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 static android.R.attr.x;
public class StatsChartFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(StatsChartFragment.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<>();
XAxisValueFormatter customXAxis = new XAxisValueFormatter();
for (Map.Entry<Float, Float> entry : analysis.statsQuantified.entrySet()) {
entries.add(new BarEntry(entry.getKey(), entry.getValue()));
/*float realValue = entry.getKey() * analysis.maxSpeedQuantifier;
String customLabel = Math.round(realValue * (1 - analysis.roundPrecision) * 10f) / 10f + " - " + Math.round(realValue * (1 + analysis.roundPrecision) * 10f) / 10f;*/
customXAxis.add("" + entry.getKey() * analysis.maxSpeedQuantifier);
}
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);
// set X axis
customXAxis.sort();
XAxis left = mStatsChart.getXAxis();
left.setValueFormatter(customXAxis);
// display precision
//mStatsChart.getDescription().setText(Math.round(analysis.roundPrecision * 100) + "%");
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 void setupLegend(Chart chart) {
List<LegendEntry> legendEntries = new ArrayList<>(3);
LegendEntry lightSleepEntry = new LegendEntry();
lightSleepEntry.label = akLightSleep.label;
lightSleepEntry.formColor = akLightSleep.color;
legendEntries.add(lightSleepEntry);
LegendEntry deepSleepEntry = new LegendEntry();
deepSleepEntry.label = akDeepSleep.label;
deepSleepEntry.formColor = akDeepSleep.color;
legendEntries.add(deepSleepEntry);
if (supportsHeartrate(getChartsHost().getDevice())) {
LegendEntry hrEntry = new LegendEntry();
hrEntry.label = HEARTRATE_LABEL;
hrEntry.formColor = HEARTRATE_COLOR;
legendEntries.add(hrEntry);
}
chart.getLegend().setCustom(legendEntries);
chart.getLegend().setTextColor(LEGEND_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 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

@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Created by nhu on 30/04/17.
*/
public class XAxisValueFormatter implements IAxisValueFormatter {
private List<String> mValues = new ArrayList<>();
public XAxisValueFormatter() {
super();
}
public void add(String label) {
mValues.add(label);
}
public void sort() {
//System.out.println("Sorting " + mValues);
Collections.sort(mValues);
}
@Override
public String getFormattedValue(float value, AxisBase axis) {
String returnString = "N/A";
try {
returnString = mValues.get((int) value).toString();
//System.out.println("Asking " + value + ", returning " + returnString);
} catch (Exception e) {
System.out.println(e.getMessage());
}
return returnString;
}
}

View File

@ -201,8 +201,6 @@ public interface DeviceCoordinator {
*/
boolean supportsHeartRateMeasurement(GBDevice device);
int getTapString();
/**
* Returns the readable name of the manufacturer.
*/
@ -242,4 +240,9 @@ 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

@ -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;
@ -57,7 +58,7 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
}
public int normalizeType(int rawType) {
switch (rawType){
switch (rawType) {
case HPlusDataRecord.TYPE_DAY_SLOT:
case HPlusDataRecord.TYPE_DAY_SUMMARY:
case HPlusDataRecord.TYPE_REALTIME:
@ -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
@ -154,8 +207,8 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
int stepsTodayCount = 0;
HPlusHealthActivitySample lastSample = null;
for(HPlusHealthActivitySample sample: samples){
if(sample.getTimestamp() >= today.getTimeInMillis() / 1000){
for (HPlusHealthActivitySample sample: samples) {
if (sample.getTimestamp() >= today.getTimeInMillis() / 1000) {
/**Strategy is:
* Calculate max steps from realtime messages
@ -170,7 +223,7 @@ public class HPlusHealthSampleProvider extends AbstractSampleProvider<HPlusHealt
sample.setSteps(ActivitySample.NOT_MEASURED);
lastSample = sample;
}else{
} else {
if (sample.getRawKind() != HPlusDataRecord.TYPE_DAY_SUMMARY) {
sample.setSteps(ActivitySample.NOT_MEASURED);
}
@ -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

@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
public enum DoNotDisturb {
OFF,
AUTOMATIC,
SCHEDULED
}

View File

@ -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;
@ -43,6 +47,9 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
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_START;
public class MiBand2Coordinator extends MiBandCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(MiBand2Coordinator.class);
@ -117,6 +124,65 @@ 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 Date getDoNotDisturbStart() {
Prefs prefs = GBApplication.getPrefs();
String time = prefs.getString(PREF_MI2_DO_NOT_DISTURB_START, "01:00");
DateFormat df = new SimpleDateFormat("HH:mm");
try {
return df.parse(time);
} catch(Exception e) {
}
return new Date();
}
public static Date getDoNotDisturbEnd() {
Prefs prefs = GBApplication.getPrefs();
String time = prefs.getString(PREF_MI2_DO_NOT_DISTURB_END, "06:00");
DateFormat df = new SimpleDateFormat("HH:mm");
try {
return df.parse(time);
} catch(Exception e) {
}
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

@ -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,24 @@ 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[] DISPLAY_XXX = new byte[] {ENDPOINT_DISPLAY, 0x03, 0x0, 0x0 };
public static final byte[] DISPLAY_YYY = new byte[] {ENDPOINT_DISPLAY, 0x10, 0x0, 0x1, 0x1 };
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

@ -35,8 +35,23 @@ 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_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

@ -34,12 +34,21 @@ 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_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 +66,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 +77,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 +105,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 +133,72 @@ 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;
}
});
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

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";
@ -166,4 +161,9 @@ public class PebbleCoordinator extends AbstractDeviceCoordinator {
public boolean needsBackgroundWebView(GBDevice device) {
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

@ -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();
@ -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.isBlacklisted(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

@ -0,0 +1,202 @@
/* Copyright (C) 2016-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.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

@ -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(heartRate == 255) {
intensity = 0;
activityKind = ActivityKind.TYPE_NOT_MEASURED;
if (battery == 255) {
battery = ActivitySample.NOT_MEASURED;
heartRate = ActivitySample.NOT_MEASURED;
}
else {
intensity = (int) ((100*heartRate)/(208-0.7*age));
activityKind = ActivityKind.TYPE_UNKNOWN;
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 {
intensity = (int) ((100 * heartRate) / (208 - 0.7 * age));
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;
@ -123,7 +124,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
break;
}
if(gbDevice.getState() == GBDevice.State.NOT_CONNECTED){
if (gbDevice.getState() == GBDevice.State.NOT_CONNECTED) {
quit();
}
@ -137,11 +138,11 @@ class HPlusHandlerThread extends GBDeviceIoThread {
requestNextSleepData();
}
if(now.compareTo(mGetDaySummaryTime) > 0) {
if (now.compareTo(mGetDaySummaryTime) > 0) {
requestDaySummaryData();
}
if(now.compareTo(mHelloTime) > 0){
if (now.compareTo(mHelloTime) > 0) {
sendHello();
}
@ -178,7 +179,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
mDaySlotRecords.clear();
try {
if(!mHPlusSupport.isConnected())
if (!mHPlusSupport.isConnected())
mHPlusSupport.connect();
TransactionBuilder builder = new TransactionBuilder("startSyncDayStats");
@ -188,7 +189,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_CURR_DATA});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
LOG.warn("HPlus: Synchronization exception: " + e);
}
@ -197,13 +198,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
}
public void sendHello(){
public void sendHello() {
try {
TransactionBuilder builder = new TransactionBuilder("hello");
builder.write(mHPlusSupport.ctrlCharacteristic, HPlusConstants.CMD_ACTION_HELLO);
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
mHelloTime = GregorianCalendar.getInstance();
@ -226,31 +227,30 @@ class HPlusHandlerThread extends GBDeviceIoThread {
try{
record = new HPlusDataRecordDaySlot(data, age);
} catch(IllegalArgumentException e){
} catch(IllegalArgumentException e) {
LOG.info((e.getMessage()));
return false;
}
Calendar now = GregorianCalendar.getInstance();
int nowSlot = now.get(Calendar.HOUR_OF_DAY) * 6 + (now.get(Calendar.MINUTE) / 10);
if(record.slot == nowSlot){
if(mCurrentDaySlot != null && mCurrentDaySlot != record){
if (record.slot == nowSlot){
if (mCurrentDaySlot != null && mCurrentDaySlot != record) {
mCurrentDaySlot.accumulate(record);
mDaySlotRecords.add(mCurrentDaySlot);
mCurrentDaySlot = null;
}else{
} else {
//Store it to a temp variable as this is an intermediate value
mCurrentDaySlot = record;
if(!mSlotsInitialSync)
if (!mSlotsInitialSync)
return true;
}
}
if(mSlotsInitialSync) {
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){
if (record.slot > nowSlot) {
record.timestamp -= 3600 * 24;
}
@ -259,7 +259,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
//Ignore the current slot as it is incomplete
if(record.slot != nowSlot)
if (record.slot != nowSlot)
mDaySlotRecords.add(record);
//Still fetching ring buffer. Request the next slots
@ -271,14 +271,14 @@ class HPlusHandlerThread extends GBDeviceIoThread {
}
//Keep buffering
if(record.slot != 143)
if (record.slot != 143)
return true;
} else {
mGetDaySlotsTime = GregorianCalendar.getInstance();
mGetDaySlotsTime.add(Calendar.DAY_OF_MONTH, 1);
}
if(mDaySlotRecords.size() > 0) {
if (mDaySlotRecords.size() > 0) {
//Sort the samples
Collections.sort(mDaySlotRecords, new Comparator<HPlusDataRecordDaySlot>() {
public int compare(HPlusDataRecordDaySlot one, HPlusDataRecordDaySlot other) {
@ -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<>();
@ -293,23 +295,62 @@ class HPlusHandlerThread extends GBDeviceIoThread {
for (HPlusDataRecordDaySlot storedRecord : mDaySlotRecords) {
//Invalid records (no data) will be ignored
if(!storedRecord.isValid())
if (!storedRecord.isValid())
continue;
HPlusHealthActivitySample sample = createSample(dbHandler, storedRecord.timestamp);
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) {
@ -329,7 +370,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @param data the message from the device
* @return boolean indicating success or fail
*/
public boolean processIncomingSleepData(byte[] data){
public boolean processIncomingSleepData(byte[] data) {
HPlusDataRecordSleep record;
try{
@ -353,7 +394,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
List<HPlusHealthActivityOverlay> overlayList = new ArrayList<>();
List<HPlusDataRecord.RecordInterval> intervals = record.getIntervals();
for(HPlusDataRecord.RecordInterval interval : intervals){
for(HPlusDataRecord.RecordInterval interval : intervals) {
overlayList.add(new HPlusHealthActivityOverlay(interval.timestampFrom, interval.timestampTo, interval.activityKind, deviceId, userId, null));
}
@ -386,7 +427,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public boolean processRealtimeStats(byte[] data, int age) {
HPlusDataRecordRealtime record;
try{
try {
record = new HPlusDataRecordRealtime(data, age);
} catch(IllegalArgumentException e){
LOG.info((e.getMessage()));
@ -395,24 +436,13 @@ class HPlusHandlerThread extends GBDeviceIoThread {
//Skip duplicated messages as the device seems to send the same record multiple times
//This can be used to detect the user is moving (not sleeping)
if(prevRealTimeRecord != null && record.same(prevRealTimeRecord))
if (prevRealTimeRecord != null && record.same(prevRealTimeRecord))
return true;
prevRealTimeRecord = record;
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());
@ -456,9 +486,9 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public boolean processDaySummary(byte[] data) {
HPlusDataRecordDaySummary record;
try{
try {
record = new HPlusDataRecordDaySummary(data);
} catch(IllegalArgumentException e){
} catch(IllegalArgumentException e) {
LOG.info((e.getMessage()));
return false;
}
@ -498,7 +528,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
public boolean processVersion(byte[] data) {
int major, minor;
if(data.length >= 11){
if (data.length >= 11) {
major = data[10] & 0xFF;
minor = data[9] & 0xFF;
@ -506,8 +536,8 @@ class HPlusHandlerThread extends GBDeviceIoThread {
int hwMinor = data[1] & 0xFF;
getDevice().setFirmwareVersion2(hwMajor + "." + hwMinor);
mHPlusSupport.setUnicodeSupport((data[3] != 0));
}else {
mHPlusSupport.setUnicodeSupport(data[3] != 0);
} else {
major = data[2] & 0xFF;
minor = data[1] & 0xFF;
}
@ -526,7 +556,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
TransactionBuilder builder = new TransactionBuilder("requestSleepStats");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_SLEEP});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
@ -548,23 +578,23 @@ class HPlusHandlerThread extends GBDeviceIoThread {
//Sync to current time
mGetDaySlotsTime = now;
if(mSlotsInitialSync) {
if(mLastSlotReceived == 143) {
if (mSlotsInitialSync) {
if (mLastSlotReceived == 143) {
mSlotsInitialSync = false;
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD); //Sync complete. Delay timer forever
mLastSlotReceived = -1;
mLastSlotRequested = mLastSlotReceived + 1;
return;
}else {
} else {
mGetDaySlotsTime.add(Calendar.SECOND, CURRENT_DAY_SYNC_RETRY_PERIOD);
}
}else{
} else {
//Sync complete. Delay timer forever
mGetDaySlotsTime.set(Calendar.SECOND, CURRENT_DAY_SYNC_PERIOD);
return;
}
if(mLastSlotReceived == 143)
if (mLastSlotReceived == 143)
mLastSlotReceived = -1;
byte hour = (byte) ((mLastSlotReceived + 1)/ 6);
@ -581,19 +611,19 @@ class HPlusHandlerThread extends GBDeviceIoThread {
TransactionBuilder builder = new TransactionBuilder("getNextDaySlot");
builder.write(mHPlusSupport.ctrlCharacteristic, msg);
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
}
/**
* Request a batch of data with the summary of the previous days
*/
public void requestDaySummaryData(){
public void requestDaySummaryData() {
try {
TransactionBuilder builder = new TransactionBuilder("startSyncDaySummary");
builder.write(mHPlusSupport.ctrlCharacteristic, new byte[]{HPlusConstants.CMD_GET_DAY_DATA});
mHPlusSupport.performConnected(builder.getTransaction());
}catch(Exception e){
} catch(Exception e) {
}
mGetDaySummaryTime = GregorianCalendar.getInstance();
@ -606,7 +636,7 @@ class HPlusHandlerThread extends GBDeviceIoThread {
* @param timestamp The sample timestamp
* @return The sample just created
*/
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp){
private HPlusHealthActivitySample createSample(DBHandler dbHandler, int timestamp) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId();
HPlusHealthActivitySample sample = new HPlusHealthActivitySample(

View File

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

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

@ -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;
@ -1075,12 +1078,25 @@ 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);
}
builder.queue(getQueue());
} catch (IOException e) {
@ -1128,6 +1144,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 +1166,78 @@ 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 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;
}
public void phase2Initialize(TransactionBuilder builder) {
LOG.info("phase2Initialize...");
enableFurtherNotifications(builder, true);
@ -1147,7 +1246,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
setTimeFormat(builder);
setWearLocation(builder);
setFitnessGoal(builder);
setDisplayItems(builder);
setDoNotDisturb(builder);
setRotateWristToSwitchInfo(builder);
setActivateDisplayOnLiftWrist(builder);
setGoalNotification(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);
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
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

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

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

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

@ -0,0 +1,100 @@
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

@ -0,0 +1,39 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity$PlaceholderFragment">
<com.github.mikephil.charting.charts.HorizontalBarChart
android:id="@+id/statschart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="20"
android:layout_below="@+id/statsXAxisText"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/statsYAxisText"></com.github.mikephil.charting.charts.HorizontalBarChart>
<TextView
android:id="@+id/statsXAxisText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:paddingLeft="10dp"
android:text="@string/stats_x_axis_label" />
<TextView
android:id="@+id/statsYAxisText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginStart="-95dp"
android:elegantTextHeight="false"
android:rotation="90"
android:singleLine="true"
android:text="@string/stats_y_axis_label"
android:translationX="40dp" />
</RelativeLayout>

View File

@ -12,13 +12,13 @@
<string name="controlcenter_disconnect">Trennen</string>
<string name="controlcenter_delete_device">Gerät löschen</string>
<string name="controlcenter_delete_device_name">%1$s löschen</string>
<string name="controlcenter_delete_device_dialogmessage">Das wird das Gerät und alle zugehörigen Daten löschen!</string>
<string name="controlcenter_navigation_drawer_open">Navigations-Menü öffnen</string>
<string name="controlcenter_navigation_drawer_close">Navigations-Menü schließen</string>
<string name="controlcenter_snackbar_need_longpress">Halte die Karte lange gedrückt, um die Verbindung zu trennen.</string>
<string name="controlcenter_snackbar_disconnecting">Trenne ...</string>
<string name="controlcenter_snackbar_connecting">Verbinde ...</string>
<string name="controlcenter_snackbar_requested_screenshot">Screenshot des Gerätes wird erstellt.</string>
<string name="controlcenter_delete_device_dialogmessage">Dies wird das Gerät und alle zugehörigen Daten löschen!</string>
<string name="controlcenter_navigation_drawer_open">Navigationsmenü öffnen</string>
<string name="controlcenter_navigation_drawer_close">Navigationsmenü schließen</string>
<string name="controlcenter_snackbar_need_longpress">Halte die Karte lange gedrückt, um die Verbindung zu trennen</string>
<string name="controlcenter_snackbar_disconnecting">Trenne</string>
<string name="controlcenter_snackbar_connecting">Verbinde</string>
<string name="controlcenter_snackbar_requested_screenshot">Erstelle Screenshot des Gerätes</string>
<string name="title_activity_debug">Debug</string>
<!--Strings related to AppManager-->
<string name="title_activity_appmanager">App Manager</string>
@ -33,8 +33,8 @@
<string name="appmanager_health_deactivate">deaktivieren</string>
<string name="appmanager_hrm_activate">HRM aktivieren</string>
<string name="appmanager_hrm_deactivate">HRM deaktivieren</string>
<string name="appmanager_weather_activate">System Wetter-App aktivieren</string>
<string name="appmanager_weather_deactivate">System Wetter-App deaktivieren</string>
<string name="appmanager_weather_activate">Wetter-App des Systems aktivieren</string>
<string name="appmanager_weather_deactivate">Wetter-App des Systems deaktivieren</string>
<string name="appmanager_weather_install_provider">Installiere die Wetter-Benachrichtigungs-App</string>
<string name="app_configure">Konfigurieren</string>
<string name="app_move_to_top">Nach oben</string>
@ -42,11 +42,11 @@
<string name="title_activity_appblacklist">Sperre für Benachrichtigungen</string>
<!--Strings related to FwAppInstaller-->
<string name="title_activity_fw_app_insaller">FW/App Installer</string>
<string name="fw_upgrade_notice">Es soll die Firmware %s anstelle der aktuell installierten Version aufs Mi Band gespielt werden.</string>
<string name="fw_multi_upgrade_notice">Es sollen die Firmwares %1$s und %2$s anstelle der aktuell installierten Versionen aufs Mi Band gespielt werden.</string>
<string name="fw_upgrade_notice">Es soll die Firmware %s anstelle der aktuell installierten Version auf das Mi Band gespielt werden.</string>
<string name="fw_multi_upgrade_notice">Es sollen die Firmwares %1$s und %2$s anstelle der aktuell installierten Versionen auf das Mi Band gespielt werden.</string>
<string name="miband_firmware_known">Diese Firmware ist getestet worden und ist zu Gadgetbridge kompatibel.</string>
<string name="miband_firmware_unknown_warning">Diese Firmware ist nicht getestet und könnte inkompatibel mit Gadgetbridge sein.\n\nEs wird nicht empfohlen sie zu installieren!</string>
<string name="miband_firmware_suggest_whitelist">Falls die Firmware trotzdem installiert werden soll und nachher alles korrekt funktioniert, melde bitte den Gadgetbridge Entwicklern, dass die Firmware %s tauglich ist.</string>
<string name="miband_firmware_unknown_warning">Diese Firmware ist nicht getestet und könnte inkompatibel mit Gadgetbridge sein.\n\nEs wird nicht empfohlen sie auf dem Mi Band zu installieren!</string>
<string name="miband_firmware_suggest_whitelist">Falls die Firmware trotzdem installiert werden soll und nachher alles korrekt funktioniert, melde bitte den Gadgetbridge Entwicklern, dass die Firmware %s funktioniert.</string>
<!--Strings related to Settings-->
<string name="title_activity_settings">Einstellungen</string>
<string name="pref_header_general">Allgemeine Einstellungen</string>
@ -70,7 +70,7 @@
<string name="pref_title_notifications_call">Anrufe</string>
<string name="pref_title_notifications_sms">SMS</string>
<string name="pref_title_notifications_pebblemsg">Pebble Nachrichten</string>
<string name="pref_summary_notifications_pebblemsg">Unterstützung für Anwendungen, die Benachrichtigungen an die Pebble via PebbleKit senden.</string>
<string name="pref_summary_notifications_pebblemsg">Unterstützung für Anwendungen, die Benachrichtigungen an die Pebble mit dem PebbleKit senden.</string>
<string name="pref_title_notifications_generic">Andere Benachrichtigungen</string>
<string name="pref_title_whenscreenon">… auch wenn der Bildschirm an ist</string>
<string name="pref_title_notification_filter">Bitte nicht stören</string>
@ -104,11 +104,11 @@
<string name="pref_summary_enable_outgoing_call">Falls dies deaktiviert wird, vibriert die Pebble 2/LE nicht bei ausgehenden Anrufen</string>
<string name="pref_title_enable_pebblekit">Erlaube Zugriff von anderen Android Apps</string>
<string name="pref_summary_enable_pebblekit">Experimentelle Unterstützung für Android Apps, die PebbleKit benutzen</string>
<string name="pref_header_pebble_timeline">Pebble Zeitstrang</string>
<string name="pref_header_pebble_timeline">Pebble Timeline</string>
<string name="pref_title_sunrise_sunset">Sonnenauf- und -untergang </string>
<string name="pref_summary_sunrise_sunset">Sende Sonnenauf- und -untergangszeiten abhänging vom Standort auf die Pebble Timeline</string>
<string name="pref_title_enable_calendar_sync">Synchronisiere Kalender </string>
<string name="pref_summary_enable_calendar_sync">Sende Termine an den Zeitstrang</string>
<string name="pref_title_enable_calendar_sync">Synchronisiere Kalender</string>
<string name="pref_summary_enable_calendar_sync">Sende Termine an die Timeline</string>
<string name="pref_title_autoremove_notifications">Verworfene Benachrichtigungen automatisch entfernen</string>
<string name="pref_summary_autoremove_notifications">Benachrichtigungen werden automatisch von der Pebble entfernt, wenn sie auf dem Android-Gerät verworfen werden</string>
<string name="pref_title_pebble_privacy_mode">Privatsphäre-Modus</string>
@ -177,7 +177,7 @@
<string name="title_activity_mi_band_pairing">Kopple Dein Mi Band</string>
<string name="pairing">Koppeln mit %s…</string>
<string name="pairing_creating_bond_with">Bindung mit %1$s (%2$s) herstellen</string>
<string name="pairing_unable_to_pair_with">Kann nicht mit %1$s (%2$s) koppeln</string>
<string name="pairing_unable_to_pair_with">Kann mit %1$s (%2$s) nicht koppeln</string>
<string name="pairing_in_progress">Bindung findet statt: %1$s (%2$s)</string>
<string name="pairing_already_bonded">Bereits mit %1$s (%2$s) gebunden, stelle Verbindung her......</string>
<string name="message_cannot_pair_no_mac">Keine MAC Adresse erhalten, kann nicht koppeln.</string>
@ -271,8 +271,8 @@
<string name="updatefirmwareoperation_update_complete_rebooting">Firmware-Installation erfolgreich beendet, Gerät wird neu gestartet…</string>
<string name="updatefirmwareoperation_write_failed">Schreiben der Firmware fehlgeschlagen</string>
<string name="chart_steps">Schritte</string>
<string name="calories">Kalorien </string>
<string name="distance">Distanz </string>
<string name="calories">Kalorien</string>
<string name="distance">Distanz</string>
<string name="liveactivity_live_activity">Live Aktivität</string>
<string name="weeksteps_today_steps_description">Schritte heute, Ziel: %1$s</string>
<string name="pref_title_dont_ack_transfer">Transfer von Aktivitätsdaten nicht bestätigen</string>
@ -367,4 +367,13 @@
<string name="mi2_fw_installhandler_fw53_hint">Installiere Version %1$s vor dem Installieren der Firmware!</string>
<string name="mi2_enable_text_notifications">Text Benachrichtigung</string>
<string name="off">aus</string>
<string name="discovery_attempting_to_pair">Versuche zu koppeln mit %1$s</string>
<string name="discovery_bonding_failed_immediately">Kopplung mit %1$s soeben fehlgeschlagen.</string>
<string name="discovery_trying_to_connect_to">Versuche zu verbinden mit: %1$s</string>
<string name="discovery_enable_bluetooth">Aktiviere Bluetooth um Geräte zu finden.</string>
<string name="discovery_successfully_bonded">Erfolgreich gekoppelt mit %1$s.</string>
<string name="discovery_pair_title">Koppeln mit %1$s?</string>
<string name="discovery_pair_question">Wähle Koppeln um dein Gerät zu koppeln. Falls dies fehlschlägt, versuche es erneut ohne Kopplung.</string>
<string name="discovery_yes_pair">Koppeln</string>
<string name="discovery_dont_pair">Nicht koppeln</string>
</resources>

View File

@ -220,6 +220,9 @@
<string name="pref_screen_notification_profile_generic_chat">Chat</string>
<string name="pref_screen_notification_profile_generic_navigation">Navegación</string>
<string name="pref_screen_notification_profile_generic_social">Red Social</string>
<string name="stats_title">Zonas de velocidad</string>
<string name="stats_x_axis_label">Total de minutos</string>
<string name="stats_y_axis_label">Pasos por minuto</string>
<string name="control_center_find_lost_device">Encuentra un dispositivo perdido</string>
<string name="control_center_cancel_to_stop_vibration">Cancelar para detener la vibración.</string>
<string name="title_activity_charts">Tu actividad</string>

View File

@ -220,6 +220,9 @@
<string name="pref_screen_notification_profile_generic_chat">Tchat</string>
<string name="pref_screen_notification_profile_generic_navigation">Navigation</string>
<string name="pref_screen_notification_profile_generic_social">Réseau social</string>
<string name="stats_title">Zones de vitesse</string>
<string name="stats_x_axis_label">Total de minutes</string>
<string name="stats_y_axis_label">Pas par minute</string>
<string name="control_center_find_lost_device">Trouver l\'appareil perdu</string>
<string name="control_center_cancel_to_stop_vibration">Annuler pour arrêter les vibrations</string>
<string name="title_activity_charts">Votre activité</string>

View File

@ -220,6 +220,9 @@
<string name="pref_screen_notification_profile_generic_chat">チャット</string>
<string name="pref_screen_notification_profile_generic_navigation">ナビゲーション</string>
<string name="pref_screen_notification_profile_generic_social">ソーシャルネットワーク</string>
<string name="stats_title">スピードゾーン</string>
<string name="stats_x_axis_label">合計分数</string>
<string name="stats_y_axis_label">毎分の歩数</string>
<string name="control_center_find_lost_device">なくしたデバイスを探す</string>
<string name="control_center_cancel_to_stop_vibration">キャンセルで振動を停止します。</string>
<string name="title_activity_charts">あなたの活動</string>

View File

@ -270,6 +270,9 @@
<string name="chart_steps">Passos</string>
<string name="calories">Calorias</string>
<string name="distance">Distância</string>
<string name="clock">Relógio</string>
<string name="heart_rate">Ritmo Cardíaco</string>
<string name="battery">Bateria</string>
<string name="liveactivity_live_activity">Atividade em tempo real</string>
<string name="weeksteps_today_steps_description">Passos hoje, objetivo: %1$s</string>
<string name="pref_title_dont_ack_transfer">Não confirmar a transferência de dados de atividade</string>
@ -297,7 +300,16 @@
<string name="miband2_prefs_dateformat">Mi2: Formato da Data</string>
<string name="dateformat_time">Hora</string>
<string name="dateformat_date_time"><![CDATA[Hora & Data]]></string>
<string name="mi2_prefs_goal_notification">Notificação de objectivo</string>
<string name="mi2_prefs_goal_notification_summary">A pulseira vai vibrar quando o objectivo diário de passos for atingido.</string>
<string name="mi2_prefs_display_items">Items do ecrã</string>
<string name="mi2_prefs_display_items_summary">Escolher os items a mostrar no ecrã</string>
<string name="mi2_prefs_activate_display_on_lift">Ativar ecrã do dispositivo quando o levantar</string>
<string name="mi2_prefs_rotate_wrist_to_switch_info">Rodar o pulso para mudar de ecrã</string>
<string name="mi2_prefs_do_not_disturb">Não incomodar</string>
<string name="mi2_prefs_do_not_disturb_summary">A pulseira não recebe notificações enquanto activo</string>
<string name="mi2_prefs_do_not_disturb_start">Hora de início</string>
<string name="mi2_prefs_do_not_disturb_end">Hora de fim</string>
<string name="FetchActivityOperation_about_to_transfer_since">Prestes a transferir dados desde %1$s</string>
<string name="waiting_for_reconnect">aguarde para tornar a ligar</string>
<string name="activity_prefs_about_you">Sobre você</string>
@ -363,6 +375,9 @@
<string name="mi2_enable_text_notifications">Notificações de texto</string>
<string name="mi2_enable_text_notifications_summary"><![CDATA[Necessita de firmware >= 1.0.1.28 e Mili_pro.ft* instalado.]]></string>
<string name="off">desligado</string>
<string name="mi2_dnd_off">Desligado</string>
<string name="mi2_dnd_automatic">Automático (deteção de sono)</string>
<string name="mi2_dnd_scheduled">Agendado (intervalo de tempo)</string>
<string name="discovery_attempting_to_pair">A tentar emparelhar com %1$s</string>
<string name="discovery_enable_bluetooth">Ative o Bluetooth para encontrar os dispositivos.</string>
<string name="discovery_pair_title">Emparelhar com %1$s?</string>

View File

@ -135,6 +135,39 @@
<item>@string/p_dateformat_datetime</item>
</string-array>
<string-array name="mi2_do_not_disturb">
<item>@string/mi2_dnd_off</item>
<item>@string/mi2_dnd_automatic</item>
<item>@string/mi2_dnd_scheduled</item>
</string-array>
<string-array name="mi2_do_not_disturb_values">
<item>@string/p_off</item>
<item>@string/p_automatic</item>
<item>@string/p_scheduled</item>
</string-array>
<string-array name="pref_mi2_display_items">
<item>@string/chart_steps</item>
<item>@string/distance</item>
<item>@string/calories</item>
<item>@string/heart_rate</item>
<item>@string/battery</item>
</string-array>
<string-array name="pref_mi2_display_items_values">
<item>@string/p_steps</item>
<item>@string/p_distance</item>
<item>@string/p_calories</item>
<item>@string/p_heart_rate</item>
<item>@string/p_battery</item>
</string-array>
<string-array name="pref_mi2_display_items_default">
<item>@string/p_steps</item>
<item>@string/p_distance</item>
<item>@string/p_calories</item>
<item>@string/p_heart_rate</item>
<item>@string/p_battery</item>
</string-array>
<string-array name="pref_entries_unit_system">
<item>@string/unit_metric</item>
<item>@string/unit_imperial</item>

View File

@ -258,6 +258,10 @@
<string name="pref_screen_notification_profile_generic_navigation">Navigation</string>
<string name="pref_screen_notification_profile_generic_social">Social Network</string>
<string name="stats_title">Speed Zones</string>
<string name="stats_x_axis_label">Total minutes</string>
<string name="stats_y_axis_label">Steps per minute</string>
<string name="control_center_find_lost_device">Find lost Device</string>
<string name="control_center_cancel_to_stop_vibration">Cancel to stop vibration.</string>
<string name="title_activity_charts">Your Activity</string>
@ -311,6 +315,9 @@
<string name="chart_steps">Steps</string>
<string name="calories">Calories</string>
<string name="distance">Distance</string>
<string name="clock">Clock</string>
<string name="heart_rate">Heart Rate</string>
<string name="battery">Battery</string>
<string name="liveactivity_live_activity">Live Activity</string>
<string name="weeksteps_today_steps_description">Steps today, target: %1$s</string>
<string name="pref_title_dont_ack_transfer">Do not ack activity data transfer</string>
@ -339,7 +346,16 @@
<string name="miband2_prefs_dateformat">Mi2: Date Format</string>
<string name="dateformat_time">Time</string>
<string name="dateformat_date_time"><![CDATA[Time & Date]]></string>
<string name="mi2_prefs_goal_notification">Goal notification</string>
<string name="mi2_prefs_goal_notification_summary">The band will vibrate when the daily steps goal is reached</string>
<string name="mi2_prefs_display_items">Display items</string>
<string name="mi2_prefs_display_items_summary">Choose the items displayed on the band screen</string>
<string name="mi2_prefs_activate_display_on_lift">Activate display upon lift</string>
<string name="mi2_prefs_rotate_wrist_to_switch_info">Rotate wrist to switch info</string>
<string name="mi2_prefs_do_not_disturb">Do Not Disturb</string>
<string name="mi2_prefs_do_not_disturb_summary">The band won\'t receive notifications while active</string>
<string name="mi2_prefs_do_not_disturb_start">Start time</string>
<string name="mi2_prefs_do_not_disturb_end">End time</string>
<string name="FetchActivityOperation_about_to_transfer_since">About to transfer data since %1$s</string>
<string name="waiting_for_reconnect">waiting for reconnect</string>
@ -420,6 +436,9 @@
<string name="mi2_enable_text_notifications">Text notifications</string>
<string name="mi2_enable_text_notifications_summary"><![CDATA[Needs firmware >= 1.0.1.28 and Mili_pro.ft* installed.]]></string>
<string name="off">off</string>
<string name="mi2_dnd_off">Off</string>
<string name="mi2_dnd_automatic">Automatic (sleep detection)</string>
<string name="mi2_dnd_scheduled">Scheduled (time interval)</string>
<string name="discovery_attempting_to_pair">Attempting to pair with %1$s</string>
<string name="discovery_bonding_failed_immediately">Bonding with %1$s failed immediately.</string>
<string name="discovery_trying_to_connect_to">Trying to connect to: %1$s</string>

View File

@ -12,6 +12,17 @@
<item name="p_dateformat_time" type="string">dateformat_time</item>
<item name="p_dateformat_datetime" type="string">dateformat_datetime</item>
<item name="p_clock" type="string">clock</item>
<item name="p_steps" type="string">steps</item>
<item name="p_distance" type="string">distance</item>
<item name="p_calories" type="string">calories</item>
<item name="p_heart_rate" type="string">heart_rate</item>
<item name="p_battery" type="string">battery</item>
<item name="p_off" type="string">off</item>
<item name="p_automatic" type="string">automatic</item>
<item name="p_scheduled" type="string">scheduled</item>
<item name="p_unit_metric" type="string">metric</item>
<item name="p_unit_imperial" type="string">imperial</item>

View File

@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog>
<release version="0.19.3" versioncode="96">
<change>Pebble: Fix crash when calendar access permission has been denied</change>
<change>Pebble: Fix wrong timestamps with Morpheuz running on Firmware >=3</change>
<change>Mi Band 2: Improve reliability when fetching activity data</change>
<change>HPlus: Fix intensity calculation without continuous connectivity</change>
<change>HPlus: Fix Unicode handling</change>
<change>HPlus: Initial not work detection</change>
<change>Fix memory leak</change>
<change>Only show Realtime Chart on devices supporting it</change>
</release>
<release version="0.19.2" versioncode="95">
<change>Pebble: Fix recurring calendar events only appearing once per week</change>
<change>HPlus: Fix crash when receiving calls without phone number</change>

View File

@ -24,6 +24,12 @@
android:maxLength="5"
android:title="@string/miband_prefs_fitness_goal" />
<CheckBoxPreference
android:defaultValue="false"
android:key="mi2_goal_notification"
android:summary="@string/mi2_prefs_goal_notification_summary"
android:title="@string/mi2_prefs_goal_notification" />
<EditTextPreference
android:defaultValue="0"
android:inputType="number"
@ -43,11 +49,26 @@
android:maxLength="2"
android:title="@string/miband_prefs_device_time_offset_hours" />
<MultiSelectListPreference
android:dialogTitle="@string/mi2_prefs_display_items"
android:defaultValue="@array/pref_mi2_display_items_default"
android:entries="@array/pref_mi2_display_items"
android:entryValues="@array/pref_mi2_display_items_values"
android:key="mi2_display_items"
android:summary="@string/mi2_prefs_display_items_summary"
android:title="@string/mi2_prefs_display_items"/>
<CheckBoxPreference
android:defaultValue="true"
android:key="mi2_activate_display_on_lift_wrist"
android:title="@string/mi2_prefs_activate_display_on_lift" />
<CheckBoxPreference
android:defaultValue="false"
android:dependency="mi2_activate_display_on_lift_wrist"
android:key="mi2_rotate_wrist_to_switch_info"
android:title="@string/mi2_prefs_rotate_wrist_to_switch_info" />
<CheckBoxPreference
android:defaultValue="true"
android:key="mi2_enable_text_notifications"
@ -65,6 +86,37 @@
</PreferenceCategory>
<PreferenceScreen
android:key="mi2_do_not_disturb_key"
android:title="@string/mi2_prefs_do_not_disturb"
android:persistent="false"
android:summary="@string/mi2_prefs_do_not_disturb_summary">
<!-- workaround for missing toolbar -->
<PreferenceCategory
android:title="@string/mi2_prefs_do_not_disturb"
/>
<ListPreference
android:defaultValue="@string/p_off"
android:entries="@array/mi2_do_not_disturb"
android:entryValues="@array/mi2_do_not_disturb_values"
android:key="mi2_do_not_disturb"
android:title="@string/mi2_prefs_do_not_disturb"
android:summary="%s" />
<nodomain.freeyourgadget.gadgetbridge.util.TimePreference
android:defaultValue="01:00"
android:key="mi2_do_not_disturb_start"
android:title="@string/mi2_prefs_do_not_disturb_start" />
<nodomain.freeyourgadget.gadgetbridge.util.TimePreference
android:defaultValue="06:00"
android:key="mi2_do_not_disturb_end"
android:title="@string/mi2_prefs_do_not_disturb_end" />
</PreferenceScreen>
<PreferenceCategory
android:key="pref_category_miband_notification"
android:title="@string/pref_header_vibration_settings">

View File

@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
classpath 'com.android.tools.build:gradle:2.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip