mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-26 00:21:45 +01:00
Merge branch 'master' into background-javascript
This commit is contained in:
commit
eb7e635cdc
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@ -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!*
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -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
|
||||
|
@ -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
|
||||
|
15
README.md
15
README.md
@ -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)
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
public enum DoNotDisturb {
|
||||
OFF,
|
||||
AUTOMATIC,
|
||||
SCHEDULED
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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(){
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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+";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)) {
|
||||
|
@ -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");
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
39
app/src/main/res/layout/fragment_statschart.xml
Normal file
39
app/src/main/res/layout/fragment_statschart.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user