diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 3911aaaaf..835af4edd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -411,6 +411,7 @@ public class DeviceSettingsPreferenceConst { 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_ANC_MODE = "pref_soundcore_anc_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"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 1234c7168..4082884e5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -747,6 +747,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION); addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE); addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING); + addPreferenceHandlerFor(PREF_SOUNDCORE_ANC_MODE); addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE); addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE); addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/q30/SoundcoreQ30Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/q30/SoundcoreQ30Coordinator.java new file mode 100644 index 000000000..6522c08e2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/q30/SoundcoreQ30Coordinator.java @@ -0,0 +1,71 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.q30; + +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.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.q30.SoundcoreQ30DeviceSupport; + +public class SoundcoreQ30Coordinator extends AbstractDeviceCoordinator { + @Override + public int getDeviceNameResource() { + return R.string.devicetype_soundcore_q30; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_headphones; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_headphones_disabled; + } + + @Override + public String getManufacturer() { + return "Anker"; + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("Soundcore Q30"); + } + + @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 BatteryConfig[] getBatteryConfig(final GBDevice device) { + BatteryConfig battery = new BatteryConfig(0, R.drawable.ic_battery, R.string.battery); + return new BatteryConfig[]{battery}; + } + + @Override + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_soundcore_q30); + return deviceSpecificSettings; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return SoundcoreQ30DeviceSupport.class; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 245545a07..7a949abd8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -268,6 +268,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoo import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty4_nc.SoundcoreLiberty4NCCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.SoundcoreMotion300Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.q30.SoundcoreQ30Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator; @@ -536,6 +537,7 @@ public enum DeviceType { SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class), SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class), SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class), + SOUNDCORE_Q30(SoundcoreQ30Coordinator.class), MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class), BOSE_QC35(QC35Coordinator.class), HONORBAND3(HonorBand3Coordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty/SoundcoreLibertyProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty/SoundcoreLibertyProtocol.java index 950fc7564..4285c330f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty/SoundcoreLibertyProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty/SoundcoreLibertyProtocol.java @@ -76,15 +76,15 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol { private void decodeAudioMode(byte[] payload) { SharedPreferences prefs = getDevicePrefs().getPreferences(); SharedPreferences.Editor editor = prefs.edit(); - String soundmode = "off"; + String ambient_sound_mode = "off"; int anc_strength = 0; if (payload[0] == 0x00) { - soundmode = "noise_cancelling"; + ambient_sound_mode = "noise_cancelling"; } else if (payload[0] == 0x01) { - soundmode = "ambient_sound"; + ambient_sound_mode = "ambient_sound"; } else if (payload[0] == 0x02) { - soundmode = "off"; + ambient_sound_mode = "off"; } if (payload[1] == 0x10) { @@ -99,7 +99,7 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol { boolean adaptive_anc = (payload[3] == 0x01); boolean windnoiseReduction = (payload[4] == 0x01); - editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode); + editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, ambient_sound_mode); 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); @@ -210,19 +210,19 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol { private byte[] encodeAudioMode() { Prefs prefs = getDevicePrefs(); - byte anc_mode; + byte ambient_sound_mode; switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) { case "noise_cancelling": - anc_mode = 0x00; + ambient_sound_mode = 0x00; break; case "ambient_sound": - anc_mode = 0x01; + ambient_sound_mode = 0x01; break; case "off": - anc_mode = 0x02; + ambient_sound_mode = 0x02; break; default: - LOG.error("Invalid Audio Mode selected"); + LOG.error("Invalid Ambient Mode selected"); return null; } @@ -246,7 +246,7 @@ public class SoundcoreLibertyProtocol extends AbstractSoundcoreProtocol { 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[]{anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01}; + byte[] payload = new byte[]{ambient_sound_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01}; return new SoundcorePacket((short) 0x8106, payload).encode(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30DeviceSupport.java new file mode 100644 index 000000000..e4a12a07d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30DeviceSupport.java @@ -0,0 +1,29 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30; + +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class SoundcoreQ30DeviceSupport extends AbstractSerialDeviceSupport { + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + return new SoundcoreQ30Protocol(getDevice()); + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new SoundcoreQ30IOThread(getDevice(), getContext(), (SoundcoreQ30Protocol) getDeviceProtocol(),this, getBluetoothAdapter()); + } + + @Override + public boolean connect() { + getDeviceIOThread().start(); + return true; + } + + @Override + public boolean useAutoConnect() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30IOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30IOThread.java new file mode 100644 index 000000000..b7ffc0947 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30IOThread.java @@ -0,0 +1,52 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30; + +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; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty.SoundcoreLibertyProtocol; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class SoundcoreQ30IOThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(SoundcoreQ30IOThread.class); + private final SoundcoreQ30Protocol mSoundcoreProtocol; + + public SoundcoreQ30IOThread(GBDevice gbDevice, Context context, SoundcoreQ30Protocol deviceProtocol, AbstractSerialDeviceSupport deviceSupport, BluetoothAdapter btAdapter) { + super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter); + mSoundcoreProtocol = deviceProtocol; + } + + @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); + } + + @Override + protected void initialize() { + write(mSoundcoreProtocol.encodeDeviceInfoRequest()); + super.initialize(); + } + + @NonNull + protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { + return UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30Protocol.java new file mode 100644 index 000000000..4b5523e4b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/q30/SoundcoreQ30Protocol.java @@ -0,0 +1,157 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.q30; + +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.util.ArrayList; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.AbstractSoundcoreProtocol; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class SoundcoreQ30Protocol extends AbstractSoundcoreProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(SoundcoreQ30Protocol.class); + + protected SoundcoreQ30Protocol(GBDevice device) { + super(device); + } + + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + ByteBuffer buf = ByteBuffer.wrap(responseData); + SoundcorePacket packet = SoundcorePacket.decode(buf); + + if (packet == null) + return null; + + List devEvts = new ArrayList<>(); + short cmd = packet.getCommand(); + byte[] payload = packet.getPayload(); + + if (cmd == (short) 0x0101) { + // a lot of other data is in here, anything interesting? + String firmware1 = readString(payload, 39, 5); + String firmware2 = ""; + String serialNumber = readString(payload, 44, 16); + devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber)); + } else if (cmd == (short) 0x0106) { // ANC Mode Updated by Button + decodeAudioMode(payload); + } else if (cmd == (short) 0x0301) { // Battery Update + int battery = payload[0] * 20; // untested + devEvts.add(buildBatteryInfo(0, battery)); + } else if (cmd == (short) 0x8106) { + // Acknowledgement for changed Ambient Mode + // empty payload + } else { + LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData)); + } + return devEvts.toArray(new GBDeviceEvent[devEvts.size()]); + } + + + @Override + public byte[] encodeSendConfiguration(String config) { + switch (config) { + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL: + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ANC_MODE: + return encodeAudioMode(); + + default: + LOG.debug("Unsupported CONFIG: " + config); + } + + return super.encodeSendConfiguration(config); + } + + /** + * 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 ambient_sound_mode; + switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) { + case "noise_cancelling": + ambient_sound_mode = 0x00; + break; + case "ambient_sound": + ambient_sound_mode = 0x01; + break; + case "off": + ambient_sound_mode = 0x02; + break; + default: + LOG.error("Invalid Ambient Mode selected"); + return null; + } + + byte anc_mode; + switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ANC_MODE, "transport")) { + case "transport": + anc_mode = 0x00; + break; + case "outdoor": + anc_mode = 0x01; + break; + case "indoor": + anc_mode = 0x02; + break; + default: + LOG.error("Invalid ANC Mode selected"); + return null; + } + + byte[] payload = new byte[]{ambient_sound_mode, anc_mode, 0x01}; + return new SoundcorePacket((short) 0x8106, payload).encode(); + } + + /** + * Gets triggered when the button on the device is pressed or transparency toggled with the right palm. + */ + private void decodeAudioMode(byte[] payload) { + SharedPreferences prefs = getDevicePrefs().getPreferences(); + SharedPreferences.Editor editor = prefs.edit(); + String ambient_sound_mode = "off"; + String anc_mode = "transport"; + + if (payload[0] == 0x00) { + ambient_sound_mode = "noise_cancelling"; + } else if (payload[0] == 0x01) { + ambient_sound_mode = "ambient_sound"; + } else if (payload[0] == 0x02) { + ambient_sound_mode = "off"; + } + + if (payload[1] == 0x00) { + anc_mode = "transport"; + } else if (payload[1] == 0x01) { + anc_mode = "outdoor"; + } else if (payload[1] == 0x02) { + anc_mode = "indoor"; + } + + // payload has two more bytes + // payload[2] always 1 ? + // payload[3] checksum ? + + editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, ambient_sound_mode); + editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ANC_MODE, anc_mode); + editor.apply(); + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 9e2875dda..9a96e933a 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3762,6 +3762,18 @@ game_mode + + @string/prefs_active_noise_cancelling_transport + @string/prefs_active_noise_cancelling_indoor + @string/prefs_active_noise_cancelling_outdoor + + + + transport + indoor + outdoor + + @string/pref_media_volumedown @string/pref_media_volumeup diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fc189089..484f61242 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1860,6 +1860,7 @@ Soundcore Liberty 3 Pro Soundcore Liberty 4 NC Soundcore Motion 300 + Soundcore Q30 Moondrop Space Travel Binary sensor Honor Band 3 @@ -2580,6 +2581,9 @@ High Low Block noises of the surroundings + Transport + Indoor + Outdoor Pressure relief with ambient sound Prevent feeling of pressure in ears when not using Active Noise Cancelling Left diff --git a/app/src/main/res/xml/devicesettings_soundcore_liberty.xml b/app/src/main/res/xml/devicesettings_soundcore_liberty.xml index 6a280e922..914d5fa20 100644 --- a/app/src/main/res/xml/devicesettings_soundcore_liberty.xml +++ b/app/src/main/res/xml/devicesettings_soundcore_liberty.xml @@ -48,8 +48,6 @@ - - diff --git a/app/src/main/res/xml/devicesettings_soundcore_q30.xml b/app/src/main/res/xml/devicesettings_soundcore_q30.xml new file mode 100644 index 000000000..40ea8ddda --- /dev/null +++ b/app/src/main/res/xml/devicesettings_soundcore_q30.xml @@ -0,0 +1,26 @@ + + + + + + + + + +