mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Huawei: Basic stress support. Calibration and enabling
This commit is contained in:
@@ -209,6 +209,10 @@
|
||||
android:name=".devices.garmin.GarminRealtimeSettingsActivity"
|
||||
android:label="@string/loading"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".devices.huawei.ui.HuaweiStressCalibrationActivity"
|
||||
android:label="@string/huawei_stress_calibrate"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.ActivitySummariesActivity"
|
||||
android:label="@string/activity_summaries"
|
||||
|
||||
+5
@@ -99,6 +99,11 @@ public final class HuaweiConstants {
|
||||
public static final String PREF_HUAWEI_HEART_RATE_LOW_ALERT = "pref_huawei_heart_rate_low_alert";
|
||||
public static final String PREF_HUAWEI_HEART_RATE_HIGH_ALERT= "pref_huawei_heart_rate_high_alert";
|
||||
public static final String PREF_HUAWEI_SPO_LOW_ALERT= "pref_huawei_spo_low_alert";
|
||||
public static final String PREF_HUAWEI_STRESS_SWITCH= "pref_huawei_stress_switch";
|
||||
public static final String PREF_HUAWEI_STRESS_CALIBRATE= "pref_huawei_stress_calibrate";
|
||||
public static final String PREF_HUAWEI_STRESS_LAST_DATA= "huawei_stress_last_data";
|
||||
|
||||
|
||||
|
||||
public static final String PKG_NAME = "com.huawei.devicegroupmanage";
|
||||
}
|
||||
|
||||
+17
@@ -73,6 +73,7 @@ public class HuaweiCoordinator {
|
||||
ByteBuffer notificationConstraints = null;
|
||||
|
||||
private boolean supportsTruSleepNewSync = false;
|
||||
private boolean supportsRriNewSync = false;
|
||||
private boolean supportsGpsNewSync = false;
|
||||
|
||||
private boolean supportsWorkoutNewSteps = false;
|
||||
@@ -301,6 +302,10 @@ public class HuaweiCoordinator {
|
||||
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.HEALTH, R.xml.devicesettings_temperature_automatic_enable);
|
||||
}
|
||||
|
||||
if(supportsAutoStress()) {
|
||||
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.HEALTH, R.xml.devicesettings_huawei_stress);
|
||||
}
|
||||
|
||||
// Notifications
|
||||
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.NOTIFICATIONS);
|
||||
notifications.add(R.xml.devicesettings_notifications_enable);
|
||||
@@ -681,6 +686,10 @@ public class HuaweiCoordinator {
|
||||
return supportsCommandForService(0x01, 0x21);
|
||||
}
|
||||
|
||||
public boolean supportsAutoStress() {
|
||||
return supportsCommandForService(0x20, 0x09);
|
||||
}
|
||||
|
||||
public boolean supportsExternalCalendarService() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(184);
|
||||
@@ -1005,6 +1014,14 @@ public class HuaweiCoordinator {
|
||||
this.supportsTruSleepNewSync = supportsTruSleepNewSync;
|
||||
}
|
||||
|
||||
public boolean getSupportsRriNewSync() {
|
||||
return supportsRriNewSync;
|
||||
}
|
||||
|
||||
public void setSupportsRriNewSync(boolean supportsRriNewSync) {
|
||||
this.supportsRriNewSync = supportsRriNewSync;
|
||||
}
|
||||
|
||||
public boolean isSupportsGpsNewSync() {
|
||||
return supportsGpsNewSync;
|
||||
}
|
||||
|
||||
+9
@@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.EphemerisFile
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.HrRriTest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.OTA;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.P2P;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
|
||||
@@ -608,6 +609,14 @@ public class HuaweiPacket {
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case HrRriTest.id:
|
||||
switch (this.commandId) {
|
||||
case HrRriTest.RriData.id:
|
||||
return new HrRriTest.RriData.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case MusicControl.id:
|
||||
switch (this.commandId) {
|
||||
case MusicControl.MusicStatusResponse.id:
|
||||
|
||||
+21
@@ -16,10 +16,13 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Parcel;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
@@ -28,8 +31,11 @@ import java.util.Set;
|
||||
import java.util.Collections;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AppBlacklistActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRealtimeSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@@ -43,6 +49,8 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstant
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_LOW_ALERT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_REALTIME_MODE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_SPO_LOW_ALERT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_STRESS_CALIBRATE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_TRUSLEEP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_WORKMODE;
|
||||
|
||||
@@ -114,6 +122,9 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_HEART_RATE_LOW_ALERT);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_HEART_RATE_HIGH_ALERT);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_SPO_LOW_ALERT);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_STRESS_SWITCH);
|
||||
handler.addPreferenceHandlerFor(PREF_HUAWEI_STRESS_CALIBRATE);
|
||||
|
||||
|
||||
final Preference forceOptions = handler.findPreference(PREF_FORCE_OPTIONS);
|
||||
if (forceOptions != null) {
|
||||
@@ -138,6 +149,16 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
|
||||
if (this.coordinator.supportsWorkouts())
|
||||
reparseWorkout.setVisible(true);
|
||||
}
|
||||
|
||||
final Preference stressCalibrate = handler.findPreference("pref_huawei_stress_perform_calibrate");
|
||||
if (stressCalibrate != null) {
|
||||
stressCalibrate.setOnPreferenceClickListener(preference -> {
|
||||
final Intent intent = new Intent(handler.getContext(), HuaweiStressCalibrationActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, handler.getDevice());
|
||||
handler.getContext().startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class HuaweiStressParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressParser.class);
|
||||
|
||||
public static class StressData {
|
||||
public long startTime;
|
||||
public long endTime;
|
||||
public int algorithm;
|
||||
public byte score;
|
||||
public float scoreFactor;
|
||||
public byte level;
|
||||
public byte cFlag;
|
||||
public byte type;
|
||||
public byte accFlag;
|
||||
public byte ppgFlag;
|
||||
public List<Float> features = new ArrayList<>();
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("StressData{");
|
||||
sb.append("startTime=").append(startTime);
|
||||
sb.append(", endTime=").append(endTime);
|
||||
sb.append(", algorithm=").append(algorithm);
|
||||
sb.append(", score=").append(score);
|
||||
sb.append(", scoreFactor=").append(scoreFactor);
|
||||
sb.append(", level=").append(level);
|
||||
sb.append(", cFlag=").append(cFlag);
|
||||
sb.append(", type=").append(type);
|
||||
sb.append(", accFlag=").append(accFlag);
|
||||
sb.append(", ppgFlag=").append(ppgFlag);
|
||||
sb.append(", features=").append(features);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class RriFileData {
|
||||
public long fileSize;
|
||||
public int version;
|
||||
public int bitmap;
|
||||
public List<StressData> stressData;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder("RriFileData{");
|
||||
sb.append("fileSize=").append(fileSize);
|
||||
sb.append(", version=").append(version);
|
||||
sb.append(", bitmap=").append(bitmap);
|
||||
sb.append(", stressData=").append(stressData);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static String stressDataToJsonStr(StressData data) {
|
||||
JSONObject obj = new JSONObject();
|
||||
try {
|
||||
obj.put("startTime", data.startTime);
|
||||
obj.put("endTime", data.endTime);
|
||||
obj.put("algorithm", data.algorithm);
|
||||
obj.put("score", data.score);
|
||||
obj.put("scoreFactor", data.scoreFactor);
|
||||
obj.put("level", data.level);
|
||||
obj.put("cFlag", data.cFlag);
|
||||
obj.put("type", data.type);
|
||||
obj.put("accFlag", data.accFlag);
|
||||
obj.put("ppgFlag", data.ppgFlag);
|
||||
JSONArray arr = new JSONArray();
|
||||
for(Float f: data.features) {
|
||||
arr.put(f);
|
||||
}
|
||||
obj.put("features", arr);
|
||||
return obj.toString();
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static StressData stressDataFromJsonStr(String jsonStr) {
|
||||
StressData st = new StressData();
|
||||
try {
|
||||
JSONObject obj = new JSONObject(jsonStr);
|
||||
st.startTime = obj.getLong("startTime");
|
||||
st.endTime = obj.getLong("endTime");
|
||||
st.algorithm = obj.getInt("algorithm");
|
||||
st.score = (byte) obj.getInt("score");
|
||||
st.scoreFactor = (byte) obj.getInt("scoreFactor");
|
||||
st.level = (byte) obj.getInt("level");
|
||||
st.cFlag = (byte) obj.getInt("cFlag");
|
||||
st.type = (byte) obj.getInt("type");
|
||||
st.accFlag = (byte) obj.getInt("accFlag");
|
||||
st.ppgFlag = (byte) obj.getInt("ppgFlag");
|
||||
JSONArray arr = obj.getJSONArray("features");
|
||||
for(int i = 0; i < arr.length(); i++) {
|
||||
st.features.add((float) arr.getDouble(i));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
if(st.endTime == 0 || st.score == 0 || st.features.size() != 12) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
public static RriFileData parseRri(byte[] rriData) {
|
||||
if (rriData == null || rriData.length < 48) {
|
||||
LOG.error("stress data is invalid");
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.wrap(rriData);
|
||||
buffer.order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
RriFileData fileData = new RriFileData();
|
||||
|
||||
fileData.fileSize = buffer.getLong();
|
||||
fileData.bitmap = buffer.getShort();
|
||||
fileData.version = buffer.getShort();
|
||||
|
||||
|
||||
LOG.info("configFileSize :{}", fileData.fileSize);
|
||||
LOG.info("configBitmap :{}", fileData.bitmap);
|
||||
LOG.info("configVersion :{}", fileData.version);
|
||||
|
||||
// TODO: I don't have files with other version. Implement when discovered.
|
||||
if (fileData.version == 3) {
|
||||
// Skip unknown or reserved data
|
||||
buffer.position(48);
|
||||
|
||||
fileData.stressData = new ArrayList<>();
|
||||
|
||||
// Each entry 66 bytes.
|
||||
while (buffer.remaining() >= 66) {
|
||||
ByteBuffer entryBuffer = ByteBuffer.allocate(66);
|
||||
buffer.get(entryBuffer.array());
|
||||
|
||||
StressData stressData = new StressData();
|
||||
int startTime = entryBuffer.getInt();
|
||||
if (startTime == 0) {
|
||||
LOG.warn("startTime == 0, skip ??");
|
||||
continue;
|
||||
}
|
||||
stressData.startTime = startTime * 1000L;
|
||||
int endTime = entryBuffer.getInt();
|
||||
if (endTime == 0) {
|
||||
LOG.warn("endTime == 0, skip ??");
|
||||
continue;
|
||||
}
|
||||
stressData.endTime = endTime * 1000L;
|
||||
stressData.algorithm = entryBuffer.getInt();
|
||||
stressData.score = entryBuffer.get();
|
||||
stressData.level = entryBuffer.get();
|
||||
stressData.cFlag = entryBuffer.get();
|
||||
stressData.type = entryBuffer.get();
|
||||
stressData.accFlag = entryBuffer.get();
|
||||
stressData.ppgFlag = entryBuffer.get();
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
stressData.features.add(entryBuffer.getFloat());
|
||||
}
|
||||
fileData.stressData.add(stressData);
|
||||
}
|
||||
// Sort data by end time. We need to save latest data for sending to device. Not sure.
|
||||
if (fileData.stressData != null && !fileData.stressData.isEmpty()) {
|
||||
fileData.stressData.sort((a, b) -> {
|
||||
if (a != null && b != null) {
|
||||
return Long.compare(a.endTime, b.endTime);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Rri file: unknown version: {}", fileData.version);
|
||||
}
|
||||
LOG.info("fileData:{}", fileData);
|
||||
return fileData;
|
||||
|
||||
}
|
||||
}
|
||||
+2
@@ -1560,6 +1560,7 @@ public class DeviceConfig {
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public boolean truSleepNewSync = false;
|
||||
public boolean rriNewSync = false;
|
||||
public boolean gpsNewSync = false;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
@@ -1578,6 +1579,7 @@ public class DeviceConfig {
|
||||
// Tag 2 -> File support
|
||||
byte value = this.tlv.getByte(0x02);
|
||||
truSleepNewSync = (value & 2) != 0;
|
||||
rriNewSync = (value & 4) != 0;
|
||||
gpsNewSync = (value & 8) != 0;
|
||||
}
|
||||
|
||||
|
||||
+29
-7
@@ -90,6 +90,24 @@ public class FileDownloadService0A {
|
||||
}
|
||||
}
|
||||
|
||||
public static class RriFileRequest extends HuaweiPacket {
|
||||
public RriFileRequest(ParamsProvider paramsProvider, int startTime, int endTime) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FileDownloadService0A.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x02, (byte) 0x04)
|
||||
.put(0x8f, new HuaweiTLV()
|
||||
.put(0x10, startTime)
|
||||
.put(0x11, endTime)
|
||||
);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public String[] fileNames;
|
||||
|
||||
@@ -113,16 +131,19 @@ public class FileDownloadService0A {
|
||||
public static final int id = 0x02;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, boolean truSleep) {
|
||||
public Request(ParamsProvider paramsProvider, byte fileType) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FileDownloadService0A.id;
|
||||
this.commandId = id;
|
||||
|
||||
if (truSleep)
|
||||
this.tlv = new HuaweiTLV().put(0x06, (byte) 0x01);
|
||||
else
|
||||
this.tlv = new HuaweiTLV().put(0x06, (byte) 0x02).put(0x07, (byte) 0x03);
|
||||
|
||||
// NOTE: empty for debug files
|
||||
if(fileType != 0x0) {
|
||||
this.tlv = new HuaweiTLV().put(0x06, fileType);
|
||||
if (fileType == 0x02)
|
||||
this.tlv.put(0x07, (byte) 0x03);
|
||||
}
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
@@ -214,7 +235,7 @@ public class FileDownloadService0A {
|
||||
public static class Response extends HuaweiPacket {
|
||||
public boolean isOk;
|
||||
public String filename;
|
||||
public int offset;
|
||||
public int offset = 0;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
@@ -225,7 +246,8 @@ public class FileDownloadService0A {
|
||||
if (isOk) {
|
||||
if (this.tlv.contains(0x01))
|
||||
filename = this.tlv.getString(0x01);
|
||||
offset = this.tlv.getInteger(0x02);
|
||||
if (this.tlv.contains(0x02))
|
||||
offset = this.tlv.getInteger(0x02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -33,6 +33,7 @@ public class FileDownloadService2C {
|
||||
public enum FileType {
|
||||
SLEEP_STATE,
|
||||
SLEEP_DATA,
|
||||
RRI,
|
||||
GPS,
|
||||
UNKNOWN; // Never use this as input
|
||||
|
||||
@@ -42,6 +43,8 @@ public class FileDownloadService2C {
|
||||
return (byte) 0x0e;
|
||||
case SLEEP_DATA:
|
||||
return (byte) 0x0f;
|
||||
case RRI:
|
||||
return (byte) 0x10;
|
||||
case GPS:
|
||||
return (byte) 0x11;
|
||||
default:
|
||||
@@ -55,6 +58,8 @@ public class FileDownloadService2C {
|
||||
return FileType.SLEEP_STATE;
|
||||
case 0x0f:
|
||||
return FileType.SLEEP_DATA;
|
||||
case 0x10:
|
||||
return FileType.RRI;
|
||||
case 0x11:
|
||||
return FileType.GPS;
|
||||
default:
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class HrRriTest {
|
||||
public static final byte id = 0x19;
|
||||
|
||||
public static class OpenOrClose {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, byte type) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = HrRriTest.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x1, type);
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RriData {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public static class rriSqiData {
|
||||
public short rri;
|
||||
public byte sqi;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SubContainer{" +
|
||||
"rri=" + rri +
|
||||
", sqi=" + sqi +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public List<rriSqiData> containers;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
HuaweiTLV container = this.tlv.getObject(0x82);
|
||||
List<HuaweiTLV> subContainers = container.getObjects(0x83);
|
||||
this.containers = new ArrayList<>();
|
||||
for (HuaweiTLV subContainerTlv : subContainers) {
|
||||
rriSqiData rriSqi = new rriSqiData();
|
||||
rriSqi.rri = subContainerTlv.getShort(0x04);
|
||||
rriSqi.sqi = subContainerTlv.getByte(0x05);
|
||||
this.containers.add(rriSqi);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class Stress {
|
||||
public static final byte id = 0x20;
|
||||
|
||||
public static class AutomaticStress {
|
||||
public static final byte id = 0x09;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, byte status, byte score, List<Float> feature, int time) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Stress.id;
|
||||
this.commandId = id;
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, status);
|
||||
if(feature != null && feature.size() == 12) {
|
||||
this.tlv.put(0x02, score);
|
||||
ByteBuffer feat = ByteBuffer.allocate(12 * 4);
|
||||
for (Float f: feature) {
|
||||
feat.putFloat(f);
|
||||
//feat.putInt(Float.floatToIntBits(f));
|
||||
}
|
||||
this.tlv.put(0x03, feat.array());
|
||||
this.tlv.put(0x04, time);
|
||||
}
|
||||
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+811
@@ -0,0 +1,811 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class HuaweiStressHRVCalculation {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressHRVCalculation.class);
|
||||
|
||||
/*
|
||||
This file contain realization of mostly standard time-domain and frequency-domain HRV analysis methods.
|
||||
With parameters and restrictions specific for Huawei devices.
|
||||
*/
|
||||
|
||||
private int calculateAndRemoveOutliers(int[] rriData, byte[] outliers) {
|
||||
int dataSum = 0;
|
||||
int outlierCount = 0;
|
||||
|
||||
for (int i = 0; i < rriData.length; i++) {
|
||||
if ((rriData[i] <= 400) || (rriData[i] >= 1400)) {
|
||||
outliers[i] = 1;
|
||||
outlierCount++;
|
||||
} else {
|
||||
dataSum = dataSum + rriData[i];
|
||||
}
|
||||
}
|
||||
if (rriData.length == outlierCount) {
|
||||
return -1;
|
||||
}
|
||||
int firstNotOutlierIndex = -1;
|
||||
float averageValue = (float) dataSum / (float) (rriData.length - outlierCount);
|
||||
for (int i = 0; i < rriData.length; i++) {
|
||||
if (outliers[i] == 0) {
|
||||
float currentRri = (float) rriData[i];
|
||||
if (currentRri <= averageValue * 1.2 && currentRri >= averageValue * 0.8) {
|
||||
if (firstNotOutlierIndex < 0) {
|
||||
firstNotOutlierIndex = i;
|
||||
}
|
||||
} else {
|
||||
outliers[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstNotOutlierIndex < 0) {
|
||||
return -1;
|
||||
}
|
||||
int prevValue = rriData[firstNotOutlierIndex];
|
||||
for (int i = firstNotOutlierIndex + 1; i < rriData.length; i++) {
|
||||
if (outliers[i] == 0) {
|
||||
float currentRri = (float) rriData[i];
|
||||
if (currentRri <= prevValue * 0.8 || currentRri >= prevValue * 1.2) {
|
||||
outliers[i] = 1;
|
||||
}
|
||||
prevValue = rriData[i];
|
||||
}
|
||||
}
|
||||
return firstNotOutlierIndex;
|
||||
}
|
||||
|
||||
public static class TimeDomainData {
|
||||
public short medianNNICount = 0;
|
||||
public float sdHR = 0;
|
||||
public short rangeNNI = 0;
|
||||
public float rmsSD = 0;
|
||||
public short medianNNI = 0;
|
||||
public float normVar = 0;
|
||||
public short rriCount = 0;
|
||||
public float meanHR = 0;
|
||||
public float pNN50 = 0;
|
||||
public float sdNN = 0;
|
||||
}
|
||||
|
||||
private TimeDomainData calculateTimeDomain(int[] rriData, int firstNotOutlierIndex, byte[] outliers) {
|
||||
|
||||
TimeDomainData result = new TimeDomainData();
|
||||
|
||||
int rriMax = rriData[firstNotOutlierIndex];
|
||||
int rriMin = rriData[firstNotOutlierIndex];
|
||||
|
||||
int rri50Count = 0;
|
||||
int rriCount = 0;
|
||||
int rriDiffSum = 0;
|
||||
int rriDiffSqrtSum = 0;
|
||||
int rriSum = 0;
|
||||
float hrSum = 0;
|
||||
|
||||
int previousValue = 0;
|
||||
boolean isFirstValue = true;
|
||||
for (int i = 0; i < rriData.length; i++) {
|
||||
if ((outliers[i] == 0) && (rriData[i] != 0)) {
|
||||
rriSum += rriData[i];
|
||||
hrSum += (60000.0F / (float) rriData[i]);
|
||||
rriMax = Math.max(rriData[i], rriMax);
|
||||
rriMin = Math.min(rriData[i], rriMin);
|
||||
if (!isFirstValue) {
|
||||
rriCount++;
|
||||
int diff = rriData[i] - previousValue;
|
||||
rriDiffSum += diff;
|
||||
if (Math.abs(diff) > 50) {
|
||||
rri50Count++;
|
||||
}
|
||||
rriDiffSqrtSum += diff * diff;
|
||||
}
|
||||
previousValue = rriData[i];
|
||||
isFirstValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (rriCount == 0) {
|
||||
LOG.error("rriCount is zero");
|
||||
return null;
|
||||
}
|
||||
result.rriCount = (short) (rriCount + 1);
|
||||
result.meanHR = (hrSum / (float) (rriCount + 1));
|
||||
result.pNN50 = ((float) rri50Count / (float) rriCount) * 100.0F;
|
||||
|
||||
int rriMinBy20 = (rriMin / 20) * 20;
|
||||
int rriMaxBy20 = (rriMax / 20) * 20;
|
||||
int categoriesNumber = ((rriMaxBy20 - rriMinBy20) / 20) + 1;
|
||||
if (categoriesNumber >= 50) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int[] categoriesBy20 = new int[categoriesNumber + 1];
|
||||
byte[] categoriesCount = new byte[categoriesNumber + 1];
|
||||
for (int i = 0; i <= categoriesNumber; i++) {
|
||||
categoriesBy20[i] = rriMinBy20 + (i * 20);
|
||||
}
|
||||
|
||||
float hrDiffSqrtSum = 0.0F;
|
||||
float rrDiffSqrtSum = 0.0F;
|
||||
|
||||
float avrRR = rriSum / (float) result.rriCount;
|
||||
for (int i = 0; i < rriData.length; i++) {
|
||||
if ((outliers[i] == 0) && (rriData[i] != 0)) {
|
||||
float rrDiff = (float) rriData[i] - avrRR;
|
||||
rrDiffSqrtSum += (rrDiff * rrDiff);
|
||||
double hrDiff = (60000.0F / (double) rriData[i]) - result.meanHR;
|
||||
hrDiffSqrtSum += (float) (hrDiff * hrDiff);
|
||||
for (int j = 0; j < categoriesNumber; j++) {
|
||||
if ((categoriesBy20[j] <= rriData[i]) && (rriData[i] < categoriesBy20[j] + 20)) {
|
||||
categoriesCount[j]++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sdHR = (float) Math.sqrt(hrDiffSqrtSum / (float) (rriCount + 1));
|
||||
result.sdNN = (float) Math.sqrt(rrDiffSqrtSum / (float) (rriCount + 1));
|
||||
result.rmsSD = (float) Math.sqrt((float) rriDiffSqrtSum / (float) rriCount);
|
||||
|
||||
float avgDiffSqrt = (float) (rriDiffSum * rriDiffSum) / rriCount;
|
||||
float varSum = (float) (Math.sqrt(((float) rriDiffSqrtSum - avgDiffSqrt) / (float) rriCount) / 1.4142135F); // Math.sqrt(2) = 1.4142135F
|
||||
float varDiff = (rrDiffSqrtSum / (float) rriCount) * 2 - (varSum * varSum);
|
||||
if ((float) Math.sqrt(varDiff) == 0.0) {
|
||||
return null;
|
||||
}
|
||||
result.normVar = varSum / (float) Math.sqrt(varDiff);
|
||||
|
||||
int sectionSum = 0;
|
||||
int sectionCount = 0;
|
||||
int maxValue = 0;
|
||||
for (int i = 0; i < categoriesNumber; i++) {
|
||||
if (categoriesCount[i] == 0) {
|
||||
continue;
|
||||
}
|
||||
if (categoriesCount[i] > maxValue) {
|
||||
maxValue = categoriesCount[i];
|
||||
sectionCount = 1;
|
||||
sectionSum = i;
|
||||
continue;
|
||||
}
|
||||
if (categoriesCount[i] == maxValue) {
|
||||
sectionCount++;
|
||||
sectionSum += i;
|
||||
}
|
||||
}
|
||||
if (sectionCount == 0) {
|
||||
return null;
|
||||
}
|
||||
int meanCategoriesIndex = (int) ((float) sectionSum / (float) sectionCount + 0.5);
|
||||
result.medianNNICount = categoriesCount[meanCategoriesIndex];
|
||||
result.medianNNI = (short) categoriesBy20[meanCategoriesIndex];
|
||||
result.rangeNNI = (short) (rriMaxBy20 - rriMinBy20);
|
||||
return result;
|
||||
}
|
||||
|
||||
private int interpolateRriData(int[] rriData, int[] rriCumulativeSum, float baseValue, float[] rriResample, int maxSamples, byte[] outliers) {
|
||||
float targetValue = 0;
|
||||
float previousCumulative = 0;
|
||||
float lastRri = 0;
|
||||
int currIdx = 0;
|
||||
int resampleIdx = 0;
|
||||
for (int i = 0; i < rriData.length; i++) {
|
||||
if (outliers[i] != 0) {
|
||||
continue;
|
||||
}
|
||||
if (resampleIdx > 129) {
|
||||
return 0;
|
||||
}
|
||||
if (currIdx == 0) {
|
||||
rriResample[resampleIdx] = rriData[i];
|
||||
targetValue = (baseValue + 0.5F);
|
||||
previousCumulative = baseValue;
|
||||
lastRri = (float) rriData[i];
|
||||
resampleIdx++;
|
||||
currIdx = 1;
|
||||
} else {
|
||||
float currentCumulative = (float) (rriCumulativeSum[i] / 1000.0);
|
||||
float cumulativeDelta = currentCumulative - targetValue;
|
||||
if (cumulativeDelta >= -0.0001) {
|
||||
if (currentCumulative == previousCumulative) {
|
||||
return 0;
|
||||
}
|
||||
rriResample[resampleIdx] = (lastRri +
|
||||
(((float) rriData[i] - lastRri) / (currentCumulative - previousCumulative)) *
|
||||
(targetValue - previousCumulative));
|
||||
resampleIdx++;
|
||||
currIdx++;
|
||||
if (currIdx == maxSamples) {
|
||||
break;
|
||||
}
|
||||
targetValue = baseValue + ((float) currIdx * 0.5F);
|
||||
for (int j = currIdx; j < maxSamples; j++) {
|
||||
if ((currentCumulative == previousCumulative) || (((currentCumulative - targetValue) < -0.0001)))
|
||||
break;
|
||||
if (resampleIdx > 129) {
|
||||
return 0;
|
||||
}
|
||||
rriResample[resampleIdx] =
|
||||
(lastRri + (((float) rriData[i] - lastRri) / (currentCumulative - previousCumulative)) *
|
||||
(targetValue - previousCumulative));
|
||||
|
||||
resampleIdx++;
|
||||
currIdx++;
|
||||
if (currIdx == maxSamples) break;
|
||||
targetValue = baseValue + ((float) currIdx * 0.5F);
|
||||
}
|
||||
lastRri = (float) rriData[i];
|
||||
previousCumulative = currentCumulative;
|
||||
if (currIdx == maxSamples) break;
|
||||
} else if ((cumulativeDelta < -0.0001)) {
|
||||
lastRri = rriData[i];
|
||||
previousCumulative = currentCumulative;
|
||||
}
|
||||
}
|
||||
}
|
||||
return resampleIdx;
|
||||
}
|
||||
|
||||
private boolean applyHanningWindow(float[] rriResample, int maxLimit, int length) {
|
||||
if ((length >= maxLimit) || (length == 0) || (rriResample == null) || (maxLimit <= 128)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int halfLength = length / 2;
|
||||
if ((halfLength >= maxLimit)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean isLengthEven = (length % 2) == 0;
|
||||
|
||||
if (((length > 255) && isLengthEven) || ((length > 256) && (!isLengthEven))) {
|
||||
for (int n = 0; n < 128; n++) {
|
||||
double angle = ((double) (n + 1) * 2.0 * Math.PI) / (double) (length + 1);
|
||||
rriResample[n] = rriResample[n] * (float) ((1.0 - Math.cos(angle)) * 0.5);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (halfLength != 0) {
|
||||
int n2 = length - 1;
|
||||
for (int n = 0; n < halfLength; n++) {
|
||||
double angle = ((double) (n + 1) * 2.0 * Math.PI) / (double) (length + 1);
|
||||
float window = (float) ((1.0 - Math.cos(angle)) * 0.5);
|
||||
rriResample[n] = rriResample[n] * window;
|
||||
if (n2 < 0) {
|
||||
return true;
|
||||
}
|
||||
if (length < 129) {
|
||||
rriResample[n2] = rriResample[n2] * window;
|
||||
} else {
|
||||
if ((length - 128) <= n) {
|
||||
rriResample[n2] = rriResample[n2] * window;
|
||||
}
|
||||
}
|
||||
n2--;
|
||||
}
|
||||
}
|
||||
if (isLengthEven) {
|
||||
return true;
|
||||
}
|
||||
int midIndex = ((length + 1) / 2) - 1;
|
||||
if ((midIndex >= 0) && (midIndex < maxLimit)) {
|
||||
double angle = ((double) (midIndex + 1) * 2.0 * Math.PI) / (double) (length + 1);
|
||||
rriResample[midIndex] = (float) ((double) rriResample[midIndex] * (1.0 - Math.cos(angle)) * 0.5);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int prepareFrequencyDomainData(int firstNotOutlierIndex, int[] rriData, int[] rriCumulativeSum, float[] rriResample, byte[] outliers) {
|
||||
|
||||
int baseSumValue = rriCumulativeSum[0];
|
||||
if (firstNotOutlierIndex > 0) {
|
||||
baseSumValue = rriCumulativeSum[firstNotOutlierIndex];
|
||||
}
|
||||
double timeSpan = (double) (rriCumulativeSum[rriCumulativeSum.length - 1] - baseSumValue) / 1000.0;
|
||||
|
||||
int resampleSize = interpolateRriData(rriData, rriCumulativeSum, (float) (baseSumValue / 1000.0), rriResample, (int) (timeSpan * 2) + 1, outliers);
|
||||
if ((resampleSize == 0) || (resampleSize >= rriResample.length)) {
|
||||
LOG.error("interpolateRriData error");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Detrending data
|
||||
float sumValues = 0.0F;
|
||||
int sumSqrtIdx = 0;
|
||||
int sumIdx = 0;
|
||||
float sumIdxValues = 0.0F;
|
||||
for (int i = 0; i < resampleSize; i++) {
|
||||
sumIdx += i;
|
||||
sumSqrtIdx += (i * i);
|
||||
sumValues += rriResample[i];
|
||||
sumIdxValues += (i * rriResample[i]);
|
||||
}
|
||||
|
||||
final float meanIndex = sumIdx / (float) resampleSize;
|
||||
float varianceTerm = sumSqrtIdx - ((float) resampleSize * meanIndex * meanIndex);
|
||||
if (varianceTerm == 0.0) {
|
||||
LOG.error("varianceTerm is zero");
|
||||
return 0;
|
||||
}
|
||||
|
||||
final float meanValue = sumValues / (float) resampleSize;
|
||||
final float slope = (sumIdxValues - ((float) resampleSize * meanIndex * meanValue)) / varianceTerm;
|
||||
final float intercept = meanValue - (slope * meanIndex);
|
||||
|
||||
// Apply detrending to data
|
||||
for (int i = 0; i < resampleSize; i++) {
|
||||
rriResample[i] -= (intercept + slope * i);
|
||||
}
|
||||
|
||||
if (!applyHanningWindow(rriResample, rriResample.length, resampleSize)) {
|
||||
LOG.error("applyHanningWindow error");
|
||||
return 0;
|
||||
}
|
||||
return resampleSize;
|
||||
}
|
||||
|
||||
public static class FrequencyDomainData {
|
||||
float freqStep;
|
||||
byte lfCount;
|
||||
byte vlfCount;
|
||||
byte hfCount;
|
||||
float totalPsd;
|
||||
float peakPower;
|
||||
float peakFrequency;
|
||||
float[] vlfSamples = new float[10];
|
||||
float[] hfSamples = new float[40];
|
||||
|
||||
float vlfPsd;
|
||||
float hfPsd;
|
||||
float lfPsd;
|
||||
float lfHfVlfTotalPsd;
|
||||
|
||||
int vlfPeakIndex;
|
||||
int hfPeakIndex;
|
||||
}
|
||||
|
||||
|
||||
private boolean transformComplexComponents(float[] fftData, int size, int stepSize) {
|
||||
if ((fftData == null) || (size >= 129)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < size; i += (stepSize * 2)) {
|
||||
float rP1 = fftData[i * 2];
|
||||
float iP1 = fftData[(i * 2) + (stepSize * 2)];
|
||||
float rP2 = fftData[(i * 2) + 1];
|
||||
float iP2 = fftData[(i * 2) + (stepSize * 2) + 1];
|
||||
fftData[(i * 2) + (stepSize * 2)] = rP1 - iP1;
|
||||
fftData[(i * 2) + (stepSize * 2) + 1] = rP2 - iP2;
|
||||
fftData[i * 2] = rP1 + iP1;
|
||||
fftData[(i * 2) + 1] = rP2 + iP2;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean computeFFTStage(int stageIndex, int butterflyStep, float[] fftData, int maxIndex, float[] sinCos) {
|
||||
if ((maxIndex >= 129) || (fftData == null)) {
|
||||
return false;
|
||||
}
|
||||
int sinCosIdx = (stageIndex - 1) * 2;
|
||||
for (int i = 1; i < butterflyStep; i++) {
|
||||
for (int j = i; j < maxIndex; j += (butterflyStep * 2)) {
|
||||
float iPCurr = fftData[(j * 2) + 1];
|
||||
float iPNext = fftData[(j * 2) + (butterflyStep * 2) + 1];
|
||||
float rPCurr = fftData[j * 2];
|
||||
float rpNext = fftData[(j * 2) + (butterflyStep * 2)];
|
||||
float rDiff = rPCurr - rpNext;
|
||||
float iDiff = iPCurr - iPNext;
|
||||
fftData[(j * 2) + (butterflyStep * 2)] = ((sinCos[sinCosIdx] * rDiff) - (sinCos[sinCosIdx + 1] * iDiff));
|
||||
fftData[(j * 2) + (butterflyStep * 2) + 1] = (sinCos[sinCosIdx + 1] * rDiff) + (sinCos[sinCosIdx] * iDiff);
|
||||
fftData[j * 2] = rPCurr + rpNext;
|
||||
fftData[(j * 2) + 1] = iPCurr + iPNext;
|
||||
}
|
||||
sinCosIdx += (stageIndex * 2);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void swapComplexComponents(float[] data, int indexA, int indexB) {
|
||||
float tempR = data[indexB * 2];
|
||||
float tempI = data[(indexB * 2) + 1];
|
||||
data[indexB * 2] = data[indexA * 2];
|
||||
data[(indexB * 2) + 1] = data[(indexA * 2) + 1];
|
||||
data[indexA * 2] = tempR;
|
||||
data[(indexA * 2) + 1] = tempI;
|
||||
}
|
||||
|
||||
private boolean performFFT(float[] fftData, int currentSize) {
|
||||
int halfSize = currentSize / 2;
|
||||
int halfSize1 = halfSize - 1;
|
||||
|
||||
if (currentSize == 0 || fftData == null || (halfSize * 2) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float[] sinCos = new float[halfSize1 * 2];
|
||||
|
||||
double x = (Math.PI / (double) halfSize);
|
||||
float cosX = (float) Math.cos(x);
|
||||
float sinX = -(float) Math.sin(x);
|
||||
float cosCurr = cosX;
|
||||
float sinCurr = sinX;
|
||||
for (int i = 0; i < halfSize1; i++) {
|
||||
sinCos[i * 2] = cosCurr;
|
||||
sinCos[(i * 2) + 1] = sinCurr;
|
||||
float tmp = cosCurr * sinX;
|
||||
cosCurr = ((cosCurr * cosX) - (sinCurr * sinX));
|
||||
sinCurr = (sinCurr * cosX) + tmp;
|
||||
}
|
||||
|
||||
// FFT decomposition
|
||||
int partitionSize = currentSize;
|
||||
int butterflyStep = 1;
|
||||
for (int stageCount = 0; stageCount < currentSize; stageCount++) {
|
||||
partitionSize /= 2;
|
||||
|
||||
if (!transformComplexComponents(fftData, currentSize, partitionSize)) {
|
||||
return false;
|
||||
}
|
||||
if (!computeFFTStage(butterflyStep, partitionSize, fftData, currentSize, sinCos)) {
|
||||
return false;
|
||||
}
|
||||
if (partitionSize == 1) break;
|
||||
|
||||
butterflyStep *= 2;
|
||||
}
|
||||
|
||||
// Bit-reversal
|
||||
int swapBoundary = currentSize / 2;
|
||||
int sIdx = 0;
|
||||
|
||||
for (int tIdx = 1; tIdx < currentSize - 1; tIdx++) {
|
||||
int n1 = swapBoundary;
|
||||
while (sIdx >= n1) {
|
||||
sIdx = sIdx - n1;
|
||||
n1 = n1 / 2;
|
||||
}
|
||||
sIdx = (n1 + sIdx);
|
||||
if (sIdx > tIdx) {
|
||||
swapComplexComponents(fftData, tIdx, sIdx);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean calculateFFT(float[] rriResample, int length) {
|
||||
|
||||
final int maxLength = rriResample.length - 2;
|
||||
|
||||
float[] fftData = new float[maxLength * 2];
|
||||
|
||||
int curLen = Math.min(length, maxLength);
|
||||
|
||||
int j = 0;
|
||||
for (int i = 0; i < (curLen * 2); i += 2) {
|
||||
fftData[i] = rriResample[j]; // real part
|
||||
fftData[i + 1] = 0.0F; // imagine part
|
||||
j++;
|
||||
}
|
||||
if (!performFFT(fftData, maxLength)) {
|
||||
LOG.error("performFFT error");
|
||||
return false;
|
||||
}
|
||||
|
||||
Arrays.fill(rriResample, 0, length, 0);
|
||||
|
||||
int maxHalfLength = (maxLength / 2) + 1;
|
||||
|
||||
if (maxHalfLength == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
for (int i = 0; i < maxHalfLength; i++) {
|
||||
rriResample[i] = ((fftData[n + 1] * fftData[n + 1] + fftData[n] * fftData[n]) * 2) / ((float) maxLength * 0.5F);
|
||||
n += 2;
|
||||
}
|
||||
|
||||
rriResample[0] = (fftData[1] * fftData[1] + fftData[0] * fftData[0]) / ((float) maxLength * 0.5F);
|
||||
int li = ((maxHalfLength - 1) * 2); //last index
|
||||
rriResample[maxHalfLength - 1] = (fftData[li + 1] * fftData[li + 1] + fftData[li] * fftData[li]) / ((float) maxLength * 0.5F);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private boolean calculateFrequencyDomain(float[] rriResample, int length, FrequencyDomainData fdData) {
|
||||
|
||||
// from 0.0 to 1.0 with freqStep
|
||||
fdData.freqStep = 0.015625F;
|
||||
for (int n = 0; n < 65; n++) {
|
||||
float currentFrequency = fdData.freqStep * n;
|
||||
rriResample[n] = (float) (((rriResample[n] * 64.0) / (float) length) * 0.5);
|
||||
fdData.totalPsd += fdData.freqStep * rriResample[n];
|
||||
|
||||
if ((fdData.vlfCount >= fdData.vlfSamples.length) || (fdData.hfCount >= fdData.hfSamples.length)) {
|
||||
LOG.error("vlfCount or hfCount too big");
|
||||
return false;
|
||||
}
|
||||
// VLF: 0.0 - 0.04 Hz
|
||||
// LF: 0.04 - 0.15 Hz
|
||||
// HF: 0.15 - 0.4 Hz
|
||||
if (currentFrequency <= 0.4F) {
|
||||
fdData.lfHfVlfTotalPsd += fdData.freqStep * rriResample[n];
|
||||
|
||||
if ((currentFrequency >= 0.0F) && (currentFrequency < 0.04F)) {
|
||||
fdData.vlfPsd += fdData.freqStep * rriResample[n];
|
||||
fdData.vlfSamples[fdData.vlfCount] = rriResample[n];
|
||||
fdData.vlfCount++;
|
||||
} else if ((currentFrequency >= 0.04F) && (currentFrequency < 0.15F)) {
|
||||
fdData.lfPsd += (fdData.freqStep * rriResample[n]);
|
||||
fdData.lfCount++;
|
||||
} else if (currentFrequency >= 0.15F && currentFrequency <= 0.4F) {
|
||||
fdData.hfPsd += fdData.freqStep * rriResample[n];
|
||||
fdData.hfSamples[fdData.hfCount] = rriResample[n];
|
||||
fdData.hfCount++;
|
||||
}
|
||||
|
||||
if (currentFrequency >= 0.15F) {
|
||||
if (fdData.peakPower < rriResample[n]) {
|
||||
fdData.peakPower = rriResample[n];
|
||||
fdData.peakFrequency = currentFrequency;
|
||||
}
|
||||
} else {
|
||||
fdData.peakPower = 0;
|
||||
fdData.peakFrequency = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((fdData.hfPsd == 0.0) || (fdData.totalPsd == 0.0) || (fdData.lfHfVlfTotalPsd == fdData.vlfPsd) || (fdData.lfHfVlfTotalPsd == 0.0)) {
|
||||
LOG.error("Invalid PSD values");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int processDataTrends(float[] data, int dataSize, int[] outputIndices, int maxOutputSize) {
|
||||
|
||||
final int trendCount = dataSize - 1;
|
||||
int[] trends = new int[trendCount];
|
||||
|
||||
// Detect initial trends
|
||||
for (int i = 0; i < trendCount; i++) {
|
||||
trends[i] = Float.compare(data[i], data[i + 1]); //(data[i] > data[i + 1]) ? 1 : (data[i] < data[i + 1]) ? -1 : 0;
|
||||
}
|
||||
|
||||
// Process trend transitions
|
||||
int processedIndicesCount = 0;
|
||||
for (int i = 0; i < (dataSize - 2) && processedIndicesCount < maxOutputSize; i++) {
|
||||
final int currentTrend = trends[i];
|
||||
final int nextTrend = trends[i + 1];
|
||||
|
||||
if (nextTrend > currentTrend) {
|
||||
trends[i] = 2;
|
||||
} else if (nextTrend != currentTrend) {
|
||||
trends[i] = 0;
|
||||
} else {
|
||||
trends[i] = 1;
|
||||
}
|
||||
|
||||
if (trends[i] != 0) {
|
||||
outputIndices[processedIndicesCount++] = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return processedIndicesCount;
|
||||
}
|
||||
|
||||
private int findDominantPeak(float[] samples, byte length) {
|
||||
if ((samples == null || length == 0)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
float maxValue = 0.0F;
|
||||
int maxValueIndex = 0;
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (samples[i] > maxValue) {
|
||||
maxValue = samples[i];
|
||||
maxValueIndex = (byte) i;
|
||||
}
|
||||
}
|
||||
if (length < 3) {
|
||||
return maxValueIndex;
|
||||
}
|
||||
|
||||
int[] processedIndices = new int[40];
|
||||
int validIndicesCount = processDataTrends(samples, length, processedIndices, processedIndices.length);
|
||||
if (validIndicesCount < 0) {
|
||||
LOG.error("findDominantPeak processDataTrends error");
|
||||
return -1;
|
||||
}
|
||||
if (validIndicesCount == 0) {
|
||||
return maxValueIndex;
|
||||
}
|
||||
|
||||
float peakValue = 0.0F;
|
||||
int peakIndex = 0;
|
||||
|
||||
for (int i = 0; i < validIndicesCount; i++) {
|
||||
final int index = processedIndices[i];
|
||||
if (index >= length) {
|
||||
LOG.error( "Invalid index: {}", index);
|
||||
return -1;
|
||||
}
|
||||
if (samples[index] > peakValue) {
|
||||
peakIndex = index;
|
||||
peakValue = samples[index];
|
||||
}
|
||||
}
|
||||
return peakIndex;
|
||||
}
|
||||
|
||||
|
||||
private FrequencyDomainData calculateFrequencyDomainData(float[] rriResample, int length) {
|
||||
|
||||
FrequencyDomainData result = new FrequencyDomainData();
|
||||
|
||||
if (!calculateFFT(rriResample, length)) {
|
||||
LOG.error("calculateFFT error");
|
||||
return null;
|
||||
}
|
||||
if (!calculateFrequencyDomain(rriResample, length, result)) {
|
||||
LOG.error( "calculateFrequencyDomainParameters error");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.freqStep == 0.0) {
|
||||
LOG.error("Invalid frequency step");
|
||||
return null;
|
||||
}
|
||||
|
||||
int peakEnd = (int) ((result.peakFrequency * 1.2) / result.freqStep);
|
||||
if ((peakEnd >= length) | (result.totalPsd == 0.0)) {
|
||||
LOG.error( "Invalid peak frequency");
|
||||
return null;
|
||||
}
|
||||
|
||||
int vlfPeakIndex = findDominantPeak(result.vlfSamples, result.vlfCount);
|
||||
int hfPeakIndex = findDominantPeak(result.hfSamples, result.hfCount);
|
||||
if (hfPeakIndex < 0 || vlfPeakIndex < 0) {
|
||||
LOG.error("Peak detection failed (VLF: {}, HF: {})", vlfPeakIndex, hfPeakIndex);
|
||||
return null;
|
||||
}
|
||||
result.vlfPeakIndex = vlfPeakIndex;
|
||||
result.hfPeakIndex = hfPeakIndex;
|
||||
return result;
|
||||
}
|
||||
|
||||
private FrequencyDomainData calculateFrequencyDomain(int[] rriData, int[] rriCumulativeSum, int firstNotOutlierIndex, byte[] outliers) {
|
||||
|
||||
final float[] rriResample = new float[130];
|
||||
int resampleSize = prepareFrequencyDomainData(firstNotOutlierIndex, rriData, rriCumulativeSum, rriResample, outliers);
|
||||
if (resampleSize == 0) {
|
||||
LOG.error("prepareFrequencyDomainData error");
|
||||
return null;
|
||||
}
|
||||
|
||||
return calculateFrequencyDomainData(rriResample, resampleSize);
|
||||
}
|
||||
|
||||
|
||||
private float[] calculateHRV(int[] rriData, int[] rriCumulativeSum) {
|
||||
|
||||
byte[] outliers = new byte[rriData.length];
|
||||
|
||||
int firstNotOutlierIndex = calculateAndRemoveOutliers(rriData, outliers);
|
||||
if (firstNotOutlierIndex < 0 || firstNotOutlierIndex > rriData.length) {
|
||||
LOG.error( "Error calculate outliers");
|
||||
return null;
|
||||
}
|
||||
|
||||
TimeDomainData timeDomainData = calculateTimeDomain(rriData, firstNotOutlierIndex, outliers);
|
||||
if (timeDomainData == null) {
|
||||
LOG.error( "Time Domain error");
|
||||
return null;
|
||||
}
|
||||
|
||||
FrequencyDomainData frequencyDomainData = calculateFrequencyDomain(rriData, rriCumulativeSum, firstNotOutlierIndex, outliers);
|
||||
if (frequencyDomainData == null) {
|
||||
LOG.error( "Frequency Domain error");
|
||||
return null;
|
||||
}
|
||||
|
||||
float pVlf = (float) ((frequencyDomainData.vlfPsd / frequencyDomainData.lfHfVlfTotalPsd) * 100.0);
|
||||
float pLf = (float) ((frequencyDomainData.lfPsd / frequencyDomainData.lfHfVlfTotalPsd) * 100.0);
|
||||
|
||||
boolean isMedianNNIInRange = timeDomainData.medianNNICount >= 0 && timeDomainData.medianNNICount <= 600;
|
||||
boolean isSDHRInRange = timeDomainData.sdHR >= 1.0 && timeDomainData.sdHR <= 20.0;
|
||||
boolean isRangeNNILessInRange = timeDomainData.rangeNNI >= 0 && timeDomainData.rangeNNI <= 1000;
|
||||
boolean isRMSDInRange = timeDomainData.rmsSD >= 0.0 && timeDomainData.rmsSD <= 200.0;
|
||||
boolean isNormalizedVarianceInRange = timeDomainData.normVar >= 0.0 && timeDomainData.normVar <= 2.0;
|
||||
boolean isPVlfInRange = pVlf >= 0.0 && pVlf <= 100.0;
|
||||
boolean isNNIDifferenceInRange = timeDomainData.medianNNI >= 300 && timeDomainData.medianNNI <= 1500;
|
||||
|
||||
|
||||
if (isMedianNNIInRange && isSDHRInRange && isRangeNNILessInRange
|
||||
&& isRMSDInRange && isNNIDifferenceInRange && isNormalizedVarianceInRange && isPVlfInRange) {
|
||||
float[] result = new float[10];
|
||||
result[0] = timeDomainData.medianNNICount;
|
||||
result[1] = timeDomainData.sdHR;
|
||||
result[2] = timeDomainData.rangeNNI;
|
||||
result[3] = pVlf;
|
||||
result[4] = timeDomainData.rmsSD;
|
||||
result[5] = timeDomainData.medianNNI;
|
||||
result[6] = timeDomainData.normVar;
|
||||
result[7] = frequencyDomainData.hfPsd;
|
||||
result[8] = frequencyDomainData.lfPsd;
|
||||
result[9] = pLf;
|
||||
return result;
|
||||
}
|
||||
LOG.error( "validation fail");
|
||||
return null;
|
||||
}
|
||||
|
||||
public float[] calculateStressHRVParameters(List<Integer> deviceRriData, List<Integer> deviceSqiData, int signalTime) {
|
||||
|
||||
if ((signalTime <= 50) || (signalTime >= 70)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((deviceRriData.size() < 30) || (deviceRriData.size() > 210) || (deviceRriData.size() > deviceSqiData.size())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Integer> tmpRri = new ArrayList<>();
|
||||
List<Integer> tmpRriCumulativeSum = new ArrayList<>();
|
||||
|
||||
int sum = 0;
|
||||
for (int i = 0; i < deviceRriData.size(); i++) {
|
||||
sum = sum + deviceRriData.get(i);
|
||||
if (deviceSqiData.get(i) == 100) {
|
||||
tmpRri.add(deviceRriData.get(i));
|
||||
tmpRriCumulativeSum.add(sum);
|
||||
}
|
||||
}
|
||||
|
||||
if (tmpRri.isEmpty() || tmpRri.size() < 30) {
|
||||
LOG.error("No valid RRI signals");
|
||||
return null;
|
||||
}
|
||||
|
||||
int total = 0;
|
||||
int validCount = 0;
|
||||
|
||||
int[] filteredRri = new int[tmpRri.size()];
|
||||
int[] rriCumulativeSum = new int[tmpRriCumulativeSum.size()];
|
||||
for (int i = 0; i < tmpRri.size(); i++) {
|
||||
filteredRri[i] = tmpRri.get(i);
|
||||
rriCumulativeSum[i] = tmpRriCumulativeSum.get(i);
|
||||
|
||||
if ((filteredRri[i] >= 400) && (filteredRri[i] <= 1400)) {
|
||||
total += filteredRri[i];
|
||||
validCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (rriCumulativeSum[rriCumulativeSum.length - 1] > 65000) {
|
||||
LOG.error( "rriCumulativeSum is long");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((validCount == 0) || (total == 0)) {
|
||||
LOG.error("validCount or total is 0");
|
||||
return null;
|
||||
}
|
||||
|
||||
float sqi = (60.0F / (((float) total / validCount) / 1000.0F)) * ((float) signalTime / 60.0F);
|
||||
if (sqi < 0.2) {
|
||||
LOG.error("sqi is low");
|
||||
return null;
|
||||
}
|
||||
|
||||
return calculateHRV(filteredRri, rriCumulativeSum);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress;
|
||||
|
||||
public class HuaweiStressScoreCalculation {
|
||||
|
||||
private static final float[][] NORMALIZATION_PARAMS = {
|
||||
{19.381044F, 9.474176F},
|
||||
{5.5441837F, 1.9308523F},
|
||||
{184.17795F, 54.48858F},
|
||||
{33.487267F, 25.893078F},
|
||||
{32.97501F, 16.267557F},
|
||||
{677.911F, 130.61485F},
|
||||
{0.43178082F, 0.1747625F},
|
||||
{147.99915F, 170.24158F},
|
||||
{208.32425F, 245.64342F},
|
||||
{37.63596F, 19.478745F}
|
||||
};
|
||||
|
||||
private static final float SCORE_INIT_VALUE = 3.835272F;
|
||||
private static final float[] COEFFICIENTS = {0.16605523F, 0.24399279F, 0.0F, 0.0F, -0.07095941F, -0.20609115F, 0.0F, -0.14579488F, -0.09786916F, 0.0F};
|
||||
|
||||
private static final float MIN_SCORE = 0.0f;
|
||||
private static final float MAX_SCORE = 7.0f;
|
||||
private static final int OUTPUT_MIN = 15;
|
||||
private static final int OUTPUT_MAX = 90;
|
||||
|
||||
|
||||
|
||||
public static float calculateScoreFactor(float[] features) {
|
||||
float[] copyFeatures = features.clone();
|
||||
|
||||
for (int i = 0; i < NORMALIZATION_PARAMS.length; i++) {
|
||||
float mean = NORMALIZATION_PARAMS[i][0];
|
||||
float stdDev = NORMALIZATION_PARAMS[i][1];
|
||||
copyFeatures[i] = (copyFeatures[i] - mean) / stdDev;
|
||||
}
|
||||
|
||||
float score = SCORE_INIT_VALUE;
|
||||
for (int i = 0; i < COEFFICIENTS.length; i++) {
|
||||
score += copyFeatures[i] * COEFFICIENTS[i];
|
||||
}
|
||||
|
||||
return Math.max(MIN_SCORE, Math.min(score, MAX_SCORE));
|
||||
}
|
||||
|
||||
private static int calculateNormalizedScore(float value) {
|
||||
final int score = (int) ((value * 14.0f) + 1.5f);
|
||||
return Math.min(Math.max(score, 1), 99);
|
||||
}
|
||||
|
||||
public static byte calculateNormalizedFinalScore(float scoreFactor) {
|
||||
final float clampedValue = Math.max(MIN_SCORE, Math.min(scoreFactor, MAX_SCORE));
|
||||
final int clampedScore = calculateNormalizedScore(clampedValue);
|
||||
return (byte) Math.min(Math.max(clampedScore, OUTPUT_MIN), OUTPUT_MAX);
|
||||
}
|
||||
|
||||
public static float calibrateScoreFactor(float calibrationScore, float scoreFactor) {
|
||||
calibrationScore = Math.max(40, Math.min(calibrationScore, 70));
|
||||
|
||||
float calibrationScoreFactor = (float) (((calibrationScore - 1) * 7.0) / 98.0);
|
||||
float clampedCalibrationScoreFactor = Math.max(MIN_SCORE, Math.min(calibrationScoreFactor, MAX_SCORE));
|
||||
|
||||
return (float) ((clampedCalibrationScoreFactor * 0.8) + (scoreFactor * 0.2));
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class HuaweiStressCalibrationActivity extends AbstractGBActivity {
|
||||
private GBDevice device;
|
||||
|
||||
|
||||
protected String fragmentTag() {
|
||||
return HuaweiStressCalibrationFragment.FRAGMENT_TAG;
|
||||
}
|
||||
|
||||
|
||||
protected Fragment newFragment() {
|
||||
return HuaweiStressCalibrationFragment.newInstance(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
|
||||
setContentView(R.layout.activity_device_settings);
|
||||
|
||||
if (device == null || !device.isInitialized()) {
|
||||
GB.toast(getString(R.string.watch_not_connected), Toast.LENGTH_SHORT, GB.INFO);
|
||||
finish();
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(fragmentTag());
|
||||
if (fragment == null) {
|
||||
fragment = newFragment();
|
||||
}
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings_container, fragment, fragmentTag())
|
||||
.commit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
// Simulate a back press, so that we don't actually exit the activity when
|
||||
// in a nested PreferenceScreen
|
||||
this.getOnBackPressedDispatcher().onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
import com.google.android.material.slider.Slider;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress.HuaweiStressScoreCalculation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class HuaweiStressCalibrationFragment extends AbstractGBFragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressCalibrationFragment.class);
|
||||
|
||||
public static final String EXTRA_STRESS_PROGRESS = "huawei_stress_progress";
|
||||
public static final String EXTRA_STRESS_DATA = "huawei_stress_data";
|
||||
public static final String EXTRA_STRESS_ERROR = "huawei_stress_error";
|
||||
|
||||
static final String FRAGMENT_TAG = "HUAWEI_STRESS_CALIBRATE_FRAGMENT";
|
||||
|
||||
public static final String ACTION_STRESS_UPDATE = "nodomain.freeyourgadget.gadgetbridge.huawei.stress.start";
|
||||
public static final String ACTION_STRESS_RESULT = "nodomain.freeyourgadget.gadgetbridge.huawei.stress.result";
|
||||
|
||||
private GBDevice device;
|
||||
|
||||
private RelativeLayout layoutMeasure;
|
||||
private CircularProgressIndicator progress;
|
||||
private TextView countdownTime;
|
||||
private Button start;
|
||||
|
||||
private RelativeLayout layoutResult;
|
||||
private TextView score;
|
||||
private CheckBox isCalibrate;
|
||||
private Slider calibrateSlider;
|
||||
|
||||
|
||||
private HuaweiStressParser.StressData data = null;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
final Bundle arguments = getArguments();
|
||||
if (arguments == null) {
|
||||
return null;
|
||||
}
|
||||
this.device = arguments.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_huawei_stress_calibrate, container, false);
|
||||
|
||||
layoutMeasure = rootView.findViewById(R.id.huawei_stress_measure);
|
||||
progress = rootView.findViewById(R.id.huawei_stress_countdown);
|
||||
countdownTime = rootView.findViewById(R.id.huawei_stress_countdown_time);
|
||||
start = rootView.findViewById(R.id.huawei_stress_calibrate_start);
|
||||
|
||||
layoutResult = rootView.findViewById(R.id.huawei_stress_result);
|
||||
score = rootView.findViewById(R.id.huawei_stress_score);
|
||||
isCalibrate = rootView.findViewById(R.id.huawei_stress_calibrate);
|
||||
calibrateSlider = rootView.findViewById(R.id.huawei_stress_calibrate_slider);
|
||||
Button finish = rootView.findViewById(R.id.huawei_stress_calibrate_finish);
|
||||
Button again = rootView.findViewById(R.id.huawei_stress_calibrate_again);
|
||||
|
||||
calibrateSlider.setEnabled(false);
|
||||
|
||||
start.setOnClickListener(view -> {
|
||||
start.setVisibility(View.GONE);
|
||||
startCalibration();
|
||||
});
|
||||
|
||||
again.setOnClickListener(view -> {
|
||||
layoutResult.setVisibility(View.GONE);
|
||||
layoutMeasure.setVisibility(View.VISIBLE);
|
||||
start.setVisibility(View.VISIBLE);
|
||||
data = null;
|
||||
setProgress(60000);
|
||||
});
|
||||
|
||||
finish.setOnClickListener(view -> finishCalibrate());
|
||||
|
||||
isCalibrate.setOnClickListener(view -> calculateScore());
|
||||
|
||||
calibrateSlider.addOnChangeListener((slider, value, fromUser) -> calculateScore());
|
||||
|
||||
final IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STRESS_UPDATE);
|
||||
filter.addAction(ACTION_STRESS_RESULT);
|
||||
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filter);
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void setProgress(long j) {
|
||||
progress.setProgress((int) (j/1000));
|
||||
countdownTime.setText(formatTime(j));
|
||||
}
|
||||
|
||||
private void calculateScore() {
|
||||
if(data == null) {
|
||||
return;
|
||||
}
|
||||
LOG.info("User score: {}", data.scoreFactor);
|
||||
if(isCalibrate.isChecked()) {
|
||||
calibrateSlider.setEnabled(true);
|
||||
float userScore = calibrateSlider.getValue();
|
||||
LOG.info("User score: {}", userScore);
|
||||
float adjustedScoreFactor = HuaweiStressScoreCalculation.calibrateScoreFactor(userScore, data.scoreFactor);
|
||||
data.score = HuaweiStressScoreCalculation.calculateNormalizedFinalScore(adjustedScoreFactor);
|
||||
} else {
|
||||
calibrateSlider.setEnabled(false);
|
||||
data.score = HuaweiStressScoreCalculation.calculateNormalizedFinalScore(data.scoreFactor);
|
||||
}
|
||||
score.setText(String.format(Locale.ROOT, "%02d", data.score));
|
||||
|
||||
}
|
||||
|
||||
public HuaweiStressParser.StressData getStressData(final Intent intent) {
|
||||
String str = intent.getStringExtra(EXTRA_STRESS_DATA);
|
||||
if(TextUtils.isEmpty(str)) {
|
||||
return null;
|
||||
}
|
||||
return HuaweiStressParser.stressDataFromJsonStr(str);
|
||||
}
|
||||
|
||||
public void finishCalibrate() {
|
||||
//Maybe end time should be updated. But I am not sure.
|
||||
//stressData.endTime = endTime;
|
||||
String str = HuaweiStressParser.stressDataToJsonStr(data);
|
||||
if(TextUtils.isEmpty(str)) {
|
||||
Snackbar.make(layoutMeasure, getString(R.string.huawei_stress_calibrate_error), Snackbar.LENGTH_LONG).show();
|
||||
start.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
editor.putString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, str);
|
||||
editor.putBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, true);
|
||||
editor.apply();
|
||||
GBApplication.deviceService(device).onSendConfiguration(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH);
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
if (action == null) {
|
||||
LOG.error("Got null action");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case ACTION_STRESS_UPDATE:
|
||||
long currentCountDownTime = intent.getLongExtra(EXTRA_STRESS_PROGRESS, 0);
|
||||
setProgress(currentCountDownTime);
|
||||
break;
|
||||
case ACTION_STRESS_RESULT:
|
||||
boolean isError = intent.getBooleanExtra(EXTRA_STRESS_ERROR, true);
|
||||
if(isError) {
|
||||
setProgress(60000);
|
||||
start.setVisibility(View.VISIBLE);
|
||||
Snackbar.make(layoutMeasure, context.getString(R.string.huawei_stress_calibrate_error), Snackbar.LENGTH_LONG).show();
|
||||
} else {
|
||||
data = getStressData(intent);
|
||||
layoutMeasure.setVisibility(View.GONE);
|
||||
layoutResult.setVisibility(View.VISIBLE);
|
||||
calculateScore();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unknown action {}", action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private String formatTime(long milliSeconds) {
|
||||
return String.format(Locale.ROOT, "%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(milliSeconds) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(milliSeconds)),
|
||||
TimeUnit.MILLISECONDS.toSeconds(milliSeconds) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(milliSeconds)));
|
||||
}
|
||||
|
||||
private void setDevice(final GBDevice device) {
|
||||
final Bundle args = getArguments() != null ? getArguments() : new Bundle();
|
||||
args.putParcelable("device", device);
|
||||
setArguments(args);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
static HuaweiStressCalibrationFragment newInstance(final GBDevice device) {
|
||||
final HuaweiStressCalibrationFragment fragment = new HuaweiStressCalibrationFragment();
|
||||
fragment.setDevice(device);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected CharSequence getTitle() {
|
||||
return getString(R.string.huawei_stress_calibrate);
|
||||
}
|
||||
|
||||
|
||||
private void startCalibration() {
|
||||
GBApplication.deviceService(device).onSendConfiguration(HuaweiConstants.PREF_HUAWEI_STRESS_CALIBRATE);
|
||||
}
|
||||
}
|
||||
+22
-4
@@ -116,6 +116,7 @@ public class HuaweiFileDownloadManager {
|
||||
DEBUG,
|
||||
SLEEP_STATE,
|
||||
SLEEP_DATA,
|
||||
RRI,
|
||||
GPS,
|
||||
UNKNOWN // Never for input!
|
||||
}
|
||||
@@ -173,6 +174,10 @@ public class HuaweiFileDownloadManager {
|
||||
return new FileRequest("sleep_data.bin", FileType.SLEEP_DATA, supportsTruSleepNewSync, startTime, endTime, fileDownloadCallback);
|
||||
}
|
||||
|
||||
public static FileRequest rriFileRequest(boolean supportsRriNewSync, int startTime, int endTime, FileDownloadCallback fileDownloadCallback) {
|
||||
return new FileRequest("rrisqi_data.bin", FileType.RRI, supportsRriNewSync, startTime, endTime, fileDownloadCallback);
|
||||
}
|
||||
|
||||
private FileRequest(String filename, FileType fileType, boolean newSync, FileDownloadCallback fileDownloadCallback) {
|
||||
this.filename = filename;
|
||||
this.fileType = fileType;
|
||||
@@ -595,10 +600,23 @@ public class HuaweiFileDownloadManager {
|
||||
// Old sync only, can never be multiple at the same time
|
||||
// Assuming currentRequest is the correct one the entire time
|
||||
// Which may no longer be the case when we implement multi-download for new sync
|
||||
GetFileParametersRequest getFileParametersRequest = new GetFileParametersRequest(supportProvider,
|
||||
currentFileRequest.fileType == FileType.SLEEP_STATE ||
|
||||
currentFileRequest.fileType == FileType.SLEEP_DATA
|
||||
);
|
||||
byte fileType;
|
||||
|
||||
if (currentFileRequest.fileType == HuaweiFileDownloadManager.FileType.DEBUG)
|
||||
fileType = 0x00;
|
||||
else if(currentFileRequest.fileType == FileType.SLEEP_STATE || currentFileRequest.fileType == FileType.SLEEP_DATA)
|
||||
fileType = 0x01;
|
||||
else if (currentFileRequest.fileType == HuaweiFileDownloadManager.FileType.GPS)
|
||||
fileType = 0x02;
|
||||
else if(currentFileRequest.fileType == HuaweiFileDownloadManager.FileType.RRI)
|
||||
fileType = 0x04;
|
||||
else {
|
||||
currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadException(currentFileRequest, "Unknown download type"));
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
GetFileParametersRequest getFileParametersRequest = new GetFileParametersRequest(supportProvider, fileType);
|
||||
getFileParametersRequest.setFinalizeReq(new Request.RequestCallback() {
|
||||
@Override
|
||||
public void call() {
|
||||
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
|
||||
|
||||
import android.os.CountDownTimer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.HrRriTest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress.HuaweiStressHRVCalculation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress.HuaweiStressScoreCalculation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendHROpenCloseRequest;
|
||||
|
||||
public class HuaweiStressCalibration {
|
||||
|
||||
public interface HuaweiStressCalibrateCallback {
|
||||
void onFinish(HuaweiStressParser.StressData stressData);
|
||||
|
||||
void onProgress(long j);
|
||||
|
||||
void onError();
|
||||
}
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressCalibration.class);
|
||||
|
||||
private final HuaweiSupportProvider supportProvider;
|
||||
|
||||
private RriDataReceiver rriDataReceiver;
|
||||
|
||||
private HuaweiStressCalibrateCallback callback = null;
|
||||
|
||||
private List<Integer> rriData = new ArrayList<>();
|
||||
private List<Integer> sqiData = new ArrayList<>();
|
||||
|
||||
private long startTime = 0;
|
||||
private long endTime = 0;
|
||||
|
||||
private CountDownTimer measureTimer = null;
|
||||
|
||||
private static class RriDataReceiver extends Request {
|
||||
|
||||
List<HrRriTest.RriData.Response.rriSqiData> data;
|
||||
|
||||
public RriDataReceiver(HuaweiSupportProvider supportProvider) {
|
||||
super(supportProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleResponse(HuaweiPacket response) {
|
||||
if ((response.serviceId == HrRriTest.id && response.commandId == HrRriTest.RriData.id)) {
|
||||
receivedPacket = response;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean autoRemoveFromResponseHandler() {
|
||||
// This needs to be removed manually
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (this.receivedPacket instanceof HrRriTest.RriData.Response) {
|
||||
HrRriTest.RriData.Response response = (HrRriTest.RriData.Response) this.receivedPacket;
|
||||
data = response.containers;
|
||||
} else {
|
||||
throw new ResponseTypeMismatchException(this.receivedPacket, HrRriTest.RriData.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HuaweiStressCalibration(HuaweiSupportProvider supportProvider) {
|
||||
this.supportProvider = supportProvider;
|
||||
}
|
||||
|
||||
private void openOrClose(byte type) {
|
||||
try {
|
||||
SendHROpenCloseRequest req = new SendHROpenCloseRequest(this.supportProvider, type);
|
||||
req.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to SendHROpenCloseRequest", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean startMeasurements(HuaweiStressCalibrateCallback callback) {
|
||||
if (this.measureTimer != null) {
|
||||
// measurement in progress
|
||||
return false;
|
||||
}
|
||||
|
||||
this.callback = callback;
|
||||
|
||||
if (rriDataReceiver == null) {
|
||||
// We can only init fileDataReceiver if the device is already connected
|
||||
rriDataReceiver = new RriDataReceiver(supportProvider);
|
||||
rriDataReceiver.setFinalizeReq(new Request.RequestCallback() {
|
||||
@Override
|
||||
public void call() {
|
||||
for (int i = 0; i < rriDataReceiver.data.size(); i++) {
|
||||
HrRriTest.RriData.Response.rriSqiData d = rriDataReceiver.data.get(i);
|
||||
if (d.sqi != 0) {
|
||||
rriData.add((int) d.rri);
|
||||
sqiData.add((int) d.sqi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Request.ResponseParseException e) {
|
||||
stopMeasurements(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.supportProvider.addInProgressRequest(rriDataReceiver);
|
||||
|
||||
rriData.clear();
|
||||
sqiData.clear();
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
endTime = 0;
|
||||
openOrClose((byte) 3);
|
||||
|
||||
this.measureTimer = new CountDownTimer(60000, 1000) {
|
||||
public void onTick(long j) {
|
||||
if (callback != null) {
|
||||
callback.onProgress(j);
|
||||
}
|
||||
}
|
||||
|
||||
public void onFinish() {
|
||||
stopMeasurements(false);
|
||||
}
|
||||
};
|
||||
this.measureTimer.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void stopMeasurements(boolean error) {
|
||||
openOrClose((byte) 4);
|
||||
endTime = System.currentTimeMillis();
|
||||
CountDownTimer timer = this.measureTimer;
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
this.measureTimer = null;
|
||||
}
|
||||
this.supportProvider.removeInProgressRequests(rriDataReceiver);
|
||||
if (error) {
|
||||
if (callback != null) {
|
||||
callback.onError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOG.info("startTime = {}", startTime);
|
||||
LOG.info("endTime = {}", endTime);
|
||||
LOG.info("len = {}", rriData.size());
|
||||
LOG.info("rri data = {}", Arrays.toString(rriData.toArray()));
|
||||
LOG.info("sqi data = {}", Arrays.toString(sqiData.toArray()));
|
||||
int signalTime = (int) (endTime - startTime) / 1000;
|
||||
|
||||
HuaweiStressHRVCalculation hrvCalc = new HuaweiStressHRVCalculation();
|
||||
float[] hvrParams = hrvCalc.calculateStressHRVParameters(rriData, sqiData, signalTime);
|
||||
if (hvrParams == null || hvrParams.length != 10) {
|
||||
if (callback != null) {
|
||||
callback.onError();
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOG.info("hvrParams = {}", Arrays.toString(hvrParams));
|
||||
|
||||
float scoreFactor = HuaweiStressScoreCalculation.calculateScoreFactor(hvrParams);
|
||||
byte stressScore = HuaweiStressScoreCalculation.calculateNormalizedFinalScore(scoreFactor);
|
||||
|
||||
LOG.info("Stress Score = {}", stressScore);
|
||||
|
||||
HuaweiStressParser.StressData stressData = new HuaweiStressParser.StressData();
|
||||
|
||||
for (float p : hvrParams) {
|
||||
stressData.features.add(p);
|
||||
}
|
||||
// 10 and 11 elements are always 0
|
||||
stressData.features.add(0.0F);
|
||||
stressData.features.add(0.0F);
|
||||
|
||||
stressData.startTime = startTime;
|
||||
stressData.endTime = endTime;
|
||||
stressData.score = stressScore;
|
||||
stressData.scoreFactor = scoreFactor;
|
||||
|
||||
if (this.callback != null) {
|
||||
callback.onFinish(stressData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+148
-3
@@ -16,20 +16,21 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_HIGH_ALERT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_LOW_ALERT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_GOAL_STANDING_TIME_HOURS;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -65,11 +66,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiDictTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiGpsParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTruSleepParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
||||
@@ -154,6 +157,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetH
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMediumToStrengthThresholdRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetSkinTemperatureMeasurement;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetSpO2LowAlert;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetStressRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetTemperatureUnitSetting;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.StopFindPhoneRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.StopNotificationRequest;
|
||||
@@ -274,6 +278,7 @@ public class HuaweiSupportProvider {
|
||||
|
||||
protected HuaweiOTAManager huaweiOTAManager = new HuaweiOTAManager(this);
|
||||
|
||||
HuaweiStressCalibration stressCalibration = null;
|
||||
|
||||
public HuaweiCoordinatorSupplier getCoordinator() {
|
||||
return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator());
|
||||
@@ -1139,6 +1144,12 @@ public class HuaweiSupportProvider {
|
||||
case HuaweiConstants.PREF_HUAWEI_SPO_LOW_ALERT:
|
||||
setSpoLowAlert();
|
||||
break;
|
||||
case HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH:
|
||||
setStress();
|
||||
break;
|
||||
case HuaweiConstants.PREF_HUAWEI_STRESS_CALIBRATE:
|
||||
calibrateStress();
|
||||
break;
|
||||
case DeviceSettingsPreferenceConst.PREF_FORCE_ENABLE_SMART_ALARM:
|
||||
getAlarms();
|
||||
break;
|
||||
@@ -1344,7 +1355,7 @@ public class HuaweiSupportProvider {
|
||||
getFitnessTotalsRequest.setFinalizeReq(new RequestCallback() {
|
||||
@Override
|
||||
public void call() {
|
||||
if (!downloadTruSleepData(start, end))
|
||||
if (!(downloadTruSleepData(start, end) && downloadStressData(start, end)))
|
||||
syncState.setActivitySync(false);
|
||||
}
|
||||
|
||||
@@ -2275,6 +2286,98 @@ public class HuaweiSupportProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private void setStress() {
|
||||
boolean automaticStressEnabled = GBApplication
|
||||
.getDeviceSpecificSharedPrefs(getDevice().getAddress())
|
||||
.getBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, false);
|
||||
if(automaticStressEnabled && getLastStressData() == null) {
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
editor.putBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, false);
|
||||
editor.apply();
|
||||
GB.toast(context, context.getString(R.string.huawei_stress_no_calibration_data), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SetStressRequest req = new SetStressRequest(this, automaticStressEnabled);
|
||||
req.doPerform();
|
||||
} catch (IOException e) {
|
||||
GB.toast(context, "Failed to set stress", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Failed to set stress", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void calibrateStress() {
|
||||
|
||||
if(stressCalibration != null) {
|
||||
GB.toast(context.getString(R.string.huawei_stress_calibrate_in_progress), Toast.LENGTH_SHORT, GB.INFO);
|
||||
return;
|
||||
}
|
||||
|
||||
stressCalibration = new HuaweiStressCalibration(this);
|
||||
boolean ret = stressCalibration.startMeasurements(new HuaweiStressCalibration.HuaweiStressCalibrateCallback() {
|
||||
@Override
|
||||
public void onFinish(HuaweiStressParser.StressData stressData) {
|
||||
stressCalibration = null;
|
||||
GB.toast(context.getString(R.string.huawei_stress_calibrate_done), Toast.LENGTH_SHORT, GB.INFO);
|
||||
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_RESULT);
|
||||
String str = HuaweiStressParser.stressDataToJsonStr(stressData);
|
||||
if(!TextUtils.isEmpty(str)) {
|
||||
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, false);
|
||||
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_DATA, str);
|
||||
} else {
|
||||
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, true);
|
||||
}
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(long j) {
|
||||
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_UPDATE);
|
||||
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_PROGRESS, j);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
stressCalibration = null;
|
||||
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_RESULT);
|
||||
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, true);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
GB.toast(context.getString(R.string.huawei_stress_calibrate_error), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
});
|
||||
if(ret) {
|
||||
GB.toast(context.getString(R.string.huawei_stress_calibrate_started), Toast.LENGTH_SHORT, GB.INFO);
|
||||
} else {
|
||||
GB.toast(context.getString(R.string.huawei_stress_calibrate_in_progress), Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
public void storeLastStressData(HuaweiStressParser.StressData data) {
|
||||
String str = HuaweiStressParser.stressDataToJsonStr(data);
|
||||
if(TextUtils.isEmpty(str)) {
|
||||
LOG.error("Failed to store stress data");
|
||||
return;
|
||||
}
|
||||
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac);
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
editor.putString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, str);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public HuaweiStressParser.StressData getLastStressData() {
|
||||
String str = GBApplication
|
||||
.getDeviceSpecificSharedPrefs(this.getDevice().getAddress())
|
||||
.getString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, "");
|
||||
if(TextUtils.isEmpty(str)) {
|
||||
LOG.error("Failed to get saved stress data");
|
||||
return null;
|
||||
}
|
||||
return HuaweiStressParser.stressDataFromJsonStr(str);
|
||||
}
|
||||
|
||||
public void sendDebugRequest() {
|
||||
try {
|
||||
LOG.debug("Send debug request");
|
||||
@@ -2615,6 +2718,47 @@ public class HuaweiSupportProvider {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean downloadStressData(int start, int end) {
|
||||
if (!getHuaweiCoordinator().supportsAutoStress())
|
||||
return false;
|
||||
|
||||
huaweiFileDownloadManager.addToQueue(HuaweiFileDownloadManager.FileRequest.rriFileRequest(
|
||||
getHuaweiCoordinator().getSupportsRriNewSync(),
|
||||
start,
|
||||
end,
|
||||
new HuaweiFileDownloadManager.FileDownloadCallback() {
|
||||
@Override
|
||||
public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) {
|
||||
if (fileRequest.getData().length != 0) {
|
||||
LOG.debug("Parsing stress file");
|
||||
HuaweiStressParser.RriFileData results = HuaweiStressParser.parseRri(fileRequest.getData());
|
||||
LOG.info("stress result: {}", results);
|
||||
// TODO: process and save
|
||||
if(results != null && !results.stressData.isEmpty()) {
|
||||
HuaweiStressParser.StressData stressData = results.stressData.get(results.stressData.size() - 1);
|
||||
LOG.info("Last stored stress data: {}", stressData);
|
||||
HuaweiStressParser.StressData currentStressData = getLastStressData();
|
||||
if(currentStressData == null || stressData.endTime > currentStressData.endTime) {
|
||||
storeLastStressData(stressData);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG.debug("Stress file empty");
|
||||
}
|
||||
syncState.setActivitySync(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadException(HuaweiFileDownloadManager.HuaweiFileDownloadException e) {
|
||||
super.downloadException(e);
|
||||
syncState.setActivitySync(false);
|
||||
}
|
||||
}
|
||||
), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void endOfWorkoutSync() {
|
||||
this.syncState.setWorkoutSync(false);
|
||||
}
|
||||
@@ -2744,6 +2888,7 @@ public class HuaweiSupportProvider {
|
||||
syncState.updateState(needSync);
|
||||
}
|
||||
|
||||
|
||||
public void onTestNewFunction() {
|
||||
// Show to user
|
||||
gbDevice.setBusyTask("Downloading files...");
|
||||
|
||||
+6
@@ -57,6 +57,8 @@ public class GetFileDownloadInitRequest extends Request {
|
||||
return FileDownloadService2C.FileType.SLEEP_STATE;
|
||||
case SLEEP_DATA:
|
||||
return FileDownloadService2C.FileType.SLEEP_DATA;
|
||||
case RRI:
|
||||
return FileDownloadService2C.FileType.RRI;
|
||||
case GPS:
|
||||
return FileDownloadService2C.FileType.GPS;
|
||||
default:
|
||||
@@ -70,6 +72,8 @@ public class GetFileDownloadInitRequest extends Request {
|
||||
return HuaweiFileDownloadManager.FileType.SLEEP_STATE;
|
||||
case SLEEP_DATA:
|
||||
return HuaweiFileDownloadManager.FileType.SLEEP_DATA;
|
||||
case RRI:
|
||||
return HuaweiFileDownloadManager.FileType.RRI;
|
||||
case GPS:
|
||||
return HuaweiFileDownloadManager.FileType.GPS;
|
||||
default:
|
||||
@@ -92,6 +96,8 @@ public class GetFileDownloadInitRequest extends Request {
|
||||
return new FileDownloadService0A.FileDownloadInit.SleepFilesRequest(paramsProvider, request.getStartTime(), request.getEndTime()).serialize();
|
||||
else if (this.request.getFileType() == HuaweiFileDownloadManager.FileType.GPS)
|
||||
return new FileDownloadService0A.FileDownloadInit.GpsFileRequest(paramsProvider, request.getWorkoutId()).serialize();
|
||||
else if(this.request.getFileType() == HuaweiFileDownloadManager.FileType.RRI)
|
||||
return new FileDownloadService0A.FileDownloadInit.RriFileRequest(paramsProvider, request.getStartTime(), request.getEndTime()).serialize();
|
||||
else
|
||||
throw new RequestCreationException("Unknown file type");
|
||||
}
|
||||
|
||||
+4
-4
@@ -24,22 +24,22 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupport
|
||||
|
||||
public class GetFileParametersRequest extends Request {
|
||||
|
||||
private final boolean truSleep;
|
||||
private final byte fileType;
|
||||
|
||||
private int maxBlockSize;
|
||||
private int timeout;
|
||||
|
||||
public GetFileParametersRequest(HuaweiSupportProvider support, boolean truSleep) {
|
||||
public GetFileParametersRequest(HuaweiSupportProvider support, byte fileType) {
|
||||
super(support);
|
||||
this.serviceId = FileDownloadService0A.id;
|
||||
this.commandId = FileDownloadService0A.FileParameters.id;
|
||||
this.truSleep = truSleep;
|
||||
this.fileType = fileType;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new FileDownloadService0A.FileParameters.Request(paramsProvider, truSleep).serialize();
|
||||
return new FileDownloadService0A.FileParameters.Request(paramsProvider, fileType).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ public class GetSettingRelatedRequest extends Request {
|
||||
LOG.debug("handle Setting Related");
|
||||
|
||||
supportProvider.getHuaweiCoordinator().setSupportsTruSleepNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).truSleepNewSync);
|
||||
supportProvider.getHuaweiCoordinator().setSupportsRriNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).rriNewSync);
|
||||
supportProvider.getHuaweiCoordinator().setSupportsGpsNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).gpsNewSync);
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.HrRriTest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendHROpenCloseRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendHROpenCloseRequest.class);
|
||||
|
||||
private final byte type;
|
||||
|
||||
public SendHROpenCloseRequest(HuaweiSupportProvider support, byte type) {
|
||||
super(support);
|
||||
this.serviceId = HrRriTest.id;
|
||||
this.commandId = HrRriTest.OpenOrClose.id;
|
||||
this.type = type;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new HrRriTest.OpenOrClose.Request(paramsProvider, this.type).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Stress;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SetStressRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SetStressRequest.class);
|
||||
|
||||
private final boolean automaticStressEnabled;
|
||||
public SetStressRequest(HuaweiSupportProvider support, boolean automaticStressEnabled) {
|
||||
super(support);
|
||||
this.serviceId = Stress.id;
|
||||
this.commandId = Stress.AutomaticStress.id;
|
||||
this.addToResponse = false;
|
||||
this.automaticStressEnabled = automaticStressEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
byte status;
|
||||
List<Float> features = null;
|
||||
byte score = 0;
|
||||
long time = 0;
|
||||
if (automaticStressEnabled) {
|
||||
status = 1;
|
||||
HuaweiStressParser.StressData stressData = supportProvider.getLastStressData();
|
||||
if(stressData == null || stressData.score == 0 || stressData.endTime == 0) {
|
||||
throw new RequestCreationException("No data for activate");
|
||||
}
|
||||
features = stressData.features;
|
||||
score = stressData.score;
|
||||
time = stressData.endTime;
|
||||
LOG.info("Attempting to enable automatic stress. Stress data {}", stressData);
|
||||
} else {
|
||||
LOG.info("Attempting to disable automatic stress");
|
||||
status = 2;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Stress.AutomaticStress.Request(paramsProvider, status, score, features, (int) (time / 1000)).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<RelativeLayout
|
||||
android:id="@+id/huawei_stress_measure"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/huawei_stress_countdown"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/huawei_stress_calibrate_information" />
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/huawei_stress_countdown"
|
||||
android:layout_width="270dp"
|
||||
android:layout_height="270dp"
|
||||
app:indicatorSize="200dp"
|
||||
app:indicatorColor="@color/accent"
|
||||
app:trackThickness="10dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:indeterminate="false"
|
||||
android:max="60"
|
||||
android:progress="60" />
|
||||
<TextView
|
||||
android:id="@+id/huawei_stress_countdown_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="60:00"
|
||||
android:textSize="40sp" />
|
||||
<Button
|
||||
android:id="@+id/huawei_stress_calibrate_start"
|
||||
android:layout_width="270dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_below="@+id/huawei_stress_countdown"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/start" />
|
||||
</RelativeLayout>
|
||||
<RelativeLayout
|
||||
android:id="@+id/huawei_stress_result"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
<Button
|
||||
android:id="@+id/huawei_stress_calibrate_again"
|
||||
android:layout_width="270dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:layout_above="@+id/huawei_stress_score"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/huawei_stress_calibrate_again"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/huawei_stress_score"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="@string/n_a"
|
||||
android:textSize="80sp" />
|
||||
<CheckBox
|
||||
android:id="@+id/huawei_stress_calibrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/huawei_stress_score"
|
||||
android:checked="false"
|
||||
android:text="@string/huawei_stress_calibrate_adjust_score" />
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/huawei_stress_calibrate_slider"
|
||||
android:layout_below="@+id/huawei_stress_calibrate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="40"
|
||||
android:valueTo="70"
|
||||
android:value="55"
|
||||
android:stepSize="1" />
|
||||
<Button
|
||||
android:id="@+id/huawei_stress_calibrate_finish"
|
||||
android:layout_width="270dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_below="@+id/huawei_stress_calibrate_slider"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/huawei_stress_calibrate_finish"
|
||||
/>
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
@@ -3751,4 +3751,20 @@
|
||||
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
|
||||
<string name="ultrahuman_airplane_mode_title">airplane mode</string>
|
||||
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string>
|
||||
|
||||
<string name="huawei_stress_test_enable">Automatic stress test</string>
|
||||
<string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string>
|
||||
<string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string>
|
||||
<string name="huawei_stress_calibrate">Calibrate</string>
|
||||
<string name="huawei_stress_calibrate_summary">Calibrate automatic stress test</string>
|
||||
<string name="huawei_stress_calibrate_started">Stress calibration has been started</string>
|
||||
<string name="huawei_stress_calibrate_in_progress">Stress calibration already in progress</string>
|
||||
<string name="huawei_stress_calibrate_done">Stress calibration is done</string>
|
||||
<string name="huawei_stress_calibrate_error">Stress calibration error</string>
|
||||
<string name="huawei_stress_calibrate_information">For optimal results rest for a while before starting a test and keep still during the test. \n Fasten the device tightly, with the screen facing up and the sensor in direct contact with your skin.</string>
|
||||
<string name="huawei_stress_calibrate_start">Start</string>
|
||||
<string name="huawei_stress_calibrate_finish">Finish</string>
|
||||
<string name="huawei_stress_calibrate_again">Calibrate again</string>
|
||||
<string name="huawei_stress_calibrate_adjust_score">Adjust score</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/ic_heart"
|
||||
android:key="screen_huawei_stress"
|
||||
android:persistent="false"
|
||||
android:title="@string/menuitem_stress">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_huawei_stress_switch"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:title="@string/huawei_stress_test_enable"
|
||||
android:summary="@string/huawei_stress_test_enable_summary" />
|
||||
<Preference
|
||||
android:key="pref_huawei_stress_perform_calibrate"
|
||||
android:title="@string/huawei_stress_calibrate"
|
||||
android:summary="@string/huawei_stress_calibrate_summary">
|
||||
</Preference>
|
||||
|
||||
</PreferenceScreen>
|
||||
</androidx.preference.PreferenceScreen>
|
||||
Reference in New Issue
Block a user