/* Copyright (C) 2021 José Rebelo 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 . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.os.Handler; import android.os.ParcelUuid; import androidx.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Message; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class SonyHeadphonesIoThread extends BtClassicIoThread { private static final Logger LOG = LoggerFactory.getLogger(SonyHeadphonesIoThread.class); private final SonyHeadphonesProtocol mProtocol; private final UUID btrfcommUuidV1 = UUID.fromString("96CC203E-5068-46ad-B32D-E316F5E069BA"); private final UUID btrfcommUuidV2 = UUID.fromString("956C7B26-D49A-4BA8-B03F-B17D393CB6E2"); // Track whether we got the first init reply private final Handler handler = new Handler(); private int initRetries = 0; /** * Sometimes the headphones will ignore the first init request, so we retry a few times * TODO: Implement this in a more elegant way. Ideally, we should retry every command for which we didn't get an ACK. */ private final Runnable initSendRunnable = new Runnable() { public void run() { // If we still haven't got any reply, re-send the init if (!mProtocol.hasProtocolImplementation()) { if (initRetries++ < 2) { LOG.warn("Init retry {}", initRetries); mProtocol.decreasePendingAcks(); write(mProtocol.encodeInit()); scheduleInitRetry(); } else { LOG.error("Failed to start headphones init after {} tries", initRetries); quit(); } } } }; public SonyHeadphonesIoThread(final GBDevice gbDevice, final Context context, final SonyHeadphonesProtocol protocol, final SonyHeadphonesSupport support, final BluetoothAdapter btAdapter) { super(gbDevice, context, protocol, support, btAdapter); mProtocol = protocol; } @Override protected void initialize() { write(mProtocol.encodeInit()); scheduleInitRetry(); setUpdateState(GBDevice.State.INITIALIZING); } @Override public synchronized void write(final byte[] bytes) { // Log the human-readable message, for debugging LOG.info("Writing {}", Message.fromBytes(bytes)); super.write(bytes); } @Override protected byte[] parseIncoming(final InputStream inputStream) throws IOException { final ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); final byte[] incoming = new byte[1]; do { inputStream.read(incoming); if (incoming[0] == Message.MESSAGE_HEADER) { msgStream.reset(); } msgStream.write(incoming); } while (incoming[0] != Message.MESSAGE_TRAILER); LOG.trace("Raw message: {}", GB.hexdump(msgStream.toByteArray())); return msgStream.toByteArray(); } @NonNull @Override protected UUID getUuidToConnect(@NonNull final ParcelUuid[] uuids) { boolean hasV1 = false; boolean hasV2 = false; boolean preferV2 = getCoordinator().preferServiceV2(); for (final ParcelUuid uuid : uuids) { if (uuid.getUuid().equals(btrfcommUuidV1)) { LOG.info("Found Sony UUID V1"); hasV1 = true; } else if (uuid.getUuid().equals(btrfcommUuidV2)) { LOG.info("Found Sony UUID V2"); hasV2 = true; } } if (hasV2) { LOG.info("Using Sony UUID V2"); return btrfcommUuidV2; } else if (hasV1) { LOG.info("Using Sony UUID V1"); return btrfcommUuidV1; } LOG.warn("Failed to find a known Sony UUID, will fallback to {}", (preferV2 ? "V2" : "V1")); return preferV2 ? btrfcommUuidV2 : btrfcommUuidV1; } private void scheduleInitRetry() { LOG.info("Scheduling init retry"); handler.postDelayed(initSendRunnable, 1250); } private SonyHeadphonesCoordinator getCoordinator() { return (SonyHeadphonesCoordinator) DeviceHelper.getInstance().getCoordinator(getDevice()); } }