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