summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt9
-rw-r--r--core/java/android/bluetooth/BluetoothLeAudio.java451
-rw-r--r--core/java/android/bluetooth/BluetoothProfile.java9
3 files changed, 468 insertions, 1 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 7e60666a4f7a..627d39b9eaf2 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9051,6 +9051,15 @@ package android.bluetooth {
field @NonNull public static final android.os.Parcelable.Creator<android.bluetooth.BluetoothHidDeviceAppSdpSettings> CREATOR;
}
+ public final class BluetoothLeAudio implements java.lang.AutoCloseable android.bluetooth.BluetoothProfile {
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void close();
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) protected void finalize();
+ method @NonNull public java.util.List<android.bluetooth.BluetoothDevice> getConnectedDevices();
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public int getConnectionState(@NonNull android.bluetooth.BluetoothDevice);
+ method @NonNull public java.util.List<android.bluetooth.BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[]);
+ field public static final String ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED = "android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED";
+ }
+
public final class BluetoothManager {
method public android.bluetooth.BluetoothAdapter getAdapter();
method @RequiresPermission(android.Manifest.permission.BLUETOOTH) public java.util.List<android.bluetooth.BluetoothDevice> getConnectedDevices(int);
diff --git a/core/java/android/bluetooth/BluetoothLeAudio.java b/core/java/android/bluetooth/BluetoothLeAudio.java
new file mode 100644
index 000000000000..3f00fa6f4181
--- /dev/null
+++ b/core/java/android/bluetooth/BluetoothLeAudio.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class provides the public APIs to control the LeAudio profile.
+ *
+ * <p>BluetoothLeAudio is a proxy object for controlling the Bluetooth LE Audio
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothLeAudio proxy object.
+ *
+ * <p> Android only supports one set of connected Bluetooth LeAudio device at a time. Each
+ * method is protected with its appropriate permission.
+ */
+public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothLeAudio";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /**
+ * Intent used to broadcast the change in connection state of the LeAudio
+ * profile. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own connection state changes.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED";
+
+ /**
+ * This represents an invalid group ID.
+ *
+ * @hide
+ */
+ public static final int GROUP_ID_INVALID = IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID;
+
+ private BluetoothAdapter mAdapter;
+ private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO, "BluetoothLeAudio",
+ IBluetoothLeAudio.class.getName()) {
+ @Override
+ public IBluetoothLeAudio getServiceInterface(IBinder service) {
+ return IBluetoothLeAudio.Stub.asInterface(Binder.allowBlocking(service));
+ }
+ };
+
+ /**
+ * Create a BluetoothLeAudio proxy object for interacting with the local
+ * Bluetooth LeAudio service.
+ */
+ /*package*/ BluetoothLeAudio(Context context, ServiceListener listener) {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothLeAudio getService() {
+ return mProfileConnector.getService();
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is already connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that
+ * connection state intent for the profile will be broadcasted with
+ * the state. Users can get the connection state of the profile
+ * from this intent.
+ *
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean connect(@Nullable BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled() && isValidDevice(device)) {
+ return service.connect(device);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ }
+ }
+
+ /**
+ * Initiate disconnection from a profile
+ *
+ * <p> This API will return false in scenarios like the profile on the
+ * Bluetooth device is not in connected state etc. When this API returns,
+ * true, it is guaranteed that the connection state change
+ * intent will be broadcasted with the state. Users can get the
+ * disconnection state of the profile from this intent.
+ *
+ * <p> If the disconnection is initiated by a remote device, the state
+ * will transition from {@link #STATE_CONNECTED} to
+ * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
+ * host (local) device the state will transition from
+ * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
+ * state {@link #STATE_DISCONNECTED}. The transition to
+ * {@link #STATE_DISCONNECTING} can be used to distinguish between the
+ * two scenarios.
+ *
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean disconnect(@Nullable BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled() && isValidDevice(device)) {
+ return service.disconnect(device);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()) {
+ return service.getConnectedDevices();
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return new ArrayList<BluetoothDevice>();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return new ArrayList<BluetoothDevice>();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(
+ @NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()) {
+ return service.getDevicesMatchingConnectionStates(states);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return new ArrayList<BluetoothDevice>();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return new ArrayList<BluetoothDevice>();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public @BtProfileState int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()
+ && isValidDevice(device)) {
+ return service.getConnectionState(device);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return BluetoothProfile.STATE_DISCONNECTED;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, LeAudio audio
+ * streaming is to the active LeAudio device. If a remote device
+ * is not connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_LEAUDIO_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()
+ && ((device == null) || isValidDevice(device))) {
+ service.setActiveDevice(device);
+ return true;
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ }
+ }
+
+ /**
+ * Get the connected LeAudio devices that are active
+ *
+ * @return the list of active devices. Returns empty list on error.
+ * @hide
+ */
+ @NonNull
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public List<BluetoothDevice> getActiveDevices() {
+ if (VDBG) log("getActiveDevices()");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()) {
+ return service.getActiveDevices();
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return new ArrayList<>();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return new ArrayList<>();
+ }
+ }
+
+ /**
+ * Get device group id. Devices with same group id belong to same group (i.e left and right
+ * earbud)
+ * @param device LE Audio capable device
+ * @return group id that this device currently belongs to
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public int getGroupId(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getGroupId()");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()) {
+ return service.getGroupId(device);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return GROUP_ID_INVALID;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return GROUP_ID_INVALID;
+ }
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()
+ && isValidDevice(device)) {
+ if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ return false;
+ }
+ return service.setConnectionPolicy(device, connectionPolicy);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ }
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ try {
+ final IBluetoothLeAudio service = getService();
+ if (service != null && mAdapter.isEnabled()
+ && isValidDevice(device)) {
+ return service.getConnectionPolicy(device);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ }
+ }
+
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ private boolean isValidDevice(@Nullable BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/core/java/android/bluetooth/BluetoothProfile.java b/core/java/android/bluetooth/BluetoothProfile.java
index db851c4f33b9..c31b04e81456 100644
--- a/core/java/android/bluetooth/BluetoothProfile.java
+++ b/core/java/android/bluetooth/BluetoothProfile.java
@@ -207,12 +207,19 @@ public interface BluetoothProfile {
int HEARING_AID = 21;
/**
+ * LE Audio Device
+ *
+ * @hide
+ */
+ int LE_AUDIO = 22;
+
+ /**
* Max profile ID. This value should be updated whenever a new profile is added to match
* the largest value assigned to a profile.
*
* @hide
*/
- int MAX_PROFILE_ID = 21;
+ int MAX_PROFILE_ID = 22;
/**
* Default priority for devices that we try to auto-connect to and