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