1 /* 2 * Copyright (C) 2023 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.settings.connecteddevice.audiosharing; 18 19 import android.annotation.IntRange; 20 import android.bluetooth.BluetoothCsipSetCoordinator; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothLeBroadcastAssistant; 23 import android.bluetooth.BluetoothLeBroadcastMetadata; 24 import android.bluetooth.BluetoothLeBroadcastReceiveState; 25 import android.bluetooth.BluetoothVolumeControl; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.database.ContentObserver; 29 import android.media.AudioManager; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.provider.Settings; 33 import android.util.Log; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.annotation.VisibleForTesting; 38 import androidx.lifecycle.LifecycleOwner; 39 import androidx.preference.Preference; 40 import androidx.preference.PreferenceGroup; 41 import androidx.preference.PreferenceScreen; 42 43 import com.android.settings.bluetooth.BluetoothDeviceUpdater; 44 import com.android.settings.bluetooth.Utils; 45 import com.android.settings.connecteddevice.DevicePreferenceCallback; 46 import com.android.settings.dashboard.DashboardFragment; 47 import com.android.settingslib.bluetooth.BluetoothUtils; 48 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 49 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 50 import com.android.settingslib.bluetooth.LocalBluetoothManager; 51 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 52 import com.android.settingslib.bluetooth.VolumeControlProfile; 53 54 import java.util.Map; 55 import java.util.concurrent.ConcurrentHashMap; 56 import java.util.concurrent.CopyOnWriteArraySet; 57 import java.util.concurrent.Executor; 58 import java.util.concurrent.Executors; 59 import java.util.concurrent.atomic.AtomicBoolean; 60 61 public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController 62 implements DevicePreferenceCallback { 63 private static final String TAG = "AudioSharingVolCtlr"; 64 private static final String KEY = "audio_sharing_device_volume_group"; 65 66 @Nullable private final LocalBluetoothManager mBtManager; 67 @Nullable private final LocalBluetoothProfileManager mProfileManager; 68 @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant; 69 @Nullable private final VolumeControlProfile mVolumeControl; 70 @Nullable private final ContentResolver mContentResolver; 71 @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater; 72 private final Executor mExecutor; 73 private final ContentObserver mSettingsObserver; 74 @Nullable private PreferenceGroup mPreferenceGroup; 75 private CopyOnWriteArraySet<AudioSharingDeviceVolumePreference> mVolumePreferences = 76 new CopyOnWriteArraySet<>(); 77 private ConcurrentHashMap<Integer, Integer> mValueMap = new ConcurrentHashMap<>(); 78 private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false); 79 80 @VisibleForTesting 81 BluetoothVolumeControl.Callback mVolumeControlCallback = 82 new BluetoothVolumeControl.Callback() { 83 @Override 84 public void onDeviceVolumeChanged( 85 @NonNull BluetoothDevice device, 86 @IntRange(from = -255, to = 255) int volume) { 87 CachedBluetoothDevice cachedDevice = 88 mBtManager == null 89 ? null 90 : mBtManager.getCachedDeviceManager().findDevice(device); 91 if (cachedDevice == null) return; 92 int groupId = BluetoothUtils.getGroupId(cachedDevice); 93 mValueMap.put(groupId, volume); 94 for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) { 95 if (preference.getCachedDevice() != null 96 && BluetoothUtils.getGroupId(preference.getCachedDevice()) 97 == groupId) { 98 // If the callback return invalid volume, try to 99 // get the volume from AudioManager.STREAM_MUSIC 100 int finalVolume = getAudioVolumeIfNeeded(volume); 101 Log.d( 102 TAG, 103 "onDeviceVolumeChanged: set volume to " 104 + finalVolume 105 + " for " 106 + device.getAnonymizedAddress()); 107 AudioSharingUtils.postOnMainThread(mContext, 108 () -> preference.setProgress(finalVolume)); 109 break; 110 } 111 } 112 } 113 }; 114 115 @VisibleForTesting 116 BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 117 new BluetoothLeBroadcastAssistant.Callback() { 118 @Override 119 public void onSearchStarted(int reason) {} 120 121 @Override 122 public void onSearchStartFailed(int reason) {} 123 124 @Override 125 public void onSearchStopped(int reason) {} 126 127 @Override 128 public void onSearchStopFailed(int reason) {} 129 130 @Override 131 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} 132 133 @Override 134 public void onSourceAdded( 135 @NonNull BluetoothDevice sink, int sourceId, int reason) { 136 Log.d(TAG, "onSourceAdded: update volume list."); 137 if (mBluetoothDeviceUpdater != null) { 138 mBluetoothDeviceUpdater.forceUpdate(); 139 } 140 } 141 142 @Override 143 public void onSourceAddFailed( 144 @NonNull BluetoothDevice sink, 145 @NonNull BluetoothLeBroadcastMetadata source, 146 int reason) {} 147 148 @Override 149 public void onSourceModified( 150 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 151 152 @Override 153 public void onSourceModifyFailed( 154 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 155 156 @Override 157 public void onSourceRemoved( 158 @NonNull BluetoothDevice sink, int sourceId, int reason) { 159 Log.d(TAG, "onSourceRemoved: update volume list."); 160 if (mBluetoothDeviceUpdater != null) { 161 mBluetoothDeviceUpdater.forceUpdate(); 162 } 163 } 164 165 @Override 166 public void onSourceRemoveFailed( 167 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 168 169 @Override 170 public void onReceiveStateChanged( 171 @NonNull BluetoothDevice sink, 172 int sourceId, 173 @NonNull BluetoothLeBroadcastReceiveState state) {} 174 }; 175 AudioSharingDeviceVolumeGroupController(Context context)176 public AudioSharingDeviceVolumeGroupController(Context context) { 177 super(context, KEY); 178 mBtManager = Utils.getLocalBtManager(mContext); 179 mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager(); 180 mAssistant = 181 mProfileManager == null 182 ? null 183 : mProfileManager.getLeAudioBroadcastAssistantProfile(); 184 mVolumeControl = mProfileManager == null ? null : mProfileManager.getVolumeControlProfile(); 185 mExecutor = Executors.newSingleThreadExecutor(); 186 mContentResolver = context.getContentResolver(); 187 mSettingsObserver = new SettingsObserver(); 188 } 189 190 private class SettingsObserver extends ContentObserver { SettingsObserver()191 SettingsObserver() { 192 super(new Handler(Looper.getMainLooper())); 193 } 194 195 @Override onChange(boolean selfChange)196 public void onChange(boolean selfChange) { 197 Log.d(TAG, "onChange, fallback device group id has been changed"); 198 for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) { 199 int order = getPreferenceOrderForDevice(preference.getCachedDevice()); 200 Log.d(TAG, "onChange: set order to " + order + " for " + preference); 201 AudioSharingUtils.postOnMainThread(mContext, () -> preference.setOrder(order)); 202 } 203 } 204 } 205 206 @Override onStart(@onNull LifecycleOwner owner)207 public void onStart(@NonNull LifecycleOwner owner) { 208 super.onStart(owner); 209 registerCallbacks(); 210 } 211 212 @Override onStop(@onNull LifecycleOwner owner)213 public void onStop(@NonNull LifecycleOwner owner) { 214 super.onStop(owner); 215 unregisterCallbacks(); 216 } 217 218 @Override onDestroy(@onNull LifecycleOwner owner)219 public void onDestroy(@NonNull LifecycleOwner owner) { 220 mVolumePreferences.clear(); 221 } 222 223 @Override displayPreference(PreferenceScreen screen)224 public void displayPreference(PreferenceScreen screen) { 225 super.displayPreference(screen); 226 227 mPreferenceGroup = screen.findPreference(KEY); 228 if (mPreferenceGroup != null) { 229 mPreferenceGroup.setVisible(false); 230 } 231 232 if (isAvailable() && mBluetoothDeviceUpdater != null) { 233 mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); 234 mBluetoothDeviceUpdater.forceUpdate(); 235 } 236 } 237 238 @Override getPreferenceKey()239 public String getPreferenceKey() { 240 return KEY; 241 } 242 243 @Override onDeviceAdded(Preference preference)244 public void onDeviceAdded(Preference preference) { 245 if (!(preference instanceof AudioSharingDeviceVolumePreference)) { 246 Log.d(TAG, "Skip onDeviceAdded, invalid preference type"); 247 return; 248 } 249 var volumePref = (AudioSharingDeviceVolumePreference) preference; 250 mVolumePreferences.add(volumePref); 251 AudioSharingUtils.postOnMainThread(mContext, () -> { 252 if (mPreferenceGroup != null) { 253 if (mPreferenceGroup.getPreferenceCount() == 0) { 254 mPreferenceGroup.setVisible(true); 255 } 256 mPreferenceGroup.addPreference(volumePref); 257 } 258 }); 259 CachedBluetoothDevice cachedDevice = volumePref.getCachedDevice(); 260 String address = cachedDevice.getDevice() == null ? "null" 261 : cachedDevice.getDevice().getAnonymizedAddress(); 262 int order = getPreferenceOrderForDevice(cachedDevice); 263 Log.d(TAG, "onDeviceAdded: set order to " + order + " for " + address); 264 AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setOrder(order)); 265 int volume = mValueMap.getOrDefault(BluetoothUtils.getGroupId(cachedDevice), -1); 266 // If the volume is invalid, try to get the volume from AudioManager.STREAM_MUSIC 267 int finalVolume = getAudioVolumeIfNeeded(volume); 268 Log.d(TAG, "onDeviceAdded: set volume to " + finalVolume + " for " + address); 269 AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setProgress(finalVolume)); 270 } 271 272 @Override onDeviceRemoved(Preference preference)273 public void onDeviceRemoved(Preference preference) { 274 if (!(preference instanceof AudioSharingDeviceVolumePreference)) { 275 Log.d(TAG, "Skip onDeviceRemoved, invalid preference type"); 276 return; 277 } 278 var volumePref = (AudioSharingDeviceVolumePreference) preference; 279 if (mVolumePreferences.contains(volumePref)) { 280 mVolumePreferences.remove(volumePref); 281 } 282 String address = volumePref.getCachedDevice().getDevice() == null ? "null" 283 : volumePref.getCachedDevice().getDevice().getAnonymizedAddress(); 284 Log.d(TAG, "onDeviceRemoved: " + address); 285 AudioSharingUtils.postOnMainThread(mContext, () -> { 286 if (mPreferenceGroup != null) { 287 mPreferenceGroup.removePreference(volumePref); 288 if (mPreferenceGroup.getPreferenceCount() == 0) { 289 mPreferenceGroup.setVisible(false); 290 } 291 } 292 }); 293 } 294 295 @Override updateVisibility()296 public void updateVisibility() { 297 if (mPreferenceGroup != null && mPreferenceGroup.getPreferenceCount() == 0) { 298 mPreferenceGroup.setVisible(false); 299 return; 300 } 301 super.updateVisibility(); 302 } 303 304 @Override onAudioSharingProfilesConnected()305 public void onAudioSharingProfilesConnected() { 306 registerCallbacks(); 307 } 308 309 /** 310 * Initialize the controller. 311 * 312 * @param fragment The fragment to provide the context and metrics category for {@link 313 * AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs. 314 */ init(DashboardFragment fragment)315 public void init(DashboardFragment fragment) { 316 mBluetoothDeviceUpdater = 317 new AudioSharingDeviceVolumeControlUpdater( 318 fragment.getContext(), 319 AudioSharingDeviceVolumeGroupController.this, 320 fragment.getMetricsCategory()); 321 } 322 323 @VisibleForTesting setDeviceUpdater(@ullable AudioSharingDeviceVolumeControlUpdater updater)324 void setDeviceUpdater(@Nullable AudioSharingDeviceVolumeControlUpdater updater) { 325 mBluetoothDeviceUpdater = updater; 326 } 327 328 /** Test only: set callback registration status in tests. */ 329 @VisibleForTesting setCallbacksRegistered(boolean registered)330 void setCallbacksRegistered(boolean registered) { 331 mCallbacksRegistered.set(registered); 332 } 333 334 /** Test only: set volume map in tests. */ 335 @VisibleForTesting setVolumeMap(@ullable Map<Integer, Integer> map)336 void setVolumeMap(@Nullable Map<Integer, Integer> map) { 337 mValueMap.clear(); 338 mValueMap.putAll(map); 339 } 340 341 /** Test only: set value for private preferenceGroup in tests. */ 342 @VisibleForTesting setPreferenceGroup(@ullable PreferenceGroup group)343 void setPreferenceGroup(@Nullable PreferenceGroup group) { 344 mPreferenceGroup = group; 345 mPreference = group; 346 } 347 348 @VisibleForTesting getSettingsObserver()349 ContentObserver getSettingsObserver() { 350 return mSettingsObserver; 351 } 352 registerCallbacks()353 private void registerCallbacks() { 354 if (!isAvailable()) { 355 Log.d(TAG, "Skip registerCallbacks(). Feature is not available."); 356 return; 357 } 358 if (mAssistant == null 359 || mVolumeControl == null 360 || mBluetoothDeviceUpdater == null 361 || mContentResolver == null 362 || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) { 363 Log.d(TAG, "Skip registerCallbacks(). Profile is not ready."); 364 return; 365 } 366 if (!mCallbacksRegistered.get()) { 367 Log.d(TAG, "registerCallbacks()"); 368 mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 369 mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback); 370 mBluetoothDeviceUpdater.registerCallback(); 371 mBluetoothDeviceUpdater.refreshPreference(); 372 mContentResolver.registerContentObserver( 373 Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast()), 374 false, 375 mSettingsObserver); 376 mCallbacksRegistered.set(true); 377 } 378 } 379 unregisterCallbacks()380 private void unregisterCallbacks() { 381 if (!isAvailable()) { 382 Log.d(TAG, "Skip unregister callbacks. Feature is not available."); 383 return; 384 } 385 if (mAssistant == null 386 || mVolumeControl == null 387 || mBluetoothDeviceUpdater == null 388 || mContentResolver == null 389 || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) { 390 Log.d(TAG, "Skip unregisterCallbacks(). Profile is not ready."); 391 return; 392 } 393 if (mCallbacksRegistered.get()) { 394 Log.d(TAG, "unregisterCallbacks()"); 395 mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 396 mVolumeControl.unregisterCallback(mVolumeControlCallback); 397 mBluetoothDeviceUpdater.unregisterCallback(); 398 mContentResolver.unregisterContentObserver(mSettingsObserver); 399 mValueMap.clear(); 400 mCallbacksRegistered.set(false); 401 } 402 } 403 getAudioVolumeIfNeeded(int volume)404 private int getAudioVolumeIfNeeded(int volume) { 405 if (volume >= 0) return volume; 406 try { 407 AudioManager audioManager = mContext.getSystemService(AudioManager.class); 408 int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 409 int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC); 410 return Math.round( 411 audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 255f / (max - min)); 412 } catch (RuntimeException e) { 413 Log.e(TAG, "Fail to fetch current music stream volume, error = " + e); 414 return volume; 415 } 416 } 417 getPreferenceOrderForDevice(@onNull CachedBluetoothDevice cachedDevice)418 private int getPreferenceOrderForDevice(@NonNull CachedBluetoothDevice cachedDevice) { 419 int groupId = BluetoothUtils.getGroupId(cachedDevice); 420 // The fallback device rank first among the audio sharing device list. 421 return (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID 422 && groupId == BluetoothUtils.getPrimaryGroupIdForBroadcast(mContentResolver, 423 mBtManager)) 424 ? 0 425 : 1; 426 } 427 } 428