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.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
20 
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothDevice;
23 import android.bluetooth.BluetoothLeBroadcastAssistant;
24 import android.bluetooth.BluetoothLeBroadcastMetadata;
25 import android.bluetooth.BluetoothLeBroadcastReceiveState;
26 import android.bluetooth.BluetoothProfile;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.os.Bundle;
30 import android.util.Log;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 import androidx.annotation.VisibleForTesting;
35 import androidx.lifecycle.DefaultLifecycleObserver;
36 import androidx.lifecycle.LifecycleOwner;
37 import androidx.preference.Preference;
38 import androidx.preference.PreferenceGroup;
39 import androidx.preference.PreferenceScreen;
40 
41 import com.android.settings.SettingsActivity;
42 import com.android.settings.bluetooth.BluetoothDeviceUpdater;
43 import com.android.settings.bluetooth.Utils;
44 import com.android.settings.connecteddevice.DevicePreferenceCallback;
45 import com.android.settings.core.BasePreferenceController;
46 import com.android.settings.dashboard.DashboardFragment;
47 import com.android.settingslib.bluetooth.A2dpProfile;
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.HeadsetProfile;
54 import com.android.settingslib.bluetooth.HearingAidProfile;
55 import com.android.settingslib.bluetooth.LeAudioProfile;
56 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
57 import com.android.settingslib.bluetooth.LocalBluetoothManager;
58 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
59 
60 import java.util.Locale;
61 import java.util.concurrent.Executor;
62 import java.util.concurrent.Executors;
63 import java.util.concurrent.atomic.AtomicBoolean;
64 
65 public class AudioSharingDevicePreferenceController extends BasePreferenceController
66         implements DefaultLifecycleObserver,
67                 DevicePreferenceCallback,
68                 BluetoothCallback,
69                 LocalBluetoothProfileManager.ServiceListener {
70     private static final boolean DEBUG = BluetoothUtils.D;
71 
72     private static final String TAG = "AudioSharingDevicePrefController";
73     private static final String KEY = "audio_sharing_device_list";
74     private static final String KEY_AUDIO_SHARING_SETTINGS =
75             "connected_device_audio_sharing_settings";
76 
77     @Nullable private final LocalBluetoothManager mBtManager;
78     @Nullable private final CachedBluetoothDeviceManager mDeviceManager;
79     @Nullable private final BluetoothEventManager mEventManager;
80     @Nullable private final LocalBluetoothProfileManager mProfileManager;
81     @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
82     private final Executor mExecutor;
83     @Nullable private PreferenceGroup mPreferenceGroup;
84     @Nullable private Preference mAudioSharingSettingsPreference;
85     @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
86     @Nullable private DashboardFragment mFragment;
87     @Nullable private AudioSharingDialogHandler mDialogHandler;
88     private AtomicBoolean mIntentHandled = new AtomicBoolean(false);
89 
90     @VisibleForTesting
91     BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
92             new BluetoothLeBroadcastAssistant.Callback() {
93                 @Override
94                 public void onSearchStarted(int reason) {}
95 
96                 @Override
97                 public void onSearchStartFailed(int reason) {}
98 
99                 @Override
100                 public void onSearchStopped(int reason) {}
101 
102                 @Override
103                 public void onSearchStopFailed(int reason) {}
104 
105                 @Override
106                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
107 
108                 @Override
109                 public void onSourceAdded(
110                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
111 
112                 @Override
113                 public void onSourceAddFailed(
114                         @NonNull BluetoothDevice sink,
115                         @NonNull BluetoothLeBroadcastMetadata source,
116                         int reason) {
117                     AudioSharingUtils.toastMessage(
118                             mContext,
119                             String.format(
120                                     Locale.US,
121                                     "Fail to add source to %s reason %d",
122                                     sink.getAddress(),
123                                     reason));
124                 }
125 
126                 @Override
127                 public void onSourceModified(
128                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
129 
130                 @Override
131                 public void onSourceModifyFailed(
132                         @NonNull BluetoothDevice sink, int sourceId, int reason) {}
133 
134                 @Override
135                 public void onSourceRemoved(
136                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
137                     Log.d(TAG, "onSourceRemoved: update sharing device list.");
138                     if (mBluetoothDeviceUpdater != null) {
139                         mBluetoothDeviceUpdater.forceUpdate();
140                     }
141                 }
142 
143                 @Override
144                 public void onSourceRemoveFailed(
145                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
146                     AudioSharingUtils.toastMessage(
147                             mContext,
148                             String.format(
149                                     Locale.US,
150                                     "Fail to remove source from %s reason %d",
151                                     sink.getAddress(),
152                                     reason));
153                 }
154 
155                 @Override
156                 public void onReceiveStateChanged(
157                         @NonNull BluetoothDevice sink,
158                         int sourceId,
159                         @NonNull BluetoothLeBroadcastReceiveState state) {
160                     if (BluetoothUtils.isConnected(state)) {
161                         Log.d(TAG, "onSourceAdded: update sharing device list.");
162                         if (mBluetoothDeviceUpdater != null) {
163                             mBluetoothDeviceUpdater.forceUpdate();
164                         }
165                         if (mDeviceManager != null && mDialogHandler != null) {
166                             CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
167                             if (cachedDevice != null) {
168                                 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
169                             }
170                         }
171                     }
172                 }
173             };
174 
AudioSharingDevicePreferenceController(Context context)175     public AudioSharingDevicePreferenceController(Context context) {
176         super(context, KEY);
177         mBtManager = Utils.getLocalBtManager(mContext);
178         mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
179         mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager();
180         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
181         mAssistant =
182                 mProfileManager == null
183                         ? null
184                         : mProfileManager.getLeAudioBroadcastAssistantProfile();
185         mExecutor = Executors.newSingleThreadExecutor();
186     }
187 
188     @Override
onStart(@onNull LifecycleOwner owner)189     public void onStart(@NonNull LifecycleOwner owner) {
190         if (!isAvailable()) {
191             Log.d(TAG, "Skip onStart(), feature is not supported.");
192             return;
193         }
194         if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)
195                 && mProfileManager != null) {
196             Log.d(TAG, "Register profile service listener");
197             mProfileManager.addServiceListener(this);
198         }
199         if (mEventManager == null
200                 || mAssistant == null
201                 || mDialogHandler == null
202                 || mBluetoothDeviceUpdater == null) {
203             Log.d(TAG, "Skip onStart(), profile is not ready.");
204             return;
205         }
206         Log.d(TAG, "onStart() Register callbacks.");
207         mEventManager.registerCallback(this);
208         mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
209         mDialogHandler.registerCallbacks(mExecutor);
210         mBluetoothDeviceUpdater.registerCallback();
211         mBluetoothDeviceUpdater.refreshPreference();
212     }
213 
214     @Override
onStop(@onNull LifecycleOwner owner)215     public void onStop(@NonNull LifecycleOwner owner) {
216         if (!isAvailable()) {
217             Log.d(TAG, "Skip onStop(), feature is not supported.");
218             return;
219         }
220         if (mProfileManager != null) {
221             mProfileManager.removeServiceListener(this);
222         }
223         if (mEventManager == null
224                 || mAssistant == null
225                 || mDialogHandler == null
226                 || mBluetoothDeviceUpdater == null) {
227             Log.d(TAG, "Skip onStop(), profile is not ready.");
228             return;
229         }
230         Log.d(TAG, "onStop() Unregister callbacks.");
231         mEventManager.unregisterCallback(this);
232         mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
233         mDialogHandler.unregisterCallbacks();
234         mBluetoothDeviceUpdater.unregisterCallback();
235     }
236 
237     @Override
onServiceConnected()238     public void onServiceConnected() {
239         if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
240             if (mProfileManager != null) {
241                 mProfileManager.removeServiceListener(this);
242             }
243             if (!mIntentHandled.get()) {
244                 Log.d(TAG, "onServiceConnected: handleDeviceClickFromIntent");
245                 handleDeviceClickFromIntent();
246                 mIntentHandled.set(true);
247             }
248         }
249     }
250 
251     @Override
onServiceDisconnected()252     public void onServiceDisconnected() {
253         // Do nothing
254     }
255 
256     @Override
displayPreference(PreferenceScreen screen)257     public void displayPreference(PreferenceScreen screen) {
258         super.displayPreference(screen);
259         mPreferenceGroup = screen.findPreference(KEY);
260         if (mPreferenceGroup != null) {
261             mAudioSharingSettingsPreference =
262                     mPreferenceGroup.findPreference(KEY_AUDIO_SHARING_SETTINGS);
263             mPreferenceGroup.setVisible(false);
264         }
265         if (mAudioSharingSettingsPreference != null) {
266             mAudioSharingSettingsPreference.setVisible(false);
267         }
268 
269         if (isAvailable()) {
270             if (mBluetoothDeviceUpdater != null) {
271                 mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
272                 mBluetoothDeviceUpdater.forceUpdate();
273             }
274             if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
275                 if (!mIntentHandled.get()) {
276                     Log.d(TAG, "displayPreference: profile ready, handleDeviceClickFromIntent");
277                     handleDeviceClickFromIntent();
278                     mIntentHandled.set(true);
279                 }
280             }
281         }
282     }
283 
284     @Override
getAvailabilityStatus()285     public int getAvailabilityStatus() {
286         return AudioSharingUtils.isFeatureEnabled() && mBluetoothDeviceUpdater != null
287                 ? AVAILABLE_UNSEARCHABLE
288                 : UNSUPPORTED_ON_DEVICE;
289     }
290 
291     @Override
getPreferenceKey()292     public String getPreferenceKey() {
293         return KEY;
294     }
295 
296     @Override
onDeviceAdded(Preference preference)297     public void onDeviceAdded(Preference preference) {
298         if (mPreferenceGroup != null) {
299             if (mPreferenceGroup.getPreferenceCount() == 1) {
300                 mPreferenceGroup.setVisible(true);
301                 if (mAudioSharingSettingsPreference != null) {
302                     mAudioSharingSettingsPreference.setVisible(true);
303                 }
304             }
305             mPreferenceGroup.addPreference(preference);
306         }
307     }
308 
309     @Override
onDeviceRemoved(Preference preference)310     public void onDeviceRemoved(Preference preference) {
311         if (mPreferenceGroup != null) {
312             mPreferenceGroup.removePreference(preference);
313             if (mPreferenceGroup.getPreferenceCount() == 1) {
314                 mPreferenceGroup.setVisible(false);
315                 if (mAudioSharingSettingsPreference != null) {
316                     mAudioSharingSettingsPreference.setVisible(false);
317                 }
318             }
319         }
320     }
321 
322     @Override
onProfileConnectionStateChanged( @onNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile)323     public void onProfileConnectionStateChanged(
324             @NonNull CachedBluetoothDevice cachedDevice,
325             @ConnectionState int state,
326             int bluetoothProfile) {
327         if (mDialogHandler == null || mAssistant == null || mFragment == null) {
328             Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly");
329             return;
330         }
331         if (!isMediaDevice(cachedDevice)) {
332             Log.d(TAG, "Ignore onProfileConnectionStateChanged, not a media device");
333             return;
334         }
335         // Close related dialogs if the BT remote device is disconnected.
336         if (state == BluetoothAdapter.STATE_DISCONNECTED) {
337             boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
338             if (isLeAudioSupported
339                     && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
340                 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
341                 return;
342             }
343             if (!isLeAudioSupported && !cachedDevice.isConnected()) {
344                 mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice);
345                 return;
346             }
347         }
348         if (state != BluetoothAdapter.STATE_CONNECTED || !cachedDevice.getDevice().isConnected()) {
349             Log.d(TAG, "Ignore onProfileConnectionStateChanged, not connected state");
350             return;
351         }
352         handleOnProfileStateChanged(cachedDevice, bluetoothProfile);
353     }
354 
355     /**
356      * Initialize the controller.
357      *
358      * @param fragment The fragment to provide the context and metrics category for {@link
359      *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
360      */
init(DashboardFragment fragment)361     public void init(DashboardFragment fragment) {
362         mFragment = fragment;
363         mBluetoothDeviceUpdater =
364                 new AudioSharingBluetoothDeviceUpdater(
365                         fragment.getContext(),
366                         AudioSharingDevicePreferenceController.this,
367                         fragment.getMetricsCategory());
368         mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
369     }
370 
371     @VisibleForTesting
setBluetoothDeviceUpdater(@ullable BluetoothDeviceUpdater bluetoothDeviceUpdater)372     void setBluetoothDeviceUpdater(@Nullable BluetoothDeviceUpdater bluetoothDeviceUpdater) {
373         mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
374     }
375 
376     @VisibleForTesting
setDialogHandler(@ullable AudioSharingDialogHandler dialogHandler)377     void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) {
378         mDialogHandler = dialogHandler;
379     }
380 
381     @VisibleForTesting
setHostFragment(@ullable DashboardFragment fragment)382     void setHostFragment(@Nullable DashboardFragment fragment) {
383         mFragment = fragment;
384     }
385 
386     /** Test only: set intent handle state for test. */
387     @VisibleForTesting
setIntentHandled(boolean handled)388     void setIntentHandled(boolean handled) {
389         mIntentHandled.set(handled);
390     }
391 
handleOnProfileStateChanged( @onNull CachedBluetoothDevice cachedDevice, int bluetoothProfile)392     private void handleOnProfileStateChanged(
393             @NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
394         boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
395         // For eligible (LE audio) remote device, we only check its connected LE audio assistant
396         // profile.
397         if (isLeAudioSupported
398                 && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
399             Log.d(
400                     TAG,
401                     "Ignore onProfileConnectionStateChanged, not the le assistant profile for"
402                             + " le audio device");
403             return;
404         }
405         boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
406         // For ineligible (non LE audio) remote device, we only check its first connected profile.
407         if (!isLeAudioSupported && !isFirstConnectedProfile) {
408             Log.d(
409                     TAG,
410                     "Ignore onProfileConnectionStateChanged, not the first connected profile for"
411                             + " non le audio device");
412             return;
413         }
414         if (DEBUG) {
415             Log.d(
416                     TAG,
417                     "Start handling onProfileConnectionStateChanged for "
418                             + cachedDevice.getDevice().getAnonymizedAddress());
419         }
420         // Check nullability to pass NullAway check
421         if (mDialogHandler != null) {
422             mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false);
423         }
424     }
425 
isMediaDevice(CachedBluetoothDevice cachedDevice)426     private boolean isMediaDevice(CachedBluetoothDevice cachedDevice) {
427         return cachedDevice.getConnectableProfiles().stream()
428                 .anyMatch(
429                         profile ->
430                                 profile instanceof A2dpProfile
431                                         || profile instanceof HearingAidProfile
432                                         || profile instanceof LeAudioProfile
433                                         || profile instanceof HeadsetProfile);
434     }
435 
isFirstConnectedProfile( CachedBluetoothDevice cachedDevice, int bluetoothProfile)436     private boolean isFirstConnectedProfile(
437             CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
438         return cachedDevice.getProfiles().stream()
439                 .noneMatch(
440                         profile ->
441                                 profile.getProfileId() != bluetoothProfile
442                                         && profile.getConnectionStatus(cachedDevice.getDevice())
443                                                 == BluetoothProfile.STATE_CONNECTED);
444     }
445 
446     /**
447      * Handle device click triggered by intent.
448      *
449      * <p>When user click device from BT QS dialog, BT QS will send intent to open {@link
450      * com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment} and handle device
451      * click event under some conditions.
452      *
453      * <p>This method will be called when displayPreference if the audio sharing profiles are ready.
454      * If the profiles are not ready when the preference display, this method will be called when
455      * onServiceConnected.
456      */
handleDeviceClickFromIntent()457     private void handleDeviceClickFromIntent() {
458         if (mFragment == null
459                 || mFragment.getActivity() == null
460                 || mFragment.getActivity().getIntent() == null) {
461             Log.d(TAG, "Skip handleDeviceClickFromIntent, fragment intent is null");
462             return;
463         }
464         Intent intent = mFragment.getActivity().getIntent();
465         Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
466         BluetoothDevice device =
467                 args == null
468                         ? null
469                         : args.getParcelable(EXTRA_BLUETOOTH_DEVICE, BluetoothDevice.class);
470         CachedBluetoothDevice cachedDevice =
471                 (device == null || mDeviceManager == null)
472                         ? null
473                         : mDeviceManager.findDevice(device);
474         if (cachedDevice == null) {
475             Log.d(TAG, "Skip handleDeviceClickFromIntent, device is null");
476             return;
477         }
478         // Check nullability to pass NullAway check
479         if (device != null && !device.isConnected()) {
480             Log.d(TAG, "handleDeviceClickFromIntent: connect");
481             cachedDevice.connect();
482         } else if (mDialogHandler != null) {
483             Log.d(TAG, "handleDeviceClickFromIntent: trigger dialog handler");
484             mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
485         }
486     }
487 }
488