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