mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-25 08:05:55 +01:00
Compare commits
63 Commits
a7c4a733a4
...
0258905b4a
Author | SHA1 | Date | |
---|---|---|---|
|
0258905b4a | ||
|
938085b5fa | ||
|
ff865fbc99 | ||
|
2b4a2b702c | ||
|
d9e0e22aaa | ||
|
529daded7a | ||
|
deb7da7a5a | ||
|
3b347b29fd | ||
|
69578bdf88 | ||
|
e8b11f2ae9 | ||
|
42a9ee4fdc | ||
|
2368a96a07 | ||
|
5b2fd8100d | ||
|
6b9f447725 | ||
|
6a555bcba3 | ||
|
59cb0ee70f | ||
|
b3d09f3209 | ||
|
0bb1db06df | ||
|
fa070579be | ||
|
c0878cd214 | ||
|
809840be19 | ||
|
52687e12dd | ||
|
a1b260a145 | ||
|
8fdd770292 | ||
|
fb708d8693 | ||
|
7fcfa26a8e | ||
|
b8d036c6bc | ||
|
33664d1e83 | ||
|
736bf11852 | ||
|
970f34a776 | ||
|
945cf07cc9 | ||
|
678d1514de | ||
|
d4ca64dbe3 | ||
|
81afa942d4 | ||
|
656c912e97 | ||
|
8c49f54374 | ||
|
f3698976c9 | ||
|
95cdcd80bf | ||
|
ee088c734e | ||
|
ae6983e2ad | ||
|
ca7d9e19af | ||
|
c0883de546 | ||
|
1a21f01071 | ||
|
b1cccae3ac | ||
|
790e81a6f6 | ||
|
4ccf68af0a | ||
|
87871a46e7 | ||
|
a7d5fad2b7 | ||
|
e134a3bfbb | ||
|
e81597eb3d | ||
|
ef5f4d9fd0 | ||
|
d432800ae4 | ||
|
894f913a89 | ||
|
dd7d63fa07 | ||
|
21f8b88746 | ||
|
1450219351 | ||
|
8da2b68eed | ||
|
b7641f6e45 | ||
|
86e32f0713 | ||
|
0429c2f3c8 | ||
|
61831b8a9d | ||
|
92a76cfa7b | ||
|
fbd4cb810a |
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,8 +1,23 @@
|
||||
### Changelog
|
||||
|
||||
#### NEXT
|
||||
* Marstek B2500: Fix setting pass-though mode
|
||||
* Initial support for Redmi Buds 6 Active
|
||||
* Initial support for Sony WF-C510
|
||||
* AsteroidOS: Add volume control
|
||||
* Colmi R09: Add preference to toggle temperature measurements
|
||||
* Colmi R09: Fix temperature data parsing
|
||||
* Colmi R0x: Add support for realtime heart rate meassurements and live activity tracking
|
||||
* Huawei: Add support to set and use canned replies
|
||||
* Huawei: Fix calendar event updates
|
||||
* Huawei: match midnight on the user's timezone for all day events
|
||||
* Huawei: Remove notifications from watch
|
||||
* Marstek B2500: Display sensor temperature in Status Activity
|
||||
* Marstek B2500: Fix setting pass-though mode
|
||||
* Sony Headphones: Allow overriding supported features
|
||||
* Sony Headphones: Fix initialization for some devices
|
||||
* Sony Headphones: Update default low battery threshold
|
||||
* Recognize Fossify SMS as SMS
|
||||
* Limit live activity to just the current device
|
||||
|
||||
#### 0.83.1
|
||||
* Initial support for Garmin Fenix 6X Pro Solar
|
||||
|
@ -56,7 +56,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
final Schema schema = new Schema(93, MAIN_PACKAGE + ".entities");
|
||||
final Schema schema = new Schema(94, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -157,6 +157,7 @@ public class GBDaoGenerator {
|
||||
addMoyoungHeartRateSample(schema, user, device);
|
||||
addMoyoungSpo2Sample(schema, user, device);
|
||||
addMoyoungBloodPressureSample(schema, user, device);
|
||||
addMoyoungSleepStageSample(schema, user, device);
|
||||
|
||||
addHuaweiActivitySample(schema, user, device);
|
||||
|
||||
@ -1103,6 +1104,13 @@ public class GBDaoGenerator {
|
||||
return bpSample;
|
||||
}
|
||||
|
||||
private static Entity addMoyoungSleepStageSample(Schema schema, Entity user, Entity device) {
|
||||
Entity sleepStageSample = addEntity(schema, "MoyoungSleepStageSample");
|
||||
addCommonTimeSampleProperties("AbstractTimeSample", sleepStageSample, user, device);
|
||||
sleepStageSample.addIntProperty("stage").notNull();
|
||||
return sleepStageSample;
|
||||
}
|
||||
|
||||
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
|
||||
activitySample.setSuperclass(superClass);
|
||||
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
|
||||
|
@ -207,7 +207,7 @@ dependencies {
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.1'
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "org.mockito:mockito-core:5.14.2"
|
||||
testImplementation "org.mockito:mockito-core:5.15.2"
|
||||
testImplementation "org.robolectric:robolectric:4.14.1"
|
||||
testImplementation "org.hamcrest:hamcrest-library:3.0"
|
||||
|
||||
@ -254,7 +254,7 @@ dependencies {
|
||||
//implementation 'org.bouncycastle:bcprov-jdk18on:1.76'
|
||||
|
||||
// Android SDK bundles org.json, but we need an actual implementation to replace the stubs in tests
|
||||
testImplementation 'org.json:json:20240303'
|
||||
testImplementation 'org.json:json:20241224'
|
||||
|
||||
// Fix Duplicate class build error for conflicting kotlin-stdlib versions
|
||||
// does not seem to be currently needed, as it uses the latest across all transitive
|
||||
|
@ -38,6 +38,13 @@
|
||||
<!-- Take wake locks (e.g. for time sync) -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!--
|
||||
Necessary for asking the user to disable battery optimizations.
|
||||
GB falls under the acceptable use cases documented here:
|
||||
https://developer.android.com/training/monitoring-device-state/doze-standby.html#exemption-cases
|
||||
-->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- Read loyalty cards from Catima -->
|
||||
<uses-permission android:name="me.hackerchick.catima.READ_CARDS"/>
|
||||
<uses-permission android:name="me.hackerchick.catima.debug.READ_CARDS"/>
|
||||
|
@ -19,12 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.github.mikephil.charting.charts.BarChart;
|
||||
import com.github.mikephil.charting.components.LimitLine;
|
||||
@ -68,9 +62,8 @@ public abstract class AbstractWeekChartFragment extends AbstractActivityChartFra
|
||||
protected int mTargetValue = 0;
|
||||
|
||||
protected BarChart mWeekChart;
|
||||
protected TextView mBalanceView;
|
||||
|
||||
private int mOffsetHours = getOffsetHours();
|
||||
private final int mOffsetHours = getOffsetHours();
|
||||
|
||||
protected String getWeeksChartsLabel(Calendar day){
|
||||
if (TOTAL_DAYS > 7) {
|
||||
@ -210,35 +203,6 @@ public abstract class AbstractWeekChartFragment extends AbstractActivityChartFra
|
||||
return lineDataSet;
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
mLocale = getResources().getConfiguration().locale;
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_weeksteps_chart, container, false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
|
||||
getChartsHost().enableSwipeRefresh(scrollY == 0);
|
||||
});
|
||||
}
|
||||
|
||||
final int goal = getGoal();
|
||||
if (goal >= 0) {
|
||||
mTargetValue = goal;
|
||||
}
|
||||
|
||||
mWeekChart = rootView.findViewById(R.id.weekstepschart);
|
||||
mBalanceView = rootView.findViewById(R.id.balance);
|
||||
|
||||
setupWeekChart();
|
||||
|
||||
// refresh immediately instead of use refreshIfVisible(), for perceived performance
|
||||
refresh();
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
protected void setupWeekChart() {
|
||||
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
|
@ -31,7 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.AbstractRespiratoryRateSamp
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class RespiratoryRateDailyFragment extends RespiratoryRateFragment<RespiratoryRateFragment.RespiratoryRateDay> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(RespiratoryRateDailyFragment.class);
|
||||
|
||||
private TextView mDateView;
|
||||
private TextView sleepAvg;
|
||||
|
@ -35,7 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class RespiratoryRatePeriodFragment extends RespiratoryRateFragment<RespiratoryRatePeriodFragment.RespiratoryRateData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(RespiratoryRatePeriodFragment.class);
|
||||
|
||||
private TextView mDateView;
|
||||
private TextView sleepAvg;
|
||||
|
@ -45,7 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
|
||||
public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StepsDailyFragment.class);
|
||||
|
||||
private TextView mDateView;
|
||||
private ImageView stepsGauge;
|
||||
|
@ -21,7 +21,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
|
||||
abstract class StepsFragment<T extends ChartsData> extends AbstractChartFragment<T> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StepsDailyFragment.class);
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StepsFragment.class);
|
||||
|
||||
protected int CHART_TEXT_COLOR;
|
||||
protected int TEXT_COLOR;
|
||||
|
@ -1,5 +1,6 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@ -37,7 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
|
||||
public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.StepsData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StepsPeriodFragment.class);
|
||||
|
||||
private TextView mDateView;
|
||||
private TextView stepsAvg;
|
||||
@ -46,14 +47,17 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
private TextView distanceTotal;
|
||||
private BarChart stepsChart;
|
||||
|
||||
private TextView mBalanceView;
|
||||
|
||||
protected int CHART_TEXT_COLOR;
|
||||
protected int TEXT_COLOR;
|
||||
protected int STEPS_GOAL;
|
||||
protected boolean SHOW_BALANCE;
|
||||
|
||||
protected int BACKGROUND_COLOR;
|
||||
protected int DESCRIPTION_COLOR;
|
||||
|
||||
public static StepsPeriodFragment newInstance ( int totalDays ) {
|
||||
public static StepsPeriodFragment newInstance(int totalDays) {
|
||||
StepsPeriodFragment fragmentFirst = new StepsPeriodFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("totalDays", totalDays);
|
||||
@ -84,6 +88,16 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
stepsTotal = rootView.findViewById(R.id.steps_total);
|
||||
distanceTotal = rootView.findViewById(R.id.distance_total);
|
||||
STEPS_GOAL = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, ActivityUser.defaultUserStepsGoal);
|
||||
|
||||
mBalanceView = rootView.findViewById(R.id.balance);
|
||||
|
||||
SHOW_BALANCE = GBApplication.getPrefs().getBoolean("charts_show_balance_steps", true);
|
||||
if (SHOW_BALANCE) {
|
||||
mBalanceView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mBalanceView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setupStepsChart();
|
||||
refresh();
|
||||
|
||||
@ -126,7 +140,7 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
yAxisRight.setDrawAxisLine(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.steps);
|
||||
}
|
||||
@ -151,7 +165,7 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
@Override
|
||||
protected void updateChartsnUIThread(StepsData stepsData) {
|
||||
Date to = new Date((long) getTSEnd() * 1000);
|
||||
Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1));
|
||||
Date from = DateUtils.addDays(to, -(TOTAL_DAYS - 1));
|
||||
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
|
||||
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
|
||||
mDateView.setText(fromFormattedDate + " - " + toFormattedDate);
|
||||
@ -160,7 +174,7 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
int counter = 0;
|
||||
for(StepsDay day : stepsData.days) {
|
||||
for (StepsDay day : stepsData.days) {
|
||||
entries.add(new BarEntry(counter, day.steps));
|
||||
counter++;
|
||||
}
|
||||
@ -183,6 +197,8 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
distanceAvg.setText(valueFormatter.formatValue(stepsData.distanceDailyAvg, "km"));
|
||||
stepsTotal.setText(String.format(String.valueOf(stepsData.totalSteps)));
|
||||
distanceTotal.setText(valueFormatter.formatValue(stepsData.totalDistance, "km"));
|
||||
|
||||
mBalanceView.setText(stepsData.getBalanceMessage(getContext(), STEPS_GOAL));
|
||||
}
|
||||
|
||||
ValueFormatter getStepsChartDayValueFormatter(StepsPeriodFragment.StepsData stepsData) {
|
||||
@ -202,7 +218,8 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
stepsChart.invalidate();
|
||||
}
|
||||
|
||||
protected void setupLegend(Chart<?> chart) {}
|
||||
protected void setupLegend(Chart<?> chart) {
|
||||
}
|
||||
|
||||
protected static class StepsData extends ChartsData {
|
||||
List<StepsDay> days;
|
||||
@ -211,10 +228,11 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
long totalSteps = 0;
|
||||
double totalDistance = 0;
|
||||
StepsDay todayStepsDay;
|
||||
|
||||
protected StepsData(List<StepsDay> days) {
|
||||
this.days = days;
|
||||
int daysCounter = 0;
|
||||
for(StepsDay day : days) {
|
||||
for (StepsDay day : days) {
|
||||
this.totalSteps += day.steps;
|
||||
this.totalDistance += day.distance;
|
||||
if (day.steps > 0) {
|
||||
@ -227,5 +245,19 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
}
|
||||
this.todayStepsDay = days.get(days.size() - 1);
|
||||
}
|
||||
|
||||
protected String getBalanceMessage(final Context context, final int targetValue) {
|
||||
if (totalSteps == 0) {
|
||||
return context.getString(R.string.no_data);
|
||||
}
|
||||
|
||||
final long totalBalance = totalSteps - ((long) targetValue * days.size());
|
||||
if (totalBalance > 0) {
|
||||
return context.getString(R.string.overstep, Math.abs(totalBalance));
|
||||
} else {
|
||||
return context.getString(R.string.lack_of_step, Math.abs(totalBalance));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class StressChartFragment extends AbstractChartFragment<StressChartFragment.StressChartsData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class);
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StressChartFragment.class);
|
||||
|
||||
private LineChart mStressChart;
|
||||
private PieChart mStressLevelsPieChart;
|
||||
|
@ -67,6 +67,10 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
|
||||
private LinearLayout sleepScoreWrapper;
|
||||
private LineChart sleepScoreChart;
|
||||
|
||||
private TextView mBalanceView;
|
||||
|
||||
protected boolean SHOW_BALANCE;
|
||||
|
||||
public static WeekSleepChartFragment newInstance ( int totalDays ) {
|
||||
WeekSleepChartFragment fragmentFirst = new WeekSleepChartFragment();
|
||||
Bundle args = new Bundle();
|
||||
@ -143,6 +147,13 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
|
||||
|
||||
mBalanceView = rootView.findViewById(R.id.balance);
|
||||
|
||||
SHOW_BALANCE = GBApplication.getPrefs().getBoolean("charts_show_balance_sleep", true);
|
||||
if (SHOW_BALANCE) {
|
||||
mBalanceView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mBalanceView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (!supportsSleepScore()) {
|
||||
sleepScoreWrapper.setVisibility(View.GONE);
|
||||
} else {
|
||||
|
@ -16,6 +16,9 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.asteroidos;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
|
||||
/**
|
||||
@ -29,12 +32,18 @@ public class AsteroidOSMediaCommand {
|
||||
public static final byte COMMAND_VOLUME = 0x4;
|
||||
|
||||
public byte command;
|
||||
public AsteroidOSMediaCommand(byte value) {
|
||||
command = value;
|
||||
public byte[] raw_values;
|
||||
public Context context;
|
||||
|
||||
public AsteroidOSMediaCommand(byte[] values, Context device_context) {
|
||||
command = values[0];
|
||||
raw_values = values;
|
||||
context = device_context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the MediaCommand to a music control event
|
||||
*
|
||||
* @return the matching music control event
|
||||
*/
|
||||
public GBDeviceEventMusicControl toMusicControlEvent() {
|
||||
@ -53,9 +62,21 @@ public class AsteroidOSMediaCommand {
|
||||
event.event = GBDeviceEventMusicControl.Event.PAUSE;
|
||||
break;
|
||||
case COMMAND_VOLUME:
|
||||
setVolume(raw_values[1]);
|
||||
event = null;
|
||||
break;
|
||||
default:
|
||||
event.event = GBDeviceEventMusicControl.Event.UNKNOWN;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
private void setVolume(byte volume) {
|
||||
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
|
||||
final int volumeMax = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
|
||||
final int finalVol = (int) Math.round((volume * volumeMax) / 100f);
|
||||
if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) != finalVol)
|
||||
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int) Math.round((volume * volumeMax) / 100f), AudioManager.FLAG_SHOW_UI);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminForerunner45Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 45$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_45;
|
||||
}
|
||||
}
|
@ -40,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
@ -153,6 +154,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
|
||||
return huaweiCoordinator.getContactsSlotCount(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCannedRepliesSlotCount(GBDevice device) {
|
||||
return huaweiCoordinator.getCannedRepliesSlotCount(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return huaweiCoordinator.supportsCalendarEvents();
|
||||
|
@ -55,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
@ -282,11 +283,18 @@ public class HuaweiCoordinator {
|
||||
// Notifications
|
||||
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.NOTIFICATIONS);
|
||||
notifications.add(R.xml.devicesettings_notifications_enable);
|
||||
if (supportsNotificationsRepeatedNotify() || supportsNotificationsRemoveSingle()){
|
||||
notifications.add(R.xml.devicesettings_autoremove_notifications);
|
||||
}
|
||||
if (getCannedRepliesSlotCount(device) > 0) {
|
||||
notifications.add(R.xml.devicesettings_canned_reply_16);
|
||||
}
|
||||
if (supportsNotificationOnBluetoothLoss())
|
||||
notifications.add(R.xml.devicesettings_disconnectnotification_noshed);
|
||||
if (supportsDoNotDisturb(device))
|
||||
notifications.add(R.xml.devicesettings_donotdisturb_allday_liftwirst_notwear);
|
||||
|
||||
|
||||
// Workout
|
||||
if (supportsSendingGps())
|
||||
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.WORKOUT, R.xml.devicesettings_workout_send_gps_to_band);
|
||||
@ -660,7 +668,7 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsNotificationsReply() {
|
||||
public boolean supportsNotificationsReplyActions() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(73);
|
||||
return false;
|
||||
@ -672,18 +680,23 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsNotificationsSyncKey() {
|
||||
public boolean supportsNotificationsReply() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(89);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsNotificationsRemoveSingle() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(120);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean supportsCannedReplies() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(82);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsPromptPushMessage () {
|
||||
// do not ask for capabilities under specific condition
|
||||
@ -754,6 +767,11 @@ public class HuaweiCoordinator {
|
||||
return supportsContacts()?maxContactsCount:0;
|
||||
}
|
||||
|
||||
public int getCannedRepliesSlotCount(GBDevice device) {
|
||||
// TODO: find proper count
|
||||
return supportsCannedReplies()?10:0;
|
||||
}
|
||||
|
||||
public void setTransactionCrypted(boolean crypted) {
|
||||
this.transactionCrypted = crypted;
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
@ -162,6 +163,11 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
|
||||
return huaweiCoordinator.getContactsSlotCount(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCannedRepliesSlotCount(GBDevice device) {
|
||||
return huaweiCoordinator.getCannedRepliesSlotCount(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return huaweiCoordinator.supportsCalendarEvents();
|
||||
|
@ -463,6 +463,8 @@ public class HuaweiPacket {
|
||||
return new DeviceConfig.ActivityType.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.SettingRelated.id:
|
||||
return new DeviceConfig.SettingRelated.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.PermissionCheck.id:
|
||||
return new DeviceConfig.PermissionCheck.PermissionCheckRequest(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.SecurityNegotiation.id:
|
||||
return new DeviceConfig.SecurityNegotiation.Response(paramsProvider).fromPacket(this);
|
||||
case DeviceConfig.WearStatus.id:
|
||||
|
@ -18,18 +18,11 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@ -1688,6 +1681,48 @@ public class DeviceConfig {
|
||||
|
||||
}
|
||||
|
||||
public static class PermissionCheck {
|
||||
public static final byte id = 0x38;
|
||||
// NOTE: request from the watch
|
||||
public static class PermissionCheckRequest extends HuaweiPacket {
|
||||
public short permission = 0;
|
||||
|
||||
public PermissionCheckRequest(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (this.tlv.contains(0x01))
|
||||
this.permission = this.tlv.getShort(0x01);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PermissionCheckResponse extends HuaweiPacket {
|
||||
|
||||
public PermissionCheckResponse(
|
||||
ParamsProvider paramsProvider,
|
||||
short permission,
|
||||
short status
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, permission)
|
||||
.put(0x02, status);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class WearStatus {
|
||||
public static final int id = 0x3D;
|
||||
|
||||
|
@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
@ -37,19 +39,19 @@ public class Notifications {
|
||||
|
||||
public static class AdditionalParams {
|
||||
|
||||
public boolean supportsSyncKey = false;
|
||||
public boolean supportsReply = false;
|
||||
public boolean supportsRepeatedNotify = false;
|
||||
public boolean supportsRemoveSingle = false;
|
||||
public boolean supportsReply = false;
|
||||
public boolean supportsReplyActions = false;
|
||||
public boolean supportsTimestamp = false;
|
||||
|
||||
public String replyKey = "";
|
||||
public String notificationKey = "";
|
||||
public int notificationId = -1;
|
||||
public String channelId = "";
|
||||
public byte subscriptionId = 0;
|
||||
public String address = "";
|
||||
|
||||
|
||||
public String category = "";
|
||||
}
|
||||
|
||||
// TODO: support other types of notifications
|
||||
@ -126,29 +128,25 @@ public class Notifications {
|
||||
this.tlv.put(0x11, sourceAppId);
|
||||
|
||||
if(addParams != null) {
|
||||
if (addParams.supportsSyncKey)
|
||||
this.tlv.put(0x18, (addParams.notificationKey != null) ? addParams.notificationKey : "");
|
||||
|
||||
//this.tlv.put(0x12, "msg"); //"msg" or "imcall", maybe other - category, if not empty and productType>=34
|
||||
|
||||
//if(addParams.repeatedNotifySupports) {
|
||||
// this.tlv.put(0x13, 0); // 0x13 - reminder 15 = vibrate, 0 - default
|
||||
//}
|
||||
|
||||
if (addParams.supportsReply && notificationType == NotificationType.sms) {
|
||||
if(!TextUtils.isEmpty(addParams.category)) { // type >= 34
|
||||
this.tlv.put(0x12, addParams.category); // "imcall" also possible value, not standard for android
|
||||
}
|
||||
if (addParams.supportsReply) {
|
||||
this.tlv.put(0x18, (addParams.replyKey != null) ? addParams.replyKey : "");
|
||||
}
|
||||
if (addParams.supportsReplyActions && notificationType == NotificationType.sms) {
|
||||
this.tlv.put(0x14, addParams.subscriptionId);
|
||||
this.tlv.put(0x17, addParams.address);
|
||||
}
|
||||
|
||||
if (addParams.supportsRepeatedNotify || addParams.supportsRemoveSingle) {
|
||||
this.tlv.put(0x19, (addParams.notificationKey != null) ? addParams.notificationKey : "");
|
||||
this.tlv.put(0x20, addParams.notificationId);
|
||||
this.tlv.put(0x1d, (addParams.channelId != null) ? addParams.channelId : "");
|
||||
}
|
||||
|
||||
if (addParams.supportsTimestamp) {
|
||||
this.tlv.put(0x15, (int) (System.currentTimeMillis() / 1000));
|
||||
}
|
||||
if (addParams.supportsRepeatedNotify || addParams.supportsRemoveSingle) {
|
||||
this.tlv.put(0x19, (addParams.notificationKey != null) ? addParams.notificationKey : "");
|
||||
this.tlv.put(0x1a, addParams.notificationId);
|
||||
this.tlv.put(0x1b, (addParams.channelId != null) ? addParams.channelId : "");
|
||||
}
|
||||
}
|
||||
|
||||
this.complete = true;
|
||||
@ -347,8 +345,8 @@ public class Notifications {
|
||||
.put(0x03, notificationKey)
|
||||
.put(0x04, notificationId)
|
||||
.put(0x05, notificationChannelId);
|
||||
if (notificationCategory != null && !TextUtils.isEmpty(notificationCategory))
|
||||
this.tlv.put(0x06, notificationCategory); // category
|
||||
if (!TextUtils.isEmpty(notificationCategory))
|
||||
this.tlv.put(0x06, notificationCategory);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
@ -380,14 +378,13 @@ public class Notifications {
|
||||
public int type = 0;
|
||||
public int encoding = 0; // 3 - "utf-16"
|
||||
public int subId = 0;
|
||||
public String sender;
|
||||
public String key;
|
||||
public String addData;
|
||||
public String text;
|
||||
|
||||
public ReplyResponse(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = MusicControl.id;
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@ -402,13 +399,17 @@ public class Notifications {
|
||||
if (this.tlv.contains(0x04))
|
||||
this.key = this.tlv.getString(0x04);
|
||||
if (this.tlv.contains(0x05))
|
||||
this.sender = this.tlv.getString(0x05);
|
||||
if (this.tlv.contains(0x06))
|
||||
this.text = this.tlv.getString(0x06);
|
||||
this.addData = this.tlv.getString(0x05);
|
||||
if (this.tlv.contains(0x06)) {
|
||||
if(this.encoding == 3) {
|
||||
this.text = new String(this.tlv.getBytes(0x06), StandardCharsets.UTF_16);
|
||||
} else {
|
||||
this.text = this.tlv.getString(0x06);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: send ack if required, 7f on error.
|
||||
public static class ReplyAck extends HuaweiPacket {
|
||||
|
||||
public ReplyAck(
|
||||
@ -426,9 +427,5 @@ public class Notifications {
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -75,4 +75,9 @@ public class ColmiI28UltraCoordinator extends AbstractMoyoungDeviceCoordinator {
|
||||
public int getWorldClocksLabelLength() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRemSleep() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -173,6 +173,7 @@ public class MoyoungConstants {
|
||||
public static final byte SLEEP_SOBER = 0;
|
||||
public static final byte SLEEP_LIGHT = 1;
|
||||
public static final byte SLEEP_RESTFUL = 2;
|
||||
public static final byte SLEEP_REM = 3;
|
||||
|
||||
public static final byte CMD_QUERY_SLEEP_ACTION = 58; // (*) {i} -> {hour, x[60]}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
@ -41,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
@ -74,6 +76,7 @@ public class MoyoungActivitySampleProvider extends AbstractSampleProvider<Moyoun
|
||||
public static final int ACTIVITY_SLEEP_RESTFUL = 17;
|
||||
public static final int ACTIVITY_SLEEP_START = 18;
|
||||
public static final int ACTIVITY_SLEEP_END = 19;
|
||||
public static final int ACTIVITY_SLEEP_REM = 20;
|
||||
|
||||
public MoyoungActivitySampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
@ -115,6 +118,8 @@ public class MoyoungActivitySampleProvider extends AbstractSampleProvider<Moyoun
|
||||
return ActivityKind.LIGHT_SLEEP;
|
||||
else if (rawType == ACTIVITY_SLEEP_RESTFUL)
|
||||
return ActivityKind.DEEP_SLEEP;
|
||||
else if (rawType == ACTIVITY_SLEEP_REM)
|
||||
return ActivityKind.REM_SLEEP;
|
||||
else if (rawType == ACTIVITY_SLEEP_START || rawType == ACTIVITY_SLEEP_END)
|
||||
return ActivityKind.NOT_MEASURED;
|
||||
else if (rawType == ACTIVITY_TRAINING_WALK)
|
||||
@ -142,12 +147,29 @@ public class MoyoungActivitySampleProvider extends AbstractSampleProvider<Moyoun
|
||||
return ACTIVITY_SLEEP_LIGHT;
|
||||
else if (activityKind == ActivityKind.DEEP_SLEEP)
|
||||
return ACTIVITY_SLEEP_RESTFUL;
|
||||
else if (activityKind == ActivityKind.REM_SLEEP)
|
||||
return ACTIVITY_SLEEP_REM;
|
||||
else if (activityKind == ActivityKind.ACTIVITY)
|
||||
return ACTIVITY_NOT_MEASURED; // TODO: ?
|
||||
else
|
||||
throw new IllegalArgumentException("Invalid Gadgetbridge activity kind: " + activityKind);
|
||||
}
|
||||
|
||||
final ActivityKind sleepStageToActivityKind(final int sleepStage) {
|
||||
switch (sleepStage) {
|
||||
case MoyoungConstants.SLEEP_LIGHT:
|
||||
return ActivityKind.LIGHT_SLEEP;
|
||||
case MoyoungConstants.SLEEP_RESTFUL:
|
||||
return ActivityKind.DEEP_SLEEP;
|
||||
case MoyoungConstants.SLEEP_REM:
|
||||
return ActivityKind.REM_SLEEP;
|
||||
case MoyoungConstants.SLEEP_SOBER:
|
||||
return ActivityKind.AWAKE_SLEEP;
|
||||
default:
|
||||
return ActivityKind.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
if (rawIntensity == ActivitySample.NOT_MEASURED)
|
||||
@ -177,23 +199,10 @@ public class MoyoungActivitySampleProvider extends AbstractSampleProvider<Moyoun
|
||||
}
|
||||
|
||||
overlayHeartRate(sampleByTs, timestamp_from, timestamp_to);
|
||||
// overlaySleep(sampleByTs, timestamp_from, timestamp_to);
|
||||
|
||||
// Add empty dummy samples every 5 min to make sure the charts and stats aren't too malformed
|
||||
// This is necessary due to the Colmi rings just reporting steps/calories/distance aggregates per hour
|
||||
// for (int i=timestamp_from; i<=timestamp_to; i+=300) {
|
||||
// MoyoungActivitySample sample = sampleByTs.get(i);
|
||||
// if (sample == null) {
|
||||
// sample = new MoyoungActivitySample();
|
||||
// sample.setTimestamp(i);
|
||||
// sample.setProvider(this);
|
||||
// sample.setRawKind(ActivitySample.NOT_MEASURED);
|
||||
// sampleByTs.put(i, sample);
|
||||
// }
|
||||
// }
|
||||
overlaySleep(sampleByTs, timestamp_from, timestamp_to);
|
||||
|
||||
final List<MoyoungActivitySample> finalSamples = new ArrayList<>(sampleByTs.values());
|
||||
Collections.sort(finalSamples, (a, b) -> Integer.compare(a.getTimestamp(), b.getTimestamp()));
|
||||
Collections.sort(finalSamples, Comparator.comparingInt(MoyoungActivitySample::getTimestamp));
|
||||
|
||||
final long nanoEnd = System.nanoTime();
|
||||
final long executionTime = (nanoEnd - nanoStart) / 1000000;
|
||||
@ -221,6 +230,64 @@ public class MoyoungActivitySampleProvider extends AbstractSampleProvider<Moyoun
|
||||
}
|
||||
}
|
||||
|
||||
private void overlaySleep(final Map<Integer, MoyoungActivitySample> sampleByTs, final int timestamp_from, final int timestamp_to) {
|
||||
final MoyoungSleepStageSampleProvider sleepStageSampleProvider = new MoyoungSleepStageSampleProvider(getDevice(), getSession());
|
||||
final List<MoyoungSleepStageSample> sleepStageSamples = sleepStageSampleProvider.getAllSamples(timestamp_from * 1000L, timestamp_to * 1000L);
|
||||
|
||||
// Retrieve the last stage before this time range, as the user could have been asleep during
|
||||
// the range transition
|
||||
final MoyoungSleepStageSample lastSleepStageBeforeRange = sleepStageSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
|
||||
if (lastSleepStageBeforeRange != null && lastSleepStageBeforeRange.getStage() != MoyoungConstants.SLEEP_SOBER) {
|
||||
LOG.debug("Last sleep stage before range: ts={}, stage={}", lastSleepStageBeforeRange.getTimestamp(), lastSleepStageBeforeRange.getStage());
|
||||
sleepStageSamples.add(0, lastSleepStageBeforeRange);
|
||||
}
|
||||
// Retrieve the next sample after the time range, as the last stage could exceed it
|
||||
final MoyoungSleepStageSample nextSleepStageAfterRange = sleepStageSampleProvider.getNextSampleAfter(timestamp_to * 1000L);
|
||||
if (nextSleepStageAfterRange != null) {
|
||||
LOG.debug("Next sleep stage after range: ts={}, stage={}", nextSleepStageAfterRange.getTimestamp(), nextSleepStageAfterRange.getStage());
|
||||
sleepStageSamples.add(nextSleepStageAfterRange);
|
||||
}
|
||||
|
||||
if (sleepStageSamples.size() > 1) {
|
||||
LOG.debug("Overlaying with data from {} sleep stage samples", sleepStageSamples.size());
|
||||
} else {
|
||||
LOG.warn("Not overlaying sleep data because more than 1 sleep stage sample is required");
|
||||
return;
|
||||
}
|
||||
|
||||
MoyoungSleepStageSample prevSample = null;
|
||||
for (final MoyoungSleepStageSample sleepStageSample : sleepStageSamples) {
|
||||
if (prevSample == null) {
|
||||
prevSample = sleepStageSample;
|
||||
continue;
|
||||
}
|
||||
final ActivityKind sleepRawKind = sleepStageToActivityKind(prevSample.getStage());
|
||||
if (sleepRawKind.equals(ActivityKind.AWAKE_SLEEP)) {
|
||||
prevSample = sleepStageSample;
|
||||
continue;
|
||||
}
|
||||
// round to the nearest minute, we don't need per-second granularity
|
||||
final int tsSecondsPrev = (int) ((prevSample.getTimestamp() / 1000) / 60) * 60;
|
||||
final int tsSecondsCur = (int) ((sleepStageSample.getTimestamp() / 1000) / 60) * 60;
|
||||
for (int i = tsSecondsPrev; i < tsSecondsCur; i += 60) {
|
||||
if (i < timestamp_from || i > timestamp_to) continue;
|
||||
MoyoungActivitySample sample = sampleByTs.get(i);
|
||||
if (sample == null) {
|
||||
sample = new MoyoungActivitySample();
|
||||
sample.setTimestamp(i);
|
||||
sample.setProvider(this);
|
||||
sampleByTs.put(i, sample);
|
||||
}
|
||||
sample.setRawKind(toRawActivityKind(sleepRawKind));
|
||||
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
||||
}
|
||||
prevSample = sleepStageSample;
|
||||
}
|
||||
if (prevSample != null && !sleepStageToActivityKind(prevSample.getStage()).equals(ActivityKind.AWAKE_SLEEP)) {
|
||||
LOG.warn("Last sleep stage sample was not of type awake");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the activity kind from NOT_MEASURED to new_raw_activity_kind on the given range
|
||||
* @param timestamp_from the start timestamp
|
||||
|
@ -0,0 +1,56 @@
|
||||
/* Copyright (C) 2025 Arjan Schrijver
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class MoyoungSleepStageSampleProvider extends AbstractTimeSampleProvider<MoyoungSleepStageSample> {
|
||||
public MoyoungSleepStageSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<MoyoungSleepStageSample, ?> getSampleDao() {
|
||||
return getSession().getMoyoungSleepStageSampleDao();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return MoyoungSleepStageSampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return MoyoungSleepStageSampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MoyoungSleepStageSample createSample() {
|
||||
return new MoyoungSleepStageSample();
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@ import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaSession;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.PowerManager;
|
||||
@ -54,7 +55,6 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -383,6 +383,12 @@ public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
notificationSpec.type = AppNotificationType.getInstance().get(source);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationSpec.channelId = notification.getChannelId();
|
||||
}
|
||||
|
||||
notificationSpec.category = notification.category;
|
||||
|
||||
//FIXME: some quirks lookup table would be the minor evil here
|
||||
if (source.startsWith("com.fsck.k9")) {
|
||||
if (NotificationCompat.isGroupSummary(notification)) {
|
||||
@ -953,7 +959,9 @@ public class NotificationListener extends NotificationListenerService {
|
||||
source.equals("com.android.mms") ||
|
||||
source.equals("com.sonyericsson.conversations") ||
|
||||
source.equals("com.android.messaging") ||
|
||||
source.equals("org.smssecure.smssecure")) {
|
||||
source.equals("org.smssecure.smssecure") ||
|
||||
source.equals("org.fossify.messages") ||
|
||||
source.equals("dev.octoshrimpy.quik")) {
|
||||
if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) {
|
||||
LOG.info("Ignoring notification, it's an sms notification");
|
||||
return true;
|
||||
|
@ -180,7 +180,9 @@ public class GBDeviceService implements DeviceService {
|
||||
.putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId)
|
||||
.putExtra(EXTRA_NOTIFICATION_ICONID, notificationSpec.iconId)
|
||||
.putExtra(NOTIFICATION_PICTURE_PATH, notificationSpec.picturePath)
|
||||
.putExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, notificationSpec.dndSuppressed);
|
||||
.putExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, notificationSpec.dndSuppressed)
|
||||
.putExtra(EXTRA_NOTIFICATION_CHANNEL_ID, notificationSpec.channelId)
|
||||
.putExtra(EXTRA_NOTIFICATION_CATEGORY, notificationSpec.category);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,8 @@ public interface DeviceService extends EventHandler {
|
||||
String EXTRA_NOTIFICATION_ICONID = "notification_iconid";
|
||||
String NOTIFICATION_PICTURE_PATH = "notification_picture_path";
|
||||
String EXTRA_NOTIFICATION_DNDSUPPRESSED = "notification_dndsuppressed";
|
||||
String EXTRA_NOTIFICATION_CHANNEL_ID = "notification_channel_id";
|
||||
String EXTRA_NOTIFICATION_CATEGORY = "notification_category";
|
||||
String EXTRA_FIND_START = "find_start";
|
||||
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
|
||||
String EXTRA_CALL_COMMAND = "call_command";
|
||||
|
@ -83,6 +83,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.Ga
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner255SMusicCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner45Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner55Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner620Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner955Coordinator;
|
||||
@ -459,6 +460,7 @@ public enum DeviceType {
|
||||
GARMIN_FENIX_7X(GarminFenix7XCoordinator.class),
|
||||
GARMIN_FENIX_7_PRO(GarminFenix7ProCoordinator.class),
|
||||
GARMIN_FENIX_8(GarminFenix8Coordinator.class),
|
||||
GARMIN_FORERUNNER_45(GarminForerunner45Coordinator.class),
|
||||
GARMIN_FORERUNNER_55(GarminForerunner55Coordinator.class),
|
||||
GARMIN_FORERUNNER_165(GarminForerunner165Coordinator.class),
|
||||
GARMIN_FORERUNNER_235(GarminForerunner235Coordinator.class),
|
||||
|
@ -34,6 +34,8 @@ public class NotificationSpec {
|
||||
public String body;
|
||||
public NotificationType type;
|
||||
public String sourceName;
|
||||
public String channelId;
|
||||
public String category;
|
||||
public String[] cannedReplies;
|
||||
/**
|
||||
* Wearable actions that were attached to the incoming notifications and will be passed to the gadget (includes the "reply" action)
|
||||
|
@ -864,6 +864,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
notificationSpec.iconId = intentCopy.getIntExtra(EXTRA_NOTIFICATION_ICONID, 0);
|
||||
notificationSpec.picturePath = intent.getStringExtra(NOTIFICATION_PICTURE_PATH);
|
||||
notificationSpec.dndSuppressed = intentCopy.getIntExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, 0);
|
||||
notificationSpec.channelId = intentCopy.getStringExtra(EXTRA_NOTIFICATION_CHANNEL_ID);
|
||||
notificationSpec.category = intentCopy.getStringExtra(EXTRA_NOTIFICATION_CATEGORY);
|
||||
|
||||
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
|
||||
GBApplication.getIDSenderLookup().add(notificationSpec.getId(), notificationSpec.phoneNumber);
|
||||
|
@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.asteroidos;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@ -274,9 +275,10 @@ public class AsteroidOSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
*/
|
||||
public void handleMediaCommand (BluetoothGattCharacteristic characteristic) {
|
||||
LOG.info("handle media command");
|
||||
AsteroidOSMediaCommand command = new AsteroidOSMediaCommand(characteristic.getValue()[0]);
|
||||
AsteroidOSMediaCommand command = new AsteroidOSMediaCommand(characteristic.getValue(), getContext());
|
||||
GBDeviceEventMusicControl event = command.toMusicControlEvent();
|
||||
evaluateGBDeviceEvent(event);
|
||||
if (event != null)
|
||||
evaluateGBDeviceEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -63,14 +63,15 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetBatteryLevelRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadComplete;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsStatusRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadAck;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadChunk;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadHash;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendPermissionCheckResponse;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceConfirm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWatchfaceOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicStatusRequest;
|
||||
@ -129,6 +130,7 @@ public class AsynchronousResponse {
|
||||
handleEphemerisUploadService(response);
|
||||
handleAsyncBattery(response);
|
||||
handleNotifications(response);
|
||||
handlePermissionCheck(response);
|
||||
} catch (Request.ResponseParseException e) {
|
||||
LOG.error("Response parse exception", e);
|
||||
}
|
||||
@ -737,4 +739,29 @@ public class AsynchronousResponse {
|
||||
support.getHuaweiNotificationsManager().onReplyResponse((Notifications.NotificationReply.ReplyResponse) response);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePermissionCheck(HuaweiPacket response) {
|
||||
if (response.serviceId == DeviceConfig.id && response.commandId == DeviceConfig.PermissionCheck.id) {
|
||||
if (!(response instanceof DeviceConfig.PermissionCheck.PermissionCheckRequest)) {
|
||||
return;
|
||||
}
|
||||
DeviceConfig.PermissionCheck.PermissionCheckRequest permissionCheckResp = (DeviceConfig.PermissionCheck.PermissionCheckRequest) response;
|
||||
|
||||
// short status = 1;
|
||||
// // TODO: we should check ability to perform specific action. I do not know which action can be here,
|
||||
// // 1 is SMS permission
|
||||
// if(permissionCheckResp.permission == 1) {
|
||||
// status = 0;
|
||||
// }
|
||||
// TODO: return no permission for now. Return status 1 for activate call reject replies. Something should be set on notification to enable processing.
|
||||
// Currently watch does not send call reject to the GB. Additional research required.
|
||||
short status = 0;
|
||||
SendPermissionCheckResponse getPhoneInfoReq = new SendPermissionCheckResponse(this.support, permissionCheckResp.permission, status);
|
||||
try {
|
||||
getPhoneInfoReq.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to send permission check ACK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
@ -215,4 +216,9 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
|
||||
supportProvider.onSetCannedMessages(cannedMessagesSpec);
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
@ -224,4 +225,9 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
|
||||
supportProvider.onSetCannedMessages(cannedMessagesSpec);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRemoveRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSMSReplyAck;
|
||||
|
||||
public class HuaweiNotificationsManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiNotificationsManager.class);
|
||||
@ -42,6 +43,12 @@ public class HuaweiNotificationsManager {
|
||||
notificationSpecCache.offer(notificationSpec);
|
||||
}
|
||||
|
||||
public static String getNotificationKey(NotificationSpec notificationSpec) {
|
||||
if(!TextUtils.isEmpty(notificationSpec.key)) {
|
||||
return notificationSpec.key;
|
||||
}
|
||||
return "0|" + notificationSpec.sourceAppId + "|" + notificationSpec.getId() + "||0";
|
||||
}
|
||||
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
|
||||
@ -79,10 +86,10 @@ public class HuaweiNotificationsManager {
|
||||
SendNotificationRemoveRequest sendNotificationReq = new SendNotificationRemoveRequest(this.support,
|
||||
SendNotificationRequest.getNotificationType(notificationSpec.type), // notificationType
|
||||
notificationSpec.sourceAppId,
|
||||
notificationSpec.key,
|
||||
getNotificationKey(notificationSpec),
|
||||
id,
|
||||
"", // TODO:
|
||||
null);
|
||||
notificationSpec.channelId,
|
||||
notificationSpec.category);
|
||||
sendNotificationReq.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Sending notification remove failed", e);
|
||||
@ -91,7 +98,7 @@ public class HuaweiNotificationsManager {
|
||||
|
||||
void onReplyResponse(Notifications.NotificationReply.ReplyResponse response) {
|
||||
LOG.info(" KEY: {}, Text: {}", response.key, response.text);
|
||||
if(!this.support.getHuaweiCoordinator().supportsNotificationsReply()) {
|
||||
if(!this.support.getHuaweiCoordinator().supportsNotificationsReplyActions()) {
|
||||
LOG.info("Reply is not supported");
|
||||
return;
|
||||
}
|
||||
@ -99,16 +106,25 @@ public class HuaweiNotificationsManager {
|
||||
LOG.info("Reply is empty");
|
||||
return;
|
||||
}
|
||||
if(response.type != 1 && response.type != 2) {
|
||||
LOG.info("Reply: only type 1 and 2 supported");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationSpec notificationSpec = null;
|
||||
for (NotificationSpec spec : notificationSpecCache) {
|
||||
notificationSpec = spec;
|
||||
if (notificationSpec.key.equals(response.key)) {
|
||||
break;
|
||||
if(response.type == 1) { // generic SMS notification reply. Find by phone number
|
||||
for (NotificationSpec spec : notificationSpecCache) {
|
||||
if (spec.phoneNumber.equals(response.key)) {
|
||||
notificationSpec = spec;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(response.type == 2) {
|
||||
for (NotificationSpec spec : notificationSpecCache) {
|
||||
if (getNotificationKey(spec).equals(response.key)) {
|
||||
notificationSpec = spec;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG.info("Reply type {} is not supported", response.type);
|
||||
return;
|
||||
}
|
||||
if (notificationSpec == null) {
|
||||
LOG.info("Notification for reply is not found");
|
||||
@ -131,10 +147,23 @@ public class HuaweiNotificationsManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
this.support.evaluateGBDeviceEvent(deviceEvtNotificationControl);
|
||||
//TODO: maybe should be send reply. Service: 0x2, command: 0x10, tlv 7 and/or 1, type byte, 7f on error
|
||||
if(response.type == 1) {
|
||||
// NOTE: send response only for SMS reply
|
||||
try {
|
||||
// 0xff - OK
|
||||
// 0x7f - error
|
||||
// TODO: get response from SMSManager. Send pending intent result.
|
||||
// result can be one of the RESULT_ERROR_* from SmsManager. Not sure, need to check.
|
||||
// currently always send OK.
|
||||
byte resultCode = (byte)0xff;
|
||||
SendSMSReplyAck sendNotificationReq = new SendSMSReplyAck(this.support, resultCode);
|
||||
sendNotificationReq.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Sending sns reply ACK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -98,6 +98,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
@ -108,6 +109,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCalendarService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCannedRepliesService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PTrackService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PDataDictionarySyncService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
|
||||
@ -865,6 +867,12 @@ public class HuaweiSupportProvider {
|
||||
trackService.register();
|
||||
}
|
||||
}
|
||||
if (getHuaweiCoordinator().supportsCannedReplies()) {
|
||||
if (HuaweiP2PCannedRepliesService.getRegisteredInstance(huaweiP2PManager) == null) {
|
||||
HuaweiP2PCannedRepliesService cannedRepliesService = new HuaweiP2PCannedRepliesService(huaweiP2PManager);
|
||||
cannedRepliesService.register();
|
||||
}
|
||||
}
|
||||
if (HuaweiP2PDataDictionarySyncService.getRegisteredInstance(huaweiP2PManager) == null) {
|
||||
HuaweiP2PDataDictionarySyncService trackService = new HuaweiP2PDataDictionarySyncService(huaweiP2PManager);
|
||||
trackService.register();
|
||||
@ -2514,4 +2522,23 @@ public class HuaweiSupportProvider {
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
getHuaweiMusicManager().onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
|
||||
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
|
||||
if (cannedMessagesSpec.type != CannedMessagesSpec.TYPE_GENERIC) {
|
||||
LOG.warn("Got unsupported canned messages type: {}", cannedMessagesSpec.type);
|
||||
return;
|
||||
}
|
||||
|
||||
if(cannedMessagesSpec.cannedMessages.length == 0) {
|
||||
GB.toast(context, HuaweiSupportProvider.this.getContext().getString(R.string.canned_replies_not_empty), Toast.LENGTH_SHORT, GB.WARN);
|
||||
LOG.warn(HuaweiSupportProvider.this.getContext().getString(R.string.canned_replies_not_empty));
|
||||
}
|
||||
|
||||
HuaweiP2PCannedRepliesService cannedRepliesService = HuaweiP2PCannedRepliesService.getRegisteredInstance(huaweiP2PManager);
|
||||
if(cannedRepliesService == null) {
|
||||
LOG.warn("P2P canned replies service is not registered");
|
||||
return;
|
||||
}
|
||||
cannedRepliesService.sendReplies(cannedMessagesSpec.cannedMessages);
|
||||
}
|
||||
}
|
||||
|
@ -91,14 +91,14 @@ public abstract class HuaweiBaseP2PService {
|
||||
}
|
||||
|
||||
public void handlePacket(P2P.P2PCommand.Response packet) {
|
||||
LOG.info("HuaweiP2PCalendarService handlePacket: {} Code: {}", packet.cmdId, packet.respCode);
|
||||
LOG.info("HuaweiBaseP2PService handlePacket: {} Code: {}", packet.cmdId, packet.respCode);
|
||||
if (waitPackets.containsKey(packet.sequenceId)) {
|
||||
LOG.info("HuaweiP2PCalendarService handlePacket find handler");
|
||||
LOG.info("HuaweiBaseP2PService handlePacket find handler");
|
||||
HuaweiP2PCallback handle = waitPackets.remove(packet.sequenceId);
|
||||
if(handle != null) {
|
||||
handle.onResponse(packet.respCode, packet.respData);
|
||||
} else {
|
||||
LOG.error("HuaweiP2PCalendarService handler is null");
|
||||
LOG.error("HuaweiBaseP2PService handler is null");
|
||||
}
|
||||
} else {
|
||||
|
||||
|
@ -0,0 +1,138 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
|
||||
|
||||
public class HuaweiP2PCannedRepliesService extends HuaweiBaseP2PService {
|
||||
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PCannedRepliesService.class);
|
||||
private final AtomicBoolean isRegistered = new AtomicBoolean(false);
|
||||
|
||||
public static final String MODULE = "hw.unitedevice.smsquick";
|
||||
|
||||
|
||||
public static final int CMD_CANNED_REPLY_QUERY = 7001;
|
||||
public static final int CMD_CANNED_REPLY_UPDATE = 7002;
|
||||
public static final int CMD_CANNED_REPLY_CONNECT = 7003;
|
||||
|
||||
public HuaweiP2PCannedRepliesService(HuaweiP2PManager manager) {
|
||||
super(manager);
|
||||
LOG.info("HuaweiP2PCannedRepliesService");
|
||||
}
|
||||
|
||||
public static HuaweiP2PCannedRepliesService getRegisteredInstance(HuaweiP2PManager manager) {
|
||||
return (HuaweiP2PCannedRepliesService) manager.getRegisteredService(HuaweiP2PCannedRepliesService.MODULE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModule() {
|
||||
return HuaweiP2PCannedRepliesService.MODULE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPackage() {
|
||||
return "com.huawei.watch.home";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFingerprint() {
|
||||
return "603AC6A57E2023E00C9C93BB539CA653DF3003EBA4E92EA1904BA4AAA5D938F0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registered() {
|
||||
isRegistered.set(true);
|
||||
// NOTE: sendConnect can clean saved canned messages. Additional research required
|
||||
//sendConnect();
|
||||
sendQuery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {
|
||||
isRegistered.set(false);
|
||||
|
||||
}
|
||||
|
||||
public void sendConnect() {
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
tlv.put(0x1, CMD_CANNED_REPLY_CONNECT);
|
||||
sendCommand(tlv.serialize(), null);
|
||||
}
|
||||
|
||||
public void sendQuery() {
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
tlv.put(0x1, CMD_CANNED_REPLY_QUERY);
|
||||
sendCommand(tlv.serialize(), null);
|
||||
}
|
||||
|
||||
public void sendReplies(String[] replies) {
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
for (String reply : replies) {
|
||||
tlv.put(0x83, new HuaweiTLV().put(0x04, reply));
|
||||
}
|
||||
HuaweiTLV res = new HuaweiTLV();
|
||||
res.put(0x1, CMD_CANNED_REPLY_UPDATE).put(0x82, tlv);
|
||||
|
||||
sendCommand(res.serialize(), null);
|
||||
}
|
||||
|
||||
private void parseDeviceReplies(HuaweiTLV tlv) {
|
||||
List<HuaweiTLV> replies = tlv.getObjects(0x83);
|
||||
if (replies.isEmpty())
|
||||
return;
|
||||
final GBDeviceEventUpdatePreferences gbDeviceEventUpdatePreferences = new GBDeviceEventUpdatePreferences();
|
||||
|
||||
for (int i = 1; i <= manager.getSupportProvider().getHuaweiCoordinator().getCannedRepliesSlotCount(manager.getSupportProvider().getDevice()); i++) {
|
||||
String message = null;
|
||||
if (replies.size() >= i) {
|
||||
if (replies.get(i - 1).contains(0x04)) {
|
||||
try {
|
||||
String reply = replies.get(i - 1).getString(0x04);
|
||||
if (!TextUtils.isEmpty(reply)) {
|
||||
message = reply;
|
||||
}
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
LOG.info("No tag");
|
||||
}
|
||||
}
|
||||
}
|
||||
gbDeviceEventUpdatePreferences.withPreference("canned_reply_" + i, message);
|
||||
}
|
||||
|
||||
manager.getSupportProvider().evaluateGBDeviceEvent(gbDeviceEventUpdatePreferences);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleData(byte[] data) {
|
||||
LOG.info("HuaweiP2PCannedRepliesService handleData");
|
||||
try {
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
tlv.parse(data);
|
||||
LOG.error(tlv.toString());
|
||||
if (tlv.contains(0x01)) {
|
||||
int code = tlv.getInteger(0x01);
|
||||
if (code == CMD_CANNED_REPLY_CONNECT) {
|
||||
// send default replies, replies cannot be empty
|
||||
String[] replies = {"OK", "Yes", "No"};
|
||||
sendReplies(replies);
|
||||
}
|
||||
if (code == CMD_CANNED_REPLY_QUERY) {
|
||||
if (tlv.contains(0x82)) {
|
||||
parseDeviceReplies(tlv.getObject(0x82));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiNotificationsManager.getNotificationKey;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -29,7 +31,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendNotificationRequest extends Request {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendNotificationRequest.class);
|
||||
|
||||
private HuaweiPacket packet;
|
||||
@ -52,8 +53,7 @@ public class SendNotificationRequest extends Request {
|
||||
return Notifications.NotificationType.sms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void buildNotificationTLVFromNotificationSpec(NotificationSpec notificationSpec) {
|
||||
String title;
|
||||
if (notificationSpec.title != null)
|
||||
@ -66,16 +66,34 @@ public class SendNotificationRequest extends Request {
|
||||
body = notificationSpec.body.substring(0x0, supportProvider.getHuaweiCoordinator().getContentLength() - 0xD);
|
||||
body += "...";
|
||||
}
|
||||
|
||||
String replyKey = "";
|
||||
final boolean hasActions = (null != notificationSpec.attachedActions && !notificationSpec.attachedActions.isEmpty());
|
||||
if (hasActions) {
|
||||
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
|
||||
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
|
||||
if (action.type == NotificationSpec.Action.TYPE_WEARABLE_REPLY || action.type == NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
//NOTE: store notification key instead action key. The watch returns this key so it is more easier to find action by notification key
|
||||
replyKey = getNotificationKey(notificationSpec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.NotificationActionRequest.AdditionalParams params = new Notifications.NotificationActionRequest.AdditionalParams();
|
||||
|
||||
params.supportsSyncKey = supportProvider.getHuaweiCoordinator().supportsNotificationsSyncKey();
|
||||
params.supportsReply = supportProvider.getHuaweiCoordinator().supportsNotificationsReply();
|
||||
params.supportsRepeatedNotify = supportProvider.getHuaweiCoordinator().supportsNotificationsRepeatedNotify();
|
||||
params.supportsRemoveSingle = supportProvider.getHuaweiCoordinator().supportsNotificationsRemoveSingle();
|
||||
params.supportsReply = supportProvider.getHuaweiCoordinator().supportsNotificationsReply();
|
||||
params.supportsReplyActions = supportProvider.getHuaweiCoordinator().supportsNotificationsReplyActions();
|
||||
params.supportsTimestamp = supportProvider.getHuaweiCoordinator().supportsNotificationsTimestamp();
|
||||
|
||||
params.notificationId = notificationSpec.getId();
|
||||
params.notificationKey = notificationSpec.key;
|
||||
params.notificationKey = getNotificationKey(notificationSpec);
|
||||
params.replyKey = replyKey;
|
||||
params.channelId = notificationSpec.channelId;
|
||||
params.category = notificationSpec.category;
|
||||
params.address = notificationSpec.phoneNumber;
|
||||
|
||||
|
||||
this.packet = new Notifications.NotificationActionRequest(
|
||||
@ -92,7 +110,6 @@ public class SendNotificationRequest extends Request {
|
||||
}
|
||||
|
||||
public void buildNotificationTLVFromCallSpec(CallSpec callSpec) {
|
||||
|
||||
this.packet = new Notifications.NotificationActionRequest(
|
||||
paramsProvider,
|
||||
supportProvider.getNotificationId(),
|
||||
|
@ -0,0 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendPermissionCheckResponse extends Request {
|
||||
short permission;
|
||||
short status;
|
||||
|
||||
|
||||
public SendPermissionCheckResponse(HuaweiSupportProvider support, short permission, short status) {
|
||||
super(support);
|
||||
this.serviceId = DeviceConfig.id;
|
||||
this.commandId = DeviceConfig.PermissionCheck.id;
|
||||
this.permission = permission;
|
||||
this.status = status;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new DeviceConfig.PermissionCheck.PermissionCheckResponse(this.paramsProvider, this.permission, this.status).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendSMSReplyAck extends Request {
|
||||
private final byte resultCode;
|
||||
|
||||
public SendSMSReplyAck(HuaweiSupportProvider support,
|
||||
byte resultCode) {
|
||||
super(support);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = Notifications.NotificationReply.id;
|
||||
this.resultCode = resultCode;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Notifications.NotificationReply.ReplyAck(this.paramsProvider, this.resultCode).serialize();
|
||||
} catch(HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.MoyoungWeatherToday;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungActivitySampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungBloodPressureSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungHeartRateSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungSleepStageSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.samples.MoyoungSpo2SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumDeviceVersion;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.moyoung.settings.MoyoungEnumLanguage;
|
||||
@ -87,6 +88,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungBloodPressureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungHeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSleepStageSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MoyoungSpo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@ -1186,126 +1188,49 @@ public class MoyoungDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
if (data.length % 3 != 0)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
int prevActivityType = MoyoungActivitySampleProvider.ACTIVITY_SLEEP_START;
|
||||
int prevSampleTimestamp = -1;
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
MoyoungSleepStageSampleProvider provider = new MoyoungSleepStageSampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
|
||||
for(int i = 0; i < data.length / 3; i++)
|
||||
{
|
||||
int type = data[3*i];
|
||||
int start_h = data[3*i + 1];
|
||||
int start_m = data[3*i + 2];
|
||||
User user = DBHelper.getUser(dbHandler.getDaoSession());
|
||||
Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession());
|
||||
|
||||
LOG.info("sleep[" + daysAgo + "][" + i + "] type=" + type + ", start_h=" + start_h + ", start_m=" + start_m);
|
||||
List<MoyoungSleepStageSample> samples = new ArrayList<>();
|
||||
|
||||
// SleepAnalysis measures sleep fragment type by marking the END of the fragment.
|
||||
// The watch provides data by marking the START of the fragment.
|
||||
for(int i = 0; i < data.length / 3; i++)
|
||||
{
|
||||
int type = data[3*i];
|
||||
int start_h = data[3*i + 1];
|
||||
int start_m = data[3*i + 2];
|
||||
|
||||
// Additionally, ActivityAnalysis (used by the weekly view...) does AVERAGING when
|
||||
// adjacent samples are not of the same type..
|
||||
|
||||
// FIXME: The way Gadgetbridge does it seems kinda broken...
|
||||
|
||||
// This means that we have to convert the data when importing. Each sample gets
|
||||
// converted to two samples - one marking the beginning of the segment, and another
|
||||
// marking the end.
|
||||
|
||||
// Watch: SLEEP_LIGHT ... SLEEP_DEEP ... SLEEP_LIGHT ... SLEEP_SOBER
|
||||
// Gadgetbridge: ANYTHING,SLEEP_LIGHT ... SLEEP_LIGHT,SLEEP_DEEP ... SLEEP_DEEP,SLEEP_LIGHT ... SLEEP_LIGHT,ANYTHING
|
||||
// ^ ^- this is important, it MUST be sleep, to ensure proper detection
|
||||
// Time since the last -| of sleepStart, see SleepAnalysis.calculateSleepSessions
|
||||
// sample must be 0
|
||||
// (otherwise SleepAnalysis will include this fragment...)
|
||||
|
||||
// This means that when inserting samples:
|
||||
// * every sample is converted to (previous_sample_type, current_sample_type) happening
|
||||
// roughly at the same time (but in this order)
|
||||
// * the first sample is prefixed by unspecified activity
|
||||
// * the last sample (SOBER) is converted to unspecified activity
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
User user = DBHelper.getUser(dbHandler.getDaoSession());
|
||||
Device device = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession());
|
||||
|
||||
MoyoungActivitySampleProvider provider = new MoyoungActivitySampleProvider(getDevice(), dbHandler.getDaoSession());
|
||||
LOG.info("sleep[" + daysAgo + "][" + i + "] type=" + type + ", start_h=" + start_h + ", start_m=" + start_m);
|
||||
|
||||
Calendar thisSample = Calendar.getInstance();
|
||||
thisSample.add(Calendar.HOUR_OF_DAY, 4); // the clock assumes the sleep day changes at 20:00, so move the time forward to make the day correct
|
||||
thisSample.set(Calendar.MINUTE, 0);
|
||||
thisSample.add(Calendar.DATE, -daysAgo);
|
||||
|
||||
thisSample.add(Calendar.DAY_OF_MONTH, -daysAgo);
|
||||
thisSample.set(Calendar.HOUR_OF_DAY, start_h);
|
||||
thisSample.set(Calendar.MINUTE, start_m);
|
||||
thisSample.set(Calendar.SECOND, 0);
|
||||
thisSample.set(Calendar.MILLISECOND, 0);
|
||||
int thisSampleTimestamp = (int) (thisSample.getTimeInMillis() / 1000);
|
||||
if (start_h >= 20) {
|
||||
// Evening sleep is considered to be a day earlier
|
||||
thisSample.add(Calendar.MINUTE, -1440);
|
||||
}
|
||||
|
||||
int activityType;
|
||||
if (type == MoyoungConstants.SLEEP_SOBER)
|
||||
activityType = MoyoungActivitySampleProvider.ACTIVITY_SLEEP_END;
|
||||
else if (type == MoyoungConstants.SLEEP_LIGHT)
|
||||
activityType = MoyoungActivitySampleProvider.ACTIVITY_SLEEP_LIGHT;
|
||||
else if (type == MoyoungConstants.SLEEP_RESTFUL)
|
||||
activityType = MoyoungActivitySampleProvider.ACTIVITY_SLEEP_RESTFUL;
|
||||
else
|
||||
throw new IllegalArgumentException("Invalid sleep type");
|
||||
MoyoungSleepStageSample currentSample = new MoyoungSleepStageSample();
|
||||
currentSample.setDevice(device);
|
||||
currentSample.setUser(user);
|
||||
currentSample.setStage(type);
|
||||
currentSample.setTimestamp(thisSample.getTimeInMillis());
|
||||
samples.add(currentSample);
|
||||
|
||||
// Insert the end of previous segment sample
|
||||
MoyoungActivitySample prevSegmentSample = new MoyoungActivitySample();
|
||||
prevSegmentSample.setDevice(device);
|
||||
prevSegmentSample.setUser(user);
|
||||
prevSegmentSample.setProvider(provider);
|
||||
prevSegmentSample.setTimestamp(thisSampleTimestamp - 1);
|
||||
|
||||
prevSegmentSample.setRawKind(prevActivityType);
|
||||
prevSegmentSample.setDataSource(MoyoungActivitySampleProvider.SOURCE_SLEEP_SUMMARY);
|
||||
|
||||
// prevSegmentSample.setBatteryLevel(ActivitySample.NOT_MEASURED);
|
||||
prevSegmentSample.setSteps(ActivitySample.NOT_MEASURED);
|
||||
prevSegmentSample.setDistanceMeters(ActivitySample.NOT_MEASURED);
|
||||
prevSegmentSample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
|
||||
|
||||
prevSegmentSample.setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
// prevSegmentSample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
|
||||
// prevSegmentSample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
|
||||
// prevSegmentSample.setBloodOxidation(ActivitySample.NOT_MEASURED);
|
||||
|
||||
// addGBActivitySampleIfNotExists(provider, prevSegmentSample);
|
||||
|
||||
// Insert the start of new segment sample
|
||||
MoyoungActivitySample nextSegmentSample = new MoyoungActivitySample();
|
||||
nextSegmentSample.setDevice(device);
|
||||
nextSegmentSample.setUser(user);
|
||||
nextSegmentSample.setProvider(provider);
|
||||
nextSegmentSample.setTimestamp(thisSampleTimestamp);
|
||||
|
||||
nextSegmentSample.setRawKind(activityType);
|
||||
nextSegmentSample.setDataSource(MoyoungActivitySampleProvider.SOURCE_SLEEP_SUMMARY);
|
||||
|
||||
// nextSegmentSample.setBatteryLevel(ActivitySample.NOT_MEASURED);
|
||||
nextSegmentSample.setSteps(ActivitySample.NOT_MEASURED);
|
||||
nextSegmentSample.setDistanceMeters(ActivitySample.NOT_MEASURED);
|
||||
nextSegmentSample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
|
||||
|
||||
nextSegmentSample.setHeartRate(ActivitySample.NOT_MEASURED);
|
||||
// nextSegmentSample.setBloodPressureSystolic(ActivitySample.NOT_MEASURED);
|
||||
// nextSegmentSample.setBloodPressureDiastolic(ActivitySample.NOT_MEASURED);
|
||||
// nextSegmentSample.setBloodOxidation(ActivitySample.NOT_MEASURED);
|
||||
|
||||
// addGBActivitySampleIfNotExists(provider, nextSegmentSample);
|
||||
|
||||
// Set the activity type on all samples in this time period
|
||||
if (prevActivityType != MoyoungActivitySampleProvider.ACTIVITY_SLEEP_START)
|
||||
// provider.updateActivityInRange(prevSampleTimestamp, thisSampleTimestamp, prevActivityType);
|
||||
|
||||
prevActivityType = activityType;
|
||||
if (prevActivityType == MoyoungActivitySampleProvider.ACTIVITY_SLEEP_END)
|
||||
prevActivityType = MoyoungActivitySampleProvider.ACTIVITY_SLEEP_START;
|
||||
prevSampleTimestamp = thisSampleTimestamp;
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error saving samples: ", ex);
|
||||
GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
|
||||
LOG.debug("Adding sleep stage sample: ts={} stage={}", thisSample.getTime(), type);
|
||||
}
|
||||
|
||||
LOG.debug("Will persist {} sleep stage samples", samples.size());
|
||||
provider.addSamples(samples);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error saving sleep stage samples: ", ex);
|
||||
GB.toast(getContext(), "Error saving sleep stage samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR);
|
||||
GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@ -28,6 +29,7 @@ import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -52,11 +54,15 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
public class PermissionsUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
|
||||
|
||||
public static final String CUSTOM_PERM_IGNORE_BATT_OPTIM = "custom_perm_ignore_battery_optimization";
|
||||
public static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
|
||||
public static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
|
||||
public static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
|
||||
|
||||
public static final List<String> specialPermissions = new ArrayList<String>() {{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
add(CUSTOM_PERM_IGNORE_BATT_OPTIM);
|
||||
}
|
||||
add(CUSTOM_PERM_NOTIFICATION_LISTENER);
|
||||
add(CUSTOM_PERM_NOTIFICATION_SERVICE);
|
||||
add(CUSTOM_PERM_DISPLAY_OVER);
|
||||
@ -68,6 +74,12 @@ public class PermissionsUtils {
|
||||
|
||||
public static ArrayList<PermissionDetails> getRequiredPermissionsList(Activity activity) {
|
||||
ArrayList<PermissionDetails> permissionsList = new ArrayList<>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_IGNORE_BATT_OPTIM,
|
||||
activity.getString(R.string.permission_disable_doze_title),
|
||||
activity.getString(R.string.permission_disable_doze_summary)));
|
||||
}
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_NOTIFICATION_LISTENER,
|
||||
activity.getString(R.string.menuitem_notifications),
|
||||
@ -189,6 +201,9 @@ public class PermissionsUtils {
|
||||
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted();
|
||||
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return Settings.canDrawOverlays(context);
|
||||
} else if (permission.equals(CUSTOM_PERM_IGNORE_BATT_OPTIM) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return pm.isIgnoringBatteryOptimizations(context.getApplicationContext().getPackageName());
|
||||
} else {
|
||||
return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED;
|
||||
}
|
||||
@ -205,7 +220,9 @@ public class PermissionsUtils {
|
||||
}
|
||||
|
||||
public static void requestPermission(Activity activity, String permission) {
|
||||
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
|
||||
if (permission.equals(CUSTOM_PERM_IGNORE_BATT_OPTIM)) {
|
||||
showRequestIgnoreBatteryOptimizationDialog(activity);
|
||||
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
|
||||
showNotifyListenerPermissionsDialog(activity);
|
||||
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
|
||||
showNotifyPolicyPermissionsDialog(activity);
|
||||
@ -242,6 +259,14 @@ public class PermissionsUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
private static void showRequestIgnoreBatteryOptimizationDialog(Activity activity) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + activity.getApplicationContext().getPackageName()));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void showNotifyListenerPermissionsDialog(Activity activity) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(activity.getString(R.string.permission_notification_listener,
|
||||
|
@ -1,58 +0,0 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="40"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.github.mikephil.charting.charts.PieChart
|
||||
android:id="@+id/todaystepschart"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="40" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.github.mikephil.charting.charts.BarChart
|
||||
android:id="@+id/weekstepschart"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="20" />
|
||||
|
||||
<!--<TextView-->
|
||||
<!--android:text="Test"-->
|
||||
<!--android:layout_width="fill_parent"-->
|
||||
<!--android:layout_height="fill_parent"-->
|
||||
<!--android:layout_weight="20" />-->
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/steps_streaks_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:visibility="visible"
|
||||
app:srcCompat="@drawable/ic_events" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -20,10 +20,21 @@
|
||||
android:gravity="center"
|
||||
android:textSize="20sp"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
/>
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:gravity="center">
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250sp"
|
||||
|
@ -1,44 +0,0 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.github.mikephil.charting.charts.PieChart
|
||||
android:id="@+id/todaystepschart"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="20" />
|
||||
|
||||
<com.github.mikephil.charting.charts.BarChart
|
||||
android:id="@+id/weekstepschart"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_weight="20" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:id="@+id/steps_streaks_button"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:visibility="visible"
|
||||
app:srcCompat="@drawable/ic_events" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -893,6 +893,10 @@
|
||||
<string name="activity_list_summary_activities">Activities</string>
|
||||
<string name="dialog_hide">Hide</string>
|
||||
<string name="show_ongoing_activity">Show ongoing activity popup</string>
|
||||
<string name="charts_show_balance_sleep_title">Show sleep balance</string>
|
||||
<string name="charts_show_balance_sleep_summary">Show excess / lack of sleep in weekly and monthly charts</string>
|
||||
<string name="charts_show_balance_steps_title">Show steps balance</string>
|
||||
<string name="charts_show_balance_steps_summary">Show excess / lack of steps in weekly and monthly charts</string>
|
||||
<string name="lack_of_sleep">Lack of sleep: %1$s</string>
|
||||
<string name="overslept">Overslept: %1$s</string>
|
||||
<string name="prefs_sounds">Sounds</string>
|
||||
@ -1775,6 +1779,7 @@
|
||||
<string name="devicetype_garmin_instinct_2_solar">Garmin Instinct 2 Solar</string>
|
||||
<string name="devicetype_garmin_instinct_2_soltac">Garmin Instinct 2 SolTac</string>
|
||||
<string name="devicetype_garmin_instinct_crossover">Garmin Instinct Crossover</string>
|
||||
<string name="devicetype_garmin_forerunner_45">Garmin Forerunner 45</string>
|
||||
<string name="devicetype_garmin_forerunner_55">Garmin Forerunner 55</string>
|
||||
<string name="devicetype_garmin_forerunner_165">Garmin Forerunner 165</string>
|
||||
<string name="devicetype_garmin_forerunner_235">Garmin Forerunner 235</string>
|
||||
@ -3586,4 +3591,7 @@
|
||||
<string name="battery_minimum_charge">Minimum battery charge in %</string>
|
||||
<string name="battery_allow_pass_though_summary">When enabled, the battery can be charged while discharging</string>
|
||||
<string name="battery_allow_pass_through">Allow battery pass-through</string>
|
||||
<string name="canned_replies_not_empty">There should be at least one canned reply.</string>
|
||||
<string name="permission_disable_doze_title">Ignore battery optimizations</string>
|
||||
<string name="permission_disable_doze_summary">Allows running in the background unhindered by Android\'s battery optimizations</string>
|
||||
</resources>
|
||||
|
@ -78,6 +78,22 @@
|
||||
android:title="@string/pref_title_charts_range"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="charts_show_balance_sleep"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/charts_show_balance_sleep_summary"
|
||||
android:title="@string/charts_show_balance_sleep_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="charts_show_balance_steps"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/charts_show_balance_steps_summary"
|
||||
android:title="@string/charts_show_balance_steps_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="charts_show_ongoing_activity"
|
||||
|
@ -22,6 +22,7 @@ import java.util.Set;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
@ -44,7 +45,13 @@ public class MockSonyCoordinator extends SonyHeadphonesCoordinator {
|
||||
capabilities.add(capability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SonyHeadphonesCapabilities> getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SonyHeadphonesCapabilities> getCapabilities(final GBDevice device) {
|
||||
return getCapabilities();
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphon
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequests;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.handleMessage;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
@ -35,6 +36,7 @@ import java.util.Map;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
|
||||
@ -48,16 +50,35 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakT
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SpeakToChatEnabled;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.MockSonyCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.BatteryType;
|
||||
|
||||
public class SonyProtocolImplV2Test {
|
||||
private final MockSonyCoordinator coordinator = new MockSonyCoordinator();
|
||||
private final SonyProtocolImplV2 protocol = new SonyProtocolImplV2(null) {
|
||||
@Override
|
||||
protected SonyHeadphonesCoordinator getCoordinator() {
|
||||
return new SonyWF1000XM4Coordinator();
|
||||
return coordinator;
|
||||
}
|
||||
};
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
coordinator.getCapabilities().clear();
|
||||
// Same as the WF-1000XM4
|
||||
coordinator.getCapabilities().addAll(Arrays.asList(
|
||||
SonyHeadphonesCapabilities.BatteryDual,
|
||||
SonyHeadphonesCapabilities.BatteryCase,
|
||||
SonyHeadphonesCapabilities.AmbientSoundControl,
|
||||
SonyHeadphonesCapabilities.WindNoiseReduction,
|
||||
SonyHeadphonesCapabilities.EqualizerSimple,
|
||||
SonyHeadphonesCapabilities.AudioUpsampling,
|
||||
SonyHeadphonesCapabilities.ButtonModesLeftRight,
|
||||
SonyHeadphonesCapabilities.PauseWhenTakenOff,
|
||||
SonyHeadphonesCapabilities.AutomaticPowerOffWhenTakenOff
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAmbientSoundControl() {
|
||||
// TODO
|
||||
|
Loading…
Reference in New Issue
Block a user