Generic Audio BT Device Support

Add support for any Audio Bluetooth Device recognized by the OS. The main aim is to support incoming calls and notifications being read aloud.
The device must be paired with Android OS and be recognized as audio device.

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/4675
Reviewed-by: Arjan Schrijver <arjan5@noreply.codeberg.org>
Co-authored-by: Daniele Gobbetti <daniele+cb@gobbetti.name>
Co-committed-by: Daniele Gobbetti <daniele+cb@gobbetti.name>
This commit is contained in:
Daniele Gobbetti
2025-03-31 06:53:11 +00:00
committed by daniele
parent 6753eba880
commit 0fefa5de6b
4 changed files with 238 additions and 0 deletions
@@ -0,0 +1,81 @@
package nodomain.freeyourgadget.gadgetbridge.devices.generic_headphones;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import androidx.annotation.NonNull;
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.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.generic_headphones.GenericHeadphonesSupport;
public class GenericHeadphonesCoordinator extends AbstractDeviceCoordinator {
@Override
public int getOrderPriority() {
return Integer.MAX_VALUE;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}
@Override
public boolean supports(GBDeviceCandidate candidate) {
BluetoothDevice device = candidate.getDevice();
BluetoothClass deviceClass = device.getBluetoothClass();
int deviceType = deviceClass.getDeviceClass();
return deviceType == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET ||
deviceType == BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES ||
deviceType == BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER ||
deviceType == BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER ||
deviceType == BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO ||
deviceType == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE;
}
@Override
public String getManufacturer() {
return "generic";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return GenericHeadphonesSupport.class;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_unknown;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_headphones;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_headphones_disabled;
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_headphones);
return deviceSpecificSettings;
}
}
@@ -0,0 +1,65 @@
/* Copyright (C) 2016-2024 Andreas Shimokawa, Daniel Dakhno, 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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.externalevents;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class BluetoothDisconnectReceiver extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(BluetoothDisconnectReceiver.class);
public BluetoothDisconnectReceiver() {
}
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
if (action == null) {
return;
}
if (!action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED) || !intent.hasExtra(BluetoothDevice.EXTRA_DEVICE)) {
return;
}
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device == null) {
LOG.error("Got no device for {}", action);
return;
}
LOG.info("disconnect detected from {}", device.getAddress());
final GBDevice gbDevice = GBApplication.app().getDeviceManager().getDeviceByAddress(device.getAddress());
if (gbDevice == null) {
LOG.info("Connected device {} unknown", device.getAddress());
return;
}
GBApplication.deviceService(gbDevice).disconnect();
}
}
@@ -124,6 +124,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.Garm
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.GarminVivomoveTrendCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosmart.GarminVivosmart5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosport.GarminVivosportCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.generic_headphones.GenericHeadphonesCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.gree.GreeAcCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.HamaFit6900DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator;
@@ -628,6 +629,7 @@ public enum DeviceType {
CYCLING_SENSOR(CyclingSensorCoordinator.class),
BLE_GATT_CLIENT(BleGattClientCoordinator.class),
MARSTEK_B2500(MarstekB2500DeviceCoordinator.class),
GENERIC_HEADPHONES(GenericHeadphonesCoordinator.class),
TEST(TestDeviceCoordinator.class),
ULTRAHUMAN_RING_AIR(UltrahumanDeviceCoordinator.class);
@@ -0,0 +1,90 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.generic_headphones;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.IntentFilter;
import androidx.core.content.ContextCompat;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothDisconnectReceiver;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.HeadphoneHelper;
public class GenericHeadphonesSupport extends AbstractDeviceSupport implements HeadphoneHelper.Callback {
private HeadphoneHelper headphoneHelper;
private BluetoothDisconnectReceiver mBlueToothDisconnectReceiver = null;
private final BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
}
@Override
public void onServiceDisconnected(int profile) {
}
};
@Override
public void onSetCallState(CallSpec callSpec) {
headphoneHelper.onSetCallState(callSpec);
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
headphoneHelper.onNotification(notificationSpec);
}
@Override
public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) {
super.setContext(gbDevice, btAdapter, context);
headphoneHelper = new HeadphoneHelper(getContext(), getDevice(), this);
}
@Override
public void onSendConfiguration(String config) {
if (!headphoneHelper.onSendConfiguration(config))
super.onSendConfiguration(config);
}
@Override
public void dispose() {
if (headphoneHelper != null) {
headphoneHelper.dispose();
headphoneHelper = null;
}
if (mBlueToothDisconnectReceiver != null) {
getContext().unregisterReceiver(mBlueToothDisconnectReceiver);
mBlueToothDisconnectReceiver = null;
}
}
@Override
public boolean connect() {
if (isConnected()) {
return false;
}
gbDevice.setState(GBDevice.State.CONNECTING);
gbDevice.sendDeviceUpdateIntent(getContext(), GBDevice.DeviceUpdateSubject.CONNECTION_STATE);
getBluetoothAdapter().getProfileProxy(getContext(), profileListener, BluetoothProfile.HEADSET);
mBlueToothDisconnectReceiver = new BluetoothDisconnectReceiver();
ContextCompat.registerReceiver(getContext(), mBlueToothDisconnectReceiver, new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED), ContextCompat.RECEIVER_EXPORTED);
return true;
}
@Override
public boolean useAutoConnect() {
return true;
}
}