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