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.app.settings.SettingsEnums; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothCsipSetCoordinator; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothLeBroadcastAssistant; 26 import android.bluetooth.BluetoothLeBroadcastMetadata; 27 import android.bluetooth.BluetoothLeBroadcastReceiveState; 28 import android.bluetooth.BluetoothProfile; 29 import android.content.ContentResolver; 30 import android.content.Context; 31 import android.database.ContentObserver; 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.fragment.app.Fragment; 41 import androidx.lifecycle.LifecycleOwner; 42 import androidx.preference.PreferenceScreen; 43 44 import com.android.settings.R; 45 import com.android.settings.bluetooth.Utils; 46 import com.android.settings.overlay.FeatureFactory; 47 import com.android.settingslib.bluetooth.BluetoothCallback; 48 import com.android.settingslib.bluetooth.BluetoothEventManager; 49 import com.android.settingslib.bluetooth.BluetoothUtils; 50 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 51 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 52 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 53 import com.android.settingslib.bluetooth.LocalBluetoothManager; 54 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 55 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 56 import com.android.settingslib.utils.ThreadUtils; 57 58 import com.google.common.collect.ImmutableList; 59 60 import java.util.ArrayList; 61 import java.util.HashMap; 62 import java.util.List; 63 import java.util.Map; 64 import java.util.concurrent.Executor; 65 import java.util.concurrent.Executors; 66 import java.util.concurrent.atomic.AtomicBoolean; 67 68 /** PreferenceController to control the dialog to choose the active device for calls and alarms */ 69 public class AudioSharingCallAudioPreferenceController extends AudioSharingBasePreferenceController 70 implements BluetoothCallback { 71 private static final String TAG = "CallsAndAlarmsPreferenceController"; 72 private static final String PREF_KEY = "calls_and_alarms"; 73 74 @VisibleForTesting 75 enum ChangeCallAudioType { 76 UNKNOWN, 77 CONNECTED_EARLIER, 78 CONNECTED_LATER 79 } 80 81 @Nullable private final LocalBluetoothManager mBtManager; 82 @Nullable private final BluetoothEventManager mEventManager; 83 @Nullable private final ContentResolver mContentResolver; 84 @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant; 85 @Nullable private final CachedBluetoothDeviceManager mCacheManager; 86 private final Executor mExecutor; 87 private final ContentObserver mSettingsObserver; 88 private final MetricsFeatureProvider mMetricsFeatureProvider; 89 @Nullable private Fragment mFragment; 90 Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>(); 91 private List<AudioSharingDeviceItem> mDeviceItemsInSharingSession = new ArrayList<>(); 92 private final AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false); 93 94 @VisibleForTesting 95 final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = 96 new BluetoothLeBroadcastAssistant.Callback() { 97 @Override 98 public void onSearchStarted(int reason) {} 99 100 @Override 101 public void onSearchStartFailed(int reason) {} 102 103 @Override 104 public void onSearchStopped(int reason) {} 105 106 @Override 107 public void onSearchStopFailed(int reason) {} 108 109 @Override 110 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} 111 112 @Override 113 public void onSourceAdded( 114 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 115 116 @Override 117 public void onSourceAddFailed( 118 @NonNull BluetoothDevice sink, 119 @NonNull BluetoothLeBroadcastMetadata source, 120 int reason) {} 121 122 @Override 123 public void onSourceModified( 124 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 125 126 @Override 127 public void onSourceModifyFailed( 128 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 129 130 @Override 131 public void onSourceRemoved( 132 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 133 134 @Override 135 public void onSourceRemoveFailed( 136 @NonNull BluetoothDevice sink, int sourceId, int reason) {} 137 138 @Override 139 public void onReceiveStateChanged( 140 @NonNull BluetoothDevice sink, 141 int sourceId, 142 @NonNull BluetoothLeBroadcastReceiveState state) { 143 if (BluetoothUtils.isConnected(state)) { 144 Log.d(TAG, "onReceiveStateChanged: synced, updateSummary"); 145 updateSummary(); 146 } 147 } 148 }; 149 AudioSharingCallAudioPreferenceController(Context context)150 public AudioSharingCallAudioPreferenceController(Context context) { 151 super(context, PREF_KEY); 152 mBtManager = Utils.getLocalBtManager(mContext); 153 LocalBluetoothProfileManager profileManager = 154 mBtManager == null ? null : mBtManager.getProfileManager(); 155 mEventManager = mBtManager == null ? null : mBtManager.getEventManager(); 156 mAssistant = 157 profileManager == null 158 ? null 159 : profileManager.getLeAudioBroadcastAssistantProfile(); 160 mCacheManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager(); 161 mExecutor = Executors.newSingleThreadExecutor(); 162 mContentResolver = context.getContentResolver(); 163 mSettingsObserver = new FallbackDeviceGroupIdSettingsObserver(); 164 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 165 } 166 167 private class FallbackDeviceGroupIdSettingsObserver extends ContentObserver { FallbackDeviceGroupIdSettingsObserver()168 FallbackDeviceGroupIdSettingsObserver() { 169 super(new Handler(Looper.getMainLooper())); 170 } 171 172 @Override onChange(boolean selfChange)173 public void onChange(boolean selfChange) { 174 Log.d(TAG, "onChange, fallback device group id has been changed"); 175 var unused = 176 ThreadUtils.postOnBackgroundThread( 177 AudioSharingCallAudioPreferenceController.this::updateSummary); 178 } 179 } 180 181 @Override getPreferenceKey()182 public String getPreferenceKey() { 183 return PREF_KEY; 184 } 185 186 @Override displayPreference(@onNull PreferenceScreen screen)187 public void displayPreference(@NonNull PreferenceScreen screen) { 188 super.displayPreference(screen); 189 if (mPreference != null) { 190 mPreference.setVisible(false); 191 updateSummary(); 192 mPreference.setOnPreferenceClickListener( 193 preference -> { 194 if (mFragment == null) { 195 Log.w(TAG, "Dialog fail to show due to null host."); 196 return true; 197 } 198 updateDeviceItemsInSharingSession(); 199 if (!mDeviceItemsInSharingSession.isEmpty()) { 200 AudioSharingCallAudioDialogFragment.show( 201 mFragment, 202 mDeviceItemsInSharingSession, 203 (AudioSharingDeviceItem item) -> { 204 int currentGroupId = 205 AudioSharingUtils.getFallbackActiveGroupId( 206 mContext); 207 if (item.getGroupId() == currentGroupId) { 208 Log.d( 209 TAG, 210 "Skip set fallback active device: unchanged"); 211 return; 212 } 213 List<CachedBluetoothDevice> devices = 214 mGroupedConnectedDevices.getOrDefault( 215 item.getGroupId(), ImmutableList.of()); 216 CachedBluetoothDevice lead = 217 AudioSharingUtils.getLeadDevice(devices); 218 if (lead != null) { 219 Log.d( 220 TAG, 221 "Set fallback active device: " 222 + lead.getDevice() 223 .getAnonymizedAddress()); 224 lead.setActive(); 225 logCallAudioDeviceChange(currentGroupId, lead); 226 } else { 227 Log.d( 228 TAG, 229 "Fail to set fallback active device: no" 230 + " lead device"); 231 } 232 }); 233 } 234 return true; 235 }); 236 } 237 } 238 239 @Override onStart(@onNull LifecycleOwner owner)240 public void onStart(@NonNull LifecycleOwner owner) { 241 super.onStart(owner); 242 registerCallbacks(); 243 } 244 245 @Override onStop(@onNull LifecycleOwner owner)246 public void onStop(@NonNull LifecycleOwner owner) { 247 super.onStop(owner); 248 unregisterCallbacks(); 249 } 250 251 @Override onProfileConnectionStateChanged( @onNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile)252 public void onProfileConnectionStateChanged( 253 @NonNull CachedBluetoothDevice cachedDevice, 254 @ConnectionState int state, 255 int bluetoothProfile) { 256 if (state == BluetoothAdapter.STATE_DISCONNECTED 257 && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { 258 Log.d(TAG, "updatePreference, LE_AUDIO_BROADCAST_ASSISTANT is disconnected."); 259 // The fallback active device could be updated if the previous fallback device is 260 // disconnected. 261 updateSummary(); 262 } 263 } 264 265 /** 266 * Initialize the controller. 267 * 268 * @param fragment The fragment to host the {@link AudioSharingCallAudioDialogFragment} dialog. 269 */ init(Fragment fragment)270 public void init(Fragment fragment) { 271 this.mFragment = fragment; 272 } 273 274 @VisibleForTesting getSettingsObserver()275 ContentObserver getSettingsObserver() { 276 return mSettingsObserver; 277 } 278 279 /** Test only: set callback registration status in tests. */ 280 @VisibleForTesting setCallbacksRegistered(boolean registered)281 void setCallbacksRegistered(boolean registered) { 282 mCallbacksRegistered.set(registered); 283 } 284 registerCallbacks()285 private void registerCallbacks() { 286 if (!isAvailable()) { 287 Log.d(TAG, "Skip registerCallbacks(). Feature is not available."); 288 return; 289 } 290 if (mEventManager == null || mContentResolver == null || mAssistant == null) { 291 Log.d( 292 TAG, 293 "Skip registerCallbacks(). Init is not ready: eventManager = " 294 + (mEventManager == null) 295 + ", contentResolver" 296 + (mContentResolver == null)); 297 return; 298 } 299 if (!mCallbacksRegistered.get()) { 300 Log.d(TAG, "registerCallbacks()"); 301 mEventManager.registerCallback(this); 302 mContentResolver.registerContentObserver( 303 Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID), 304 false, 305 mSettingsObserver); 306 mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); 307 mCallbacksRegistered.set(true); 308 } 309 } 310 unregisterCallbacks()311 private void unregisterCallbacks() { 312 if (!isAvailable()) { 313 Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available."); 314 return; 315 } 316 if (mEventManager == null || mContentResolver == null || mAssistant == null) { 317 Log.d(TAG, "Skip unregisterCallbacks(). Init is not ready."); 318 return; 319 } 320 if (mCallbacksRegistered.get()) { 321 Log.d(TAG, "unregisterCallbacks()"); 322 mEventManager.unregisterCallback(this); 323 mContentResolver.unregisterContentObserver(mSettingsObserver); 324 mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); 325 mCallbacksRegistered.set(false); 326 } 327 } 328 329 /** 330 * Update the preference summary: current headset for call audio. 331 * 332 * <p>The summary should be updated when: 333 * 334 * <p>1. displayPreference. 335 * 336 * <p>2. ContentObserver#onChange: the fallback device value in SettingsProvider is changed. 337 * 338 * <p>3. onProfileConnectionStateChanged: the assistant profile of fallback device disconnected. 339 * When the last headset in audio sharing disconnected, both Settings and bluetooth framework 340 * won't set the SettingsProvider, so no ContentObserver#onChange. 341 * 342 * <p>4. onReceiveStateChanged: new headset join the audio sharing. If the headset has already 343 * been set as fallback device in SettingsProvider by bluetooth framework when the broadcast is 344 * started, Settings won't set the SettingsProvider again when the headset join the audio 345 * sharing, so there won't be ContentObserver#onChange. We need listen to onReceiveStateChanged 346 * to handle this scenario. 347 */ updateSummary()348 private void updateSummary() { 349 updateDeviceItemsInSharingSession(); 350 int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(mContext); 351 if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 352 for (AudioSharingDeviceItem item : mDeviceItemsInSharingSession) { 353 if (item.getGroupId() == fallbackActiveGroupId) { 354 Log.d( 355 TAG, 356 "updatePreference: set summary to fallback group " 357 + fallbackActiveGroupId); 358 AudioSharingUtils.postOnMainThread( 359 mContext, 360 () -> { 361 if (mPreference != null) { 362 mPreference.setSummary( 363 mContext.getString( 364 R.string.audio_sharing_call_audio_description, 365 item.getName())); 366 } 367 }); 368 return; 369 } 370 } 371 } 372 Log.d(TAG, "updatePreference: set empty summary"); 373 AudioSharingUtils.postOnMainThread( 374 mContext, 375 () -> { 376 if (mPreference != null) { 377 mPreference.setSummary(""); 378 } 379 }); 380 } 381 updateDeviceItemsInSharingSession()382 private void updateDeviceItemsInSharingSession() { 383 mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager); 384 mDeviceItemsInSharingSession = 385 AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem( 386 mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ true); 387 } 388 389 @VisibleForTesting logCallAudioDeviceChange(int currentGroupId, CachedBluetoothDevice target)390 void logCallAudioDeviceChange(int currentGroupId, CachedBluetoothDevice target) { 391 var unused = 392 ThreadUtils.postOnBackgroundThread( 393 () -> { 394 ChangeCallAudioType type = ChangeCallAudioType.UNKNOWN; 395 if (mCacheManager != null) { 396 int targetDeviceGroupId = AudioSharingUtils.getGroupId(target); 397 List<BluetoothDevice> mostRecentDevices = 398 BluetoothAdapter.getDefaultAdapter() 399 .getMostRecentlyConnectedDevices(); 400 int targetDeviceIdx = -1; 401 int currentDeviceIdx = -1; 402 for (int idx = 0; idx < mostRecentDevices.size(); idx++) { 403 BluetoothDevice device = mostRecentDevices.get(idx); 404 CachedBluetoothDevice cachedDevice = 405 mCacheManager.findDevice(device); 406 int groupId = 407 cachedDevice != null 408 ? AudioSharingUtils.getGroupId(cachedDevice) 409 : BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 410 if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { 411 if (groupId == targetDeviceGroupId) { 412 targetDeviceIdx = idx; 413 } else if (groupId == currentGroupId) { 414 currentDeviceIdx = idx; 415 } 416 } 417 if (targetDeviceIdx != -1 && currentDeviceIdx != -1) break; 418 } 419 if (targetDeviceIdx != -1 && currentDeviceIdx != -1) { 420 type = 421 targetDeviceIdx < currentDeviceIdx 422 ? ChangeCallAudioType.CONNECTED_LATER 423 : ChangeCallAudioType.CONNECTED_EARLIER; 424 } 425 } 426 mMetricsFeatureProvider.action( 427 mContext, 428 SettingsEnums.ACTION_AUDIO_SHARING_CHANGE_CALL_AUDIO, 429 type.ordinal()); 430 }); 431 } 432 } 433