• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.settingslib.bluetooth;
18 
19 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
21 
22 import android.annotation.CallbackExecutor;
23 import android.bluetooth.BluetoothAdapter;
24 import android.bluetooth.BluetoothClass;
25 import android.bluetooth.BluetoothDevice;
26 import android.bluetooth.BluetoothLeBroadcastAssistant;
27 import android.bluetooth.BluetoothLeBroadcastMetadata;
28 import android.bluetooth.BluetoothLeBroadcastReceiveState;
29 import android.bluetooth.BluetoothProfile;
30 import android.bluetooth.BluetoothProfile.ServiceListener;
31 import android.content.Context;
32 import android.os.Build;
33 import android.util.Log;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.RequiresApi;
37 
38 import com.android.settingslib.R;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.concurrent.ConcurrentHashMap;
44 import java.util.concurrent.Executor;
45 
46 /**
47  * LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the
48  * functionality of the local {@link BluetoothLeBroadcastAssistant}. Use the {@link
49  * BluetoothLeBroadcastAssistant.Callback} to get the result callback.
50  */
51 public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile {
52     private static final String TAG = "LocalBluetoothLeBroadcastAssistant";
53     private static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
54     private static final boolean DEBUG = BluetoothUtils.D;
55 
56     static final String NAME = "LE_AUDIO_BROADCAST_ASSISTANT";
57     // Order of this profile in device profiles list
58     private static final int ORDINAL = 1;
59 
60     private LocalBluetoothProfileManager mProfileManager;
61     private BluetoothLeBroadcastAssistant mService;
62     private final CachedBluetoothDeviceManager mDeviceManager;
63     private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
64     private BluetoothLeBroadcastMetadata.Builder mBuilder;
65     private boolean mIsProfileReady;
66     // Cached assistant callbacks being register before service is connected.
67     private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap =
68             new ConcurrentHashMap<>();
69 
70     private final ServiceListener mServiceListener =
71             new ServiceListener() {
72                 @Override
73                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
74                     if (DEBUG) {
75                         Log.d(TAG, "Bluetooth service connected");
76                     }
77                     mService = (BluetoothLeBroadcastAssistant) proxy;
78                     // We just bound to the service, so refresh the UI for any connected LeAudio
79                     // devices.
80                     List<BluetoothDevice> deviceList = mService.getConnectedDevices();
81                     while (!deviceList.isEmpty()) {
82                         BluetoothDevice nextDevice = deviceList.remove(0);
83                         CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
84                         // we may add a new device here, but generally this should not happen
85                         if (device == null) {
86                             if (DEBUG) {
87                                 Log.d(
88                                         TAG,
89                                         "LocalBluetoothLeBroadcastAssistant found new device: "
90                                                 + nextDevice);
91                             }
92                             device = mDeviceManager.addDevice(nextDevice);
93                         }
94                         device.onProfileStateChanged(
95                                 LocalBluetoothLeBroadcastAssistant.this,
96                                 BluetoothProfile.STATE_CONNECTED);
97                         device.refresh();
98                     }
99 
100                     mProfileManager.callServiceConnectedListeners();
101                     mIsProfileReady = true;
102                     if (DEBUG) {
103                         Log.d(
104                                 TAG,
105                                 "onServiceConnected, register mCachedCallbackExecutorMap = "
106                                         + mCachedCallbackExecutorMap);
107                     }
108                     mCachedCallbackExecutorMap.forEach(
109                             (callback, executor) -> registerServiceCallBack(executor, callback));
110                 }
111 
112                 @Override
113                 public void onServiceDisconnected(int profile) {
114                     if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
115                         Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT");
116                         return;
117                     }
118                     if (DEBUG) {
119                         Log.d(TAG, "Bluetooth service disconnected");
120                     }
121                     mProfileManager.callServiceDisconnectedListeners();
122                     mIsProfileReady = false;
123                     mCachedCallbackExecutorMap.clear();
124                 }
125             };
126 
LocalBluetoothLeBroadcastAssistant( Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager)127     public LocalBluetoothLeBroadcastAssistant(
128             Context context,
129             CachedBluetoothDeviceManager deviceManager,
130             LocalBluetoothProfileManager profileManager) {
131         mProfileManager = profileManager;
132         mDeviceManager = deviceManager;
133         BluetoothAdapter.getDefaultAdapter()
134                 .getProfileProxy(
135                         context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
136         mBuilder = new BluetoothLeBroadcastMetadata.Builder();
137     }
138 
139     /**
140      * Add a Broadcast Source to the Broadcast Sink with {@link BluetoothLeBroadcastMetadata}.
141      *
142      * @param sink Broadcast Sink to which the Broadcast Source should be added
143      * @param metadata Broadcast Source metadata to be added to the Broadcast Sink
144      * @param isGroupOp {@code true} if Application wants to perform this operation for all
145      *     coordinated set members throughout this session. Otherwise, caller would have to add,
146      *     modify, and remove individual set members.
147      */
addSource( BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp)148     public void addSource(
149             BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
150         if (mService == null) {
151             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
152             return;
153         }
154         mService.addSource(sink, metadata, isGroupOp);
155     }
156 
157     /**
158      * Add a Broadcast Source to the Broadcast Sink with the information which are separated from
159      * the qr code string.
160      *
161      * @param sink Broadcast Sink to which the Broadcast Source should be added
162      * @param sourceAddressType hardware MAC Address of the device. See {@link
163      *     BluetoothDevice.AddressType}.
164      * @param presentationDelayMicros presentation delay of this Broadcast Source in microseconds.
165      * @param sourceAdvertisingSid 1-byte long Advertising_SID of the Broadcast Source.
166      * @param broadcastId 3-byte long Broadcast_ID of the Broadcast Source.
167      * @param paSyncInterval Periodic Advertising Sync interval of the broadcast Source, {@link
168      *     BluetoothLeBroadcastMetadata#PA_SYNC_INTERVAL_UNKNOWN} if unknown.
169      * @param isEncrypted whether the Broadcast Source is encrypted.
170      * @param broadcastCode Broadcast Code for this Broadcast Source, null if code is not required.
171      * @param sourceDevice source advertiser address.
172      * @param isGroupOp {@code true} if Application wants to perform this operation for all
173      *     coordinated set members throughout this session. Otherwise, caller would have to add,
174      *     modify, and remove individual set members.
175      */
addSource( @onNull BluetoothDevice sink, int sourceAddressType, int presentationDelayMicros, int sourceAdvertisingSid, int broadcastId, int paSyncInterval, boolean isEncrypted, byte[] broadcastCode, BluetoothDevice sourceDevice, boolean isGroupOp)176     public void addSource(
177             @NonNull BluetoothDevice sink,
178             int sourceAddressType,
179             int presentationDelayMicros,
180             int sourceAdvertisingSid,
181             int broadcastId,
182             int paSyncInterval,
183             boolean isEncrypted,
184             byte[] broadcastCode,
185             BluetoothDevice sourceDevice,
186             boolean isGroupOp) {
187         if (DEBUG) {
188             Log.d(TAG, "addSource()");
189         }
190         buildMetadata(
191                 sourceAddressType,
192                 presentationDelayMicros,
193                 sourceAdvertisingSid,
194                 broadcastId,
195                 paSyncInterval,
196                 isEncrypted,
197                 broadcastCode,
198                 sourceDevice);
199         addSource(sink, mBluetoothLeBroadcastMetadata, isGroupOp);
200     }
201 
buildMetadata( int sourceAddressType, int presentationDelayMicros, int sourceAdvertisingSid, int broadcastId, int paSyncInterval, boolean isEncrypted, byte[] broadcastCode, BluetoothDevice sourceDevice)202     private void buildMetadata(
203             int sourceAddressType,
204             int presentationDelayMicros,
205             int sourceAdvertisingSid,
206             int broadcastId,
207             int paSyncInterval,
208             boolean isEncrypted,
209             byte[] broadcastCode,
210             BluetoothDevice sourceDevice) {
211         mBluetoothLeBroadcastMetadata =
212                 mBuilder.setSourceDevice(sourceDevice, sourceAddressType)
213                         .setSourceAdvertisingSid(sourceAdvertisingSid)
214                         .setBroadcastId(broadcastId)
215                         .setPaSyncInterval(paSyncInterval)
216                         .setEncrypted(isEncrypted)
217                         .setBroadcastCode(broadcastCode)
218                         .setPresentationDelayMicros(presentationDelayMicros)
219                         .build();
220     }
221 
removeSource(@onNull BluetoothDevice sink, int sourceId)222     public void removeSource(@NonNull BluetoothDevice sink, int sourceId) {
223         if (DEBUG) {
224             Log.d(TAG, "removeSource()");
225         }
226         if (mService == null) {
227             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
228             return;
229         }
230         mService.removeSource(sink, sourceId);
231     }
232 
startSearchingForSources(@onNull List<android.bluetooth.le.ScanFilter> filters)233     public void startSearchingForSources(@NonNull List<android.bluetooth.le.ScanFilter> filters) {
234         if (DEBUG) {
235             Log.d(TAG, "startSearchingForSources()");
236         }
237         if (mService == null) {
238             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
239             return;
240         }
241         mService.startSearchingForSources(filters);
242     }
243 
244     /**
245      * Return true if a search has been started by this application.
246      *
247      * @return true if a search has been started by this application
248      * @hide
249      */
isSearchInProgress()250     public boolean isSearchInProgress() {
251         if (DEBUG) {
252             Log.d(TAG, "isSearchInProgress()");
253         }
254         if (mService == null) {
255             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
256             return false;
257         }
258         return mService.isSearchInProgress();
259     }
260 
261     /**
262      * Stops an ongoing search for nearby Broadcast Sources.
263      *
264      * <p>On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be
265      * called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure,
266      * {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be called with
267      * reason code
268      *
269      * @throws IllegalStateException if callback was not registered
270      */
stopSearchingForSources()271     public void stopSearchingForSources() {
272         if (DEBUG) {
273             Log.d(TAG, "stopSearchingForSources()");
274         }
275         if (mService == null) {
276             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
277             return;
278         }
279         mService.stopSearchingForSources();
280     }
281 
282     /**
283      * Get information about all Broadcast Sources that a Broadcast Sink knows about.
284      *
285      * @param sink Broadcast Sink from which to get all Broadcast Sources
286      * @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} stored
287      *     in the Broadcast Sink
288      * @throws NullPointerException when <var>sink</var> is null
289      */
getAllSources( @onNull BluetoothDevice sink)290     public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources(
291             @NonNull BluetoothDevice sink) {
292         if (DEBUG) {
293             Log.d(TAG, "getAllSources()");
294         }
295         if (mService == null) {
296             Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
297             return new ArrayList<BluetoothLeBroadcastReceiveState>();
298         }
299         return mService.getAllSources(sink);
300     }
301 
302     /**
303      * Register Broadcast Assistant Callbacks to track its state and receivers
304      *
305      * @param executor Executor object for callback
306      * @param callback Callback object to be registered
307      */
registerServiceCallBack( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback)308     public void registerServiceCallBack(
309             @NonNull @CallbackExecutor Executor executor,
310             @NonNull BluetoothLeBroadcastAssistant.Callback callback) {
311         if (mService == null) {
312             Log.d(
313                     TAG,
314                     "registerServiceCallBack failed, the BluetoothLeBroadcastAssistant is null.");
315             mCachedCallbackExecutorMap.putIfAbsent(callback, executor);
316             return;
317         }
318 
319         try {
320             mService.registerCallback(executor, callback);
321         } catch (IllegalArgumentException e) {
322             Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage());
323         }
324     }
325 
326     /**
327      * Unregister previously registered Broadcast Assistant Callbacks
328      *
329      * @param callback Callback object to be unregistered
330      */
unregisterServiceCallBack( @onNull BluetoothLeBroadcastAssistant.Callback callback)331     public void unregisterServiceCallBack(
332             @NonNull BluetoothLeBroadcastAssistant.Callback callback) {
333         mCachedCallbackExecutorMap.remove(callback);
334         if (mService == null) {
335             Log.d(
336                     TAG,
337                     "unregisterServiceCallBack failed, the BluetoothLeBroadcastAssistant is null.");
338             return;
339         }
340 
341         try {
342             mService.unregisterCallback(callback);
343         } catch (IllegalArgumentException e) {
344             Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage());
345         }
346     }
347 
isProfileReady()348     public boolean isProfileReady() {
349         return mIsProfileReady;
350     }
351 
getProfileId()352     public int getProfileId() {
353         return BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT;
354     }
355 
accessProfileEnabled()356     public boolean accessProfileEnabled() {
357         return false;
358     }
359 
isAutoConnectable()360     public boolean isAutoConnectable() {
361         return true;
362     }
363 
getConnectionStatus(BluetoothDevice device)364     public int getConnectionStatus(BluetoothDevice device) {
365         if (mService == null) {
366             return BluetoothProfile.STATE_DISCONNECTED;
367         }
368         // LE Audio Broadcasts are not connection-oriented.
369         return mService.getConnectionState(device);
370     }
371 
getConnectedDevices()372     public List<BluetoothDevice> getConnectedDevices() {
373         if (mService == null) {
374             return new ArrayList<BluetoothDevice>(0);
375         }
376         return mService.getDevicesMatchingConnectionStates(
377                 new int[] {
378                     BluetoothProfile.STATE_CONNECTED,
379                     BluetoothProfile.STATE_CONNECTING,
380                     BluetoothProfile.STATE_DISCONNECTING
381                 });
382     }
383 
384     /** Gets devices with matched connection states. */
getDevicesMatchingConnectionStates(@onNull int[] states)385     public List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
386         if (mService == null) {
387             return new ArrayList<BluetoothDevice>(0);
388         }
389         return mService.getDevicesMatchingConnectionStates(states);
390     }
391 
isEnabled(BluetoothDevice device)392     public boolean isEnabled(BluetoothDevice device) {
393         if (mService == null || device == null) {
394             return false;
395         }
396         return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
397     }
398 
getConnectionPolicy(BluetoothDevice device)399     public int getConnectionPolicy(BluetoothDevice device) {
400         if (mService == null || device == null) {
401             return CONNECTION_POLICY_FORBIDDEN;
402         }
403         return mService.getConnectionPolicy(device);
404     }
405 
setEnabled(BluetoothDevice device, boolean enabled)406     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
407         boolean isEnabled = false;
408         if (mService == null || device == null) {
409             return false;
410         }
411         if (enabled) {
412             if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
413                 isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
414             }
415         } else {
416             isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
417         }
418 
419         return isEnabled;
420     }
421 
toString()422     public String toString() {
423         return NAME;
424     }
425 
getOrdinal()426     public int getOrdinal() {
427         return ORDINAL;
428     }
429 
getNameResource(BluetoothDevice device)430     public int getNameResource(BluetoothDevice device) {
431         return R.string.summary_empty;
432     }
433 
getSummaryResourceForDevice(BluetoothDevice device)434     public int getSummaryResourceForDevice(BluetoothDevice device) {
435         int state = getConnectionStatus(device);
436         return BluetoothUtils.getConnectionStateSummary(state);
437     }
438 
getDrawableResource(BluetoothClass btClass)439     public int getDrawableResource(BluetoothClass btClass) {
440         return 0;
441     }
442 
443     @RequiresApi(Build.VERSION_CODES.S)
finalize()444     protected void finalize() {
445         if (DEBUG) {
446             Log.d(TAG, "finalize()");
447         }
448         if (mService != null) {
449             try {
450                 BluetoothAdapter.getDefaultAdapter()
451                         .closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mService);
452                 mService = null;
453             } catch (Throwable t) {
454                 Log.w(TAG, "Error cleaning up LeAudio proxy", t);
455             }
456         }
457     }
458 }
459