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