1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-07 21:57:57 +02:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Damien 'Psolyca' Gaignon
690d01dcac
[Huawei] Remove unneeded data 2024-05-08 22:48:47 +02:00
Damien 'Psolyca' Gaignon
02b052fcaf
[Huawei] Add Huawei Watch 4 Pro gadget
fix
2024-05-08 22:48:46 +02:00
ahormann
ac8d1ed6a0 New Device Soundcore Liberty 3 Pro (#3753)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3753
Co-authored-by: ahormann <ahormann@gmx.net>
Co-committed-by: ahormann <ahormann@gmx.net>
2024-05-07 22:39:13 +00:00
MrYoranimo
508a86b8ed Xiaomi: fix determining fall asleep time
Because the previous implementation of determining the time the user
falls asleep in a given time range would take the 24 hours in advance
into account, graphs displaying sleep data would erroneously indicate
that the user has been asleep since the start of the timeframe if
the user was asleep during the rollover of the time frame 24 hours
before.

This commit change the algorithm to only fetch the last sleep stage
sample and sleep range sample from the database that occurred before
the given time range. This saves having to process 24 hours worth of
samples before the time range in both cases, and prevents taking into
account irrelevant sleep ranges.
2024-05-07 13:33:52 +02:00
MrYoranimo
f581d57c01 Xiaomi: fix sleep stages not getting parsed from sleep details files
Not all packets use the payload length byte/short for the payload
length. Instead, some packets do not carry a payload, in which case
the payload length bytes are assumed to represent some state or flag.
Therefore, for packets with a type known not to carry a payload, the
payload extraction is skipped, allowing other packets to get
successfully parsed again.
2024-05-07 13:17:10 +02:00
José Rebelo
bed67ef1fb Xiaomi: Allow transliteration 2024-05-05 12:36:23 +01:00
José Rebelo
04237b7727 Prevent query for devices that have activity card disabled
Even if the activity card was disabled, all devices would be queried for
data. This slows down the UI when there are a lot of devices, especially
if multiple of them have data and only a few have the card enabled.
2024-05-04 23:51:19 +01:00
Alik Aslanyan
dc1ffdafcd Rework Armenian transliteration to handle more edge cases around mixed letters 2024-05-04 20:45:35 +04:00
José Rebelo
eaf7c03f61 Galaxy Buds 2: Fix recognition of some versions 2024-05-04 11:06:07 +01:00
José Rebelo
7892afa304 Xiaomi: Fix barometer
Thanks to MrYoranimo for the fix.
2024-05-03 22:27:57 +01:00
29 changed files with 1204 additions and 177 deletions

View File

@ -46,6 +46,7 @@ import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
@ -233,7 +234,8 @@ public class DevicesFragment extends Fragment {
protected void doInBackground(DBHandler db) {
for (GBDevice gbDevice : deviceList) {
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
if (coordinator.supportsActivityTracking()) {
boolean showActivityCard = GBApplication.getDevicePrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD, true);
if (coordinator.supportsActivityTracking() && showActivityCard) {
long[] stepsAndSleepData = getSteps(gbDevice, db);
deviceActivityHashMap.put(gbDevice.getAddress(), stepsAndSleepData);
}

View File

@ -395,7 +395,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
if (firstTimestamp == 0) firstTimestamp = sample.getTimestamp();
if (lastTimestamp == 0) lastTimestamp = sample.getTimestamp();
if ((sample.getHeartRate() < 10 || sample.getTimestamp() > lastTimestamp + dashboardData.hrIntervalSecs) && firstTimestamp != lastTimestamp) {
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
LOG.debug("Registered worn session from {} to {}", firstTimestamp, lastTimestamp);
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
if (sample.getHeartRate() < 10) {
firstTimestamp = 0;
@ -409,7 +409,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
lastTimestamp = sample.getTimestamp();
}
if (firstTimestamp != lastTimestamp) {
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
LOG.debug("Registered worn session from {} to {}", firstTimestamp, lastTimestamp);
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
}
}

View File

@ -343,6 +343,25 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SONY_PROTOCOL_VERSION = "pref_protocol_version";
public static final String PREF_SONY_ACTUAL_PROTOCOL_VERSION = "pref_actual_protocol_version";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL = "pref_sony_ambient_sound_control";
public static final String PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL = "pref_soundcore_ambient_sound_control";
public static final String PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING = "pref_adaptive_noise_cancelling";
public static final String PREF_SOUNDCORE_WIND_NOISE_REDUCTION= "pref_soundcore_wind_noise_reduction";
public static final String PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE = "pref_soundcore_transparency_vocal_mode";
public static final String PREF_SOUNDCORE_WEARING_DETECTION = "pref_soundcore_wearing_detection";
public static final String PREF_SOUNDCORE_WEARING_TONE = "pref_soundcore_wearing_tone";
public static final String PREF_SOUNDCORE_TOUCH_TONE = "pref_soundcore_touch_tone";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED = "pref_soundcore_control_single_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED = "pref_soundcore_control_double_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED = "pref_soundcore_control_triple_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED = "pref_soundcore_control_long_press_disabled";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT = "pref_soundcore_control_single_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT = "pref_soundcore_control_double_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT = "pref_soundcore_control_triple_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT = "pref_soundcore_control_long_press_action_left";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT = "pref_soundcore_control_single_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT = "pref_soundcore_control_double_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT = "pref_soundcore_control_triple_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT = "pref_soundcore_control_long_press_action_right";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE = "pref_sony_ambient_sound_control_button_mode";
public static final String PREF_SONY_FOCUS_VOICE = "pref_sony_focus_voice";
public static final String PREF_SONY_AMBIENT_SOUND_LEVEL = "pref_sony_ambient_sound_level";

View File

@ -563,6 +563,27 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES);
addPreferenceHandlerFor(PREF_SONY_ADAPTIVE_VOLUME_CONTROL);
addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP);
addPreferenceHandlerFor(PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL);
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING);
addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);

View File

@ -34,6 +34,7 @@ public enum DeviceSpecificSettingsScreen {
DATE_TIME("pref_screen_date_time", R.xml.devicesettings_root_date_time),
WORKOUT("pref_screen_workout", R.xml.devicesettings_root_workout),
HEALTH("pref_screen_health", R.xml.devicesettings_root_health),
TOUCH_OPTIONS("pref_screen_touch_options", R.xml.devicesettings_root_touch_options),
;
private final String key;

View File

@ -1316,6 +1316,13 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
}
private void setActivityCard(ViewHolder holder, final GBDevice device, long[] dailyTotals) {
boolean showActivityCard = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD, true);
holder.cardViewActivityCardLayout.setVisibility(showActivityCard ? View.VISIBLE : View.GONE);
if (!showActivityCard) {
return;
}
int steps = (int) dailyTotals[0];
int sleep = (int) dailyTotals[1];
ActivityUser activityUser = new ActivityUser();
@ -1336,8 +1343,6 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
setUpChart(holder.SleepTimeChart);
setChartsData(holder.SleepTimeChart, sleep, sleepGoalMinutes, context.getString(R.string.prefs_activity_in_device_card_sleep_title), String.format("%1s", getHM(sleep)), context);
boolean showActivityCard = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD, true);
holder.cardViewActivityCardLayout.setVisibility(showActivityCard ? View.VISIBLE : View.GONE);
boolean showActivitySteps = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD_STEPS, true);
boolean showActivitySleep = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD_SLEEP, true);

View File

@ -100,6 +100,25 @@ public abstract class AbstractTimeSampleProvider<T extends AbstractTimeSample> i
return samples.get(0);
}
public T getLastSampleBefore(final long timestampTo) {
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no sample
return null;
}
final Property deviceIdSampleProp = getDeviceIdentifierSampleProperty();
final Property timestampSampleProp = getTimestampSampleProperty();
final List<T> samples = getSampleDao().queryBuilder()
.where(deviceIdSampleProp.eq(dbDevice.getId()),
timestampSampleProp.le(timestampTo))
.orderDesc(getTimestampSampleProperty())
.limit(1)
.list();
return !samples.isEmpty() ? samples.get(0) : null;
}
@Nullable
@Override
public T getFirstSample() {

View File

@ -23,14 +23,13 @@ import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class GalaxyBuds2DeviceCoordinator extends GalaxyBudsGenericCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Galaxy Buds2 \\(.*");
// Some devices are just called "Buds2", others "Galaxy Buds2 (..."
return Pattern.compile("(Galaxy )?Buds2( \\(.*)?");
}
@Override

View File

@ -68,6 +68,7 @@ public final class HuaweiConstants {
public static final String HU_WATCHFIT_NAME = "huawei watch fit-";
public static final String HU_WATCHFIT2_NAME = "huawei watch fit 2-";
public static final String HU_WATCHULTIMATE_NAME = "huawei watch ultimate-";
public static final String HU_WATCH4PRO_NAME = "huawei watch 4 pro-";
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
public static final String PREF_HUAWEI_WORKMODE = "workmode";

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2024 Damien Gaignon
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatch4pro;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class HuaweiWatch4ProCoordinator extends HuaweiBRCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatch4ProCoordinator.class);
public HuaweiWatch4ProCoordinator() {
super();
getHuaweiCoordinator().setTransactionCrypted(true);
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HUAWEIWATCH4PRO;
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("(" + HuaweiConstants.HU_WATCH4PRO_NAME + ").*", Pattern.CASE_INSENSITIVE);
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_huawei_watch4pro;
}
}

View File

@ -1178,7 +1178,6 @@ public class DeviceConfig {
public JSONObject value;
public JSONObject payload;
public JSONObject version;
public byte step;
// public int operationCode; // TODO
@ -1203,7 +1202,6 @@ public class DeviceConfig {
try {
this.value = new JSONObject(this.tlv.getString(0x01));
this.payload = value.getJSONObject("payload");
this.version = payload.getJSONObject("version");
// Ugly, but should work
if (payload.has("isoSalt")) {
@ -1322,7 +1320,6 @@ public class DeviceConfig {
public long requestId;
public byte[] selfAuthId;
public String groupId;
public JSONObject version = null;
public JSONObject payload = null;
public JSONObject value = null;
@ -1341,7 +1338,6 @@ public class DeviceConfig {
try {
value = new JSONObject(this.tlv.getString(0x01));
payload = value.getJSONObject("payload");
version = payload.getJSONObject("version");
if (payload.has("isoSalt")) {
this.step = 1;

View File

@ -0,0 +1,83 @@
package nodomain.freeyourgadget.gadgetbridge.devices.soundcore;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcoreLiberty3ProDeviceSupport;
public class SoundcoreLiberty3ProCoordinator extends AbstractDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_soundcore_liberty3_pro;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_galaxy_buds;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_galaxy_buds_disabled;
}
@Override
public String getManufacturer() {
return "Anker";
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Soundcore Liberty 3 Pro");
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public int getBatteryCount() {
return 3;
}
@Override
public BatteryConfig[] getBatteryConfig() {
BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_buds_pro_case, R.string.battery_case);
BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
return new BatteryConfig[]{battery1, battery2, battery3};
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_sony_headphones_ambient_sound_control_button_modes);
deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_soundcore_touch_options);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_soundcore_headphones);
return deviceSpecificSettings;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return SoundcoreLiberty3ProDeviceSupport.class;
}
}

View File

@ -457,6 +457,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
if (getCannedRepliesSlotCount(device) > 0) {
notifications.add(R.xml.devicesettings_canned_dismisscall_16);
}
notifications.add(R.xml.devicesettings_transliteration);
//
// Calendar

View File

@ -97,60 +97,80 @@ public class XiaomiSampleProvider extends AbstractSampleProvider<XiaomiActivityS
return samples;
}
/**
* See {@link nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser}
*/
private static int getActivityKindForSample(final XiaomiSleepStageSample sample) {
switch (sample.getStage()) {
case 2:
return ActivityKind.TYPE_DEEP_SLEEP;
case 3:
return ActivityKind.TYPE_LIGHT_SLEEP;
case 4:
return ActivityKind.TYPE_REM_SLEEP;
default: // default to awake
return ActivityKind.TYPE_UNKNOWN;
}
}
/**
* Overlay sleep states on activity samples, since they are stored on a separate table.
*
* @implNote This currently needs to look back a further 24h, so that we are sure that we
* got the sleep start of a sleep session at the start of the samples, if any. This is especially
* noticeable if the charts are configured in a noon-to-noon setting. FIXME: This is not ideal,
* and we may need to rethink the way sleep samples are persisted in the database for Xiaomi devices.
* @implNote In order to determine whether a sleep session was ongoing at the start of the
* given range and what the detected sleep stage was at that time, the last sleep stage and
* sleep time sample before the given range will be queried and included in the results if
* found.
*/
public void overlaySleep(final List<XiaomiActivitySample> samples, final int timestamp_from, final int timestamp_to) {
final RangeMap<Long, Integer> stagesMap = new RangeMap<>();
final XiaomiSleepStageSampleProvider sleepStagesSampleProvider = new XiaomiSleepStageSampleProvider(getDevice(), getSession());
final List<XiaomiSleepStageSample> stageSamples = sleepStagesSampleProvider.getAllSamples(
timestamp_from * 1000L - 86400000L,
// Retrieve the last stage before this time range, as the user could have been asleep during
// the range transition
final XiaomiSleepStageSample lastSleepStageBeforeRange = sleepStagesSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
if (lastSleepStageBeforeRange != null) {
LOG.debug("Last sleep stage before range: ts={}, stage={}", lastSleepStageBeforeRange.getTimestamp(), lastSleepStageBeforeRange.getStage());
stagesMap.put(lastSleepStageBeforeRange.getTimestamp(), getActivityKindForSample(lastSleepStageBeforeRange));
}
// Retrieve all sleep stage samples during the range
final List<XiaomiSleepStageSample> sleepStagesInRange = sleepStagesSampleProvider.getAllSamples(
timestamp_from * 1000L,
timestamp_to * 1000L
);
if (!stageSamples.isEmpty()) {
if (!sleepStagesInRange.isEmpty()) {
// We got actual sleep stages
LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to);
LOG.debug("Found {} sleep stage samples between {} and {}", sleepStagesInRange.size(), timestamp_from, timestamp_to);
for (final XiaomiSleepStageSample stageSample : stageSamples) {
final int activityKind;
switch (stageSample.getStage()) {
case 2: // deep
activityKind = ActivityKind.TYPE_DEEP_SLEEP;
break;
case 3: // light
activityKind = ActivityKind.TYPE_LIGHT_SLEEP;
break;
case 4: // rem
activityKind = ActivityKind.TYPE_REM_SLEEP;
break;
case 0: // final awake
case 1: // ?
case 5: // awake during the night
default:
activityKind = ActivityKind.TYPE_UNKNOWN;
break;
}
stagesMap.put(stageSample.getTimestamp(), activityKind);
for (final XiaomiSleepStageSample stageSample : sleepStagesInRange) {
stagesMap.put(stageSample.getTimestamp(), getActivityKindForSample(stageSample));
}
}
// Fetch bed and wakeup times as well.
final XiaomiSleepTimeSampleProvider sleepTimeSampleProvider = new XiaomiSleepTimeSampleProvider(getDevice(), getSession());
final List<XiaomiSleepTimeSample> sleepTimeSamples = sleepTimeSampleProvider.getAllSamples(
timestamp_from * 1000L - 86400000L,
// Find last sleep sample before the requested range, as the recorded wake up time may be
// in the current range
final XiaomiSleepTimeSample lastSleepTimesBeforeRange = sleepTimeSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
if (lastSleepTimesBeforeRange != null) {
stagesMap.put(lastSleepTimesBeforeRange.getWakeupTime(), ActivityKind.TYPE_UNKNOWN);
stagesMap.put(lastSleepTimesBeforeRange.getTimestamp(), ActivityKind.TYPE_LIGHT_SLEEP);
}
// Find all wake up and sleep samples in the current time range
final List<XiaomiSleepTimeSample> sleepTimesInRange = sleepTimeSampleProvider.getAllSamples(
timestamp_from * 1000L,
timestamp_to * 1000L
);
if (!sleepTimeSamples.isEmpty()) {
LOG.debug("Found {} sleep samples between {} and {}", sleepTimeSamples.size(), timestamp_from, timestamp_to);
for (final XiaomiSleepTimeSample stageSample : sleepTimeSamples) {
if (stageSamples.isEmpty()) {
if (!sleepTimesInRange.isEmpty()) {
LOG.debug("Found {} sleep samples between {} and {}", sleepTimesInRange.size(), timestamp_from, timestamp_to);
for (final XiaomiSleepTimeSample stageSample : sleepTimesInRange) {
if (sleepStagesInRange.isEmpty()) {
// Only overlay them as light sleep if we don't have actual sleep stages
stagesMap.put(stageSample.getTimestamp(), ActivityKind.TYPE_LIGHT_SLEEP);
}

View File

@ -118,6 +118,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband7.HuaweiBan
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband8.HuaweiBand8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweibandaw70.HuaweiBandAw70Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweitalkbandb6.HuaweiTalkBandB6Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatch4pro.HuaweiWatch4ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchfit.HuaweiWatchFitCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchfit2.HuaweiWatchFit2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt.HuaweiWatchGTCoordinator;
@ -168,6 +169,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.SoundcoreLiberty3ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator;
@ -348,6 +350,7 @@ public enum DeviceType {
SONY_LINKBUDS_S(SonyLinkBudsSCoordinator.class),
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
HONORBAND3(HonorBand3Coordinator.class),
HONORBAND4(HonorBand4Coordinator.class),
@ -369,6 +372,7 @@ public enum DeviceType {
HUAWEIWATCHFIT(HuaweiWatchFitCoordinator.class),
HUAWEIWATCHFIT2(HuaweiWatchFit2Coordinator.class),
HUAWEIWATCHULTIMATE(HuaweiWatchUltimateCoordinator.class),
HUAWEIWATCH4PRO(HuaweiWatch4ProCoordinator.class),
VESC(VescCoordinator.class),
BINARY_SENSOR(BinarySensorCoordinator.class),
FLIPPER_ZERO(FlipperZeroCoordinator.class),

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreLiberty3ProDeviceSupport extends AbstractSerialDeviceSupport {
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
protected GBDeviceProtocol createDeviceProtocol() {
return new SoundcoreLibertyProtocol(getDevice());
}
@Override
protected synchronized GBDeviceIoThread createDeviceIOThread() {
return new SoundcoreLibertyIOThread(getDevice(), getContext(),
(SoundcoreLibertyProtocol) getDeviceProtocol(),
SoundcoreLiberty3ProDeviceSupport.this, getBluetoothAdapter());
}
}

View File

@ -0,0 +1,49 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
public class SoundcoreLibertyIOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyIOThread.class);
private final SoundcoreLibertyProtocol mSoundcoreProtocol;
public SoundcoreLibertyIOThread(GBDevice gbDevice, Context context, SoundcoreLibertyProtocol deviceProtocol, SoundcoreLiberty3ProDeviceSupport deviceSupport, BluetoothAdapter btAdapter) {
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
mSoundcoreProtocol = deviceProtocol;
}
@Override
protected void initialize() {
write(mSoundcoreProtocol.encodeDeviceInfoRequest());
setUpdateState(GBDevice.State.INITIALIZED);
}
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return mSoundcoreProtocol.UUID_DEVICE_CTRL;
}
@Override
protected byte[] parseIncoming(InputStream inStream) throws IOException {
byte[] buffer = new byte[1048576]; //HUGE read
int bytes = inStream.read(buffer);
LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes));
return Arrays.copyOf(buffer, bytes);
}
}

View File

@ -0,0 +1,377 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.content.SharedPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class SoundcoreLibertyProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyProtocol.class);
private static final int battery_case = 0;
private static final int battery_earphone_left = 1;
private static final int battery_earphone_right = 2;
final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3952");
protected SoundcoreLibertyProtocol(GBDevice device) {
super(device);
}
private GBDeviceEventBatteryInfo buildBatteryInfo(int batteryIndex, int level) {
GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo();
info.batteryIndex = batteryIndex;
info.level = level;
return info;
}
private GBDeviceEventVersionInfo buildVersionInfo(String firmware1, String firmware2, String serialNumber) {
GBDeviceEventVersionInfo info = new GBDeviceEventVersionInfo();
info.hwVersion = serialNumber;
info.fwVersion = firmware1;
info.fwVersion2 = firmware2;
return info;
}
private String readString(byte[] data, int position, int size) {
if (position + size > data.length) throw new IllegalStateException();
return new String(data, position, size, StandardCharsets.UTF_8);
}
@Override
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
// Byte 0-4: Header
// Byte 5-6: Command (Audio-Mode)
// Byte 7: Size of data
// Byte 8-(x-1): Data
// Byte x: Checksum
if (responseData.length == 0) return null;
List<GBDeviceEvent> devEvts = new ArrayList<>();
byte[] command = Arrays.copyOfRange(responseData, 5, 7);
byte[] data = Arrays.copyOfRange(responseData, 8, responseData.length-1);
if (Arrays.equals(command, new byte[]{0x01, 0x01})) {
// a lot of other data is in here, anything interesting?
String firmware1 = readString(data, 7, 5);
String firmware2 = readString(data, 12, 5);
String serialNumber = readString(data, 17, 16);
devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber));
} else if (Arrays.equals(command, new byte[]{0x01, (byte) 0x8d})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x05, (byte) 0x82})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x05, 0x01})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x06, 0x01})) { //Sound Mode Update
decodeAudioMode(data);
} else if (Arrays.equals(command, new byte[]{0x01, 0x03})) { // Battery Update
int batteryLeft = data[1] * 20;
int batteryRight = data[2] * 20;
int batteryCase = data[3] * 20;
devEvts.add(buildBatteryInfo(battery_case, batteryCase));
devEvts.add(buildBatteryInfo(battery_earphone_left, batteryLeft));
devEvts.add(buildBatteryInfo(battery_earphone_right, batteryRight));
} else {
// see https://github.com/gmallios/SoundcoreManager/blob/master/soundcore-lib/src/models/packet_kind.rs
// for a mapping for other soundcore devices (similar protocol?)
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
}
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
}
private void decodeAudioMode(byte[] payload) {
SharedPreferences prefs = getDevicePrefs().getPreferences();
SharedPreferences.Editor editor = prefs.edit();
String soundmode = "off";
int anc_strength = 0;
if (payload[1] == 0x00) {
soundmode = "noise_cancelling";
} else if (payload[1] == 0x01) {
soundmode = "ambient_sound";
} else if (payload[1] == 0x02) {
soundmode = "off";
}
if (payload[2] == 0x10) {
anc_strength = 0;
} else if (payload[2] == 0x20) {
anc_strength = 1;
} else if (payload[2] == 0x30) {
anc_strength = 2;
}
boolean vocal_mode = (payload[3] == 0x01);
boolean adaptive_anc = (payload[4] == 0x01);
boolean windnoiseReduction = (payload[5] == 0x01);
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode);
editor.putInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, anc_strength);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, vocal_mode);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, adaptive_anc);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, windnoiseReduction);
editor.apply();
}
@Override
public byte[] encodeSendConfiguration(String config) {
Prefs prefs = getDevicePrefs();
String pref_string;
switch (config) {
// Ambient Sound Modes
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING:
case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL:
return encodeAudioMode();
// Control
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED:
return encodeControlTouchLockMessage(TapAction.SINGLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED:
return encodeControlTouchLockMessage(TapAction.DOUBLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED:
return encodeControlTouchLockMessage(TapAction.TRIPLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED:
return encodeControlTouchLockMessage(TapAction.LONG_PRESS, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.SINGLE_TAP, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.SINGLE_TAP, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.LONG_PRESS, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.LONG_PRESS, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE:
AmbientSoundControlButtonMode modes = AmbientSoundControlButtonMode.fromPreferences(prefs.getPreferences());
switch (modes) {
case NC_AS_OFF:
return encodeControlAmbientModeMessage(true, true, true);
case NC_AS:
return encodeControlAmbientModeMessage(true, true, false);
case NC_OFF:
return encodeControlAmbientModeMessage(true, false, true);
case AS_OFF:
return encodeControlAmbientModeMessage(false, true, true);
}
// Miscellaneous Settings
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION:
boolean wearingDetection = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION, false);
return encodeMessage((byte) 0x01, (byte) 0x81, new byte[]{0x00, encodeBoolean(wearingDetection)});
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE:
boolean wearingTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE, false);
return encodeMessage((byte) 0x01, (byte) 0x8c, new byte[]{0x00, encodeBoolean(wearingTone)});
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE:
boolean touchTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE, false);
return encodeMessage((byte) 0x01, (byte) 0x83, new byte[]{0x00, encodeBoolean(touchTone)});
default:
LOG.debug("Unsupported CONFIG: " + config);
}
return super.encodeSendConfiguration(config);
}
byte[] encodeDeviceInfoRequest() {
byte[] payload = new byte[]{0x00};
return encodeMessage((byte) 0x01, (byte) 0x01, payload);
}
byte[] encodeMysteryDataRequest1() {
byte[] payload = new byte[]{0x00, 0x00};
return encodeMessage((byte) 0x01, (byte) 0x8d, payload);
}
byte[] encodeMysteryDataRequest2() {
byte[] payload = new byte[]{0x00};
return encodeMessage((byte) 0x05, (byte) 0x01, payload);
}
byte[] encodeMysteryDataRequest3() {
byte[] payload = new byte[]{0x00, 0x00};
return encodeMessage((byte) 0x05, (byte) 0x82, payload);
}
/**
* Encodes the following settings to a payload to set the audio-mode on the headphones:
* PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL If ANC, Transparent or neither should be active
* PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING If the strenght of the ANC should be set manual or adaptively according to ambient noise
* PREF_SONY_AMBIENT_SOUND_LEVEL How strong the ANC should be in manual mode
* PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE If the Transparency should focus on vocals or should be fully transparent
* PREF_SOUNDCORE_WIND_NOISE_REDUCTION If Transparency or ANC should reduce Wind Noise
* @return The payload
*/
private byte[] encodeAudioMode() {
Prefs prefs = getDevicePrefs();
byte anc_mode;
switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) {
case "noise_cancelling":
anc_mode = 0x00;
break;
case "ambient_sound":
anc_mode = 0x01;
break;
case "off":
anc_mode = 0x02;
break;
default:
LOG.error("Invalid Audio Mode selected");
return null;
}
byte anc_strength;
switch (prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0)) {
case 0:
anc_strength = 0x10;
break;
case 1:
anc_strength = 0x20;
break;
case 2:
anc_strength = 0x30;
break;
default:
LOG.error("Invalid ANC Strength selected");
return null;
}
byte adaptive_anc = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, true));
byte vocal_mode = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, false));
byte windnoise_reduction = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, false));
byte[] payload = new byte[]{0x00, anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01};
return encodeMessage((byte) 0x06, (byte) 0x81, payload);
}
/**
* Enables or disables a tap-action
* @param action The byte that encodes the action (single/double/triple or long tap)
* @param disabled If the action should be enabled or disabled
* @return
*/
private byte[] encodeControlTouchLockMessage(TapAction action, boolean disabled) {
boolean enabled = !disabled;
byte enabled_byte;
byte[] payload;
switch (action) {
case SINGLE_TAP:
case TRIPLE_TAP:
enabled_byte = encodeBoolean(enabled);
break;
case DOUBLE_TAP:
case LONG_PRESS:
enabled_byte = enabled?(byte) 0x11: (byte) 0x10;
break;
default:
LOG.error("Invalid Tap action");
return null;
}
payload = new byte[]{0x00, 0x00, action.getCode(), enabled_byte};
return encodeMessage((byte) 0x04, (byte) 0x83, payload);
}
/**
* Assigns a function (eg play/pause) to an action (eg single tap on right bud)
* @param action The byte that encodes the action (single/double/triple or long tap)
* @param right If the right or left earbud is meant
* @param function The byte that encodes the triggered function (eg play/pause)
* @return The encoded message
*/
private byte[] encodeControlFunctionMessage(TapAction action, boolean right, TapFunction function) {
byte function_byte;
switch (action) {
case SINGLE_TAP:
case DOUBLE_TAP:
function_byte = (byte) (16*6 + function.getCode());
break;
case TRIPLE_TAP:
function_byte = (byte) (16*4 + function.getCode());
break;
case LONG_PRESS:
function_byte = (byte) (16*5 + function.getCode());
break;
default:
LOG.error("Invalid Tap action");
return null;
}
byte[] payload = new byte[] {0x00, encodeBoolean(right), action.getCode(), function_byte};
return encodeMessage((byte) 0x04, (byte) 0x81, payload);
}
/**
* Encodes between which Audio Modes a tap should switch, if it is set to switch the Audio Mode.
* Zb ANC -> -> Transparency -> Normal -> ANC -> ....
*/
private byte[] encodeControlAmbientModeMessage(boolean anc, boolean transparency, boolean normal) {
// Original app does not allow only one true flag. Unsure if Earbuds accept this state.
byte ambientModes = (byte) (4 * (normal?1:0) + 2 * (transparency?1:0) + (anc?1:0));
return encodeMessage((byte) 0x06, (byte) 0x82, new byte[] {0x00, ambientModes});
}
private byte encodeBoolean(boolean bool) {
if (bool) return 0x01;
else return 0x00;
}
private byte[] encodeMessage(byte command1, byte command2, byte[] payload) {
int size = 8 + payload.length + 1;
ByteBuffer msgBuf = ByteBuffer.allocate(size);
msgBuf.order(ByteOrder.BIG_ENDIAN);
msgBuf.put(new byte[] {0x08, (byte) 0xee, 0x00, 0x00, 0x00}); // header
msgBuf.put(command1);
msgBuf.put(command2);
msgBuf.put((byte) size);
msgBuf.put(payload);
byte checksum = -10;
checksum += command1 + command2 + size;
for (int b : payload) {
checksum += b;
}
msgBuf.put(checksum);
return msgBuf.array();
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
enum TapAction {
SINGLE_TAP((byte) 0x02),
DOUBLE_TAP((byte) 0x00),
TRIPLE_TAP((byte) 0x05),
LONG_PRESS((byte) 0x01)
;
private final byte code;
TapAction(final byte code) {
this.code = code;
}
public byte getCode() {
return code;
}
}

View File

@ -0,0 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
enum TapFunction {
VOLUME_DOWN(1),
VOLUME_UP(0),
MEDIA_NEXT( 3),
MEDIA_PREV(2),
PLAYPAUSE(6),
VOICE_ASSISTANT(5),
AMBIENT_SOUND_CONTROL(4)
;
private final int code;
TapFunction(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@ -81,6 +81,7 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// Heart rate samples
if ((header & (1 << (5 - versionDependentFields))) != 0) {
LOG.debug("Heart rate samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@ -98,6 +99,7 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// SpO2 samples
if ((header & (1 << (4 - versionDependentFields))) != 0) {
LOG.debug("SpO₂ samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@ -115,6 +117,7 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// snore samples
if (fileId.getVersion() >= 3 && (header & (1 << (3 - versionDependentFields))) != 0) {
LOG.debug("Snore level samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@ -131,27 +134,26 @@ public class SleepDetailsParser extends XiaomiActivityParser {
}
final List<XiaomiSleepStageSample> stages = new ArrayList<>();
LOG.debug("Sleep stage packets from offset {}", Integer.toHexString(buf.position()));
// Do not crash if we face a buffer underflow, as the next parsing is not 100% fool-proof,
// and we still want to persist whatever we got so far
boolean stagesParseFailed = false;
try {
while (buf.remaining() >= 17 && buf.getInt() == 0xFFFCFAFB) {
while (buf.remaining() >= 17) {
if (!readStagePacketHeader(buf)) {
break;
}
final int headerLen = buf.get() & 0xFF; // this seems to always be 17
// This timestamp is kind of weird, is seems to sometimes be in seconds
// and other times in nanoseconds. Message types 16 and 17 are in seconds
final long ts = buf.getLong();
final int unk = buf.get() & 0xFF;
final int parity = buf.get() & 0xFF; // sum of stage bit count should be uneven
final int type = buf.get() & 0xFF;
final int dataLen = ((buf.get() & 0xFF) << 8) | (buf.get() & 0xFF);
final byte[] data = new byte[dataLen];
buf.get(data);
final ByteBuffer dataBuf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
// Known types:
// - acc_unk = 0,
// - ppg_unk = 1,
@ -162,6 +164,17 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// - Summary = 16,
// - Stages = 17
if (type == 0x2 || type == 0x3 || type == 0x9 || type == 0xc || type == 0xd || type == 0xe || type == 0xf) {
// the bytes reserved for the data length are believed to be flags, as they
// do not actually have any data following the headers
continue;
}
final byte[] data = new byte[dataLen];
buf.get(data);
final ByteBuffer dataBuf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
if (type == 16) {
final int data_0 = dataBuf.get() & 0xFF;
final int sleep_index = data_0 >> 4;
@ -193,11 +206,10 @@ public class SleepDetailsParser extends XiaomiActivityParser {
sample.setAwakeDuration(wake_duration);
// FIXME: This is an array, but we end up persisting only the last sample, since
// the timestamp is the primary key
// the timestamp is the primary key
summaries.add(sample);
sample = null;
}
else if (type == 17) { // Stages
} else if (type == 17) { // Stages
long currentTime = ts * 1000;
for (int i = 0; i < dataLen / 2; i++) {
// when the change to the phase occurs
@ -250,7 +262,6 @@ public class SleepDetailsParser extends XiaomiActivityParser {
sampleProvider.addSample(summary);
}
} catch (final Exception e) {
GB.toast(support.getContext(), "Error saving sleep sample", Toast.LENGTH_LONG, GB.ERROR);
LOG.error("Error saving sleep sample", e);
@ -282,10 +293,23 @@ public class SleepDetailsParser extends XiaomiActivityParser {
}
}
return stagesParseFailed;
return !stagesParseFailed;
}
static private int decodeStage(int rawStage) {
private static boolean readStagePacketHeader(final ByteBuffer buffer) {
while (buffer.remaining() >= 17) {
if (buffer.getInt() != 0xfffcfafb) {
// rollback to second byte of header
buffer.position(buffer.position() - 3);
continue;
}
return true;
}
return false;
}
private static int decodeStage(int rawStage) {
switch (rawStage) {
case 0:
return 5; // AWAKE

View File

@ -302,7 +302,7 @@ public class XiaomiWeatherService extends AbstractXiaomiService {
"Unknown" // some string like "Moderate"
))
.setWarning(XiaomiProto.WeatherWarnings.newBuilder()) // TODO add warnings when they become available through spec
.setPressure(weatherSpec.pressure)
.setPressure(weatherSpec.pressure * 100f)
))
.build();

View File

@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -36,134 +37,181 @@ public class ArmenianTransliterator implements Transliterator {
// Or if it has 'ւ' symbol after it, then we should read it as 'u' (as double o in booze)
private static final Map<String, String> transliterateMap = new LinkedHashMap<String, String>() {
{
// Simple substitutions
Map<String, String> simpleSubstitions = new HashMap<String, String>() {
{
put("ա","a");
put("բ","b");
put("գ","g");
put("դ","d");
put("ե","e");
put("զ","z");
put("է","e");
put("ը","y");
put("թ","t");
put("ժ","j");
put("ի","i");
put("լ","l");
put("խ","x");
put("ծ","c");
put("կ","k");
put("հ","h");
put("ձ","dz");
put("ղ","x");
put("ճ","c");
put("մ","m");
put("յ","y");
put("ն","n");
put("շ","sh");
put("չ","ch");
put("պ","p");
put("ջ","j");
put("ռ","r");
put("ս","s");
put("վ","v");
put("տ","t");
put("ր","r");
put("ց","c");
put("փ","p");
put("ք","q");
put("օ","o");
put("և","ev");
put("ֆ","f");
}
};
// Letter + 'ու'
put("աու","au");
put("բու","bu");
put("գու","gu");
put("դու","du");
put("եու","eu");
put("զու","zu");
put("էու","eu");
put("ըու","yu");
put("թու","tu");
put("ժու","ju");
put("իու","iu");
put("լու","lu");
put("խու","xu");
put("ծու","cu");
put("կու","ku");
put("հու","hu");
put("ձու","dzu");
put("ղու","xu");
put("ճու","cu");
put("մու","mu");
put("յու","yu");
put("նու","nu");
put("շու","shu");
put("չու","chu");
put("պու","pu");
put("ջու","ju");
put("ռու","ru");
put("սու","su");
put("վու","vu");
put("տու","tu");
put("րու","ru");
put("ցու","cu");
put("փու","pu");
put("քու","qu");
put("օու","ou");
put("ևու","eu");
put("ֆու","fu");
put("ոու","vou");
char[] letterMapU = {
'ա',
'բ',
'գ',
'դ',
'ե',
'զ',
'է',
'ը',
'թ',
'ժ',
'ի',
'լ',
'խ',
'ծ',
'կ',
'հ',
'ձ',
'ղ',
'ճ',
'մ',
'յ',
'ն',
'շ',
'չ',
'պ',
'ջ',
'ռ',
'ս',
'վ',
'տ',
'ր',
'ց',
'փ',
'ք',
'օ',
'և',
'ֆ',
'ո',
};
for(char letter : letterMapU) {
char capitalLetter = Character.toUpperCase(letter);
final String transliteratedLetter = simpleSubstitions.get(Character.toString(letter));
final String transliteratedCapitalLetter = simpleSubstitions.get(Character.toString(capitalLetter));
put(Character.toString(letter) + "ու", transliteratedLetter + "u");
put(Character.toString(capitalLetter) + "ու", transliteratedCapitalLetter + "u");
put(Character.toString(letter) + "ՈՒ", transliteratedLetter + "U");
put(Character.toString(capitalLetter) + "ՈՒ", transliteratedCapitalLetter + "U");
put(Character.toString(letter) + "Ու", transliteratedLetter + "U");
put(Character.toString(capitalLetter) + "Ու", transliteratedCapitalLetter + "U");
put(Character.toString(letter) + "ոՒ", transliteratedLetter + "U");
put(Character.toString(capitalLetter) + "ոՒ", transliteratedCapitalLetter + "U");
}
put("ու","u");
put("Ու","U");
put("ոՒ","U");
put("ՈՒ","U");
// Letter + 'ո'
put("բո","bo");
put("գո","go");
put("դո","do");
put("զո","zo");
put("թո","to");
put("ժո","jo");
put("լո","lo");
put("խո","xo");
put("ծո","co");
put("կո","ko");
put("հո","ho");
put("ձո","dzo");
put("ղո","xo");
put("ճո","co");
put("մո","mo");
put("յո","yo");
put("նո","no");
put("շո","so");
put("չո","co");
put("պո","po");
put("ջո","jo");
put("ռո","ro");
put("սո","so");
put("վո","vo");
put("տո","to");
put("րո","ro");
put("ցո","co");
put("փո","po");
put("քո","qo");
put("ևո","eo");
put("ֆո","fo");
char[] letterMapVo = {
'բ',
'գ',
'դ',
'զ',
'թ',
'ժ',
'լ',
'խ',
'ծ',
'կ',
'հ',
'ձ',
'ղ',
'ճ',
'մ',
'յ',
'ն',
'շ',
'չ',
'պ',
'ջ',
'ռ',
'ս',
'վ',
'տ',
'ր',
'ց',
'փ',
'ք',
'և',
'ֆ',
};
for(char letter : letterMapVo) {
char capitalLetter = Character.toUpperCase(letter);
final String transliteratedLetter = simpleSubstitions.get(Character.toString(letter));
final String transliteratedCapitalLetter = simpleSubstitions.get(Character.toString(capitalLetter));
put(Character.toString(letter) + "ո", transliteratedLetter + "o");
put(Character.toString(capitalLetter) + "ո", transliteratedCapitalLetter + "o");
put(Character.toString(letter) + "Ո", transliteratedLetter + "Օ");
put(Character.toString(capitalLetter) + "Ո", transliteratedCapitalLetter + "Օ");
}
put("ո","vo");
put("Ո","VO");
// Two different ways to write, we support all.
put("եւ","ev");
put("եվ","ev");
// Simple substitutions
put("ա","a");
put("բ","b");
put("գ","g");
put("դ","d");
put("ե","e");
put("զ","z");
put("է","e");
put("ը","y");
put("թ","t");
put("ժ","j");
put("ի","i");
put("լ","l");
put("խ","x");
put("ծ","c");
put("կ","k");
put("հ","h");
put("ձ","dz");
put("ղ","x");
put("ճ","c");
put("մ","m");
put("յ","y");
put("ն","n");
put("շ","sh");
put("չ","ch");
put("պ","p");
put("ջ","j");
put("ռ","r");
put("ս","s");
put("վ","v");
put("տ","t");
put("ր","r");
put("ց","c");
put("փ","p");
put("ք","q");
put("օ","o");
put("և","ev");
put("ֆ","f");
put("Եւ","Ev");
put("Եվ","Ev");
put("ԵՒ","EV");
put("ԵՎ","EV");
// If this symbol wasn't used in the combination with others, then it's meaningless
put("ւ","");
put("Ւ","");
// Add support for capitilazed words
for (final Map.Entry<String,String> entry : ((Map<String, String>)this.clone()).entrySet()) {
final String capitalKey = WordUtils.capitalize(entry.getKey());
if(!capitalKey.equals(entry.getKey())) {
put(capitalKey, WordUtils.capitalize(entry.getValue()));
}
// Simple substitutions have last priority
for (final Map.Entry<String,String> entry : simpleSubstitions.entrySet()) {
put(entry.getKey(), entry.getValue());
put(entry.getKey().toUpperCase(), entry.getValue().toUpperCase());
}
}};

View File

@ -3441,6 +3441,27 @@
<item>as_off</item>
</string-array>
<string-array name="soundcore_button_function_names">
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_volumeup</item>
<item>@string/pref_media_next</item>
<item>@string/pref_media_previous</item>
<item>@string/pref_media_playpause</item>
<item>@string/pref_title_touch_voice_assistant</item>
<item>@string/sony_button_mode_ambient_sound_control</item>
</string-array>
<string-array name="soundcore_button_function_values">
<item>VOLUME_DOWN</item>
<item>VOLUME_UP</item>
<item>MEDIA_NEXT</item>
<item>MEDIA_PREV</item>
<item>PLAYPAUSE</item>
<item>VOICE_ASSISTANT</item>
<item>AMBIENT_SOUND_CONTROL</item>
</string-array>
<string-array name="fitness_tracking_apps_package_names">
<item>de.dennisguse.opentracks</item>
<item>de.dennisguse.opentracks.playStore</item>

View File

@ -520,6 +520,10 @@
<string name="pref_gps_satellite_search">Satellite Search</string>
<string name="pref_crown_vibration">Crown Vibration</string>
<string name="pref_alert_tone">Alert Tone</string>
<string name="pref_touch_tone">Touch Tone</string>
<string name="pref_touch_tone_summary">Plays a tone when the earbud is touched</string>
<string name="pref_wearing_tone">Wearing Tone</string>
<string name="pref_wearing_tone_summary">Plays a tone when the earbud is inserted</string>
<string name="pref_cover_to_mute">Cover to Mute</string>
<string name="pref_vibrate_for_alert">Vibrate for Alert</string>
<string name="pref_text_to_speech">Text to Speech</string>
@ -1533,6 +1537,7 @@
<string name="devicetype_sony_wi_sp600n">Sony WI-SP600N</string>
<string name="devicetype_sony_linkbuds">Sony LinkBuds</string>
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</string>
<string name="devicetype_soundcore_liberty3_pro">Soundcore Liberty 3 Pro</string>
<string name="devicetype_binary_sensor">Binary sensor</string>
<string name="devicetype_honor_band3">Honor Band 3</string>
<string name="devicetype_honor_band4">Honor Band 4</string>
@ -1554,6 +1559,7 @@
<string name="devicetype_huawei_watchfit">Huawei Watch Fit</string>
<string name="devicetype_huawei_watchfit2">Huawei Watch Fit 2</string>
<string name="devicetype_huawei_watchultimate">Huawei Watch Ultimate</string>
<string name="devicetype_huawei_watch4pro">Huawei Watch 4 Pro</string>
<string name="devicetype_femometer_vinca2">Femometer Vinca II</string>
<string name="devicetype_xiaomi_watch_lite">Xiaomi Watch Lite</string>
<string name="devicetype_redmiwatch3active">Redmi Watch 3 Active</string>
@ -2284,6 +2290,8 @@
<string name="pref_wide_area_tap_title">Wide area tap</string>
<string name="pref_adaptive_volume_control_summary">Increase volume automatically when ambient sound is loud</string>
<string name="pref_adaptive_volume_control_title">Adaptive volume control</string>
<string name="pref_adaptive_noise_cancelling_title">Adaptive ANC</string>
<string name="pref_adaptive_noise_cancelling_summary">Set the strength of the ANC automatically depending on the ambient sound level</string>
<string name="sony_speak_to_chat">Speak-to-chat</string>
<string name="sony_speak_to_chat_summary">Turn off noise cancelling automatically when you start talking.</string>
<string name="sony_speak_to_chat_sensitivity">Voice Detection Sensitivity</string>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:icon="@drawable/ic_touch"
android:key="pref_screen_touch_options"
android:persistent="false"
android:title="@string/prefs_galaxy_touch_options">
</PreferenceScreen>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_header_soundcore_ambient_sound_control"
android:title="@string/pref_header_sony_ambient_sound_control">
<ListPreference
android:defaultValue="noise_cancelling"
android:entries="@array/sony_ambient_sound_control_names"
android:entryValues="@array/sony_ambient_sound_control_values"
android:icon="@drawable/ic_hearing"
android:key="pref_soundcore_ambient_sound_control"
android:summary="%s"
android:title="@string/sony_ambient_sound" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:disableDependentsState="true"
android:icon="@drawable/ic_hearing"
android:key="pref_adaptive_noise_cancelling"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_adaptive_noise_cancelling_summary"
android:title="@string/pref_adaptive_noise_cancelling_title" />
<!-- [0, 2], low moderate and high -->
<SeekBarPreference
android:dependency="pref_adaptive_noise_cancelling"
android:defaultValue="0"
android:icon="@drawable/ic_hearing"
android:key="pref_sony_ambient_sound_level"
android:max="2"
android:title="@string/prefs_active_noise_cancelling_level" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_block"
android:key="pref_soundcore_wind_noise_reduction"
android:layout="@layout/preference_checkbox"
android:title="@string/sony_ambient_sound_wind_noise_reduction" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_voice"
android:key="pref_soundcore_transparency_vocal_mode"
android:layout="@layout/preference_checkbox"
android:title="@string/sony_ambient_sound_focus_voice" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_header_soundcore_other"
android:title="@string/pref_header_other">
<SwitchPreferenceCompat
android:defaultValue="false"
android:layout="@layout/preference_checkbox"
android:summary="@string/nothing_prefs_inear_summary"
android:title="@string/nothing_prefs_inear_title"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_wearing_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_wearing_tone_summary"
android:title="@string/pref_wearing_tone"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_touch_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_touch_tone_summary"
android:title="@string/pref_touch_tone"/>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/single_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_single_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_single_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_single_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_single_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_single_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/double_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_double_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_double_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_double_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_double_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_double_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/triple_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_triple_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_triple_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_triple_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_triple_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_triple_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/long_press">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_long_press_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_long_press_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_long_press_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_long_press_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_long_press_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -33,6 +33,19 @@ public class ArmenianTransliteratorTest extends TestCase {
new ArmenianTransliterator().transliterate("որը jet iridescent կառուցում են sheen Վիքիպեդիա կայքից օգտվողները and a distinctive ազատ խմբագրման ձևաչափով"));
}
@Test
public void testMixedCaseWords() {
Assert.assertEquals(
"Inchpes", new ArmenianTransliterator().transliterate("Ինչպես")
);
Assert.assertEquals(
"VOrՕSHEL", new ArmenianTransliterator().transliterate("ՈրՈՇԵԼ")
);
Assert.assertEquals(
"Ushadir", new ArmenianTransliterator().transliterate("Ուշադիր")
);
}
@Test
public void testTop100Words() {