diff --git a/core/api/current.txt b/core/api/current.txt index 536c118554587..734d64f93d68a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9094,6 +9094,7 @@ package android.bluetooth { field public static final int STATE_CONNECTING = 1; // 0x1 field public static final int STATE_DISCONNECTED = 0; // 0x0 field public static final int STATE_DISCONNECTING = 3; // 0x3 + field public static final int VOLUME_CONTROL = 23; // 0x17 } public static interface BluetoothProfile.ServiceListener { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index f5b40f5689d80..400601ff45707 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1666,6 +1666,16 @@ package android.bluetooth { field @NonNull public static final android.os.ParcelUuid VOLUME_CONTROL; } + public final class BluetoothVolumeControl 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 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public java.util.List getConnectedDevices(); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getConnectionPolicy(@NonNull android.bluetooth.BluetoothDevice); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public boolean setConnectionPolicy(@NonNull android.bluetooth.BluetoothDevice, int); + method @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setVolume(@Nullable android.bluetooth.BluetoothDevice, @IntRange(from=0, to=255) int); + field @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public static final String ACTION_CONNECTION_STATE_CHANGED = "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED"; + } + public final class BufferConstraint implements android.os.Parcelable { ctor public BufferConstraint(int, int, int); method public int describeContents(); diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 63221c5d89410..0dd6b4f63654f 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -2815,6 +2815,9 @@ public final class BluetoothAdapter { return true; } return false; + } else if (profile == BluetoothProfile.VOLUME_CONTROL) { + BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this); + return true; } else { return false; } @@ -2899,6 +2902,11 @@ public final class BluetoothAdapter { case BluetoothProfile.HEARING_AID: BluetoothHearingAid hearingAid = (BluetoothHearingAid) proxy; hearingAid.close(); + break; + case BluetoothProfile.VOLUME_CONTROL: + BluetoothVolumeControl vcs = (BluetoothVolumeControl) proxy; + vcs.close(); + break; } } diff --git a/core/java/android/bluetooth/BluetoothProfile.java b/core/java/android/bluetooth/BluetoothProfile.java index 201d6c495d980..d9791026ad6df 100644 --- a/core/java/android/bluetooth/BluetoothProfile.java +++ b/core/java/android/bluetooth/BluetoothProfile.java @@ -214,13 +214,19 @@ public interface BluetoothProfile { */ int LE_AUDIO = 22; + /** + * Volume Control profile + * + */ + int VOLUME_CONTROL = 23; + /** * 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 = 22; + int MAX_PROFILE_ID = 23; /** * Default priority for devices that we try to auto-connect to and diff --git a/core/java/android/bluetooth/BluetoothVolumeControl.java b/core/java/android/bluetooth/BluetoothVolumeControl.java new file mode 100644 index 0000000000000..4eb28ad399520 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothVolumeControl.java @@ -0,0 +1,320 @@ +/* + * Copyright 2021 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.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +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 Bluetooth Volume Control service. + * + *

BluetoothVolumeControl is a proxy object for controlling the Bluetooth VC + * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get + * the BluetoothVolumeControl proxy object. + * @hide + */ +@SystemApi +public final class BluetoothVolumeControl implements BluetoothProfile, AutoCloseable { + private static final String TAG = "BluetoothVolumeControl"; + private static final boolean DBG = true; + private static final boolean VDBG = false; + + private CloseGuard mCloseGuard; + + /** + * Intent used to broadcast the change in connection state of the Volume Control + * profile. + * + *

This intent will have 3 extras: + *

+ * + *

{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of + * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, + * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. + * + * @hide + */ + @SystemApi + @SuppressLint("ActionValue") + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_STATE_CHANGED = + "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED"; + + private BluetoothAdapter mAdapter; + private final BluetoothProfileConnector mProfileConnector = + new BluetoothProfileConnector(this, BluetoothProfile.VOLUME_CONTROL, TAG, + IBluetoothVolumeControl.class.getName()) { + @Override + public IBluetoothVolumeControl getServiceInterface(IBinder service) { + return IBluetoothVolumeControl.Stub.asInterface(Binder.allowBlocking(service)); + } + }; + + /** + * Create a BluetoothVolumeControl proxy object for interacting with the local + * Bluetooth Volume Control service. + */ + /*package*/ BluetoothVolumeControl(Context context, ServiceListener listener, + BluetoothAdapter adapter) { + mAdapter = adapter; + mProfileConnector.connect(context, listener); + mCloseGuard = new CloseGuard(); + mCloseGuard.open("close"); + } + + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + protected void finalize() { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } + + /*package*/ + @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED) + public void close() { + mProfileConnector.disconnect(); + } + + private IBluetoothVolumeControl getService() { return mProfileConnector.getService(); } + + /** + * Get the list of connected devices. Currently at most one. + * + * @return list of connected devices + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public @NonNull List getConnectedDevices() { + if (DBG) log("getConnectedDevices()"); + final IBluetoothVolumeControl service = getService(); + if (service != null && isEnabled()) { + try { + return service.getConnectedDevices(); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + } + if (service == null) Log.w(TAG, "Proxy not attached to service"); + return new ArrayList(); + } + + /** + * Get the list of devices matching specified states. Currently at most one. + * + * @return list of matching devices + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public List getDevicesMatchingConnectionStates(int[] states) { + if (DBG) log("getDevicesMatchingStates()"); + final IBluetoothVolumeControl service = getService(); + if (service != null && isEnabled()) { + try { + return service.getDevicesMatchingConnectionStates(states); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return new ArrayList(); + } + } + if (service == null) Log.w(TAG, "Proxy not attached to service"); + return new ArrayList(); + } + + /** + * Get connection state of device + * + * @return device connection state + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public int getConnectionState(BluetoothDevice device) { + if (DBG) log("getConnectionState(" + device + ")"); + final IBluetoothVolumeControl service = getService(); + if (service != null && isEnabled() && isValidDevice(device)) { + try { + return service.getConnectionState(device); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return BluetoothProfile.STATE_DISCONNECTED; + } + } + if (service == null) Log.w(TAG, "Proxy not attached to service"); + return BluetoothProfile.STATE_DISCONNECTED; + } + + /** + * Tells remote device to set an absolute volume. + * + * @param volume Absolute volume to be set on remote device. + * Minimum value is 0 and maximum value is 255 + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public void setVolume(@Nullable BluetoothDevice device, + @IntRange(from = 0, to = 255) int volume) { + if (DBG) + log("setVolume(" + volume + ")"); + final IBluetoothVolumeControl service = getService(); + try { + if (service != null && isEnabled()) { + service.setVolume(device, volume); + return; + } + if (service == null) + Log.w(TAG, "Proxy not attached to service"); + } catch (RemoteException e) { + Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); + } + } + + /** + * Set priority of the profile + * + *

The device should already be paired. + * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF}, + * + * @param device Paired bluetooth device + * @param priority + * @return true if priority is set, false on error + * @hide + */ + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean setPriority(BluetoothDevice device, int priority) { + if (DBG) log("setPriority(" + device + ", " + priority + ")"); + return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority)); + } + + /** + * Set connection policy of the profile + * + *

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 + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean setConnectionPolicy(@NonNull BluetoothDevice device, + @ConnectionPolicy int connectionPolicy) { + if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); + final IBluetoothVolumeControl service = getService(); + if (service != null && isEnabled() && isValidDevice(device)) { + if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN + && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) { + return false; + } + try { + return service.setConnectionPolicy(device, connectionPolicy); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return false; + } + } + if (service == null) Log.w(TAG, "Proxy not attached to service"); + return false; + } + + /** + * Get the priority of the profile. + * + *

The priority can be any of: + * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} + * + * @param device Bluetooth device + * @return priority of the device + * @hide + */ + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public int getPriority(BluetoothDevice device) { + if (VDBG) log("getPriority(" + device + ")"); + return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device)); + } + + /** + * Get the connection policy of the profile. + * + *

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 + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { + if (VDBG) log("getConnectionPolicy(" + device + ")"); + final IBluetoothVolumeControl service = getService(); + if (service != null && isEnabled() && isValidDevice(device)) { + try { + return service.getConnectionPolicy(device); + } catch (RemoteException e) { + Log.e(TAG, Log.getStackTraceString(new Throwable())); + return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + } + } + if (service == null) Log.w(TAG, "Proxy not attached to service"); + return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + } + + private boolean isEnabled() { + return mAdapter.getState() == BluetoothAdapter.STATE_ON; + } + + private static boolean isValidDevice(@Nullable BluetoothDevice device) { + return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 8db1bbfbec1d5..33f259878a24f 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -190,6 +190,8 @@ android:name="android.bluetooth.hearingaid.profile.action.PLAYING_STATE_CHANGED" /> +