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