mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
initial support
This commit is contained in:
committed by
José Rebelo
parent
69180dc3a4
commit
5fb412c296
+189
@@ -0,0 +1,189 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.soundbrenner;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BLE UUIDs and protocol constants for the Soundbrenner Core.
|
||||||
|
* Reverse-engineered using nRF Connect + HCI Snoop Log (May/June 2026).
|
||||||
|
*
|
||||||
|
* === 32-byte packet structure for UUID_CHARACTERISTIC_CONFIG ===
|
||||||
|
*
|
||||||
|
* Bytes 0-3: BPM as IEEE 754 float, big-endian
|
||||||
|
*
|
||||||
|
* Byte 4: Bytes 4: time signature
|
||||||
|
* Upper nibble = numerator - 1
|
||||||
|
* Lower nibble = log2(denominator)
|
||||||
|
* Examples: 4/4=0x32, 3/4=0x22, 3/8=0x23, 5/4=0x42
|
||||||
|
*
|
||||||
|
* Byte 5: beat strengths beats 1-4, 2 bits per beat MSB-first
|
||||||
|
* Byte 6: beat strengths beats 5-8; unused slots = 0x00 (wire default)
|
||||||
|
* Inverted encoding: UI 0->11, 1->00, 2->01, 3->10
|
||||||
|
* Example (3:2:1:0) -> 10 01 00 11 = 0x93
|
||||||
|
*
|
||||||
|
* Byte 7: unused, fixed 0x00
|
||||||
|
*
|
||||||
|
* Byte 8: 0x00 (fixed)
|
||||||
|
*
|
||||||
|
* Bytes 9-10: Subdivision type constant, 16-bit big-endian.
|
||||||
|
* Formula: (subdivisionSteps - 1) * (9352 + 2 * numerator)
|
||||||
|
* 4/4 examples: eighth=0x2490, triplet=0x4920, sixteenth=0x6DB0
|
||||||
|
* 5/4 examples: eighth=0x2492, triplet=0x4924, sixteenth=0x6DB6
|
||||||
|
*
|
||||||
|
* Bytes 11-14: 0x00 (fixed)
|
||||||
|
*
|
||||||
|
* Bytes 15-(15+numerator-1): Beat pattern byte, one per beat.
|
||||||
|
* Bits 7-4 = active subdivision steps (bit 7=step1 .. bit 4=step4)
|
||||||
|
* Bits 3-0 = always 0000
|
||||||
|
* Examples: quarter=0x80, both eighths=0xC0, all triplets=0xE0,
|
||||||
|
* all sixteenths=0xF0
|
||||||
|
*
|
||||||
|
* Bytes (15+numerator)-30: 0x00 (inactive beat slots)
|
||||||
|
*
|
||||||
|
* Byte 31: 0x00 (footer)
|
||||||
|
*/
|
||||||
|
public final class SoundbrennerConstants {
|
||||||
|
|
||||||
|
private SoundbrennerConstants() {}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Services
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public static final UUID UUID_SERVICE_METRONOME =
|
||||||
|
UUID.fromString("f3f6ce01-b257-4336-acd8-3010817837e4");
|
||||||
|
public static final UUID UUID_SERVICE_VIBRATION =
|
||||||
|
UUID.fromString("77ef8bb5-7d4d-4a66-bd64-3f93cc01ccda");
|
||||||
|
/** Unknown service – retained for completeness, not yet used. */
|
||||||
|
public static final UUID UUID_SERVICE_UNKNOWN_A =
|
||||||
|
UUID.fromString("af7da518-f5f6-46d7-8ed1-546c33daf83b");
|
||||||
|
/** Unknown service – retained for completeness, not yet used. */
|
||||||
|
public static final UUID UUID_SERVICE_UNKNOWN_B =
|
||||||
|
UUID.fromString("c8befab9-5cc3-4af1-9102-394c40987d1e");
|
||||||
|
/** Unknown service – probably used for handshake. */
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_CTRL =
|
||||||
|
UUID.fromString("061a5b49-8c3a-561e-9f34-dae895a3477a");
|
||||||
|
//** Standard BLE Battery Service (0x180F / 0x2A19) */
|
||||||
|
public static final UUID UUID_SERVICE_BATTERY =
|
||||||
|
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_BATTERY_LEVEL =
|
||||||
|
UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Characteristics
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* READ + WRITE + NOTIFY.
|
||||||
|
* Write: 32-byte configuration packet (BPM, time signature, beat strengths, subdivision).
|
||||||
|
* Notify: device pushes the same packet when the user changes settings on the watch.
|
||||||
|
*/
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_CONFIG =
|
||||||
|
UUID.fromString("5ae0ba81-1053-4183-826e-7a6be885c142");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WRITE + INDICATE.
|
||||||
|
* Start metronome: {@link #CMD_METRONOME_START}.
|
||||||
|
* Stop metronome: {@link #CMD_METRONOME_STOP}.
|
||||||
|
*/
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_CONTROL =
|
||||||
|
UUID.fromString("920475b8-17ee-4474-a500-35037152b860");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WRITE + INDICATE.
|
||||||
|
* Continuous vibration – exact payload still under investigation.
|
||||||
|
*/
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_VIBRATION =
|
||||||
|
UUID.fromString("252aeb30-c6ec-43ec-8072-5b08f13dae36");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* READ + WRITE.
|
||||||
|
* Unknown purpose; the official app reads this before every config write.
|
||||||
|
*/
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_RW =
|
||||||
|
UUID.fromString("203b9729-4d3f-4898-8fb1-1ed1e6759229");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* READ + WRITE + NOTIFY.
|
||||||
|
* Unknown purpose; lives inside the metronome service.
|
||||||
|
*/
|
||||||
|
public static final UUID UUID_CHARACTERISTIC_UNKNOWN_NOTIFY =
|
||||||
|
UUID.fromString("05094f38-7d29-450d-8f23-c9d784923668");
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Control commands
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public static final byte CMD_METRONOME_START = 0x01;
|
||||||
|
public static final byte CMD_METRONOME_STOP = 0x02;
|
||||||
|
/** magic byte **/
|
||||||
|
public static final byte[] CMD_PRE_START = new byte[]{ (byte) 0xd1 };
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Packet layout constants
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public static final int PACKET_SIZE = 32;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Subdivision beat-pattern bytes
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Quarter note: only subdivision step 1 active. */
|
||||||
|
public static final byte BEAT_PATTERN_QUARTER = (byte) 0x80;
|
||||||
|
/** Both eighth notes active. */
|
||||||
|
public static final byte BEAT_PATTERN_EIGHTH = (byte) 0xC0;
|
||||||
|
/** All three triplet subdivisions active. */
|
||||||
|
public static final byte BEAT_PATTERN_TRIPLET = (byte) 0xE0;
|
||||||
|
/** All four sixteenth-note subdivisions active. */
|
||||||
|
public static final byte BEAT_PATTERN_SIXTEENTH = (byte) 0xF0;
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Shared-preference keys
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public static final String PREF_SOUNDBRENNER_PREFIX = "soundbrenner_";
|
||||||
|
public static final String PREF_BEAT_ACCENT_PREFIX = "soundbrenner_beat_accent_";
|
||||||
|
|
||||||
|
public static final String PREF_BPM = "soundbrenner_bpm";
|
||||||
|
public static final String PREF_TIME_SIGNATURE = "soundbrenner_time_signature";
|
||||||
|
public static final String PREF_SUBDIVISION = "soundbrenner_subdivision";
|
||||||
|
|
||||||
|
/** Stores the last known running/stopped state so the UI can restore it. */
|
||||||
|
public static final String PREF_METRONOME_RUNNING = "soundbrenner_metronome_running";
|
||||||
|
|
||||||
|
// Individual beat accent strengths (UI values 0-3); beats 1-8.
|
||||||
|
public static final String PREF_BEAT_1 = "soundbrenner_beat_accent_1";
|
||||||
|
public static final String PREF_BEAT_2 = "soundbrenner_beat_accent_2";
|
||||||
|
public static final String PREF_BEAT_3 = "soundbrenner_beat_accent_3";
|
||||||
|
public static final String PREF_BEAT_4 = "soundbrenner_beat_accent_4";
|
||||||
|
public static final String PREF_BEAT_5 = "soundbrenner_beat_accent_5";
|
||||||
|
public static final String PREF_BEAT_6 = "soundbrenner_beat_accent_6";
|
||||||
|
public static final String PREF_BEAT_7 = "soundbrenner_beat_accent_7";
|
||||||
|
public static final String PREF_BEAT_8 = "soundbrenner_beat_accent_8";
|
||||||
|
public static final String PREF_BEAT_9 = "soundbrenner_beat_accent_9";
|
||||||
|
public static final String PREF_BEAT_10 = "soundbrenner_beat_accent_10";
|
||||||
|
public static final String PREF_BEAT_11 = "soundbrenner_beat_accent_11";
|
||||||
|
public static final String PREF_BEAT_12 = "soundbrenner_beat_accent_12";
|
||||||
|
public static final String PREF_BEAT_13 = "soundbrenner_beat_accent_13";
|
||||||
|
public static final String PREF_BEAT_14 = "soundbrenner_beat_accent_14";
|
||||||
|
public static final String PREF_BEAT_15 = "soundbrenner_beat_accent_15";
|
||||||
|
public static final String PREF_BEAT_16 = "soundbrenner_beat_accent_16";
|
||||||
|
|
||||||
|
/** Ordered list of all per-beat accent pref keys; index 0 = beat 1. */
|
||||||
|
public static final String[] PREF_BEATS = {
|
||||||
|
PREF_BEAT_1, PREF_BEAT_2, PREF_BEAT_3, PREF_BEAT_4,
|
||||||
|
PREF_BEAT_5, PREF_BEAT_6, PREF_BEAT_7, PREF_BEAT_8,
|
||||||
|
PREF_BEAT_9, PREF_BEAT_10, PREF_BEAT_11, PREF_BEAT_12,
|
||||||
|
PREF_BEAT_13, PREF_BEAT_14, PREF_BEAT_15, PREF_BEAT_16
|
||||||
|
};
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Default values
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public static final int DEFAULT_BPM = 120;
|
||||||
|
public static final String DEFAULT_TIME_SIG = "4/4";
|
||||||
|
public static final int DEFAULT_SUBDIVISION = 1; // 1=quarter, 2=eighth, 3=triplet, 4=sixteenth
|
||||||
|
public static final int DEFAULT_BEAT_ACCENT_1 = 3; // accent (loudest) on beat 1
|
||||||
|
public static final int DEFAULT_BEAT_ACCENT_N = 1; // soft on all other beats
|
||||||
|
}
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.soundbrenner;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundbrenner.SoundbrennerSupport;
|
||||||
|
|
||||||
|
|
||||||
|
public class SoundbrennerCoordinator extends AbstractBLEDeviceCoordinator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturer() {
|
||||||
|
return "Soundbrenner";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Pattern getSupportedDeviceName() {
|
||||||
|
return Pattern.compile("Soundbrenner Core.*");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<? extends DeviceSupport> getDeviceSupportClass(final GBDevice device) {
|
||||||
|
return SoundbrennerSupport.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDeviceNameResource() {
|
||||||
|
return R.string.devicetype_soundbrenner_core;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDefaultIconResource() {
|
||||||
|
return R.drawable.ic_notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DeviceCoordinator.DeviceKind getDeviceKind(@NonNull GBDevice device) {
|
||||||
|
return DeviceCoordinator.DeviceKind.WATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Device-specific settings
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||||
|
return new int[]{
|
||||||
|
R.xml.devicesettings_soundbrenner_core
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(@NonNull final GBDevice device) {
|
||||||
|
return new SoundbrennerSettingsCustomizer();
|
||||||
|
}
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Start/Stop buttons in gadget card
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DeviceCardAction> getCustomActions() {
|
||||||
|
return Arrays.asList(
|
||||||
|
|
||||||
|
// START
|
||||||
|
new DeviceCardAction() {
|
||||||
|
@Override
|
||||||
|
public int getIcon(@NonNull final GBDevice device) {
|
||||||
|
return R.drawable.ic_play;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getDescription(@NonNull final GBDevice device,
|
||||||
|
@NonNull final Context context) {
|
||||||
|
return context.getString(R.string.soundbrenner_action_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull final GBDevice device,
|
||||||
|
@NonNull final Context context) {
|
||||||
|
GBApplication.deviceService(device)
|
||||||
|
.onSendConfiguration(
|
||||||
|
SoundbrennerConstants.PREF_METRONOME_RUNNING + "_start");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// STOP
|
||||||
|
new DeviceCardAction() {
|
||||||
|
@Override
|
||||||
|
public int getIcon(@NonNull final GBDevice device) {
|
||||||
|
return R.drawable.ic_stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String getDescription(@NonNull final GBDevice device,
|
||||||
|
@NonNull final Context context) {
|
||||||
|
return context.getString(R.string.soundbrenner_action_stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull final GBDevice device,
|
||||||
|
@NonNull final Context context) {
|
||||||
|
GBApplication.deviceService(device)
|
||||||
|
.onSendConfiguration(
|
||||||
|
SoundbrennerConstants.PREF_METRONOME_RUNNING + "_stop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -440,6 +440,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundbrenner.SoundbrennerCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.aerofit.SoundcoreAeroFit2Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.aerofit.SoundcoreAeroFit2Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty4_nc.SoundcoreLiberty4NCCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty4_nc.SoundcoreLiberty4NCCoordinator;
|
||||||
@@ -838,6 +839,7 @@ public enum DeviceType {
|
|||||||
VICTRON_SMARTSHUNT(VictronSmartShuntCoordinator.class),
|
VICTRON_SMARTSHUNT(VictronSmartShuntCoordinator.class),
|
||||||
SINILINK(SinilinkCoordinator.class),
|
SINILINK(SinilinkCoordinator.class),
|
||||||
ONETOUCH(OneTouchCoordinator.class),
|
ONETOUCH(OneTouchCoordinator.class),
|
||||||
|
SOUNDBRENNER_CORE(SoundbrennerCoordinator.class),
|
||||||
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
||||||
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
|
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
|
||||||
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
|
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
|
||||||
|
|||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.soundbrenner;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings customizer for the Soundbrenner Core.
|
||||||
|
*
|
||||||
|
* Shows/hides per-beat accent SeekBars depending on the selected time-signature
|
||||||
|
* numerator. Only beats 1..numerator are visible; the rest are hidden.
|
||||||
|
*
|
||||||
|
* All preference keys are referenced via {@link SoundbrennerConstants}.
|
||||||
|
*/
|
||||||
|
public class SoundbrennerSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Parcelable boilerplate
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public static final Creator<SoundbrennerSettingsCustomizer> CREATOR =
|
||||||
|
new Creator<SoundbrennerSettingsCustomizer>() {
|
||||||
|
@Override
|
||||||
|
public SoundbrennerSettingsCustomizer createFromParcel(final Parcel in) {
|
||||||
|
return new SoundbrennerSettingsCustomizer(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SoundbrennerSettingsCustomizer[] newArray(final int size) {
|
||||||
|
return new SoundbrennerSettingsCustomizer[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public SoundbrennerSettingsCustomizer() {}
|
||||||
|
|
||||||
|
protected SoundbrennerSettingsCustomizer(final Parcel in) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() { return 0; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull final Parcel dest, final int flags) {}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// DeviceSpecificSettingsCustomizer
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called after any preference in this screen changes and has been persisted.
|
||||||
|
* Re-evaluates beat-bar visibility when the time signature is updated.
|
||||||
|
*
|
||||||
|
* The new value is read directly from the ListPreference object because
|
||||||
|
* DeviceSpecificSettingsHandler does not expose a getPrefs() accessor.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onPreferenceChange(final Preference preference,
|
||||||
|
final DeviceSpecificSettingsHandler handler) {
|
||||||
|
if (SoundbrennerConstants.PREF_TIME_SIGNATURE.equals(preference.getKey())
|
||||||
|
&& preference instanceof ListPreference) {
|
||||||
|
final String timeSig = ((ListPreference) preference).getValue();
|
||||||
|
updateBeatAccentVisibility(handler, parseNumerator(timeSig));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger onSendConfiguration() in SoundbrennerSupport so the BLE
|
||||||
|
// config packet is written to the device whenever any preference changes.
|
||||||
|
handler.notifyPreferenceChanged(preference.getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called once when the settings screen is first built.
|
||||||
|
* Registers the change handler and sets initial beat-bar visibility from
|
||||||
|
* the {@code prefs} snapshot that Gadgetbridge passes in.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void customizeSettings(final DeviceSpecificSettingsHandler handler,
|
||||||
|
final Prefs prefs,
|
||||||
|
final String rootKey) {
|
||||||
|
// Register so onPreferenceChange fires for ALL Soundbrenner preferences.
|
||||||
|
handler.addPreferenceHandlerFor(SoundbrennerConstants.PREF_TIME_SIGNATURE);
|
||||||
|
handler.addPreferenceHandlerFor(SoundbrennerConstants.PREF_BPM);
|
||||||
|
handler.addPreferenceHandlerFor(SoundbrennerConstants.PREF_SUBDIVISION);
|
||||||
|
for (final String beatKey : SoundbrennerConstants.PREF_BEATS) {
|
||||||
|
handler.addPreferenceHandlerFor(beatKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply initial visibility from the persisted time-signature value.
|
||||||
|
final String timeSig = prefs.getString(
|
||||||
|
SoundbrennerConstants.PREF_TIME_SIGNATURE,
|
||||||
|
SoundbrennerConstants.DEFAULT_TIME_SIG);
|
||||||
|
updateBeatAccentVisibility(handler, parseNumerator(timeSig));
|
||||||
|
|
||||||
|
// Connect Start Stop Button
|
||||||
|
Preference togglePref = handler.findPreference("soundbrenner_metronome_toggle");
|
||||||
|
if (togglePref != null) {
|
||||||
|
togglePref.setOnPreferenceClickListener(preference -> {
|
||||||
|
// Kommando via GBDeviceEventConfigurationUpdate an den Support schicken
|
||||||
|
GBApplication.deviceService(handler.getDevice())
|
||||||
|
.onSendConfiguration(SoundbrennerConstants.PREF_METRONOME_RUNNING + "_toggle");
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getPreferenceKeysWithSummary() {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Private helpers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show SeekBars for beats 1..numerator, hide beats (numerator+1)..8.
|
||||||
|
*/
|
||||||
|
private void updateBeatAccentVisibility(final DeviceSpecificSettingsHandler handler,
|
||||||
|
final int numerator) {
|
||||||
|
for (int i = 0; i < SoundbrennerConstants.PREF_BEATS.length; i++) {
|
||||||
|
final Preference pref = handler.findPreference(SoundbrennerConstants.PREF_BEATS[i]);
|
||||||
|
if (pref != null) {
|
||||||
|
pref.setVisible(i < numerator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the numerator from a string like "4/4" or "3/8".
|
||||||
|
* Returns 4 on any parse error.
|
||||||
|
*/
|
||||||
|
private int parseNumerator(final String timeSig) {
|
||||||
|
try {
|
||||||
|
if (timeSig != null && timeSig.contains("/")) {
|
||||||
|
return Integer.parseInt(timeSig.split("/")[0]);
|
||||||
|
}
|
||||||
|
} catch (final NumberFormatException ignored) {}
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
+515
@@ -0,0 +1,515 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundbrenner;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothGatt;
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundbrenner.SoundbrennerConstants;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||||
|
|
||||||
|
public class SoundbrennerSupport extends AbstractBTLESingleDeviceSupport {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SoundbrennerSupport.class);
|
||||||
|
|
||||||
|
/** Mirrors the last command sent so the UI and onCharacteristicChanged stay in sync. */
|
||||||
|
private boolean metronomeRunning = false;
|
||||||
|
|
||||||
|
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||||
|
private final BatteryInfoProfile<SoundbrennerSupport> batteryInfoProfile;
|
||||||
|
|
||||||
|
public SoundbrennerSupport() {
|
||||||
|
super(LOG);
|
||||||
|
addSupportedService(SoundbrennerConstants.UUID_SERVICE_METRONOME);
|
||||||
|
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
|
||||||
|
|
||||||
|
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
||||||
|
batteryInfoProfile.addListener(new IntentListener() {
|
||||||
|
@Override
|
||||||
|
public void notify(Intent intent) {
|
||||||
|
if (BatteryInfoProfile.ACTION_BATTERY_INFO.equals(intent.getAction())) {
|
||||||
|
handleBatteryInfo((BatteryInfo)
|
||||||
|
intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addSupportedProfile(batteryInfoProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// AbstractBTLESingleDeviceSupport
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||||
|
LOG.info("Initializing Soundbrenner Core");
|
||||||
|
|
||||||
|
// Request maximum MTU first – the 32-byte config packet exceeds the default 20-byte limit.
|
||||||
|
builder.requestMtu(512);
|
||||||
|
|
||||||
|
// Battery Service: handled via BatteryInfoProfile (standard GATT Battery Service)
|
||||||
|
batteryInfoProfile.requestBatteryInfo(builder);
|
||||||
|
batteryInfoProfile.enableNotify(builder, true);
|
||||||
|
|
||||||
|
builder.setDeviceState(GBDevice.State.INITIALIZING);
|
||||||
|
|
||||||
|
// Enable NOTIFY on CHAR_CONFIG to receive watch-side parameter changes.
|
||||||
|
BluetoothGattCharacteristic configChar =
|
||||||
|
getCharacteristic(SoundbrennerConstants.UUID_CHARACTERISTIC_CONFIG);
|
||||||
|
if (configChar != null) {
|
||||||
|
builder.notify(configChar, true);
|
||||||
|
} else {
|
||||||
|
LOG.warn("CHAR_CONFIG not found during init – notifications disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable INDICATE on CHAR_CONTROL to track start/stop confirmations.
|
||||||
|
BluetoothGattCharacteristic controlChar =
|
||||||
|
getCharacteristic(SoundbrennerConstants.UUID_CHARACTERISTIC_CONTROL);
|
||||||
|
if (controlChar != null) {
|
||||||
|
builder.notify(controlChar, true);
|
||||||
|
} else {
|
||||||
|
LOG.warn("CHAR_CONTROL not found during init – indications disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore previously persisted metronome state flag.
|
||||||
|
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(
|
||||||
|
getDevice().getAddress());
|
||||||
|
metronomeRunning = prefs.getBoolean(SoundbrennerConstants.PREF_METRONOME_RUNNING, false);
|
||||||
|
|
||||||
|
builder.setDeviceState(GBDevice.State.INITIALIZED);
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean useAutoConnect() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Metronome control
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the metronome on/off. If turning on, the current configuration
|
||||||
|
* is written first so the device is always in a consistent state.
|
||||||
|
*/
|
||||||
|
public void toggleMetronome() {
|
||||||
|
try {
|
||||||
|
if (metronomeRunning) {
|
||||||
|
stopMetronome();
|
||||||
|
} else {
|
||||||
|
startMetronome();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error toggling metronome", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write config then send start command. */
|
||||||
|
public void startMetronome() throws IOException {
|
||||||
|
TransactionBuilder builder = performInitialized("StartMetronome");
|
||||||
|
writeConfigPacket(builder);
|
||||||
|
sendPreStartCommand(builder); // 0xd1 to 0x0063 before start
|
||||||
|
writeConfigPacket(builder); // seconds Config Write as in snoop log
|
||||||
|
sendControlCommand(builder, SoundbrennerConstants.CMD_METRONOME_START);
|
||||||
|
builder.queue();
|
||||||
|
persistMetronomeRunning(true);
|
||||||
|
metronomeRunning = true;
|
||||||
|
LOG.info("Metronome start command queued");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Send stop command only (no config write needed). */
|
||||||
|
public void stopMetronome() throws IOException {
|
||||||
|
TransactionBuilder builder = performInitialized("StopMetronome");
|
||||||
|
sendControlCommand(builder, SoundbrennerConstants.CMD_METRONOME_STOP);
|
||||||
|
builder.queue();
|
||||||
|
persistMetronomeRunning(false);
|
||||||
|
metronomeRunning = false;
|
||||||
|
LOG.info("Metronome stop command queued");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Configuration
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by Gadgetbridge whenever a device-specific preference changes.
|
||||||
|
* Checks the key against known prefix constants – no hard-coded strings.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onSendConfiguration(String key) {
|
||||||
|
// UI button toggle
|
||||||
|
if ((SoundbrennerConstants.PREF_METRONOME_RUNNING + "_toggle").equals(key)) {
|
||||||
|
toggleMetronome();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Device-card Start/Stop buttons
|
||||||
|
if ((SoundbrennerConstants.PREF_METRONOME_RUNNING + "_start").equals(key)) {
|
||||||
|
try {
|
||||||
|
startMetronome();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to start metronome", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((SoundbrennerConstants.PREF_METRONOME_RUNNING + "_stop").equals(key)) {
|
||||||
|
try {
|
||||||
|
stopMetronome();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to stop metronome", e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSoundbrennerPref = key.startsWith(SoundbrennerConstants.PREF_SOUNDBRENNER_PREFIX)
|
||||||
|
|| key.startsWith(SoundbrennerConstants.PREF_BEAT_ACCENT_PREFIX);
|
||||||
|
|
||||||
|
if (!isSoundbrennerPref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.info("Soundbrenner pref changed: {} – rebuilding config packet", key);
|
||||||
|
try {
|
||||||
|
TransactionBuilder builder = performInitialized("SendConfig");
|
||||||
|
writeConfigPacket(builder);
|
||||||
|
builder.queue();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to send configuration packet", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// GATT callbacks
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||||
|
BluetoothGattCharacteristic characteristic,
|
||||||
|
byte[] value) {
|
||||||
|
if (SoundbrennerConstants.UUID_CHARACTERISTIC_CONTROL.equals(
|
||||||
|
characteristic.getUuid())) {
|
||||||
|
handleControlNotify(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SoundbrennerConstants.UUID_CHARACTERISTIC_CONFIG.equals(
|
||||||
|
characteristic.getUuid())) {
|
||||||
|
handleConfigNotify(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCharacteristicRead(BluetoothGatt gatt,
|
||||||
|
BluetoothGattCharacteristic characteristic,
|
||||||
|
byte[] value,
|
||||||
|
int status) {
|
||||||
|
return super.onCharacteristicRead(gatt, characteristic, value, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Private – packet builder
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build and write the full 32-byte configuration packet from the current
|
||||||
|
* device-specific SharedPreferences.
|
||||||
|
*
|
||||||
|
* All SeekBarPreference values are stored as {@code int} via {@code putInt}.
|
||||||
|
*/
|
||||||
|
private void writeConfigPacket(TransactionBuilder builder) {
|
||||||
|
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(
|
||||||
|
getDevice().getAddress());
|
||||||
|
|
||||||
|
// --- Read preferences ---
|
||||||
|
// PREF_BPM is stored by SeekBarPreference as int.
|
||||||
|
int bpm = prefs.getInt(SoundbrennerConstants.PREF_BPM,
|
||||||
|
SoundbrennerConstants.DEFAULT_BPM);
|
||||||
|
|
||||||
|
// PREF_TIME_SIGNATURE is a ListPreference → stored as String.
|
||||||
|
String timeSig = prefs.getString(SoundbrennerConstants.PREF_TIME_SIGNATURE,
|
||||||
|
SoundbrennerConstants.DEFAULT_TIME_SIG);
|
||||||
|
|
||||||
|
// PREF_SUBDIVISION is a ListPreference → stored as String, never as int.
|
||||||
|
// Using getInt() here causes a ClassCastException → crash.
|
||||||
|
int subdivision;
|
||||||
|
try {
|
||||||
|
subdivision = Integer.parseInt(
|
||||||
|
prefs.getString(SoundbrennerConstants.PREF_SUBDIVISION,
|
||||||
|
String.valueOf(SoundbrennerConstants.DEFAULT_SUBDIVISION)));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
LOG.error("Failed to parse subdivision pref, using default");
|
||||||
|
subdivision = SoundbrennerConstants.DEFAULT_SUBDIVISION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse time signature (e.g. "4/4" -> numerator=4, denominator=4)
|
||||||
|
int numerator = 4;
|
||||||
|
int denominator = 4;
|
||||||
|
try {
|
||||||
|
String[] parts = timeSig.split("/");
|
||||||
|
numerator = Integer.parseInt(parts[0]);
|
||||||
|
denominator = Integer.parseInt(parts[1]);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to parse time signature '{}', using 4/4", timeSig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read per-beat accent strengths (beats 1..8; unused beats default to soft)
|
||||||
|
int[] beatStrengths = new int[8];
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
int defaultStrength = (i == 0)
|
||||||
|
? SoundbrennerConstants.DEFAULT_BEAT_ACCENT_1
|
||||||
|
: SoundbrennerConstants.DEFAULT_BEAT_ACCENT_N;
|
||||||
|
beatStrengths[i] = prefs.getInt(SoundbrennerConstants.PREF_BEATS[i], defaultStrength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Build packet (structure mirrors the validated buildPacket reference) ---
|
||||||
|
byte[] packet = new byte[SoundbrennerConstants.PACKET_SIZE];
|
||||||
|
|
||||||
|
// Bytes 0-3: BPM as IEEE 754 float, big-endian
|
||||||
|
int bpmBits = Float.floatToIntBits((float) bpm);
|
||||||
|
packet[0] = (byte) ((bpmBits >> 24) & 0xFF);
|
||||||
|
packet[1] = (byte) ((bpmBits >> 16) & 0xFF);
|
||||||
|
packet[2] = (byte) ((bpmBits >> 8) & 0xFF);
|
||||||
|
packet[3] = (byte) ( bpmBits & 0xFF);
|
||||||
|
|
||||||
|
// Bytes 4: time signature
|
||||||
|
// High nibble = (numerator-1), low nibble = log2(denominator)
|
||||||
|
int log2denom = (int) (Math.log(denominator) / Math.log(2));
|
||||||
|
byte timeSigByte = (byte) (((numerator - 1) & 0xF) << 4 | (log2denom & 0xF));
|
||||||
|
packet[4] = timeSigByte;
|
||||||
|
|
||||||
|
// Byte 5: beat strengths beats 1-4, 2 bits per beat MSB-first
|
||||||
|
// Byte 6: beat strengths beats 5-8; unused slots = 0x00 (wire default)
|
||||||
|
// Byte 7: unused, fixed 0x00
|
||||||
|
int strengthHi = 0;
|
||||||
|
int strengthLo = 0x00;
|
||||||
|
for (int i = 0; i < Math.min(numerator, 4); i++) {
|
||||||
|
strengthHi |= (strengthToDeviceBits(beatStrengths[i]) << (6 - i * 2));
|
||||||
|
}
|
||||||
|
for (int i = 4; i < Math.min(numerator, 8); i++) {
|
||||||
|
int slot = i - 4;
|
||||||
|
strengthLo &= ~(0x3 << (6 - slot * 2));
|
||||||
|
strengthLo |= (strengthToDeviceBits(beatStrengths[i]) << (6 - slot * 2));
|
||||||
|
}
|
||||||
|
packet[5] = (byte) strengthHi;
|
||||||
|
packet[6] = (byte) strengthLo;
|
||||||
|
packet[7] = (byte) 0x00;
|
||||||
|
|
||||||
|
// Byte 8: fixed 0x00
|
||||||
|
packet[8] = (byte) 0x00;
|
||||||
|
|
||||||
|
// Bytes 9-10: subdivision constant, 16-bit big-endian
|
||||||
|
// Formula: (steps - 1) * (9352 + 2 * numerator)
|
||||||
|
int subdivConst = (subdivision - 1) * (9352 + 2 * numerator);
|
||||||
|
packet[9] = (byte) ((subdivConst >> 8) & 0xFF);
|
||||||
|
packet[10] = (byte) ( subdivConst & 0xFF);
|
||||||
|
|
||||||
|
// Bytes 11-14: fixed 0x00
|
||||||
|
packet[11] = (byte) 0x00;
|
||||||
|
packet[12] = (byte) 0x00;
|
||||||
|
packet[13] = (byte) 0x00;
|
||||||
|
packet[14] = (byte) 0x00;
|
||||||
|
|
||||||
|
// Bytes 15..(15+numerator-1): one pattern byte per active beat
|
||||||
|
// subdivisionPatternByte() returns the bitmask shifted to bits 7-4
|
||||||
|
byte beatPatternByte = subdivisionPatternByte(subdivision);
|
||||||
|
for (int i = 0; i < numerator; i++) {
|
||||||
|
packet[15 + i] = beatPatternByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes (15+numerator)..30: inactive beat slots = 0x00
|
||||||
|
for (int i = 15 + numerator; i <= 30; i++) {
|
||||||
|
packet[i] = (byte) 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte 31: footer
|
||||||
|
packet[31] = (byte) 0x00;
|
||||||
|
|
||||||
|
// --- Write ---
|
||||||
|
BluetoothGattCharacteristic configChar =
|
||||||
|
getCharacteristic(SoundbrennerConstants.UUID_CHARACTERISTIC_CONFIG);
|
||||||
|
if (configChar != null) {
|
||||||
|
builder.write(configChar, packet);
|
||||||
|
LOG.debug("Config packet queued: bpm={}, timeSig={}, subdiv={}", bpm, timeSig, subdivision);
|
||||||
|
} else {
|
||||||
|
LOG.error("CHAR_CONFIG not found – cannot write configuration");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map UI accent strength (0-3) to the 2-bit inverted wire value.
|
||||||
|
* UI to wire: 0 to 3 (silent), 1 to 0 (soft), 2 to 1 (medium), 3 to 2 (strong)
|
||||||
|
*/
|
||||||
|
private int strengthToDeviceBits(int uiStrength) {
|
||||||
|
return (uiStrength == 0) ? 3 : (uiStrength - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the beat-pattern byte for a given subdivision step count.
|
||||||
|
* Bits 7-4 = active subdivision steps; bits 3-0 = always 0.
|
||||||
|
*/
|
||||||
|
private byte subdivisionPatternByte(int steps) {
|
||||||
|
switch (steps) {
|
||||||
|
case 2: return SoundbrennerConstants.BEAT_PATTERN_EIGHTH;
|
||||||
|
case 3: return SoundbrennerConstants.BEAT_PATTERN_TRIPLET;
|
||||||
|
case 4: return SoundbrennerConstants.BEAT_PATTERN_SIXTEENTH;
|
||||||
|
default: return SoundbrennerConstants.BEAT_PATTERN_QUARTER; // 1 = quarter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Write a single-byte command to CHAR_CONTROL. */
|
||||||
|
private void sendControlCommand(TransactionBuilder builder, byte command) {
|
||||||
|
BluetoothGattCharacteristic controlChar =
|
||||||
|
getCharacteristic(SoundbrennerConstants.UUID_CHARACTERISTIC_CONTROL);
|
||||||
|
if (controlChar != null) {
|
||||||
|
builder.write(controlChar, new byte[]{command});
|
||||||
|
} else {
|
||||||
|
LOG.error("CHAR_CONTROL not found – cannot send command 0x{:02X}", command & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the magic pre-start byte (0xd1) to the unknown control characteristic
|
||||||
|
* (handle 0x0063, UUID 061a5b49). The official app always sends this before
|
||||||
|
* writing the final config and issuing CMD_METRONOME_START.
|
||||||
|
*/
|
||||||
|
private void sendPreStartCommand(TransactionBuilder builder) {
|
||||||
|
BluetoothGattCharacteristic unknownCtrl =
|
||||||
|
getCharacteristic(SoundbrennerConstants.UUID_CHARACTERISTIC_UNKNOWN_CTRL);
|
||||||
|
if (unknownCtrl != null) {
|
||||||
|
builder.write(unknownCtrl, SoundbrennerConstants.CMD_PRE_START);
|
||||||
|
LOG.debug("Pre-start byte (0xd1) queued on 0x0063");
|
||||||
|
} else {
|
||||||
|
LOG.warn("UUID_CHARACTERISTIC_UNKNOWN_CTRL not found – skipping pre-start");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Private – notify handlers
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an INDICATE from CHAR_CONTROL (device confirms start/stop).
|
||||||
|
*/
|
||||||
|
private void handleControlNotify(byte[] value) {
|
||||||
|
if (value == null || value.length == 0) return;
|
||||||
|
if (value[0] == SoundbrennerConstants.CMD_METRONOME_START) {
|
||||||
|
metronomeRunning = true;
|
||||||
|
persistMetronomeRunning(true);
|
||||||
|
LOG.info("Device confirmed: metronome started");
|
||||||
|
} else if (value[0] == SoundbrennerConstants.CMD_METRONOME_STOP) {
|
||||||
|
metronomeRunning = false;
|
||||||
|
persistMetronomeRunning(false);
|
||||||
|
LOG.info("Device confirmed: metronome stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBatteryInfo(BatteryInfo info) {
|
||||||
|
if (info == null) return;
|
||||||
|
batteryCmd.level = (short) info.getPercentCharged();
|
||||||
|
handleGBDeviceEvent(batteryCmd);
|
||||||
|
LOG.info("Battery level: {}%", batteryCmd.level);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Handle a NOTIFY from CHAR_CONFIG (user changed settings on the watch).
|
||||||
|
* Parses the 32-byte packet and writes the new values back to SharedPreferences
|
||||||
|
* so the Gadgetbridge UI stays in sync.
|
||||||
|
*/
|
||||||
|
private void handleConfigNotify(byte[] value) {
|
||||||
|
if (value == null || value.length < SoundbrennerConstants.PACKET_SIZE) {
|
||||||
|
LOG.warn("Received short/null config notify payload ({} bytes)",
|
||||||
|
value == null ? 0 : value.length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
// Bytes 0-3: BPM
|
||||||
|
float bpm = buf.getFloat();
|
||||||
|
|
||||||
|
// Byte 4: time signature
|
||||||
|
buf.position(4);
|
||||||
|
int timeSigByte = buf.get() & 0xFF;
|
||||||
|
int numerator = (timeSigByte >> 4) + 1;
|
||||||
|
int log2denom = timeSigByte & 0x0F;
|
||||||
|
int denominator = 1 << log2denom;
|
||||||
|
String timeSig = numerator + "/" + denominator;
|
||||||
|
|
||||||
|
// Byte 5: beat strengths 1-4 (position is already 5 after the get() above)
|
||||||
|
int rawStrengths1_4 = buf.get() & 0xFF;
|
||||||
|
|
||||||
|
// Byte 6: beat strengths 5-8
|
||||||
|
int rawStrengths5_8 = buf.get() & 0xFF;
|
||||||
|
|
||||||
|
// Bytes 9-10: subdivision constant -> steps
|
||||||
|
buf.position(9);
|
||||||
|
int subdivConst = buf.getShort() & 0xFFFF;
|
||||||
|
int base = 9352 + 2 * numerator;
|
||||||
|
int subdivision = (base > 0) ? (subdivConst / base) + 1 : 1;
|
||||||
|
if (subdivision < 1 || subdivision > 4) subdivision = 1;
|
||||||
|
|
||||||
|
// Decode strengths
|
||||||
|
int[] beatStrengths = new int[8];
|
||||||
|
for (int slot = 0; slot < 4; slot++) {
|
||||||
|
int wire = (rawStrengths1_4 >> (6 - slot * 2)) & 0x03;
|
||||||
|
beatStrengths[slot] = wireToUiStrength(wire);
|
||||||
|
}
|
||||||
|
for (int slot = 0; slot < 4; slot++) {
|
||||||
|
int wire = (rawStrengths5_8 >> (6 - slot * 2)) & 0x03;
|
||||||
|
beatStrengths[4 + slot] = wireToUiStrength(wire);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist back to SharedPreferences
|
||||||
|
SharedPreferences.Editor editor = GBApplication
|
||||||
|
.getDeviceSpecificSharedPrefs(getDevice().getAddress())
|
||||||
|
.edit();
|
||||||
|
editor.putInt(SoundbrennerConstants.PREF_BPM, (int) bpm);
|
||||||
|
editor.putString(SoundbrennerConstants.PREF_TIME_SIGNATURE, timeSig);
|
||||||
|
// PREF_SUBDIVISION is a ListPreference → must be stored as String.
|
||||||
|
editor.putString(SoundbrennerConstants.PREF_SUBDIVISION, String.valueOf(subdivision));
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
editor.putInt(SoundbrennerConstants.PREF_BEATS[i], beatStrengths[i]);
|
||||||
|
}
|
||||||
|
editor.apply();
|
||||||
|
|
||||||
|
LOG.info("Config notify from watch parsed: bpm={}, timeSig={}, subdiv={}", bpm, timeSig, subdivision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Invert the wire encoding back to a UI strength value. */
|
||||||
|
private int wireToUiStrength(int wire) {
|
||||||
|
switch (wire & 0x03) {
|
||||||
|
case 0b11: return 0;
|
||||||
|
case 0b00: return 1;
|
||||||
|
case 0b01: return 2;
|
||||||
|
case 0b10: return 3;
|
||||||
|
default: return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persist the running flag so it survives reconnects. */
|
||||||
|
private void persistMetronomeRunning(boolean running) {
|
||||||
|
GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())
|
||||||
|
.edit()
|
||||||
|
.putBoolean(SoundbrennerConstants.PREF_METRONOME_RUNNING, running)
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4563,6 +4563,50 @@
|
|||||||
<item>anc_mode__anc_off</item>
|
<item>anc_mode__anc_off</item>
|
||||||
<item>anc_mode__transparency_off</item>
|
<item>anc_mode__transparency_off</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="soundbrenner_time_sig_entries">
|
||||||
|
<item>4/4</item>
|
||||||
|
<item>2/4</item>
|
||||||
|
<item>3/4</item>
|
||||||
|
<item>5/4</item>
|
||||||
|
<item>6/4</item>
|
||||||
|
<item>3/8</item>
|
||||||
|
<item>4/8</item>
|
||||||
|
<item>5/8</item>
|
||||||
|
<item>6/8</item>
|
||||||
|
<item>9/8</item>
|
||||||
|
<item>12/8</item>
|
||||||
|
<item>2/2</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="soundbrenner_time_sig_values">
|
||||||
|
<item>4/4</item>
|
||||||
|
<item>2/4</item>
|
||||||
|
<item>3/4</item>
|
||||||
|
<item>5/4</item>
|
||||||
|
<item>6/4</item>
|
||||||
|
<item>3/8</item>
|
||||||
|
<item>4/8</item>
|
||||||
|
<item>5/8</item>
|
||||||
|
<item>6/8</item>
|
||||||
|
<item>9/8</item>
|
||||||
|
<item>12/8</item>
|
||||||
|
<item>2/2</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="soundbrenner_subdivision_entries">
|
||||||
|
<item>Quarter notes</item>
|
||||||
|
<item>Eighth notes</item>
|
||||||
|
<item>Triplets</item>
|
||||||
|
<item>Sixteenth notes</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="soundbrenner_subdivision_values">
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>3</item>
|
||||||
|
<item>4</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="soundcore_anc_mode_names">
|
<string-array name="soundcore_anc_mode_names">
|
||||||
<item>@string/prefs_active_noise_cancelling_transport</item>
|
<item>@string/prefs_active_noise_cancelling_transport</item>
|
||||||
|
|||||||
@@ -2155,6 +2155,7 @@
|
|||||||
<string name="devicetype_sony_wi_sp600n" translatable="false">Sony WI-SP600N</string>
|
<string name="devicetype_sony_wi_sp600n" translatable="false">Sony WI-SP600N</string>
|
||||||
<string name="devicetype_sony_linkbuds" translatable="false">Sony LinkBuds</string>
|
<string name="devicetype_sony_linkbuds" translatable="false">Sony LinkBuds</string>
|
||||||
<string name="devicetype_sony_linkbuds_s" translatable="false">Sony LinkBuds S</string>
|
<string name="devicetype_sony_linkbuds_s" translatable="false">Sony LinkBuds S</string>
|
||||||
|
<string name="devicetype_soundbrenner_core" translatable="false">Soundbrenner Core</string>
|
||||||
<string name="devicetype_soundcore_liberty3_pro" translatable="false">Soundcore Liberty 3 Pro</string>
|
<string name="devicetype_soundcore_liberty3_pro" translatable="false">Soundcore Liberty 3 Pro</string>
|
||||||
<string name="devicetype_soundcore_liberty4_nc" translatable="false">Soundcore Liberty 4 NC</string>
|
<string name="devicetype_soundcore_liberty4_nc" translatable="false">Soundcore Liberty 4 NC</string>
|
||||||
<string name="devicetype_soundcore_motion300" translatable="false">Soundcore Motion 300</string>
|
<string name="devicetype_soundcore_motion300" translatable="false">Soundcore Motion 300</string>
|
||||||
@@ -3536,6 +3537,35 @@
|
|||||||
<string name="moondrop_touch_action_anc_mode">Switch Active Noise Cancelling Mode</string>
|
<string name="moondrop_touch_action_anc_mode">Switch Active Noise Cancelling Mode</string>
|
||||||
<string name="moondrop_touch_trigger_long_press_1s">Long Press (1s)</string>
|
<string name="moondrop_touch_trigger_long_press_1s">Long Press (1s)</string>
|
||||||
<string name="moondrop_touch_trigger_long_press_3s">Long Press (3s)</string>
|
<string name="moondrop_touch_trigger_long_press_3s">Long Press (3s)</string>
|
||||||
|
<string name="soundbrenner_category_control">Control</string>
|
||||||
|
<string name="soundbrenner_pref_metronome_toggle_title">Start/Stop</string>
|
||||||
|
<string name="soundbrenner_pref_metronome_toggle_summary">Start or stop metronome</string>
|
||||||
|
<string name="soundbrenner_action_start">Start</string>
|
||||||
|
<string name="soundbrenner_action_stop">Stop</string>
|
||||||
|
<string name="soundbrenner_category_tempo">Tempo</string>
|
||||||
|
<string name="soundbrenner_category_time_signature">Time Signature</string>
|
||||||
|
<string name="soundbrenner_category_subdivision">Subdivision</string>
|
||||||
|
<string name="soundbrenner_category_accents">Beat Accents</string>
|
||||||
|
<string name="soundbrenner_pref_bpm_title">BPM</string>
|
||||||
|
<string name="soundbrenner_pref_bpm_summary">Beats per minute (20–400)</string>
|
||||||
|
<string name="soundbrenner_pref_time_sig_title">Time Signature</string>
|
||||||
|
<string name="soundbrenner_pref_subdivision_title">Subdivision</string>
|
||||||
|
<string name="soundbrenner_pref_beat_1_title">Beat 1 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_2_title">Beat 2 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_3_title">Beat 3 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_4_title">Beat 4 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_5_title">Beat 5 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_6_title">Beat 6 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_7_title">Beat 7 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_8_title">Beat 8 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_9_title">Beat 9 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_10_title">Beat 10 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_11_title">Beat 11 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_12_title">Beat 12 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_13_title">Beat 13 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_14_title">Beat 14 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_15_title">Beat 15 Accent</string>
|
||||||
|
<string name="soundbrenner_pref_beat_16_title">Beat 16 Accent</string>
|
||||||
<string name="soundcore_voice_prompts">Voice Prompts</string>
|
<string name="soundcore_voice_prompts">Voice Prompts</string>
|
||||||
<string name="soundcore_button_brightness">Button Brightness</string>
|
<string name="soundcore_button_brightness">Button Brightness</string>
|
||||||
<string name="soundcore_button_brightness_low">Low</string>
|
<string name="soundcore_button_brightness_low">Low</string>
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Device-specific settings screen for the Soundbrenner Core.
|
||||||
|
Registered in SoundbrennerCoordinator.getSupportedDeviceSpecificSettings().
|
||||||
|
|
||||||
|
Key names are defined in SoundbrennerConstants – no hard-coded strings here.
|
||||||
|
|
||||||
|
Note on preference storage:
|
||||||
|
SeekBarPreference (BPM, beat accents) calls putInt internally → read with getInt().
|
||||||
|
ListPreference (time signature, subdivision) stores String → read with getString().
|
||||||
|
-->
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<!-- ===================================================================
|
||||||
|
Control buttons (Start/Stop)
|
||||||
|
=================================================================== -->
|
||||||
|
<PreferenceCategory android:title="@string/soundbrenner_category_control">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="soundbrenner_metronome_toggle"
|
||||||
|
android:title="@string/soundbrenner_pref_metronome_toggle_title"
|
||||||
|
android:summary="@string/soundbrenner_pref_metronome_toggle_summary"
|
||||||
|
android:icon="@drawable/ic_play" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ===================================================================
|
||||||
|
Tempo
|
||||||
|
=================================================================== -->
|
||||||
|
<PreferenceCategory android:title="@string/soundbrenner_category_tempo">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
BPM: 20-300, default 100.
|
||||||
|
Key: SoundbrennerConstants.PREF_BPM = "soundbrenner_bpm"
|
||||||
|
-->
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_bpm"
|
||||||
|
android:title="@string/soundbrenner_pref_bpm_title"
|
||||||
|
android:summary="@string/soundbrenner_pref_bpm_summary"
|
||||||
|
android:defaultValue="100"
|
||||||
|
app:min="20"
|
||||||
|
android:max="300"
|
||||||
|
app:showSeekBarValue="true" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<!-- ===================================================================
|
||||||
|
Time signature
|
||||||
|
=================================================================== -->
|
||||||
|
<PreferenceCategory android:title="@string/soundbrenner_category_time_signature">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Time signature selector.
|
||||||
|
Key: SoundbrennerConstants.PREF_TIME_SIGNATURE = "soundbrenner_time_signature"
|
||||||
|
Entries defined in res/values/arrays.xml (soundbrenner_time_sig_entries / _values).
|
||||||
|
-->
|
||||||
|
<ListPreference
|
||||||
|
android:key="soundbrenner_time_signature"
|
||||||
|
android:title="@string/soundbrenner_pref_time_sig_title"
|
||||||
|
android:summary="%s"
|
||||||
|
android:defaultValue="4/4"
|
||||||
|
android:entries="@array/soundbrenner_time_sig_entries"
|
||||||
|
android:entryValues="@array/soundbrenner_time_sig_values" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<!-- ===================================================================
|
||||||
|
Subdivision
|
||||||
|
=================================================================== -->
|
||||||
|
<PreferenceCategory android:title="@string/soundbrenner_category_subdivision">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Subdivision steps: 1=quarter, 2=eighth, 3=triplet, 4=sixteenth.
|
||||||
|
Key: SoundbrennerConstants.PREF_SUBDIVISION = "soundbrenner_subdivision"
|
||||||
|
Entries defined in res/values/arrays.xml.
|
||||||
|
-->
|
||||||
|
<ListPreference
|
||||||
|
android:key="soundbrenner_subdivision"
|
||||||
|
android:title="@string/soundbrenner_pref_subdivision_title"
|
||||||
|
android:summary="%s"
|
||||||
|
android:defaultValue="1"
|
||||||
|
android:entries="@array/soundbrenner_subdivision_entries"
|
||||||
|
android:entryValues="@array/soundbrenner_subdivision_values" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<!-- ===================================================================
|
||||||
|
Beat accent strengths (beats 1-8)
|
||||||
|
Visibility of beats 2-8 is controlled dynamically by
|
||||||
|
SoundbrennerSettingsCustomizer based on the selected numerator.
|
||||||
|
=================================================================== -->
|
||||||
|
<PreferenceCategory android:title="@string/soundbrenner_category_accents">
|
||||||
|
|
||||||
|
<!-- Beat 1 – always visible -->
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_1"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_1_title"
|
||||||
|
android:defaultValue="3"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<!-- Beats 2-8: shown/hidden by SoundbrennerSettingsCustomizer -->
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_2"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_2_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_3"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_3_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_4"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_4_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_5"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_5_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_6"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_6_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_7"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_7_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
<SeekBarPreference
|
||||||
|
android:key="soundbrenner_beat_accent_8"
|
||||||
|
android:title="@string/soundbrenner_pref_beat_8_title"
|
||||||
|
android:defaultValue="1"
|
||||||
|
app:min="0"
|
||||||
|
android:max="3" />
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
Reference in New Issue
Block a user