1 /* 2 * Copyright (C) 2024 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.AudioInputControl.MUTE_DISABLED; 20 import static android.bluetooth.AudioInputControl.MUTE_MUTED; 21 import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED; 22 23 import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME; 24 25 import android.bluetooth.AudioInputControl; 26 import android.bluetooth.BluetoothDevice; 27 import android.bluetooth.BluetoothProfile; 28 import android.util.ArrayMap; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Map; 37 import java.util.concurrent.Executor; 38 39 /** 40 * AmbientVolumeController manages the {@link AudioInputControl}s of 41 * {@link AudioInputControl#AUDIO_INPUT_TYPE_AMBIENT} on the remote device. 42 */ 43 public class AmbientVolumeController implements LocalBluetoothProfileManager.ServiceListener { 44 45 private static final boolean DEBUG = true; 46 private static final String TAG = "AmbientController"; 47 48 private final LocalBluetoothProfileManager mProfileManager; 49 private final VolumeControlProfile mVolumeControlProfile; 50 private final Map<BluetoothDevice, List<AudioInputControl>> mDeviceAmbientControlsMap = 51 new ArrayMap<>(); 52 private final Map<BluetoothDevice, AmbientCallback> mDeviceCallbackMap = new ArrayMap<>(); 53 final Map<BluetoothDevice, RemoteAmbientState> mDeviceAmbientStateMap = 54 new ArrayMap<>(); 55 @Nullable 56 private final AmbientVolumeControlCallback mCallback; 57 AmbientVolumeController( @onNull LocalBluetoothProfileManager profileManager, @Nullable AmbientVolumeControlCallback callback)58 public AmbientVolumeController( 59 @NonNull LocalBluetoothProfileManager profileManager, 60 @Nullable AmbientVolumeControlCallback callback) { 61 mProfileManager = profileManager; 62 mVolumeControlProfile = profileManager.getVolumeControlProfile(); 63 if (mVolumeControlProfile != null && !mVolumeControlProfile.isProfileReady()) { 64 mProfileManager.addServiceListener(this); 65 } 66 mCallback = callback; 67 } 68 69 @Override onServiceConnected()70 public void onServiceConnected() { 71 if (mVolumeControlProfile != null && mVolumeControlProfile.isProfileReady()) { 72 mProfileManager.removeServiceListener(this); 73 if (mCallback != null) { 74 mCallback.onVolumeControlServiceConnected(); 75 } 76 } 77 } 78 79 @Override onServiceDisconnected()80 public void onServiceDisconnected() { 81 // Do nothing 82 } 83 84 /** 85 * Registers the same {@link AmbientCallback} on all ambient control points of the remote 86 * device. The {@link AmbientCallback} will pass the event to registered 87 * {@link AmbientVolumeControlCallback} if exists. 88 * 89 * @param executor the executor to run the callback 90 * @param device the remote device 91 */ registerCallback(@onNull Executor executor, @NonNull BluetoothDevice device)92 public void registerCallback(@NonNull Executor executor, @NonNull BluetoothDevice device) { 93 AmbientCallback ambientCallback = new AmbientCallback(device, mCallback); 94 synchronized (mDeviceCallbackMap) { 95 mDeviceCallbackMap.put(device, ambientCallback); 96 } 97 98 // register callback on all ambient input control points of this device 99 List<AudioInputControl> controls = getAmbientControls(device); 100 controls.forEach((control) -> { 101 try { 102 control.registerCallback(executor, ambientCallback); 103 } catch (IllegalArgumentException e) { 104 // The callback was already registered 105 Log.i(TAG, "Skip registering the callback, " + e.getMessage()); 106 } 107 }); 108 } 109 110 /** 111 * Unregisters the {@link AmbientCallback} on all ambient control points of the remote 112 * device which is previously registered with {@link #registerCallback}. 113 * 114 * @param device the remote device 115 */ unregisterCallback(@onNull BluetoothDevice device)116 public void unregisterCallback(@NonNull BluetoothDevice device) { 117 AmbientCallback ambientCallback; 118 synchronized (mDeviceCallbackMap) { 119 ambientCallback = mDeviceCallbackMap.remove(device); 120 } 121 if (ambientCallback == null) { 122 // callback not found, no need to unregister 123 return; 124 } 125 126 // unregister callback on all ambient input control points of this device 127 List<AudioInputControl> controls = getAmbientControls(device); 128 controls.forEach(control -> { 129 try { 130 control.unregisterCallback(ambientCallback); 131 } catch (IllegalArgumentException e) { 132 // The callback was never registered or was already unregistered 133 Log.i(TAG, "Skip unregistering the callback, " + e.getMessage()); 134 } 135 }); 136 } 137 138 /** 139 * Gets the gain setting max value from first ambient control point of the remote device. 140 * 141 * @param device the remote device 142 */ getAmbientMax(@onNull BluetoothDevice device)143 public int getAmbientMax(@NonNull BluetoothDevice device) { 144 List<AudioInputControl> ambientControls = getAmbientControls(device); 145 int value = INVALID_VOLUME; 146 if (!ambientControls.isEmpty()) { 147 value = ambientControls.getFirst().getGainSettingMax(); 148 } 149 return value; 150 } 151 152 /** 153 * Gets the gain setting min value from first ambient control point of the remote device. 154 * 155 * @param device the remote device 156 */ getAmbientMin(@onNull BluetoothDevice device)157 public int getAmbientMin(@NonNull BluetoothDevice device) { 158 List<AudioInputControl> ambientControls = getAmbientControls(device); 159 int value = INVALID_VOLUME; 160 if (!ambientControls.isEmpty()) { 161 value = ambientControls.getFirst().getGainSettingMin(); 162 } 163 return value; 164 } 165 166 /** 167 * Gets the latest values in {@link RemoteAmbientState}. 168 * 169 * @param device the remote device 170 * @return the {@link RemoteAmbientState} represents current remote ambient control point state 171 */ 172 @Nullable refreshAmbientState(@ullable BluetoothDevice device)173 public RemoteAmbientState refreshAmbientState(@Nullable BluetoothDevice device) { 174 if (device == null || !device.isConnected()) { 175 return null; 176 } 177 int gainSetting = getAmbient(device); 178 int mute = getMute(device); 179 return new RemoteAmbientState(gainSetting, mute); 180 } 181 182 /** 183 * Gets the gain setting value from first ambient control point of the remote device and 184 * stores it in cached {@link RemoteAmbientState}. 185 * 186 * When any audio input point receives {@link AmbientCallback#onGainSettingChanged(int)} 187 * callback, only the changed value which is different from the value stored in the cached 188 * state will be notified to the {@link AmbientVolumeControlCallback} of this controller. 189 * 190 * @param device the remote device 191 */ getAmbient(@onNull BluetoothDevice device)192 public int getAmbient(@NonNull BluetoothDevice device) { 193 List<AudioInputControl> ambientControls = getAmbientControls(device); 194 int value = INVALID_VOLUME; 195 if (!ambientControls.isEmpty()) { 196 synchronized (mDeviceAmbientStateMap) { 197 value = ambientControls.getFirst().getGainSetting(); 198 RemoteAmbientState state = mDeviceAmbientStateMap.getOrDefault(device, 199 new RemoteAmbientState(INVALID_VOLUME, MUTE_DISABLED)); 200 RemoteAmbientState updatedState = new RemoteAmbientState(value, state.mute); 201 mDeviceAmbientStateMap.put(device, updatedState); 202 } 203 } 204 return value; 205 } 206 207 /** 208 * Sets the gain setting value to all ambient control points of the remote device. 209 * 210 * @param device the remote device 211 * @param value the gain setting value to be updated 212 */ setAmbient(@onNull BluetoothDevice device, int value)213 public void setAmbient(@NonNull BluetoothDevice device, int value) { 214 if (DEBUG) { 215 Log.d(TAG, "setAmbient, value:" + value + ", device:" + device); 216 } 217 List<AudioInputControl> ambientControls = getAmbientControls(device); 218 ambientControls.forEach(control -> control.setGainSetting(value)); 219 } 220 221 /** 222 * Gets the mute state from first ambient control point of the remote device and 223 * stores it in cached {@link RemoteAmbientState}. The value will be one of 224 * {@link AudioInputControl.Mute}. 225 * 226 * When any audio input point receives {@link AmbientCallback#onMuteChanged(int)} callback, 227 * only the changed value which is different from the value stored in the cached state will 228 * be notified to the {@link AmbientVolumeControlCallback} of this controller. 229 * 230 * @param device the remote device 231 */ getMute(@onNull BluetoothDevice device)232 public int getMute(@NonNull BluetoothDevice device) { 233 List<AudioInputControl> ambientControls = getAmbientControls(device); 234 int value = MUTE_DISABLED; 235 if (!ambientControls.isEmpty()) { 236 synchronized (mDeviceAmbientStateMap) { 237 value = ambientControls.getFirst().getMute(); 238 RemoteAmbientState state = mDeviceAmbientStateMap.getOrDefault(device, 239 new RemoteAmbientState(INVALID_VOLUME, MUTE_DISABLED)); 240 RemoteAmbientState updatedState = new RemoteAmbientState(state.gainSetting, value); 241 mDeviceAmbientStateMap.put(device, updatedState); 242 } 243 } 244 return value; 245 } 246 247 /** 248 * Sets the mute state to all ambient control points of the remote device. 249 * 250 * @param device the remote device 251 * @param muted the mute state to be updated 252 */ setMuted(@onNull BluetoothDevice device, boolean muted)253 public void setMuted(@NonNull BluetoothDevice device, boolean muted) { 254 if (DEBUG) { 255 Log.d(TAG, "setMuted, muted:" + muted + ", device:" + device); 256 } 257 List<AudioInputControl> ambientControls = getAmbientControls(device); 258 ambientControls.forEach(control -> { 259 try { 260 control.setMute(muted ? MUTE_MUTED : MUTE_NOT_MUTED); 261 } catch (IllegalStateException e) { 262 // Sometimes remote will throw this exception due to initialization not done 263 // yet. Catch it to prevent crashes on UI. 264 Log.w(TAG, "Remote mute state is currently disabled."); 265 } 266 }); 267 } 268 269 /** 270 * Checks if there's any valid ambient control point exists on the remote device 271 * 272 * @param device the remote device 273 */ isAmbientControlAvailable(@onNull BluetoothDevice device)274 public boolean isAmbientControlAvailable(@NonNull BluetoothDevice device) { 275 final boolean hasAmbientControlPoint = !getAmbientControls(device).isEmpty(); 276 final boolean connectedToVcp = mVolumeControlProfile.getConnectionStatus(device) 277 == BluetoothProfile.STATE_CONNECTED; 278 return hasAmbientControlPoint && connectedToVcp; 279 } 280 281 @NonNull getAmbientControls(@onNull BluetoothDevice device)282 private List<AudioInputControl> getAmbientControls(@NonNull BluetoothDevice device) { 283 if (mVolumeControlProfile == null) { 284 return Collections.emptyList(); 285 } 286 synchronized (mDeviceAmbientControlsMap) { 287 if (mDeviceAmbientControlsMap.containsKey(device)) { 288 return mDeviceAmbientControlsMap.get(device); 289 } 290 List<AudioInputControl> ambientControls = 291 mVolumeControlProfile.getAudioInputControlServices(device).stream().filter( 292 this::isValidAmbientControl).toList(); 293 if (!ambientControls.isEmpty()) { 294 mDeviceAmbientControlsMap.put(device, ambientControls); 295 } 296 return ambientControls; 297 } 298 } 299 isValidAmbientControl(AudioInputControl control)300 private boolean isValidAmbientControl(AudioInputControl control) { 301 boolean isAmbientControl = 302 control.getAudioInputType() == AudioInputControl.AUDIO_INPUT_TYPE_AMBIENT; 303 boolean isManual = control.getGainMode() == AudioInputControl.GAIN_MODE_MANUAL 304 || control.getGainMode() == AudioInputControl.GAIN_MODE_MANUAL_ONLY; 305 boolean isActive = 306 control.getAudioInputStatus() == AudioInputControl.AUDIO_INPUT_STATUS_ACTIVE; 307 308 return isAmbientControl && isManual && isActive; 309 } 310 311 /** 312 * Callback providing information about the status and received events of 313 * {@link AmbientVolumeController}. 314 */ 315 public interface AmbientVolumeControlCallback { 316 317 /** This method is called when the Volume Control Service is connected */ onVolumeControlServiceConnected()318 default void onVolumeControlServiceConnected() { 319 } 320 321 /** 322 * This method is called when one of the remote device's ambient control point's gain 323 * settings value is changed. 324 * 325 * @param device the remote device 326 * @param gainSettings the new gain setting value 327 */ onAmbientChanged(@onNull BluetoothDevice device, int gainSettings)328 default void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) { 329 } 330 331 /** 332 * This method is called when one of the remote device's ambient control point's mute 333 * state is changed. 334 * 335 * @param device the remote device 336 * @param mute the new mute state 337 */ onMuteChanged(@onNull BluetoothDevice device, int mute)338 default void onMuteChanged(@NonNull BluetoothDevice device, int mute) { 339 } 340 341 /** 342 * This method is called when any command to the remote device's ambient control point 343 * is failed. 344 * 345 * @param device the remote device. 346 */ onCommandFailed(@onNull BluetoothDevice device)347 default void onCommandFailed(@NonNull BluetoothDevice device) { 348 } 349 } 350 351 /** 352 * A wrapper callback that will pass {@link AudioInputControl.AudioInputCallback} with extra 353 * device information to {@link AmbientVolumeControlCallback}. 354 */ 355 class AmbientCallback implements AudioInputControl.AudioInputCallback { 356 357 private final BluetoothDevice mDevice; 358 private final AmbientVolumeControlCallback mCallback; 359 AmbientCallback(@onNull BluetoothDevice device, @Nullable AmbientVolumeControlCallback callback)360 AmbientCallback(@NonNull BluetoothDevice device, 361 @Nullable AmbientVolumeControlCallback callback) { 362 mDevice = device; 363 mCallback = callback; 364 } 365 366 @Override onGainSettingChanged(int gainSetting)367 public void onGainSettingChanged(int gainSetting) { 368 if (mCallback != null) { 369 synchronized (mDeviceAmbientStateMap) { 370 RemoteAmbientState previousState = mDeviceAmbientStateMap.get(mDevice); 371 if (previousState.gainSetting != gainSetting) { 372 mCallback.onAmbientChanged(mDevice, gainSetting); 373 } 374 } 375 } 376 } 377 378 @Override onSetGainSettingFailed()379 public void onSetGainSettingFailed() { 380 Log.w(TAG, "onSetGainSettingFailed, device=" + mDevice); 381 if (mCallback != null) { 382 mCallback.onCommandFailed(mDevice); 383 } 384 } 385 386 @Override onMuteChanged(int mute)387 public void onMuteChanged(int mute) { 388 if (mCallback != null) { 389 synchronized (mDeviceAmbientStateMap) { 390 RemoteAmbientState previousState = mDeviceAmbientStateMap.get(mDevice); 391 if (previousState.mute != mute) { 392 mCallback.onMuteChanged(mDevice, mute); 393 } 394 } 395 } 396 } 397 398 @Override onSetMuteFailed()399 public void onSetMuteFailed() { 400 Log.w(TAG, "onSetMuteFailed, device=" + mDevice); 401 if (mCallback != null) { 402 mCallback.onCommandFailed(mDevice); 403 } 404 } 405 } 406 RemoteAmbientState(int gainSetting, int mute)407 public record RemoteAmbientState(int gainSetting, int mute) { 408 public boolean isMutable() { 409 return mute != MUTE_DISABLED; 410 } 411 public boolean isMuted() { 412 return mute == MUTE_MUTED; 413 } 414 } 415 } 416