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