ultrahuman: redesign device support

This commit is contained in:
Thomas Kuehne
2025-08-26 21:38:51 +02:00
committed by José Rebelo
parent f5e015ff57
commit 42033aad18
14 changed files with 983 additions and 579 deletions
@@ -23,9 +23,10 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,12 +34,15 @@ import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.Locale;
import lineageos.weather.util.WeatherUtils;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
// TODO: verify GUI lifecycle
// TODO: updates when multiple devices are active
// TODO: polish and localize the GUI
public class UltrahumanBreathingActivity extends AbstractGBActivity {
@@ -47,25 +51,44 @@ public class UltrahumanBreathingActivity extends AbstractGBActivity {
private TextView UiStatus;
private TextView UiHR;
private TextView UiHRV;
//private TextView UiMystery;
private TextView UiTemp;
private TextView UiTime;
private TextView UiBatGadget;
private boolean MetricUnits;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ultrahuman_breathing);
findViewById(R.id.ultrahuman_breathing_start).setOnClickListener(new StartListener());
findViewById(R.id.ultrahuman_breathing_stop).setOnClickListener(new StopListener());
UiStatus = findViewById(R.id.ultrahuman_breathing_status);
UiHR = findViewById(R.id.ultrahuman_breathing_HR);
UiTemp = findViewById(R.id.ultrahuman_breathing_temp);
UiHRV = findViewById(R.id.ultrahuman_breathing_HRV);
UiTime = findViewById(R.id.ultrahuman_breathing_time);
findViewById(R.id.ultrahuman_exercise_start1).setOnClickListener(new ExerciseClick(UltrahumanExercise.BREATHING_START));
findViewById(R.id.ultrahuman_exercise_stop1).setOnClickListener(new ExerciseClick(UltrahumanExercise.BREATHING_STOP));
UiStatus = findViewById(R.id.ultrahuman_exercise_type);
UiTime = findViewById(R.id.ultrahuman_exercise_time);
UiHR = findViewById(R.id.ultrahuman_exercise_hr_current);
UiTemp = findViewById(R.id.ultrahuman_exercise_temperature_current);
UiBatGadget = findViewById(R.id.ultrahuman_exercise_batGadget);
UiHRV = findViewById(R.id.ultrahuman_exercise_hrv_current);
//UiMystery = findViewById(R.id.ultrahuman_breathing_mystery);
IntentFilter filter = new IntentFilter();
filter.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
filter.addAction(UltrahumanConstants.ACTION_EXERCISE_UPDATE);
ContextCompat.registerReceiver(getApplicationContext(), UpdateReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(UpdateReceiver, filter);
final GBPrefs prefs = GBApplication.getPrefs();
final String metric = getString(R.string.p_unit_metric);
final String unitSystem = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, metric);
MetricUnits = unitSystem.equals(metric);
Button temperatureUom = findViewById(R.id.ultrahuman_exercise_temperature_uom);
temperatureUom.setText(MetricUnits ? R.string.unit_celsius : R.string.unit_fahrenheit);
changeExercise(UltrahumanExercise.CHECK);
}
@@ -80,55 +103,80 @@ public class UltrahumanBreathingActivity extends AbstractGBActivity {
@Override
public void onDestroy() {
getApplicationContext().unregisterReceiver(UpdateReceiver);
LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(UpdateReceiver);
super.onDestroy();
}
private class StartListener implements View.OnClickListener {
@Override
public void onClick(View v) {
changeExercise(UltrahumanExercise.BREATHING_START);
private class ExerciseClick implements View.OnClickListener {
final UltrahumanExercise Exercise;
ExerciseClick(UltrahumanExercise exercise){
Exercise = exercise;
}
}
private class StopListener implements View.OnClickListener {
@Override
public void onClick(View v) {
changeExercise(UltrahumanExercise.BREATHING_STOP);
changeExercise(Exercise);
}
}
private class ExerciseUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
UltrahumanExerciseData data = (UltrahumanExerciseData) intent.getSerializableExtra(UltrahumanConstants.EXTRA_EXERCISE);
UltrahumanExerciseData data = (UltrahumanExerciseData) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
if (data != null) {
String update = String.format(Locale.US, "b:%s%% t:%x", data.BatteryLevel, 0xFF & data.Exercise);
UiStatus.setText(update);
String exercise = String.format(Locale.ROOT, "[%02x]", 0xFF & data.Exercise);
UiStatus.setText(exercise);
if(data.BatteryLevel > -1) {
String batGadget = getString(R.string.ultrahuman_exercise_charge, data.BatteryLevel);
UiBatGadget.setText(batGadget);
}
if (data.Timestamp > -1) {
// todo - display local time
final Calendar calendar = DateTimeUtils.getCalendarUTC();
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(data.Timestamp * 1000L);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
String time = String.format(Locale.US, "%02d:%02d:%02d", hour, minute, second);
final String time = getString(R.string.ultrahuman_exercise_time_format, hour, minute, second);
UiTime.setText(time);
if (data.HR > -1) {
String hr = getString(R.string.ultrahuman_exercise_hr_format, data.HR);
UiHR.setText(hr);
} else {
UiHR.setText("");
}
if (data.Temperature > -1) {
double degree;
if (MetricUnits) {
degree = data.Temperature;
} else {
degree = WeatherUtils.celsiusToFahrenheit(data.Temperature);
}
String temp = getString(R.string.ultrahuman_exercise_temperature_format, degree);
UiTemp.setText(temp);
} else {
UiTemp.setText("");
}
if (data.HRV > 0) {
String hrv = getString(R.string.ultrahuman_exercise_hrv_format, data.HRV);
UiHRV.setText(hrv);
} else if (data.MeasurementType != 0x72) {
UiHRV.setText("");
}
}
if (data.HR > -1) {
String hr = String.format(Locale.US, "%s bpm", data.HR);
UiHR.setText(hr);
}
if (data.HRV > 0) {
String hrv = String.format(Locale.US, "%s ms", data.HRV);
UiHRV.setText(hrv);
}
if (data.Temperature > -1) {
String temp = String.format(Locale.US, "%.3f °C", data.Temperature);
UiTemp.setText(temp);
}
/*if (data.Mystery != null) {
UiMystery.setText(data.Mystery + " ?");
} else {
UiMystery.setText("");
}*/
}
}
}
@@ -33,8 +33,10 @@ public final class UltrahumanConstants {
public static final byte OPERATION_ACTIVATE_AIRPLANE_MODE = 0x70;
public static final byte OPERATION_BREATHING_START = 0x72;
public static final byte OPERATION_BREATHING_STOP = 0x74;
public static final byte OPERATION_DISABLE_POWERSAVE = (byte) 0xD1;
public static final byte OPERATION_ENABLE_POWERSAVE = (byte) 0xD2;
public static final byte OPERATION_ENABLE_SPO2 = (byte) 0xD1;
public static final byte OPERATION_DISABLE_SPO2 = (byte) 0xD2;
public static final byte OPERATION_EXERCISE_START = 0x62;
public static final byte OPERATION_EXERCISE_STOP = 0x64;
public static final byte OPERATION_GET_FIRST_RECORDING_NR = 0x07;
public static final byte OPERATION_GET_LAST_RECORDING_NR = 0x08;
public static final byte OPERATION_GET_RECORDINGS = 0x04;
@@ -43,15 +45,15 @@ public final class UltrahumanConstants {
public static final byte OPERATION_SETTIME = 0x02;
public static final UUID UUID_SERVICE_DATA = UUID.fromString("86f66000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_DATA = UUID.fromString("86f66001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_DATA = UUID.fromString("86f66001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_SERVICE_REQUEST = UUID.fromString("86f65000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_COMMAND = UUID.fromString("86f65001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_RESPONSE = UUID.fromString("86f65002-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_COMMAND = UUID.fromString("86f65001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_RESPONSE = UUID.fromString("86f65002-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_SERVICE_STATE = UUID.fromString("86f61000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_STATE = UUID.fromString("86f61001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_STATE = UUID.fromString("86f61001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_SERVICE_TODO = UUID.fromString("86f63000-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_CHARACTERISTIC_TODO = UUID.fromString("86f63001-f706-58a0-95b2-1fb9261e4dc7");
public static final UUID UUID_TODO = UUID.fromString("86f63001-f706-58a0-95b2-1fb9261e4dc7");
}
@@ -20,6 +20,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
@@ -33,10 +35,12 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
private final int Description;
private final int Question;
@Nullable
private final String IntentAction;
@Nullable
private final Class<?> IntentClass;
public UltrahumanDeviceCardAction(int icon, int description, int question, String action) {
UltrahumanDeviceCardAction(int icon, int description, int question, String action) {
Icon = icon;
Description = description;
Question = question;
@@ -44,7 +48,7 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
IntentClass = null;
}
public UltrahumanDeviceCardAction(int icon, int description, Class<?> cls) {
UltrahumanDeviceCardAction(int icon, int description, Class<?> cls) {
Icon = icon;
Description = description;
Question = 0;
@@ -64,7 +68,9 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
@Override
public void onClick(GBDevice device, Context context) {
if (Question != 0) {
if (Question == 0) {
sendIntent(device, context);
} else {
new MaterialAlertDialogBuilder(context)
.setTitle(Description)
.setMessage(Question)
@@ -73,8 +79,6 @@ public class UltrahumanDeviceCardAction implements DeviceCardAction {
-> sendIntent(device, context))
.setNegativeButton(android.R.string.no, null)
.show();
} else {
sendIntent(device, context);
}
}
@@ -34,6 +34,7 @@ import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
import nodomain.freeyourgadget.gadgetbridge.devices.GenericHeartRateSampleProvider;
@@ -80,7 +81,7 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
editor.putBoolean(GBPrefs.DEVICE_CONNECT_BACK, true);
editor.putBoolean(GBPrefs.DEVICE_AUTO_RECONNECT, true);
// when the battery is low the gadget loses it's clock
// the gadget loses it's clock when the battery is low
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_TIME_SYNC, true);
// O2 measurement with smart rings is still work in progress
@@ -132,6 +133,13 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
return UltrahumanDeviceSupport.class;
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(GBDevice device) {
final DeviceSpecificSettings settings = new DeviceSpecificSettings();
settings.addRootScreen(R.xml.devicesettings_ultrahuman_air);
return settings;
}
@Override
public String getManufacturer() {
return "Ultrahuman Healthcare Pvt Ltd";
@@ -167,11 +175,6 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
return Pattern.compile("^UH_[0-9A-F]{12}$");
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{R.xml.devicesettings_ultrahuman_air};
}
@Override
public TimeSampleProvider<? extends TemperatureSample> getTemperatureSampleProvider(GBDevice device, DaoSession session) {
return new GenericTemperatureSampleProvider(device, session);
@@ -188,7 +191,7 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
}
@Override
public boolean supportsActivityDataFetching(final GBDevice device) {
public boolean supportsActivityDataFetching(@NonNull final GBDevice device) {
return true;
}
@@ -198,32 +201,37 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
}
@Override
public boolean supportsContinuousTemperature(final GBDevice device) {
public boolean supportsContinuousTemperature(@NonNull final GBDevice device) {
return true;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
public boolean supportsHeartRateMeasurement(@NonNull GBDevice device) {
return true;
}
@Override
public boolean supportsHrvMeasurement(final GBDevice device) {
public boolean supportsHrvMeasurement(@NonNull final GBDevice device) {
return true;
}
@Override
public boolean supportsManualHeartRateMeasurement(final GBDevice device) {
public boolean supportsManualHeartRateMeasurement(@NonNull final GBDevice device) {
return false;
}
@Override
public boolean supportsRealtimeData(@NonNull GBDevice device) {
return true;
}
@Override
public boolean supportsSleepMeasurement(@NonNull GBDevice device) {
return false;
}
@Override
public boolean supportsSpo2(GBDevice device) {
public boolean supportsSpo2(@NonNull GBDevice device) {
return true;
}
@@ -233,7 +241,7 @@ public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator {
}
@Override
public boolean supportsTemperatureMeasurement(final GBDevice device) {
public boolean supportsTemperatureMeasurement(@NonNull final GBDevice device) {
return true;
}
@@ -18,9 +18,11 @@
package nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman;
public enum UltrahumanExercise {
CHECK((byte) 0),
CHECK(UltrahumanConstants.OPERATION_CHECK_DATA),
BREATHING_START(UltrahumanConstants.OPERATION_BREATHING_START),
BREATHING_STOP(UltrahumanConstants.OPERATION_BREATHING_STOP);
BREATHING_STOP(UltrahumanConstants.OPERATION_BREATHING_STOP),
EXERCISE_START(UltrahumanConstants.OPERATION_EXERCISE_START),
EXERCISE_STOP(UltrahumanConstants.OPERATION_EXERCISE_STOP);
public final byte Code;
@@ -19,20 +19,31 @@ package nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman;
import java.io.Serializable;
public class UltrahumanExerciseData implements Serializable {
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
/**
* Helper for DeviceService#ACTION_REALTIME_SAMPLES and UltrahumanConstants.ACTION_EXERCISE_UPDATE intents.
*/
public class UltrahumanExerciseData implements ActivitySample, Serializable{
public int BatteryLevel;
public int Exercise;
public int Timestamp;
public int MeasurementType;
public int HR;
public int HRV;
public float Temperature;
public int Timestamp;
public String Mystery;
public UltrahumanExerciseData() {
BatteryLevel = -1;
Exercise = -1;
HR = -1;
HRV = -1;
Temperature = -1;
Temperature = -1.0f;
Timestamp = -1;
}
@@ -41,4 +52,64 @@ public class UltrahumanExerciseData implements Serializable {
BatteryLevel = batteryLevel;
Exercise = exercise;
}
public UltrahumanExerciseData(int batteryLevel, int exercise, byte type) {
this(batteryLevel,exercise);
MeasurementType = type;
}
@Override
public SampleProvider<?> getProvider() {
return null;
}
@Override
public int getRawKind() {
return 0;
}
@Override
public ActivityKind getKind() {
return ActivityKind.UNKNOWN;
}
@Override
public int getRawIntensity() {
return 0;
}
@Override
public float getIntensity() {
return 0.0f;
}
@Override
public int getSteps() {
return 0;
}
@Override
public int getDistanceCm() {
return 0;
}
@Override
public int getActiveCalories() {
return 0;
}
@Override
public int getHeartRate() {
return HR;
}
@Override
public void setHeartRate(int value) {
HR = value;
}
@Override
public int getTimestamp() {
return Timestamp;
}
}
@@ -35,18 +35,15 @@ public class UltrahumanActivitySampleProvider extends AbstractSampleProvider<Ult
}
public static ActivityKind normalizeTypeStatic(int rawType) {
switch (rawType) {
case 5:
return ActivityKind.BREATHWORK;
case 6:
return ActivityKind.EXERCISE;
default:
return (rawType >= 100) ? ActivityKind.NOT_WORN : ActivityKind.UNKNOWN;
}
return switch (rawType) {
case 5 -> ActivityKind.BREATHWORK;
case 6 -> ActivityKind.EXERCISE;
default -> (rawType >= 100) ? ActivityKind.NOT_WORN : ActivityKind.UNKNOWN;
};
}
public static float normalizeIntensityStatic(int rawIntensity) {
return rawIntensity / 150f;
return rawIntensity / 150.0f;
}
@NonNull
@@ -80,18 +77,13 @@ public class UltrahumanActivitySampleProvider extends AbstractSampleProvider<Ult
@Override
public int toRawActivityKind(ActivityKind activityKind) {
switch (activityKind) {
case UNKNOWN:
return 1;
case BREATHWORK:
return 5;
case EXERCISE:
return 6;
case NOT_WORN:
return 100;
default:
return 0;
}
return switch (activityKind) {
case UNKNOWN -> 1;
case BREATHWORK -> 5;
case EXERCISE -> 6;
case NOT_WORN -> 100;
default -> 0;
};
}
@Override
@@ -22,11 +22,11 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants;
public enum UltrahumanCharacteristic {
COMMAND(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND),
RESPONSE(UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE),
STATE(UltrahumanConstants.UUID_CHARACTERISTIC_STATE),
DATA(UltrahumanConstants.UUID_CHARACTERISTIC_DATA),
TODO(UltrahumanConstants.UUID_CHARACTERISTIC_TODO);
COMMAND(UltrahumanConstants.UUID_COMMAND),
RESPONSE(UltrahumanConstants.UUID_RESPONSE),
STATE(UltrahumanConstants.UUID_STATE),
DATA(UltrahumanConstants.UUID_DATA),
TODO(UltrahumanConstants.UUID_TODO);
public final UUID uuid;
@@ -0,0 +1,39 @@
/* Copyright (C) 2025 Thomas Kuehne
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman
import android.bluetooth.BluetoothGattCharacteristic
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWriteAction
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils
/** calculate date/time on the fly to avoid setting an outdated value */
class UltrahumanWriteTime(characteristic: BluetoothGattCharacteristic) :
ConditionalWriteAction(characteristic) {
override fun checkCondition(): ByteArray {
val epoc: Long = DateTimeUtils.getEpochSeconds()
val value = byteArrayOf(
UltrahumanConstants.OPERATION_SETTIME,
(epoc and 0xff).toByte(),
((epoc shr 8) and 0xff).toByte(),
((epoc shr 16) and 0xff).toByte(),
((epoc shr 24) and 0xff).toByte()
)
return value
}
}
@@ -26,6 +26,7 @@ import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Calendar;
import java.util.Date;
@@ -334,4 +335,17 @@ public class DateTimeUtils {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss", Locale.ROOT);
return format.format(date);
}
/// number of seconds since UTC epoch of 1970-01-01T00:00:00Z
public static long getEpochSeconds() {
final long epoc;
if (GBApplication.isRunningOreoOrLater()) {
epoc = Instant.now().getEpochSecond();
} else {
Calendar calendar = getCalendarUTC();
long millis = calendar.getTimeInMillis();
epoc = Math.round(millis / 1000.0d);
}
return epoc;
}
}
+1 -1
View File
@@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M190,840Q144,840 112,808Q80,776 80,730L80,556Q80,551 81,545.5Q82,540 84,535L188,266Q200,235 228,217.5Q256,200 289,200Q331,200 360.5,229.5Q390,259 390,301L390,362L312,440Q303,449 303.5,461Q304,473 313,482Q322,491 334.5,491Q347,491 356,482L450,388L450,110Q450,97 458.5,88.5Q467,80 480,80Q493,80 501.5,88.5Q510,97 510,110L510,388L605,483Q614,492 626.5,491.5Q639,491 648,482Q657,473 657,460.5Q657,448 648,439L570,362L570,301Q570,259 599.5,229.5Q629,200 671,200Q704,200 732,217.5Q760,235 772,266L876,535Q878,540 879,545.5Q880,551 880,556L880,730Q880,776 848,808Q816,840 770,840L630,840Q584,840 552,808Q520,776 520,730L520,664Q520,662 520.5,660Q521,658 521,656L541,504L480,442L418,504L439,656Q439,658 439.5,660Q440,662 440,664L440,730Q440,776 408,808Q376,840 330,840L190,840Z"/>
android:pathData="M200,840Q149,840 114.5,805.5Q80,771 80,720L80,553L185,272Q197,239 227,219.5Q257,200 292,200Q337,200 368.5,232.5Q400,265 400,311L400,360L320,360L320,311Q320,298 311,289Q302,280 290,280Q280,280 271.5,285.5Q263,291 260,300L160,568L160,720Q160,737 171.5,748.5Q183,760 200,760L320,760Q337,760 348.5,748.5Q360,737 360,720L360,640L440,640L440,720Q440,771 405,805.5Q370,840 320,840L200,840ZM759,840L639,840Q589,840 554,805.5Q519,771 519,720L519,640L599,640L599,720Q599,737 610.5,748.5Q622,760 639,760L759,760Q776,760 787.5,748.5Q799,737 799,720L799,568L699,300Q695,291 687,285.5Q679,280 669,280Q656,280 647.5,289Q639,298 639,311L639,360L559,360L559,311Q559,265 590.5,232.5Q622,200 667,200Q702,200 731.5,219.5Q761,239 774,272L879,553L879,720Q879,771 844,805.5Q809,840 759,840ZM320,504L320,504L320,504L320,504Q320,504 320,504Q320,504 320,504L320,504Q320,504 320,504Q320,504 320,504L320,504L320,504Q320,504 320,504Q320,504 320,504Q320,504 320,504Q320,504 320,504L320,504L320,504L320,504L320,504ZM639,504L639,504L639,504L639,504L639,504L639,504Q639,504 639,504Q639,504 639,504Q639,504 639,504Q639,504 639,504L639,504L639,504Q639,504 639,504Q639,504 639,504L639,504Q639,504 639,504Q639,504 639,504L639,504L639,504ZM480,457L376,560L320,504L440,384L440,80L520,80L520,384L640,504L583,560L480,457Z"/>
</vector>
@@ -1,75 +1,184 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
android:layout_height="match_parent">
<TextView
android:id="@+id/ultrahuman_breathing_status"
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="b:000% t:00"
android:textAlignment="center" />
android:columnCount="2">
<TextView
android:id="@+id/ultrahuman_breathing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="00:00:00"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_HR"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="000 bpm"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_temp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="00.000 °C"
android:textAlignment="center" />
<TextView
android:id="@+id/ultrahuman_breathing_HRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autoSizeTextType="uniform"
android:text="000 ms"
android:textAlignment="center" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="horizontal">
<TextView
android:id="@+id/ultrahuman_exercise_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowSpan="3"
android:layout_gravity="fill"
android:autoSizeTextType="uniform"
android:gravity="center"
android:text="00:00:00" />
<Button
android:id="@+id/ultrahuman_breathing_start"
android:id="@+id/ultrahuman_exercise_batGadget"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:text="@string/ultrahuman_breathing_start" />
android:enabled="false"
android:text="100 %" />
<Button
android:id="@+id/ultrahuman_breathing_stop"
android:id="@+id/ultrahuman_exercise_type"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:text="@string/ultrahuman_breathing_stop" />
</LinearLayout>
</LinearLayout>
android:clickable="false"
android:enabled="true"
android:text="DT" />
<Button
android:id="@+id/ultrahuman_exercise_batAndroid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:clickable="false"
android:enabled="false"
android:text="" />
<TextView
android:id="@+id/ultrahuman_exercise_hr_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowSpan="3"
android:layout_gravity="fill"
android:autoSizeTextType="uniform"
android:gravity="center"
android:text="80" />
<Button
android:id="@+id/ultrahuman_exercise_hr_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false"
android:text="" />
<Button
android:id="@+id/ultrahuman_exercise_hr_uom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:clickable="false"
android:text="BPM" />
<Button
android:id="@+id/ultrahuman_exercise_hr_min"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:text="" />
<TextView
android:id="@+id/ultrahuman_exercise_temperature_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowSpan="3"
android:layout_gravity="fill"
android:autoSizeTextType="uniform"
android:clickable="false"
android:gravity="center"
android:text="36.000" />
<Button
android:id="@+id/ultrahuman_exercise_temperature_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false"
android:text="" />
<Button
android:id="@+id/ultrahuman_exercise_temperature_uom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:clickable="false"
android:text="@string/unit_celsius" />
<Button
android:id="@+id/ultrahuman_exercise_temperature_min"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false"
android:text="" />
<TextView
android:id="@+id/ultrahuman_exercise_hrv_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowSpan="3"
android:layout_gravity="fill"
android:autoSizeTextType="uniform"
android:clickable="false"
android:gravity="center"
android:text="120" />
<Button
android:id="@+id/ultrahuman_exercise_hrv_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false"
android:text="" />
<Button
android:id="@+id/ultrahuman_exercise_hrv_uom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:clickable="false"
android:text="ms" />
<Button
android:id="@+id/ultrahuman_exercise_hrv_min"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false"
android:text="" />
<Button
android:id="@+id/ultrahuman_exercise_start1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_rowSpan="3"
android:layout_gravity="fill"
android:autoSizeTextType="uniform"
android:text="@string/start"
app:icon="@drawable/ic_play" />
<Button
android:id="@+id/ultrahuman_exercise_connection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false"
android:text="" />
<Button
android:id="@+id/ultrahuman_exercise_stop1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:text="@string/stop" />
<Button
android:id="@+id/ultrahuman_exercise_stop2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:enabled="false" />
</GridLayout>
</ScrollView>
+7
View File
@@ -4112,6 +4112,13 @@
<string name="ultrahuman_breathing_stop">Stop</string>
<string name="ultrahuman_unhandled_error_response">unhandled response %2$#02x %3$#02x for operation %1$#02x</string>
<string name="ultrahuman_unhandled_operation_response">unhandled operation response %s</string>
<string name="ultrahuman_exercise_time_format" translatable="false">%1$02d:%2$02d:%3$02d</string>
<string name="ultrahuman_exercise_hr_format" translatable="false">%d</string>
<string name="ultrahuman_exercise_temperature_format" translatable="false">%.3f</string>
<string name="ultrahuman_exercise_hrv_format" translatable="false">%d</string>
<string name="ultrahuman_exercise_charge" translatable="false">%d%%</string>
<string name="unit_celsius"></string>
<string name="unit_fahrenheit"></string>
<string name="weight_scale_jin">jin - 市斤</string>
<string name="weight_scale_jin_format">%1$.2f 市斤</string>
<string name="weight_scale_kilogram">kilogram - kg</string>