Compare commits

..

63 Commits

Author SHA1 Message Date
Arjan Schrijver
0258905b4a Moyoung: Implement syncing sleep data 2025-01-08 23:06:13 +01:00
Arjan Schrijver
938085b5fa Moyoung: Add support for world clocks 2025-01-08 22:41:54 +01:00
Arjan Schrijver
ff865fbc99 Moyoung: Change bonding style to lazy to allow phone calls through watch 2025-01-08 22:41:54 +01:00
Arjan Schrijver
2b4a2b702c Moyoung: Add inactivity reminder preference 2025-01-08 22:41:54 +01:00
Arjan Schrijver
d9e0e22aaa Moyoung: Add power saving mode preference 2025-01-08 22:41:54 +01:00
Arjan Schrijver
529daded7a Moyoung: Send volume level with music info 2025-01-08 22:41:54 +01:00
Arjan Schrijver
deb7da7a5a Moyoung: Add option for letting the device follow the phone DND setting 2025-01-08 22:41:54 +01:00
Arjan Schrijver
3b347b29fd Moyoung: Support sending, receiving and deleting alarms 2025-01-08 22:41:54 +01:00
Arjan Schrijver
69578bdf88 Moyoung: Fix DND and Lift Wrist settings 2025-01-08 22:41:54 +01:00
Arjan Schrijver
e8b11f2ae9 Moyoung: Support the Misirun C17 2025-01-08 22:41:54 +01:00
Arjan Schrijver
42a9ee4fdc Moyoung: Send weather location and sunrise/sunset 2025-01-08 22:41:54 +01:00
Arjan Schrijver
2368a96a07 Moyoung: Implement sending calendar items 2025-01-08 22:41:54 +01:00
Arjan Schrijver
5b2fd8100d Moyoung: Implement sending music info and state 2025-01-08 22:41:54 +01:00
Arjan Schrijver
6b9f447725 Moyoung: Fix malformed notifications when sender/title contains colon 2025-01-08 22:41:54 +01:00
Arjan Schrijver
6a555bcba3 Moyoung: Implement and improve several device settings 2025-01-08 22:41:54 +01:00
Arjan Schrijver
59cb0ee70f Moyoung: Add music and volume control 2025-01-08 22:41:54 +01:00
Arjan Schrijver
b3d09f3209 Moyoung: Implement HR measurement interval setting 2025-01-08 22:41:54 +01:00
Arjan Schrijver
0bb1db06df Moyoung: Implement language setting 2025-01-08 22:41:54 +01:00
Arjan Schrijver
fa070579be Moyoung: Fix HR history packet parsing and activity sample provider 2025-01-08 22:41:54 +01:00
Arjan Schrijver
c0878cd214 Moyoung: Send cached weather info on request 2025-01-08 22:41:54 +01:00
Arjan Schrijver
809840be19 Moyoung: Code and settings improvements 2025-01-08 22:41:54 +01:00
Arjan Schrijver
52687e12dd Moyoung: Improve notifications 2025-01-08 22:41:54 +01:00
Arjan Schrijver
a1b260a145 Moyoung: Support syncing historical HR measurements 2025-01-08 22:41:54 +01:00
Arjan Schrijver
8fdd770292 Moyoung: Fix weather forecast being one day off 2025-01-08 22:41:54 +01:00
Arjan Schrijver
fb708d8693 Moyoung: Add find my phone functionality 2025-01-08 22:41:54 +01:00
Arjan Schrijver
7fcfa26a8e Moyoung: Make fixed MTU device-specific 2025-01-08 22:41:54 +01:00
Arjan Schrijver
b8d036c6bc Moyoung: Fixes for settings, sync, logging, weather, live activity 2025-01-08 22:41:54 +01:00
Arjan Schrijver
33664d1e83 Moyoung: Improve logging 2025-01-08 22:41:54 +01:00
Arjan Schrijver
736bf11852 Moyoung: Persist received data in new tables 2025-01-08 22:41:54 +01:00
Arjan Schrijver
970f34a776 Moyoung: Split up (modernize) database tables 2025-01-08 22:41:54 +01:00
Arjan Schrijver
945cf07cc9 Moyoung: Modernize classes and methods and fix compiler errors 2025-01-08 22:41:54 +01:00
Arjan Schrijver
678d1514de Rename DaFit references to Moyoung to reflect the protocol used 2025-01-08 22:41:54 +01:00
krzys-h
d4ca64dbe3 Da Fit: Setting alarms 2025-01-08 22:41:54 +01:00
krzys-h
81afa942d4 Da Fit: Training data transfer 2025-01-08 22:41:54 +01:00
krzys-h
656c912e97 Da Fit: Add device settings 2025-01-08 22:41:54 +01:00
krzys-h
8c49f54374 Da Fit: Add weather sync 2025-01-08 22:41:54 +01:00
krzys-h
f3698976c9 Da Fit: Add activity fetching and logging 2025-01-08 22:41:54 +01:00
krzys-h
95cdcd80bf Da Fit: Add handling of heart rate, blood pressure and oxidation measurements 2025-01-08 22:41:54 +01:00
krzys-h
ee088c734e Da Fit: Add notification support 2025-01-08 22:41:53 +01:00
krzys-h
ae6983e2ad Da Fit: Add time sync 2025-01-08 22:41:53 +01:00
krzys-h
ca7d9e19af Da Fit: Add device support, reverse engineering notes and base protocol implementation 2025-01-08 22:41:53 +01:00
José Rebelo
c0883de546 Garmin Forerunner 45: Initial support 2025-01-06 18:05:53 +00:00
Me7c7
1a21f01071 Huawei: UTF-16 encoding support for reply. 2025-01-06 14:06:57 +02:00
Me7c7
b1cccae3ac Huawei: fix SMS reply from the watch 2025-01-06 13:21:21 +02:00
Arjan Schrijver
790e81a6f6 Add disabling battery optimizations to permissions screen 2025-01-06 09:18:18 +01:00
Me7c7
4ccf68af0a Huawei: Fix notifications handling without key 2025-01-05 14:41:27 +02:00
Simon Brand
87871a46e7 Use correct logger in some classes 2025-01-05 09:04:53 +00:00
Sebastian Dröge
a7d5fad2b7 Ignore dev.octoshrimpy.quik as another SMS application
See https://github.com/octoshrimpy/quik
2025-01-05 10:47:01 +02:00
José Rebelo
e134a3bfbb Remove stray fragment_weeksteps_chart landscape layout 2025-01-04 22:09:05 +00:00
José Rebelo
e81597eb3d Add option to hide sleep and steps balance (#2476) 2025-01-04 13:53:21 +00:00
José Rebelo
ef5f4d9fd0 Restore steps balance on weekly and monthly charts 2025-01-04 13:46:33 +00:00
José Rebelo
d432800ae4 Sony Headphones: Fix unit tests 2025-01-04 13:25:05 +00:00
José Rebelo
894f913a89 Update changelog 2025-01-04 13:13:48 +00:00
Me7c7
dd7d63fa07 Huawei: Set phone number to notification. 2025-01-04 13:07:14 +00:00
Me7c7
21f8b88746 Ignore org.fossify.messages. It is SMS 2025-01-04 13:07:14 +00:00
Me7c7
1450219351 Huawei: Allow to set and edit canned replies 2025-01-04 13:07:14 +00:00
Me7c7
8da2b68eed Huawei: reply only if quick actions allow it 2025-01-04 13:07:14 +00:00
Me7c7
b7641f6e45 Huawei: permission check request processing 2025-01-04 13:07:14 +00:00
Me7c7
86e32f0713 Huawei: fixes related to replies 2025-01-04 13:07:14 +00:00
Me7c7
0429c2f3c8 Add cahannel and category to notifcation 2025-01-04 13:07:14 +00:00
dependency-bot
61831b8a9d Update dependency org.json:json to v20241224 2025-01-04 13:03:37 +00:00
dependency-bot
92a76cfa7b Update dependency org.mockito:mockito-core to v5.15.2 2025-01-04 01:10:02 +00:00
~noodlez1232
fbd4cb810a AsteroidOS: Add volume control
This pull request adds volume control to the AsteroidOS implementation.
It's not quite what I wanted from it, but it's probably good enough.
Hopefully it's good enough for other people too
2025-01-03 12:19:17 -08:00
50 changed files with 881 additions and 355 deletions

View File

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

View File

@ -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");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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));
}
}
}
}

View File

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

View File

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

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -75,4 +75,9 @@ public class ColmiI28UltraCoordinator extends AbstractMoyoungDeviceCoordinator {
public int getWorldClocksLabelLength() {
return 30;
}
@Override
public boolean supportsRemSleep() {
return true;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

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

View File

@ -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);
}
}
}

View File

@ -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(),

View File

@ -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);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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