• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.VisibleForTesting;
34 import androidx.annotation.WorkerThread;
35 import androidx.lifecycle.DefaultLifecycleObserver;
36 import androidx.lifecycle.LifecycleOwner;
37 import androidx.preference.PreferenceScreen;
38 
39 import com.android.settings.bluetooth.Utils;
40 import com.android.settings.core.BasePreferenceController;
41 import com.android.settings.dashboard.DashboardFragment;
42 import com.android.settingslib.bluetooth.BluetoothCallback;
43 import com.android.settingslib.bluetooth.BluetoothEventManager;
44 import com.android.settingslib.bluetooth.BluetoothUtils;
45 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
46 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
47 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
48 import com.android.settingslib.bluetooth.LocalBluetoothManager;
49 import com.android.settingslib.flags.Flags;
50 import com.android.settingslib.utils.ThreadUtils;
51 
52 import java.util.concurrent.Executor;
53 import java.util.concurrent.Executors;
54 
55 public class AudioSharingJoinHandlerController extends BasePreferenceController
56         implements DefaultLifecycleObserver, BluetoothCallback {
57     private static final String TAG = "AudioSharingJoinHandlerCtrl";
58     private static final String KEY = "audio_sharing_join_handler";
59 
60     @Nullable private final LocalBluetoothManager mBtManager;
61     @Nullable private final BluetoothEventManager mEventManager;
62     @Nullable private final CachedBluetoothDeviceManager mDeviceManager;
63     @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
64     private final Executor mExecutor;
65     @Nullable private DashboardFragment mFragment;
66     @Nullable private AudioSharingDialogHandler mDialogHandler;
67     @VisibleForTesting
68     BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
69             new BluetoothLeBroadcastAssistant.Callback() {
70                 @Override
71                 public void onSearchStarted(int reason) {
72                 }
73 
74                 @Override
75                 public void onSearchStartFailed(int reason) {
76                 }
77 
78                 @Override
79                 public void onSearchStopped(int reason) {
80                 }
81 
82                 @Override
83                 public void onSearchStopFailed(int reason) {
84                 }
85 
86                 @Override
87                 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
88                 }
89 
90                 @Override
91                 public void onSourceAdded(
92                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
93                     Log.d(TAG, "onSourceAdded: dismiss stale dialog.");
94                     if (mDeviceManager != null && mDialogHandler != null) {
95                         CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
96                         if (cachedDevice != null) {
97                             mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
98                         }
99                     }
100                 }
101 
102                 @Override
103                 public void onSourceAddFailed(
104                         @NonNull BluetoothDevice sink,
105                         @NonNull BluetoothLeBroadcastMetadata source,
106                         int reason) {
107                 }
108 
109                 @Override
110                 public void onSourceModified(
111                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
112                 }
113 
114                 @Override
115                 public void onSourceModifyFailed(
116                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
117                 }
118 
119                 @Override
120                 public void onSourceRemoved(
121                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
122                 }
123 
124                 @Override
125                 public void onSourceRemoveFailed(
126                         @NonNull BluetoothDevice sink, int sourceId, int reason) {
127                 }
128 
129                 @Override
130                 public void onReceiveStateChanged(
131                         @NonNull BluetoothDevice sink,
132                         int sourceId,
133                         @NonNull BluetoothLeBroadcastReceiveState state) {
134                 }
135             };
136 
AudioSharingJoinHandlerController(@onNull Context context, @NonNull String preferenceKey)137     public AudioSharingJoinHandlerController(@NonNull Context context,
138             @NonNull String preferenceKey) {
139         super(context, preferenceKey);
140         mBtManager = Utils.getLocalBtManager(mContext);
141         mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
142         mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager();
143         mAssistant = mBtManager == null ? null
144                 : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
145         mExecutor = Executors.newSingleThreadExecutor();
146     }
147 
148     /**
149      * Initialize the controller.
150      *
151      * @param fragment The fragment to provide the context and metrics category for {@link
152      *                 AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
153      */
init(@onNull DashboardFragment fragment)154     public void init(@NonNull DashboardFragment fragment) {
155         mFragment = fragment;
156         mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
157     }
158 
159     @Override
onStart(@onNull LifecycleOwner owner)160     public void onStart(@NonNull LifecycleOwner owner) {
161         var unused = ThreadUtils.postOnBackgroundThread(() -> {
162             if (!isAvailable()) {
163                 Log.d(TAG, "Skip onStart(), feature is not supported.");
164                 return;
165             }
166             if (mEventManager == null || mDialogHandler == null || mAssistant == null) {
167                 Log.d(TAG, "Skip onStart(), profile is not ready.");
168                 return;
169             }
170             Log.d(TAG, "onStart() Register callbacks.");
171             mEventManager.registerCallback(this);
172             mAssistant.registerServiceCallBack(mExecutor, mAssistantCallback);
173             mDialogHandler.registerCallbacks(mExecutor);
174         });
175     }
176 
177     @Override
onStop(@onNull LifecycleOwner owner)178     public void onStop(@NonNull LifecycleOwner owner) {
179         var unused = ThreadUtils.postOnBackgroundThread(() -> {
180             if (!isAvailable()) {
181                 Log.d(TAG, "Skip onStop(), feature is not supported.");
182                 return;
183             }
184             if (mEventManager == null || mDialogHandler == null || mAssistant == null) {
185                 Log.d(TAG, "Skip onStop(), profile is not ready.");
186                 return;
187             }
188             Log.d(TAG, "onStop() Unregister callbacks.");
189             mEventManager.unregisterCallback(this);
190             mAssistant.unregisterServiceCallBack(mAssistantCallback);
191             mDialogHandler.unregisterCallbacks();
192         });
193     }
194 
195 
196     @Override
getAvailabilityStatus()197     public int getAvailabilityStatus() {
198         return (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
199                 && BluetoothUtils.isAudioSharingUIAvailable(mContext))
200                 ? AVAILABLE_UNSEARCHABLE
201                 : UNSUPPORTED_ON_DEVICE;
202     }
203 
204     @Override
getPreferenceKey()205     public String getPreferenceKey() {
206         return KEY;
207     }
208 
209     @Override
getSliceHighlightMenuRes()210     public int getSliceHighlightMenuRes() {
211         return 0;
212     }
213 
214     @Override
displayPreference(@onNull PreferenceScreen screen)215     public void displayPreference(@NonNull PreferenceScreen screen) {
216         super.displayPreference(screen);
217         if (mFragment == null
218                 || mFragment.getActivity() == null
219                 || mFragment.getActivity().getIntent() == null) {
220             Log.d(TAG, "Skip handleDeviceConnectedFromIntent, fragment intent is null");
221             return;
222         }
223         Intent intent = mFragment.getActivity().getIntent();
224         var unused =
225                 ThreadUtils.postOnBackgroundThread(() -> handleDeviceConnectedFromIntent(intent));
226     }
227 
228     @Override
onProfileConnectionStateChanged( @onNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile)229     public void onProfileConnectionStateChanged(
230             @NonNull CachedBluetoothDevice cachedDevice,
231             @ConnectionState int state,
232             int bluetoothProfile) {
233         if (mDialogHandler == null || mFragment == null) {
234             Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly");
235             return;
236         }
237         // Close related dialogs if the BT remote device is disconnected.
238         if (state == BluetoothAdapter.STATE_DISCONNECTED) {
239             boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice);
240             if (isLeAudioSupported
241                     && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
242                 Log.d(TAG, "closeOpeningDialogsForLeaDevice");
243                 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
244             } else if (!isLeAudioSupported && !cachedDevice.isConnected()) {
245                 Log.d(TAG, "closeOpeningDialogsForNonLeaDevice");
246                 mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice);
247             }
248         }
249     }
250 
251     @Override
onBluetoothStateChanged(@dapterState int bluetoothState)252     public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
253         if (bluetoothState == BluetoothAdapter.STATE_OFF) {
254             finishActivity();
255         }
256     }
257 
258     /** Handle just connected device via intent. */
259     @WorkerThread
handleDeviceConnectedFromIntent(@onNull Intent intent)260     public void handleDeviceConnectedFromIntent(@NonNull Intent intent) {
261         BluetoothDevice device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE,
262                 BluetoothDevice.class);
263         CachedBluetoothDevice cachedDevice =
264                 (device == null || mDeviceManager == null)
265                         ? null
266                         : mDeviceManager.findDevice(device);
267         if (cachedDevice == null) {
268             Log.d(TAG, "Skip handleDeviceConnectedFromIntent and finish activity, device is null");
269             finishActivity();
270             return;
271         }
272         if (mDialogHandler == null) {
273             Log.d(TAG, "Skip handleDeviceConnectedFromIntent and finish activity, handler is null");
274             finishActivity();
275             return;
276         }
277         Log.d(TAG, "handleDeviceConnectedFromIntent, device = " + device.getAnonymizedAddress());
278         if (!mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false)) {
279             Log.d(TAG, "handleDeviceConnectedFromIntent, finish activity");
280             finishActivity();
281         }
282     }
283 
finishActivity()284     private void finishActivity() {
285         AudioSharingUtils.postOnMainThread(mContext, () -> {
286             if (mFragment != null && mFragment.getActivity() != null) {
287                 Log.d(TAG, "Finish activity");
288                 mFragment.getActivity().finish();
289             }
290         });
291     }
292 
293     @VisibleForTesting
setDialogHandler(@ullable AudioSharingDialogHandler dialogHandler)294     void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) {
295         mDialogHandler = dialogHandler;
296     }
297 }
298