• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.bluetooth;
18 
19 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
21 
22 import android.annotation.CallbackExecutor;
23 import android.annotation.IntRange;
24 import android.bluetooth.AudioInputControl;
25 import android.bluetooth.BluetoothAdapter;
26 import android.bluetooth.BluetoothClass;
27 import android.bluetooth.BluetoothDevice;
28 import android.bluetooth.BluetoothProfile;
29 import android.bluetooth.BluetoothVolumeControl;
30 import android.content.Context;
31 import android.os.Build;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.RequiresApi;
36 
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 import java.util.concurrent.Executor;
41 
42 /** VolumeControlProfile handles Bluetooth Volume Control Controller role */
43 public class VolumeControlProfile implements LocalBluetoothProfile {
44     private static final String TAG = "VolumeControlProfile";
45     private static boolean DEBUG = true;
46     static final String NAME = "VCP";
47     // Order of this profile in device profiles list
48     private static final int ORDINAL = 1;
49 
50     private Context mContext;
51     private final CachedBluetoothDeviceManager mDeviceManager;
52     private final LocalBluetoothProfileManager mProfileManager;
53 
54     private BluetoothVolumeControl mService;
55     private boolean mIsProfileReady;
56 
57     // These callbacks run on the main thread.
58     private final class VolumeControlProfileServiceListener
59             implements BluetoothProfile.ServiceListener {
60 
61         @RequiresApi(Build.VERSION_CODES.S)
onServiceConnected(int profile, BluetoothProfile proxy)62         public void onServiceConnected(int profile, BluetoothProfile proxy) {
63             if (DEBUG) {
64                 Log.d(TAG, "Bluetooth service connected");
65             }
66             mService = (BluetoothVolumeControl) proxy;
67             // We just bound to the service, so refresh the UI for any connected
68             // VolumeControlProfile devices.
69             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
70             while (!deviceList.isEmpty()) {
71                 BluetoothDevice nextDevice = deviceList.remove(0);
72                 CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
73                 // we may add a new device here, but generally this should not happen
74                 if (device == null) {
75                     if (DEBUG) {
76                         Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice);
77                     }
78                     device = mDeviceManager.addDevice(nextDevice);
79                 }
80                 device.onProfileStateChanged(
81                         VolumeControlProfile.this, BluetoothProfile.STATE_CONNECTED);
82                 device.refresh();
83             }
84 
85             mProfileManager.callServiceConnectedListeners();
86             mIsProfileReady = true;
87         }
88 
onServiceDisconnected(int profile)89         public void onServiceDisconnected(int profile) {
90             if (DEBUG) {
91                 Log.d(TAG, "Bluetooth service disconnected");
92             }
93             mProfileManager.callServiceDisconnectedListeners();
94             mIsProfileReady = false;
95         }
96     }
97 
VolumeControlProfile( Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)98     VolumeControlProfile(
99             Context context,
100             CachedBluetoothDeviceManager deviceManager,
101             LocalBluetoothProfileManager profileManager) {
102         mContext = context;
103         mDeviceManager = deviceManager;
104         mProfileManager = profileManager;
105 
106         BluetoothAdapter.getDefaultAdapter()
107                 .getProfileProxy(
108                         context,
109                         new VolumeControlProfile.VolumeControlProfileServiceListener(),
110                         BluetoothProfile.VOLUME_CONTROL);
111     }
112 
113     /**
114      * Registers a {@link BluetoothVolumeControl.Callback} that will be invoked during the operation
115      * of this profile.
116      *
117      * <p>Repeated registration of the same <var>callback</var> object will have no effect after the
118      * first call to this method, even when the <var>executor</var> is different. API caller would
119      * have to call {@link #unregisterCallback(BluetoothVolumeControl.Callback)} with the same
120      * callback object before registering it again.
121      *
122      * @param executor an {@link Executor} to execute given callback
123      * @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
124      * @throws IllegalArgumentException if a null executor or callback is given
125      */
registerCallback( @onNull @allbackExecutor Executor executor, @NonNull BluetoothVolumeControl.Callback callback)126     public void registerCallback(
127             @NonNull @CallbackExecutor Executor executor,
128             @NonNull BluetoothVolumeControl.Callback callback) {
129         if (mService == null) {
130             Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
131             return;
132         }
133         mService.registerCallback(executor, callback);
134     }
135 
136     /**
137      * Unregisters the specified {@link BluetoothVolumeControl.Callback}.
138      *
139      * <p>The same {@link BluetoothVolumeControl.Callback} object used when calling {@link
140      * #registerCallback(Executor, BluetoothVolumeControl.Callback)} must be used.
141      *
142      * <p>Callbacks are automatically unregistered when application process goes away
143      *
144      * @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
145      * @throws IllegalArgumentException when callback is null or when no callback is registered
146      */
unregisterCallback(@onNull BluetoothVolumeControl.Callback callback)147     public void unregisterCallback(@NonNull BluetoothVolumeControl.Callback callback) {
148         if (mService == null) {
149             Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
150             return;
151         }
152         mService.unregisterCallback(callback);
153     }
154 
155     /**
156      * Tells the remote device to set a volume offset to the absolute volume.
157      *
158      * @param device {@link BluetoothDevice} representing the remote device
159      * @param volumeOffset volume offset to be set on the remote device
160      */
setVolumeOffset( BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset)161     public void setVolumeOffset(
162             BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset) {
163         if (mService == null) {
164             Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
165             return;
166         }
167         if (device == null) {
168             Log.w(TAG, "Device is null. Cannot set volume offset.");
169             return;
170         }
171         mService.setVolumeOffset(device, volumeOffset);
172     }
173 
174     /**
175      * Provides information about the possibility to set volume offset on the remote device. If the
176      * remote device supports Volume Offset Control Service, it is automatically connected.
177      *
178      * @param device {@link BluetoothDevice} representing the remote device
179      * @return {@code true} if volume offset function is supported and available to use on the
180      *     remote device. When Bluetooth is off, the return value should always be {@code false}.
181      */
isVolumeOffsetAvailable(BluetoothDevice device)182     public boolean isVolumeOffsetAvailable(BluetoothDevice device) {
183         if (mService == null) {
184             Log.w(TAG, "Proxy not attached to service. Cannot get is volume offset available.");
185             return false;
186         }
187         if (device == null) {
188             Log.w(TAG, "Device is null. Cannot get is volume offset available.");
189             return false;
190         }
191         return mService.isVolumeOffsetAvailable(device);
192     }
193 
194     /**
195      * Tells the remote device to set a volume.
196      *
197      * @param device {@link BluetoothDevice} representing the remote device
198      * @param volume volume to be set on the remote device
199      * @param isGroupOp whether to set the volume to remote devices within the same CSIP group
200      */
setDeviceVolume( BluetoothDevice device, @IntRange(from = 0, to = 255) int volume, boolean isGroupOp)201     public void setDeviceVolume(
202             BluetoothDevice device,
203             @IntRange(from = 0, to = 255) int volume,
204             boolean isGroupOp) {
205         if (mService == null) {
206             Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
207             return;
208         }
209         if (device == null) {
210             Log.w(TAG, "Device is null. Cannot set volume offset.");
211             return;
212         }
213         mService.setDeviceVolume(device, volume, isGroupOp);
214     }
215 
216     /**
217      * Returns a list of {@link AudioInputControl} objects associated with a Bluetooth device.
218      *
219      * @param device The remote Bluetooth device.
220      * @return A list of {@link AudioInputControl} objects, or an empty list if no AICS instances
221      *     are found or if an error occurs.
222      * @hide
223      */
getAudioInputControlServices( @onNull BluetoothDevice device)224     public @NonNull List<AudioInputControl> getAudioInputControlServices(
225             @NonNull BluetoothDevice device) {
226         if (mService == null) {
227             return Collections.emptyList();
228         }
229         return mService.getAudioInputControlServices(device);
230     }
231 
232     @Override
accessProfileEnabled()233     public boolean accessProfileEnabled() {
234         return false;
235     }
236 
237     @Override
isAutoConnectable()238     public boolean isAutoConnectable() {
239         return true;
240     }
241 
242     /**
243      * Gets VolumeControlProfile devices matching connection states{ {@code
244      * BluetoothProfile.STATE_CONNECTED}, {@code BluetoothProfile.STATE_CONNECTING}, {@code
245      * BluetoothProfile.STATE_DISCONNECTING}}
246      *
247      * @return Matching device list
248      */
getConnectedDevices()249     public List<BluetoothDevice> getConnectedDevices() {
250         if (mService == null) {
251             return new ArrayList<BluetoothDevice>(0);
252         }
253         return mService.getDevicesMatchingConnectionStates(
254                 new int[] {
255                     BluetoothProfile.STATE_CONNECTED,
256                     BluetoothProfile.STATE_CONNECTING,
257                     BluetoothProfile.STATE_DISCONNECTING
258                 });
259     }
260 
261     @Override
getConnectionStatus(BluetoothDevice device)262     public int getConnectionStatus(BluetoothDevice device) {
263         if (mService == null) {
264             return BluetoothProfile.STATE_DISCONNECTED;
265         }
266         return mService.getConnectionState(device);
267     }
268 
269     @Override
isEnabled(BluetoothDevice device)270     public boolean isEnabled(BluetoothDevice device) {
271         if (mService == null || device == null) {
272             return false;
273         }
274         return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
275     }
276 
277     @Override
getConnectionPolicy(BluetoothDevice device)278     public int getConnectionPolicy(BluetoothDevice device) {
279         if (mService == null || device == null) {
280             return CONNECTION_POLICY_FORBIDDEN;
281         }
282         return mService.getConnectionPolicy(device);
283     }
284 
285     @Override
setEnabled(BluetoothDevice device, boolean enabled)286     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
287         boolean isSuccessful = false;
288         if (mService == null || device == null) {
289             return false;
290         }
291         if (DEBUG) {
292             Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled);
293         }
294         if (enabled) {
295             if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
296                 isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
297             }
298         } else {
299             isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
300         }
301 
302         return isSuccessful;
303     }
304 
305     @Override
isProfileReady()306     public boolean isProfileReady() {
307         return mIsProfileReady;
308     }
309 
310     @Override
getProfileId()311     public int getProfileId() {
312         return BluetoothProfile.VOLUME_CONTROL;
313     }
314 
toString()315     public String toString() {
316         return NAME;
317     }
318 
319     @Override
getOrdinal()320     public int getOrdinal() {
321         return ORDINAL;
322     }
323 
324     @Override
getNameResource(BluetoothDevice device)325     public int getNameResource(BluetoothDevice device) {
326         return 0; // VCP profile not displayed in UI
327     }
328 
329     @Override
getSummaryResourceForDevice(BluetoothDevice device)330     public int getSummaryResourceForDevice(BluetoothDevice device) {
331         return 0; // VCP profile not displayed in UI
332     }
333 
334     @Override
getDrawableResource(BluetoothClass btClass)335     public int getDrawableResource(BluetoothClass btClass) {
336         // no icon for VCP
337         return 0;
338     }
339 }
340