1 /*
2  * Copyright 2021 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.bluetooth.leaudio;
19 
20 import android.app.Application;
21 import android.bluetooth.*;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.os.ParcelUuid;
27 import android.util.Log;
28 
29 import androidx.annotation.Nullable;
30 import androidx.core.util.Pair;
31 import androidx.lifecycle.LiveData;
32 import androidx.lifecycle.MutableLiveData;
33 
34 import java.lang.reflect.InvocationTargetException;
35 import java.lang.reflect.Method;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.ListIterator;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.Optional;
46 import java.util.Set;
47 import java.util.UUID;
48 import java.util.concurrent.ExecutorService;
49 import java.util.concurrent.Executors;
50 import java.util.stream.Collectors;
51 
52 public class BluetoothProxy {
53     private static BluetoothProxy INSTANCE;
54     private final Application application;
55     private final BluetoothAdapter bluetoothAdapter;
56     private BluetoothLeAudio bluetoothLeAudio = null;
57     private BluetoothLeBroadcast mBluetoothLeBroadcast = null;
58     private BluetoothLeBroadcastAssistant mBluetoothLeBroadcastAssistant = null;
59     private Set<BluetoothDevice> mBroadcastScanDelegatorDevices = new HashSet<>();
60     private BluetoothCsipSetCoordinator bluetoothCsis = null;
61     private BluetoothVolumeControl bluetoothVolumeControl = null;
62     private BluetoothHapClient bluetoothHapClient = null;
63     private BluetoothProfile.ServiceListener profileListener = null;
64     private BluetoothHapClient.Callback hapCallback = null;
65     private OnBassEventListener mBassEventListener;
66     private OnLocalBroadcastEventListener mLocalBroadcastEventListener;
67     private final IntentFilter adapterIntentFilter;
68     private final IntentFilter bassIntentFilter;
69     private IntentFilter intentFilter;
70     private final ExecutorService mExecutor;
71 
72     private final Map<Integer, UUID> mGroupLocks = new HashMap<>();
73 
74     private int GROUP_NODE_ADDED = 1;
75     private int GROUP_NODE_REMOVED = 2;
76 
77     private boolean mLeAudioCallbackRegistered = false;
78     private BluetoothLeAudio.Callback mLeAudioCallbacks =
79             new BluetoothLeAudio.Callback() {
80                 @Override
81                 public void onCodecConfigChanged(int groupId, BluetoothLeAudioCodecStatus status) {}
82 
83                 @Override
84                 public void onGroupStatusChanged(int groupId, int groupStatus) {
85                     List<LeAudioDeviceStateWrapper> valid_devices = null;
86                     valid_devices =
87                             allLeAudioDevicesMutable.getValue().stream()
88                                     .filter(
89                                             state ->
90                                                     state.leAudioData.nodeStatusMutable.getValue()
91                                                                     != null
92                                                             && state.leAudioData
93                                                                     .nodeStatusMutable
94                                                                     .getValue()
95                                                                     .first
96                                                                     .equals(groupId))
97                                     .collect(Collectors.toList());
98                     for (LeAudioDeviceStateWrapper dev : valid_devices) {
99                         dev.leAudioData.groupStatusMutable.postValue(
100                                 new Pair<>(groupId, new Pair<>(groupStatus, 0)));
101                     }
102                 }
103 
104                 @Override
105                 public void onGroupNodeAdded(BluetoothDevice device, int groupId) {
106                     Log.d("LeCB:", device + " group added " + groupId);
107                     if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
108                         Log.d("LeCB:", "invalid parameter");
109                         return;
110                     }
111                     Optional<LeAudioDeviceStateWrapper> valid_device_opt =
112                             allLeAudioDevicesMutable.getValue().stream()
113                                     .filter(
114                                             state ->
115                                                     state.device
116                                                             .getAddress()
117                                                             .equals(device.getAddress()))
118                                     .findAny();
119 
120                     if (!valid_device_opt.isPresent()) {
121                         Log.d("LeCB:", "Device not present");
122                         return;
123                     }
124 
125                     LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
126                     LeAudioDeviceStateWrapper.LeAudioData svc_data = valid_device.leAudioData;
127 
128                     svc_data.nodeStatusMutable.postValue(new Pair<>(groupId, GROUP_NODE_ADDED));
129                     svc_data.groupStatusMutable.postValue(new Pair<>(groupId, new Pair<>(-1, -1)));
130                 }
131 
132                 @Override
133                 public void onGroupNodeRemoved(BluetoothDevice device, int groupId) {
134                     if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
135                         Log.d("LeCB:", "invalid parameter");
136                         return;
137                     }
138 
139                     Log.d("LeCB:", device + " group added " + groupId);
140                     if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
141                         Log.d("LeCB:", "invalid parameter");
142                         return;
143                     }
144 
145                     Optional<LeAudioDeviceStateWrapper> valid_device_opt =
146                             allLeAudioDevicesMutable.getValue().stream()
147                                     .filter(
148                                             state ->
149                                                     state.device
150                                                             .getAddress()
151                                                             .equals(device.getAddress()))
152                                     .findAny();
153 
154                     if (!valid_device_opt.isPresent()) {
155                         Log.d("LeCB:", "Device not present");
156                         return;
157                     }
158 
159                     LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
160                     LeAudioDeviceStateWrapper.LeAudioData svc_data = valid_device.leAudioData;
161 
162                     svc_data.nodeStatusMutable.postValue(new Pair<>(groupId, GROUP_NODE_REMOVED));
163                     svc_data.groupStatusMutable.postValue(new Pair<>(groupId, new Pair<>(-1, -1)));
164                 }
165             };
166 
167     private final MutableLiveData<Boolean> enabledBluetoothMutable;
168     private final BroadcastReceiver adapterIntentReceiver =
169             new BroadcastReceiver() {
170                 @Override
171                 public void onReceive(Context context, Intent intent) {
172                     String action = intent.getAction();
173                     if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
174                         int toState =
175                                 intent.getIntExtra(
176                                         BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
177                         if (toState == BluetoothAdapter.STATE_ON) {
178                             enabledBluetoothMutable.postValue(true);
179                         } else if (toState == BluetoothAdapter.STATE_OFF) {
180                             enabledBluetoothMutable.postValue(false);
181                         }
182                     }
183                 }
184             };
185     private final MutableLiveData<List<LeAudioDeviceStateWrapper>> allLeAudioDevicesMutable;
186     private final BroadcastReceiver leAudioIntentReceiver =
187             new BroadcastReceiver() {
188                 @Override
189                 public void onReceive(Context context, Intent intent) {
190                     String action = intent.getAction();
191                     final BluetoothDevice device =
192                             intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
193 
194                     if (allLeAudioDevicesMutable.getValue() != null) {
195                         if (device != null) {
196                             Optional<LeAudioDeviceStateWrapper> valid_device_opt =
197                                     allLeAudioDevicesMutable.getValue().stream()
198                                             .filter(
199                                                     state ->
200                                                             state.device
201                                                                     .getAddress()
202                                                                     .equals(device.getAddress()))
203                                             .findAny();
204 
205                             if (valid_device_opt.isPresent()) {
206                                 LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
207                                 LeAudioDeviceStateWrapper.LeAudioData svc_data =
208                                         valid_device.leAudioData;
209                                 int group_id;
210 
211                                 // Handle Le Audio actions
212                                 switch (action) {
213                                     case BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED:
214                                         {
215                                             final int toState =
216                                                     intent.getIntExtra(
217                                                             BluetoothLeAudio.EXTRA_STATE, -1);
218                                             if (toState == BluetoothLeAudio.STATE_CONNECTED
219                                                     || toState
220                                                             == BluetoothLeAudio.STATE_DISCONNECTED)
221                                                 svc_data.isConnectedMutable.postValue(
222                                                         toState
223                                                                 == BluetoothLeAudio
224                                                                         .STATE_CONNECTED);
225 
226                                             group_id = bluetoothLeAudio.getGroupId(device);
227                                             svc_data.nodeStatusMutable.postValue(
228                                                     new Pair<>(group_id, GROUP_NODE_ADDED));
229                                             svc_data.groupStatusMutable.postValue(
230                                                     new Pair<>(group_id, new Pair<>(-1, -1)));
231                                             break;
232                                         }
233                                 }
234                             }
235                         }
236                     }
237                 }
238             };
239 
240     private final BroadcastReceiver hapClientIntentReceiver =
241             new BroadcastReceiver() {
242                 @Override
243                 public void onReceive(Context context, Intent intent) {
244                     String action = intent.getAction();
245                     final BluetoothDevice device =
246                             intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
247 
248                     if (allLeAudioDevicesMutable.getValue() != null) {
249                         if (device != null) {
250                             Optional<LeAudioDeviceStateWrapper> valid_device_opt =
251                                     allLeAudioDevicesMutable.getValue().stream()
252                                             .filter(
253                                                     state ->
254                                                             state.device
255                                                                     .getAddress()
256                                                                     .equals(device.getAddress()))
257                                             .findAny();
258 
259                             if (valid_device_opt.isPresent()) {
260                                 LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
261                                 LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
262 
263                                 switch (action) {
264                                     case BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED:
265                                         {
266                                             final int toState =
267                                                     intent.getIntExtra(
268                                                             BluetoothHapClient.EXTRA_STATE, -1);
269                                             svc_data.hapStateMutable.postValue(toState);
270                                             break;
271                                         }
272                                         // Hidden API
273                                     case "android.bluetooth.action.HAP_DEVICE_AVAILABLE":
274                                         {
275                                             final int features =
276                                                     intent.getIntExtra(
277                                                             "android.bluetooth.extra.HAP_FEATURES",
278                                                             -1);
279                                             svc_data.hapFeaturesMutable.postValue(features);
280                                             break;
281                                         }
282                                     default:
283                                         // Do nothing
284                                         break;
285                                 }
286                             }
287                         }
288                     }
289                 }
290             };
291 
292     private final BroadcastReceiver volumeControlIntentReceiver =
293             new BroadcastReceiver() {
294                 @Override
295                 public void onReceive(Context context, Intent intent) {
296                     String action = intent.getAction();
297                     final BluetoothDevice device =
298                             intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
299 
300                     if (allLeAudioDevicesMutable.getValue() != null) {
301                         if (device != null) {
302                             Optional<LeAudioDeviceStateWrapper> valid_device_opt =
303                                     allLeAudioDevicesMutable.getValue().stream()
304                                             .filter(
305                                                     state ->
306                                                             state.device
307                                                                     .getAddress()
308                                                                     .equals(device.getAddress()))
309                                             .findAny();
310 
311                             if (valid_device_opt.isPresent()) {
312                                 LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
313                                 LeAudioDeviceStateWrapper.VolumeControlData svc_data =
314                                         valid_device.volumeControlData;
315 
316                                 switch (action) {
317                                     case BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED:
318                                         final int toState =
319                                                 intent.getIntExtra(
320                                                         BluetoothVolumeControl.EXTRA_STATE, -1);
321                                         if (toState == BluetoothVolumeControl.STATE_CONNECTED
322                                                 || toState
323                                                         == BluetoothVolumeControl
324                                                                 .STATE_DISCONNECTED)
325                                             svc_data.isConnectedMutable.postValue(
326                                                     toState
327                                                             == BluetoothVolumeControl
328                                                                     .STATE_CONNECTED);
329                                         break;
330                                 }
331                             }
332                         }
333                     }
334                 }
335             };
336     private final MutableLiveData<BluetoothLeBroadcastMetadata> mBroadcastUpdateMutableLive;
337     private final MutableLiveData<Pair<Integer /* reason */, Integer /* broadcastId */>>
338             mBroadcastPlaybackStartedMutableLive;
339     private final MutableLiveData<Pair<Integer /* reason */, Integer /* broadcastId */>>
340             mBroadcastPlaybackStoppedMutableLive;
341     private final MutableLiveData<Integer /* broadcastId */> mBroadcastAddedMutableLive;
342     private final MutableLiveData<Pair<Integer /* reason */, Integer /* broadcastId */>>
343             mBroadcastRemovedMutableLive;
344     private final MutableLiveData<String> mBroadcastStatusMutableLive;
345     private final BluetoothLeBroadcast.Callback mBroadcasterCallback =
346             new BluetoothLeBroadcast.Callback() {
347                 @Override
348                 public void onBroadcastStarted(int reason, int broadcastId) {
349                     if ((reason != BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST)
350                             && (reason != BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST)) {
351                         mBroadcastStatusMutableLive.postValue(
352                                 "Unable to create broadcast: "
353                                         + broadcastId
354                                         + ", reason: "
355                                         + reason);
356                     }
357 
358                     mBroadcastAddedMutableLive.postValue(broadcastId);
359                     if (mLocalBroadcastEventListener != null) {
360                         mLocalBroadcastEventListener.onBroadcastStarted(broadcastId);
361                     }
362                 }
363 
364                 @Override
365                 public void onBroadcastStartFailed(int reason) {
366                     mBroadcastStatusMutableLive.postValue(
367                             "Unable to START broadcast due to reason: " + reason);
368                 }
369 
370                 @Override
371                 public void onBroadcastStopped(int reason, int broadcastId) {
372                     mBroadcastRemovedMutableLive.postValue(new Pair<>(reason, broadcastId));
373                     if (mLocalBroadcastEventListener != null) {
374                         mLocalBroadcastEventListener.onBroadcastStopped(broadcastId);
375                     }
376                 }
377 
378                 @Override
379                 public void onBroadcastStopFailed(int reason) {
380                     mBroadcastStatusMutableLive.postValue(
381                             "Unable to STOP broadcast due to reason: " + reason);
382                 }
383 
384                 @Override
385                 public void onPlaybackStarted(int reason, int broadcastId) {
386                     mBroadcastPlaybackStartedMutableLive.postValue(new Pair<>(reason, broadcastId));
387                 }
388 
389                 @Override
390                 public void onPlaybackStopped(int reason, int broadcastId) {
391                     mBroadcastPlaybackStoppedMutableLive.postValue(new Pair<>(reason, broadcastId));
392                 }
393 
394                 @Override
395                 public void onBroadcastUpdated(int reason, int broadcastId) {
396                     mBroadcastStatusMutableLive.postValue(
397                             "Broadcast "
398                                     + broadcastId
399                                     + "has been updated due to reason: "
400                                     + reason);
401                     if (mLocalBroadcastEventListener != null) {
402                         mLocalBroadcastEventListener.onBroadcastUpdated(broadcastId);
403                     }
404                 }
405 
406                 @Override
407                 public void onBroadcastUpdateFailed(int reason, int broadcastId) {
408                     mBroadcastStatusMutableLive.postValue(
409                             "Unable to UPDATE broadcast "
410                                     + broadcastId
411                                     + " due to reason: "
412                                     + reason);
413                 }
414 
415                 @Override
416                 public void onBroadcastMetadataChanged(
417                         int broadcastId, BluetoothLeBroadcastMetadata metadata) {
418                     mBroadcastUpdateMutableLive.postValue(metadata);
419                     if (mLocalBroadcastEventListener != null) {
420                         mLocalBroadcastEventListener.onBroadcastMetadataChanged(
421                                 broadcastId, metadata);
422                     }
423                 }
424             };
425 
426     // TODO: Add behaviors in empty methods if necessary.
427     private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
428             new BluetoothLeBroadcastAssistant.Callback() {
429                 @Override
430                 public void onSearchStarted(int reason) {}
431 
432                 @Override
433                 public void onSearchStartFailed(int reason) {}
434 
435                 @Override
436                 public void onSearchStopped(int reason) {}
437 
438                 @Override
439                 public void onSearchStopFailed(int reason) {}
440 
441                 @Override
442                 public void onSourceFound(BluetoothLeBroadcastMetadata source) {
443                     Log.d("BluetoothProxy", "onSourceFound");
444                     if (mBassEventListener != null) {
445                         mBassEventListener.onSourceFound(source);
446                     }
447                 }
448 
449                 @Override
450                 public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {}
451 
452                 @Override
453                 public void onSourceAddFailed(
454                         BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {}
455 
456                 @Override
457                 public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {}
458 
459                 @Override
460                 public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
461 
462                 @Override
463                 public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {}
464 
465                 @Override
466                 public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {}
467 
468                 @Override
469                 public void onReceiveStateChanged(
470                         BluetoothDevice sink,
471                         int sourceId,
472                         BluetoothLeBroadcastReceiveState state) {
473                     Log.d("BluetoothProxy", "onReceiveStateChanged");
474                     if (allLeAudioDevicesMutable.getValue() != null) {
475                         Optional<LeAudioDeviceStateWrapper> valid_device_opt =
476                                 allLeAudioDevicesMutable.getValue().stream()
477                                         .filter(
478                                                 stateWrapper ->
479                                                         stateWrapper
480                                                                 .device
481                                                                 .getAddress()
482                                                                 .equals(sink.getAddress()))
483                                         .findAny();
484 
485                         if (!valid_device_opt.isPresent()) return;
486 
487                         LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
488                         LeAudioDeviceStateWrapper.BassData svc_data = valid_device.bassData;
489 
490                         /**
491                          * From "Introducing-Bluetooth-LE-Audio-book" 8.6.3.1:
492                          *
493                          * <p>The Source_ID is an Acceptor generated number which is used to
494                          * identify a specific set of broadcast device and BIG information. It is
495                          * local to an Acceptor and used as a reference for a Broadcast Assistant.
496                          * In the case of a Coordinated Set of Acceptors, such as a left and right
497                          * earbud, the Source_IDs are not related and may be different, even if both
498                          * are receiving the same BIS, as each Acceptor independently creates their
499                          * own Source ID values
500                          */
501 
502                         /** Broadcast receiver's endpoint identifier. */
503                         synchronized (this) {
504                             HashMap<Integer, BluetoothLeBroadcastReceiveState> states =
505                                     svc_data.receiverStatesMutable.getValue();
506                             if (states == null) states = new HashMap<>();
507                             states.put(state.getSourceId(), state);
508 
509                             // Use SetValue instead of PostValue() since we want to make it
510                             // synchronous due to getValue() we do here as well
511                             // Otherwise we could miss the update and store only the last
512                             // receiver ID
513                             //                    svc_data.receiverStatesMutable.setValue(states);
514                             svc_data.receiverStatesMutable.postValue(states);
515                         }
516                     }
517                 }
518             };
519 
520     private final BroadcastReceiver bassIntentReceiver =
521             new BroadcastReceiver() {
522                 @Override
523                 public void onReceive(Context context, Intent intent) {
524                     String action = intent.getAction();
525                     if (action.equals(
526                             BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED)) {
527                         final BluetoothDevice device =
528                                 intent.getParcelableExtra(
529                                         BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
530 
531                         if (allLeAudioDevicesMutable.getValue() != null) {
532                             if (device != null) {
533                                 Optional<LeAudioDeviceStateWrapper> valid_device_opt =
534                                         allLeAudioDevicesMutable.getValue().stream()
535                                                 .filter(
536                                                         state ->
537                                                                 state.device
538                                                                         .getAddress()
539                                                                         .equals(
540                                                                                 device
541                                                                                         .getAddress()))
542                                                 .findAny();
543 
544                                 if (valid_device_opt.isPresent()) {
545                                     LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
546                                     LeAudioDeviceStateWrapper.BassData svc_data =
547                                             valid_device.bassData;
548 
549                                     final int toState =
550                                             intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
551                                     if (toState == BluetoothProfile.STATE_CONNECTED
552                                             || toState == BluetoothProfile.STATE_DISCONNECTED)
553                                         svc_data.isConnectedMutable.postValue(
554                                                 toState == BluetoothProfile.STATE_CONNECTED);
555                                 }
556                             }
557                         }
558                     }
559                     // TODO: Remove this if unnecessary.
560                     //          case
561                     // BluetoothBroadcastAudioScan.ACTION_BASS_BROADCAST_ANNONCEMENT_AVAILABLE:
562                     //              // FIXME: Never happen since there is no valid device with this
563                     // intent
564                     //              break;
565                 }
566             };
567 
BluetoothProxy(Application application)568     private BluetoothProxy(Application application) {
569         this.application = application;
570         bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
571 
572         enabledBluetoothMutable = new MutableLiveData<>();
573         allLeAudioDevicesMutable = new MutableLiveData<>();
574 
575         mBroadcastUpdateMutableLive = new MutableLiveData<>();
576         mBroadcastStatusMutableLive = new MutableLiveData<>();
577 
578         mBroadcastPlaybackStartedMutableLive = new MutableLiveData<>();
579         mBroadcastPlaybackStoppedMutableLive = new MutableLiveData<>();
580         mBroadcastAddedMutableLive = new MutableLiveData();
581         mBroadcastRemovedMutableLive = new MutableLiveData<>();
582 
583         MutableLiveData<String> mBroadcastStatusMutableLive;
584 
585         mExecutor = Executors.newSingleThreadExecutor();
586 
587         adapterIntentFilter = new IntentFilter();
588         adapterIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
589         adapterIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
590         application.registerReceiver(
591                 adapterIntentReceiver, adapterIntentFilter, Context.RECEIVER_EXPORTED);
592 
593         bassIntentFilter = new IntentFilter();
594         bassIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
595         bassIntentFilter.addAction(BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED);
596         application.registerReceiver(
597                 bassIntentReceiver, bassIntentFilter, Context.RECEIVER_EXPORTED);
598     }
599 
600     // Lazy constructing Singleton acquire method
getBluetoothProxy(Application application)601     public static BluetoothProxy getBluetoothProxy(Application application) {
602         if (INSTANCE == null) {
603             INSTANCE = new BluetoothProxy(application);
604         }
605         return (INSTANCE);
606     }
607 
initProfiles()608     public void initProfiles() {
609         if (profileListener != null) return;
610 
611         hapCallback =
612                 new BluetoothHapClient.Callback() {
613                     @Override
614                     public void onPresetSelected(
615                             BluetoothDevice device, int presetIndex, int statusCode) {
616                         Optional<LeAudioDeviceStateWrapper> valid_device_opt =
617                                 allLeAudioDevicesMutable.getValue().stream()
618                                         .filter(
619                                                 state ->
620                                                         state.device
621                                                                 .getAddress()
622                                                                 .equals(device.getAddress()))
623                                         .findAny();
624 
625                         if (!valid_device_opt.isPresent()) return;
626 
627                         LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
628                         LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
629 
630                         svc_data.hapActivePresetIndexMutable.postValue(presetIndex);
631 
632                         svc_data.hapStatusMutable.postValue(
633                                 "Preset changed to " + presetIndex + ", reason: " + statusCode);
634                     }
635 
636                     @Override
637                     public void onPresetSelectionFailed(BluetoothDevice device, int statusCode) {
638                         Optional<LeAudioDeviceStateWrapper> valid_device_opt =
639                                 allLeAudioDevicesMutable.getValue().stream()
640                                         .filter(
641                                                 state ->
642                                                         state.device
643                                                                 .getAddress()
644                                                                 .equals(device.getAddress()))
645                                         .findAny();
646 
647                         if (!valid_device_opt.isPresent()) return;
648 
649                         LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
650                         LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
651 
652                         svc_data.hapStatusMutable.postValue(
653                                 "Select preset failed with status " + statusCode);
654                     }
655 
656                     @Override
657                     public void onPresetSelectionForGroupFailed(int hapGroupId, int statusCode) {
658                         List<LeAudioDeviceStateWrapper> valid_devices = null;
659                         if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
660                             valid_devices =
661                                     allLeAudioDevicesMutable.getValue().stream()
662                                             .filter(
663                                                     state ->
664                                                             state.leAudioData.nodeStatusMutable
665                                                                                     .getValue()
666                                                                             != null
667                                                                     && state.leAudioData
668                                                                             .nodeStatusMutable
669                                                                             .getValue()
670                                                                             .first
671                                                                             .equals(hapGroupId))
672                                             .collect(Collectors.toList());
673 
674                         if (valid_devices != null) {
675                             for (LeAudioDeviceStateWrapper device : valid_devices) {
676                                 device.hapData.hapStatusMutable.postValue(
677                                         "Select preset for group "
678                                                 + hapGroupId
679                                                 + " failed with status "
680                                                 + statusCode);
681                             }
682                         }
683                     }
684 
685                     @Override
686                     public void onPresetInfoChanged(
687                             BluetoothDevice device,
688                             List<BluetoothHapPresetInfo> presetInfoList,
689                             int statusCode) {
690                         Optional<LeAudioDeviceStateWrapper> valid_device_opt =
691                                 allLeAudioDevicesMutable.getValue().stream()
692                                         .filter(
693                                                 state ->
694                                                         state.device
695                                                                 .getAddress()
696                                                                 .equals(device.getAddress()))
697                                         .findAny();
698 
699                         if (!valid_device_opt.isPresent()) return;
700 
701                         LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
702                         LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
703 
704                         svc_data.hapStatusMutable.postValue(
705                                 "Preset list changed due to status " + statusCode);
706                         svc_data.hapPresetsMutable.postValue(presetInfoList);
707                     }
708 
709                     @Override
710                     public void onSetPresetNameFailed(BluetoothDevice device, int status) {
711                         Optional<LeAudioDeviceStateWrapper> valid_device_opt =
712                                 allLeAudioDevicesMutable.getValue().stream()
713                                         .filter(
714                                                 state ->
715                                                         state.device
716                                                                 .getAddress()
717                                                                 .equals(device.getAddress()))
718                                         .findAny();
719 
720                         if (!valid_device_opt.isPresent()) return;
721 
722                         LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
723                         LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
724 
725                         svc_data.hapStatusMutable.postValue("Name set error: " + status);
726                     }
727 
728                     @Override
729                     public void onSetPresetNameForGroupFailed(int hapGroupId, int status) {
730                         List<LeAudioDeviceStateWrapper> valid_devices = null;
731                         if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
732                             valid_devices =
733                                     allLeAudioDevicesMutable.getValue().stream()
734                                             .filter(
735                                                     state ->
736                                                             state.leAudioData.nodeStatusMutable
737                                                                                     .getValue()
738                                                                             != null
739                                                                     && state.leAudioData
740                                                                             .nodeStatusMutable
741                                                                             .getValue()
742                                                                             .first
743                                                                             .equals(hapGroupId))
744                                             .collect(Collectors.toList());
745 
746                         if (valid_devices != null) {
747                             for (LeAudioDeviceStateWrapper device : valid_devices) {
748                                 device.hapData.hapStatusMutable.postValue(
749                                         "Group Name set error: " + status);
750                             }
751                         }
752                     }
753                 };
754 
755         profileListener =
756                 new BluetoothProfile.ServiceListener() {
757                     @Override
758                     public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) {
759                         Log.d(
760                                 "BluetoothProxy",
761                                 "onServiceConnected(): i = "
762                                         + i
763                                         + " bluetoothProfile = "
764                                         + bluetoothProfile);
765                         switch (i) {
766                             case BluetoothProfile.CSIP_SET_COORDINATOR:
767                                 bluetoothCsis = (BluetoothCsipSetCoordinator) bluetoothProfile;
768                                 break;
769                             case BluetoothProfile.LE_AUDIO:
770                                 bluetoothLeAudio = (BluetoothLeAudio) bluetoothProfile;
771                                 if (!mLeAudioCallbackRegistered) {
772                                     try {
773                                         bluetoothLeAudio.registerCallback(
774                                                 mExecutor, mLeAudioCallbacks);
775                                         mLeAudioCallbackRegistered = true;
776                                     } catch (Exception e) {
777                                         Log.e(
778                                                 "Unicast:",
779                                                 " Probably not supported: Exception on registering"
780                                                         + " callbacks: "
781                                                         + e);
782                                     }
783                                 }
784                                 break;
785                             case BluetoothProfile.VOLUME_CONTROL:
786                                 bluetoothVolumeControl = (BluetoothVolumeControl) bluetoothProfile;
787                                 break;
788                             case BluetoothProfile.HAP_CLIENT:
789                                 bluetoothHapClient = (BluetoothHapClient) bluetoothProfile;
790                                 try {
791                                     bluetoothHapClient.registerCallback(mExecutor, hapCallback);
792                                 } catch (IllegalArgumentException e) {
793                                     Log.e("HAP", "Application callback already registered.");
794                                 }
795                                 break;
796                             case BluetoothProfile.LE_AUDIO_BROADCAST:
797                                 mBluetoothLeBroadcast = (BluetoothLeBroadcast) bluetoothProfile;
798                                 try {
799                                     mBluetoothLeBroadcast.registerCallback(
800                                             mExecutor, mBroadcasterCallback);
801                                 } catch (IllegalArgumentException e) {
802                                     Log.e("Broadcast", "Application callback already registered.");
803                                 }
804                                 break;
805                             case BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT:
806                                 Log.d(
807                                         "BluetoothProxy",
808                                         "LE_AUDIO_BROADCAST_ASSISTANT Service connected");
809                                 mBluetoothLeBroadcastAssistant =
810                                         (BluetoothLeBroadcastAssistant) bluetoothProfile;
811                                 try {
812                                     mBluetoothLeBroadcastAssistant.registerCallback(
813                                             mExecutor, mBroadcastAssistantCallback);
814                                 } catch (IllegalArgumentException e) {
815                                     Log.e("BASS", "Application callback already registered.");
816                                 }
817                                 break;
818                         }
819                         queryLeAudioDevices();
820                     }
821 
822                     @Override
823                     public void onServiceDisconnected(int i) {}
824                 };
825 
826         initCsisProxy();
827         initLeAudioProxy();
828         initVolumeControlProxy();
829         initHapProxy();
830         initLeAudioBroadcastProxy();
831         initBassProxy();
832     }
833 
cleanupProfiles()834     public void cleanupProfiles() {
835         if (profileListener == null) return;
836 
837         cleanupCsisProxy();
838         cleanupLeAudioProxy();
839         cleanupVolumeControlProxy();
840         cleanupHapProxy();
841         cleanupLeAudioBroadcastProxy();
842         cleanupBassProxy();
843 
844         profileListener = null;
845     }
846 
initCsisProxy()847     private void initCsisProxy() {
848         if (!isCoordinatedSetProfileSupported()) return;
849         if (bluetoothCsis == null) {
850             bluetoothAdapter.getProfileProxy(
851                     this.application, profileListener, BluetoothProfile.CSIP_SET_COORDINATOR);
852         }
853     }
854 
cleanupCsisProxy()855     private void cleanupCsisProxy() {
856         if (!isCoordinatedSetProfileSupported()) return;
857         if (bluetoothCsis != null) {
858             bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothCsis);
859         }
860     }
861 
initLeAudioProxy()862     private void initLeAudioProxy() {
863         if (!isLeAudioUnicastSupported()) return;
864         if (bluetoothLeAudio == null) {
865             bluetoothAdapter.getProfileProxy(
866                     this.application, profileListener, BluetoothProfile.LE_AUDIO);
867         }
868 
869         intentFilter = new IntentFilter();
870         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
871         intentFilter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
872         application.registerReceiver(
873                 leAudioIntentReceiver, intentFilter, Context.RECEIVER_EXPORTED);
874     }
875 
cleanupLeAudioProxy()876     private void cleanupLeAudioProxy() {
877         if (!isLeAudioUnicastSupported()) return;
878         if (bluetoothLeAudio != null) {
879             bluetoothAdapter.closeProfileProxy(BluetoothProfile.LE_AUDIO, bluetoothLeAudio);
880             application.unregisterReceiver(leAudioIntentReceiver);
881         }
882     }
883 
initVolumeControlProxy()884     private void initVolumeControlProxy() {
885         if (!isVolumeControlClientSupported()) return;
886         bluetoothAdapter.getProfileProxy(
887                 this.application, profileListener, BluetoothProfile.VOLUME_CONTROL);
888 
889         intentFilter = new IntentFilter();
890         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
891         intentFilter.addAction(BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED);
892         application.registerReceiver(
893                 volumeControlIntentReceiver, intentFilter, Context.RECEIVER_EXPORTED);
894     }
895 
cleanupVolumeControlProxy()896     private void cleanupVolumeControlProxy() {
897         if (!isVolumeControlClientSupported()) return;
898         if (bluetoothVolumeControl != null) {
899             bluetoothAdapter.closeProfileProxy(
900                     BluetoothProfile.VOLUME_CONTROL, bluetoothVolumeControl);
901             application.unregisterReceiver(volumeControlIntentReceiver);
902         }
903     }
904 
initHapProxy()905     private void initHapProxy() {
906         if (!isLeAudioHearingAccessClientSupported()) return;
907         bluetoothAdapter.getProfileProxy(
908                 this.application, profileListener, BluetoothProfile.HAP_CLIENT);
909 
910         intentFilter = new IntentFilter();
911         intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
912         intentFilter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
913         intentFilter.addAction("android.bluetooth.action.HAP_DEVICE_AVAILABLE");
914         application.registerReceiver(
915                 hapClientIntentReceiver, intentFilter, Context.RECEIVER_EXPORTED);
916     }
917 
cleanupHapProxy()918     private void cleanupHapProxy() {
919         if (!isLeAudioHearingAccessClientSupported()) return;
920         if (bluetoothHapClient != null) {
921             bluetoothHapClient.unregisterCallback(hapCallback);
922             bluetoothAdapter.closeProfileProxy(BluetoothProfile.HAP_CLIENT, bluetoothHapClient);
923             application.unregisterReceiver(hapClientIntentReceiver);
924         }
925     }
926 
initBassProxy()927     private void initBassProxy() {
928         if (!isLeAudioBroadcastScanAssistanSupported()) return;
929         bluetoothAdapter.getProfileProxy(
930                 this.application, profileListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
931     }
932 
cleanupBassProxy()933     private void cleanupBassProxy() {
934         if (!isLeAudioBroadcastScanAssistanSupported()) return;
935         if (mBluetoothLeBroadcastAssistant != null) {
936             mBluetoothLeBroadcastAssistant.unregisterCallback(mBroadcastAssistantCallback);
937             bluetoothAdapter.closeProfileProxy(
938                     BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBluetoothLeBroadcastAssistant);
939         }
940     }
941 
checkForEnabledBluetooth()942     private Boolean checkForEnabledBluetooth() {
943         Boolean current_state = bluetoothAdapter.isEnabled();
944 
945         // Force the update since event may not come if bt was already enabled
946         if (!Objects.equals(enabledBluetoothMutable.getValue(), current_state))
947             enabledBluetoothMutable.setValue(current_state);
948 
949         return current_state;
950     }
951 
queryLeAudioDevices()952     public void queryLeAudioDevices() {
953         if (checkForEnabledBluetooth()) {
954             // Consider those with the ASC service as valid devices
955             List<LeAudioDeviceStateWrapper> validDevices = new ArrayList<>();
956             for (BluetoothDevice dev : bluetoothAdapter.getBondedDevices()) {
957                 LeAudioDeviceStateWrapper state_wrapper = new LeAudioDeviceStateWrapper(dev);
958                 Boolean valid_device = false;
959 
960                 if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
961                         .contains(
962                                 ParcelUuid.fromString(
963                                         application.getString(R.string.svc_uuid_le_audio)))) {
964                     if (state_wrapper.leAudioData == null)
965                         state_wrapper.leAudioData = new LeAudioDeviceStateWrapper.LeAudioData();
966                     valid_device = true;
967 
968                     if (bluetoothLeAudio != null) {
969                         state_wrapper.leAudioData.isConnectedMutable.postValue(
970                                 bluetoothLeAudio.getConnectionState(dev)
971                                         == BluetoothLeAudio.STATE_CONNECTED);
972                         int group_id = bluetoothLeAudio.getGroupId(dev);
973                         state_wrapper.leAudioData.nodeStatusMutable.setValue(
974                                 new Pair<>(group_id, GROUP_NODE_ADDED));
975                         state_wrapper.leAudioData.groupStatusMutable.setValue(
976                                 new Pair<>(group_id, new Pair<>(-1, -1)));
977                     }
978                 }
979 
980                 if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
981                         .contains(
982                                 ParcelUuid.fromString(
983                                         application.getString(R.string.svc_uuid_volume_control)))) {
984                     if (state_wrapper.volumeControlData == null)
985                         state_wrapper.volumeControlData =
986                                 new LeAudioDeviceStateWrapper.VolumeControlData();
987                     valid_device = true;
988 
989                     if (bluetoothVolumeControl != null) {
990                         state_wrapper.volumeControlData.isConnectedMutable.postValue(
991                                 bluetoothVolumeControl.getConnectionState(dev)
992                                         == BluetoothVolumeControl.STATE_CONNECTED);
993                         // FIXME: We don't have the api to get the volume and mute states? :(
994                     }
995                 }
996 
997                 if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
998                         .contains(
999                                 ParcelUuid.fromString(
1000                                         application.getString(R.string.svc_uuid_has)))) {
1001                     if (state_wrapper.hapData == null)
1002                         state_wrapper.hapData = new LeAudioDeviceStateWrapper.HapData();
1003                     valid_device = true;
1004 
1005                     if (bluetoothHapClient != null) {
1006                         state_wrapper.hapData.hapStateMutable.postValue(
1007                                 bluetoothHapClient.getConnectionState(dev));
1008                         boolean is_connected =
1009                                 bluetoothHapClient.getConnectionState(dev)
1010                                         == BluetoothHapClient.STATE_CONNECTED;
1011                         if (is_connected) {
1012                             // Use hidden API
1013                             try {
1014                                 Method getFeaturesMethod =
1015                                         BluetoothHapClient.class.getDeclaredMethod(
1016                                                 "getFeatures", BluetoothDevice.class);
1017                                 getFeaturesMethod.setAccessible(true);
1018                                 state_wrapper.hapData.hapFeaturesMutable.postValue(
1019                                         (Integer)
1020                                                 getFeaturesMethod.invoke(bluetoothHapClient, dev));
1021                             } catch (NoSuchMethodException
1022                                     | IllegalAccessException
1023                                     | InvocationTargetException e) {
1024                                 state_wrapper.hapData.hapStatusMutable.postValue(
1025                                         "Hidden API for getFeatures not accessible.");
1026                             }
1027 
1028                             state_wrapper.hapData.hapPresetsMutable.postValue(
1029                                     bluetoothHapClient.getAllPresetInfo(dev));
1030                             try {
1031                                 Method getActivePresetIndexMethod =
1032                                         BluetoothHapClient.class.getDeclaredMethod(
1033                                                 "getActivePresetIndex", BluetoothDevice.class);
1034                                 getActivePresetIndexMethod.setAccessible(true);
1035                                 state_wrapper.hapData.hapActivePresetIndexMutable.postValue(
1036                                         (Integer)
1037                                                 getActivePresetIndexMethod.invoke(
1038                                                         bluetoothHapClient, dev));
1039                             } catch (NoSuchMethodException
1040                                     | IllegalAccessException
1041                                     | InvocationTargetException e) {
1042                                 state_wrapper.hapData.hapStatusMutable.postValue(
1043                                         "Hidden API for getFeatures not accessible.");
1044                             }
1045                         }
1046                     }
1047                 }
1048 
1049                 if (Arrays.asList(dev.getUuids() != null ? dev.getUuids() : new ParcelUuid[0])
1050                         .contains(
1051                                 ParcelUuid.fromString(
1052                                         application.getString(
1053                                                 R.string.svc_uuid_broadcast_audio)))) {
1054                     if (state_wrapper.bassData == null)
1055                         state_wrapper.bassData = new LeAudioDeviceStateWrapper.BassData();
1056                     valid_device = true;
1057 
1058                     if (mBluetoothLeBroadcastAssistant != null) {
1059                         boolean is_connected =
1060                                 mBluetoothLeBroadcastAssistant.getConnectionState(dev)
1061                                         == BluetoothProfile.STATE_CONNECTED;
1062                         state_wrapper.bassData.isConnectedMutable.setValue(is_connected);
1063                     }
1064                 }
1065 
1066                 if (valid_device) validDevices.add(state_wrapper);
1067             }
1068 
1069             // Async update
1070             allLeAudioDevicesMutable.postValue(validDevices);
1071         }
1072     }
1073 
connectLeAudio(BluetoothDevice device, boolean connect)1074     public void connectLeAudio(BluetoothDevice device, boolean connect) {
1075         if (bluetoothLeAudio != null) {
1076             if (connect) {
1077                 try {
1078                     Method connectMethod =
1079                             BluetoothLeAudio.class.getDeclaredMethod(
1080                                     "connect", BluetoothDevice.class);
1081                     connectMethod.setAccessible(true);
1082                     connectMethod.invoke(bluetoothLeAudio, device);
1083                 } catch (NoSuchMethodException
1084                         | IllegalAccessException
1085                         | InvocationTargetException e) {
1086                     // Do nothing
1087                 }
1088             } else {
1089                 try {
1090                     Method disconnectMethod =
1091                             BluetoothLeAudio.class.getDeclaredMethod(
1092                                     "disconnect", BluetoothDevice.class);
1093                     disconnectMethod.setAccessible(true);
1094                     disconnectMethod.invoke(bluetoothLeAudio, device);
1095                 } catch (NoSuchMethodException
1096                         | IllegalAccessException
1097                         | InvocationTargetException e) {
1098                     // Do nothing
1099                 }
1100             }
1101         }
1102     }
1103 
streamAction(Integer group_id, int action, Integer content_type)1104     public void streamAction(Integer group_id, int action, Integer content_type) {
1105         if (bluetoothLeAudio != null) {
1106             switch (action) {
1107                 case 0:
1108                     // No longer available, not needed
1109                     // bluetoothLeAudio.groupStream(group_id, content_type);
1110                     break;
1111                 case 1:
1112                     // No longer available, not needed
1113                     // bluetoothLeAudio.groupSuspend(group_id);
1114                     break;
1115                 case 2:
1116                     // No longer available, not needed
1117                     // bluetoothLeAudio.groupStop(group_id);
1118                     break;
1119                 default:
1120                     break;
1121             }
1122         }
1123     }
1124 
groupSet(BluetoothDevice device, Integer group_id)1125     public void groupSet(BluetoothDevice device, Integer group_id) {
1126         if (bluetoothLeAudio == null) return;
1127 
1128         try {
1129             Method groupAddNodeMethod =
1130                     BluetoothLeAudio.class.getDeclaredMethod(
1131                             "groupAddNode", int.class, BluetoothDevice.class);
1132             groupAddNodeMethod.setAccessible(true);
1133             groupAddNodeMethod.invoke(bluetoothLeAudio, group_id, device);
1134         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1135             // Do nothing
1136         }
1137     }
1138 
groupUnset(BluetoothDevice device, Integer group_id)1139     public void groupUnset(BluetoothDevice device, Integer group_id) {
1140         if (bluetoothLeAudio == null) return;
1141 
1142         try {
1143             Method groupRemoveNodeMethod =
1144                     BluetoothLeAudio.class.getDeclaredMethod(
1145                             "groupRemoveNode", int.class, BluetoothDevice.class);
1146             groupRemoveNodeMethod.setAccessible(true);
1147             groupRemoveNodeMethod.invoke(bluetoothLeAudio, group_id, device);
1148         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1149             // Do nothing
1150         }
1151     }
1152 
groupSetLock(Integer group_id, boolean lock)1153     public void groupSetLock(Integer group_id, boolean lock) {
1154         if (bluetoothCsis == null) return;
1155 
1156         Log.d("Lock", "lock: " + lock);
1157         if (lock) {
1158             if (mGroupLocks.containsKey(group_id)) {
1159                 Log.e(
1160                         "Lock",
1161                         "group" + group_id + " is already in locking process or locked: " + lock);
1162                 return;
1163             }
1164 
1165             UUID uuid =
1166                     bluetoothCsis.lockGroup(
1167                             group_id,
1168                             mExecutor,
1169                             (int group, int op_status, boolean is_locked) -> {
1170                                 Log.d("LockCb", "lock: " + is_locked + " status: " + op_status);
1171                                 if (((op_status == BluetoothStatusCodes.SUCCESS)
1172                                                 || (op_status
1173                                                         == BluetoothStatusCodes
1174                                                                 .ERROR_CSIP_LOCKED_GROUP_MEMBER_LOST))
1175                                         && (group != BluetoothLeAudio.GROUP_ID_INVALID)) {
1176                                     allLeAudioDevicesMutable
1177                                             .getValue()
1178                                             .forEach(
1179                                                     (dev_wrapper) -> {
1180                                                         if (dev_wrapper.leAudioData
1181                                                                                 .nodeStatusMutable
1182                                                                                 .getValue()
1183                                                                         != null
1184                                                                 && dev_wrapper
1185                                                                         .leAudioData
1186                                                                         .nodeStatusMutable
1187                                                                         .getValue()
1188                                                                         .first
1189                                                                         .equals(group_id)) {
1190                                                             dev_wrapper.leAudioData
1191                                                                     .groupLockStateMutable
1192                                                                     .postValue(
1193                                                                             new Pair<
1194                                                                                     Integer,
1195                                                                                     Boolean>(
1196                                                                                     group,
1197                                                                                     is_locked));
1198                                                         }
1199                                                     });
1200                                 } else {
1201                                     // TODO: Set error status so it could be notified/toasted to the
1202                                     // user
1203                                 }
1204 
1205                                 if (!is_locked) mGroupLocks.remove(group_id);
1206                             });
1207             // Store the lock key
1208             mGroupLocks.put(group_id, uuid);
1209         } else {
1210             if (!mGroupLocks.containsKey(group_id)) return;
1211 
1212             // Use the stored lock key
1213             bluetoothCsis.unlockGroup(mGroupLocks.get(group_id));
1214             mGroupLocks.remove(group_id);
1215         }
1216     }
1217 
connectBass(BluetoothDevice device, boolean connect)1218     public void connectBass(BluetoothDevice device, boolean connect) {
1219         if (mBluetoothLeBroadcastAssistant != null) {
1220             if (connect) {
1221                 mBluetoothLeBroadcastAssistant.setConnectionPolicy(
1222                         device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
1223             } else {
1224                 mBluetoothLeBroadcastAssistant.setConnectionPolicy(
1225                         device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
1226             }
1227         }
1228     }
1229 
scanForBroadcasts(@ullable BluetoothDevice scanDelegator, boolean scan)1230     public boolean scanForBroadcasts(@Nullable BluetoothDevice scanDelegator, boolean scan) {
1231         if (mBluetoothLeBroadcastAssistant != null) {
1232             // Note: startSearchingForSources() does not support scanning on behalf of
1233             // a specific device - it only searches for all BASS connected devices.
1234             // Therefore, we manage the list of the devices and start/stop the scanning.
1235             if (scan) {
1236                 if (scanDelegator != null) {
1237                     mBroadcastScanDelegatorDevices.add(scanDelegator);
1238                 }
1239                 try {
1240                     mBluetoothLeBroadcastAssistant.startSearchingForSources(new ArrayList<>());
1241                 } catch (IllegalArgumentException e) {
1242                     Log.e("BluetoothProxy", " Unexpected " + e);
1243                 }
1244                 if (mBassEventListener != null) {
1245                     mBassEventListener.onScanningStateChanged(true);
1246                 }
1247             } else {
1248                 if (scanDelegator != null) {
1249                     mBroadcastScanDelegatorDevices.remove(scanDelegator);
1250                 }
1251                 if (mBroadcastScanDelegatorDevices.isEmpty()) {
1252                     try {
1253                         mBluetoothLeBroadcastAssistant.stopSearchingForSources();
1254                         if (mBassEventListener != null) {
1255                             mBassEventListener.onScanningStateChanged(false);
1256                         }
1257                     } catch (IllegalArgumentException e) {
1258                         Log.e("BluetoothProxy", " Unexpected " + e);
1259                     }
1260                 }
1261             }
1262             return true;
1263         }
1264         return false;
1265     }
1266 
stopBroadcastObserving()1267     public boolean stopBroadcastObserving() {
1268         if (mBluetoothLeBroadcastAssistant != null) {
1269             mBroadcastScanDelegatorDevices.clear();
1270             try {
1271                 mBluetoothLeBroadcastAssistant.stopSearchingForSources();
1272             } catch (IllegalArgumentException e) {
1273                 Log.e("BluetoothProxy", " Unexpected " + e);
1274             }
1275 
1276             if (mBassEventListener != null) {
1277                 mBassEventListener.onScanningStateChanged(false);
1278             }
1279             return true;
1280         }
1281         return false;
1282     }
1283 
1284     // TODO: Uncomment this method if necessary
1285     //    public boolean getBroadcastReceiverState(BluetoothDevice device, int receiver_id) {
1286     //        if (mBluetoothLeBroadcastAssistant != null) {
1287     //            return mBluetoothLeBroadcastAssistant.getBroadcastReceiverState(device,
1288     // receiver_id);
1289     //        }
1290     //        return false;
1291     //    }
1292 
addBroadcastSource( BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata)1293     public boolean addBroadcastSource(
1294             BluetoothDevice sink, BluetoothLeBroadcastMetadata sourceMetadata) {
1295         if (mBluetoothLeBroadcastAssistant != null) {
1296             mBluetoothLeBroadcastAssistant.addSource(sink, sourceMetadata, true /* isGroupOp */);
1297             return true;
1298         }
1299         return false;
1300     }
1301 
modifyBroadcastSource( BluetoothDevice sink, int sourceId, BluetoothLeBroadcastMetadata metadata)1302     public boolean modifyBroadcastSource(
1303             BluetoothDevice sink, int sourceId, BluetoothLeBroadcastMetadata metadata) {
1304         if (mBluetoothLeBroadcastAssistant != null) {
1305             mBluetoothLeBroadcastAssistant.modifySource(sink, sourceId, metadata);
1306             return true;
1307         }
1308         return false;
1309     }
1310 
removeBroadcastSource(BluetoothDevice sink, int sourceId)1311     public boolean removeBroadcastSource(BluetoothDevice sink, int sourceId) {
1312         if (mBluetoothLeBroadcastAssistant != null) {
1313             mBluetoothLeBroadcastAssistant.removeSource(sink, sourceId);
1314             return true;
1315         }
1316         return false;
1317     }
1318 
setVolume(BluetoothDevice device, int volume)1319     public void setVolume(BluetoothDevice device, int volume) {
1320         if (bluetoothLeAudio != null && !bluetoothLeAudio.getConnectedDevices().isEmpty()) {
1321             bluetoothLeAudio.setVolume(volume);
1322         } else if (bluetoothVolumeControl != null) {
1323             bluetoothVolumeControl.setVolumeOffset(device, volume);
1324         }
1325     }
1326 
getBluetoothEnabled()1327     public LiveData<Boolean> getBluetoothEnabled() {
1328         return enabledBluetoothMutable;
1329     }
1330 
getAllLeAudioDevices()1331     public LiveData<List<LeAudioDeviceStateWrapper>> getAllLeAudioDevices() {
1332         return allLeAudioDevicesMutable;
1333     }
1334 
connectHap(BluetoothDevice device, boolean connect)1335     public void connectHap(BluetoothDevice device, boolean connect) {
1336         if (bluetoothHapClient != null) {
1337             if (connect) {
1338                 bluetoothHapClient.setConnectionPolicy(
1339                         device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
1340             } else {
1341                 bluetoothHapClient.setConnectionPolicy(
1342                         device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
1343             }
1344         }
1345     }
1346 
hapReadPresetInfo(BluetoothDevice device, int preset_index)1347     public boolean hapReadPresetInfo(BluetoothDevice device, int preset_index) {
1348         if (bluetoothHapClient == null) return false;
1349 
1350         BluetoothHapPresetInfo new_preset = null;
1351 
1352         // Use hidden API
1353         try {
1354             Method getPresetInfoMethod =
1355                     BluetoothHapClient.class.getDeclaredMethod(
1356                             "getPresetInfo", BluetoothDevice.class, int.class);
1357             getPresetInfoMethod.setAccessible(true);
1358 
1359             new_preset =
1360                     (BluetoothHapPresetInfo)
1361                             getPresetInfoMethod.invoke(bluetoothHapClient, device, preset_index);
1362             if (new_preset == null) return false;
1363         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1364             // Do nothing'
1365             return false;
1366         }
1367 
1368         Optional<LeAudioDeviceStateWrapper> valid_device_opt =
1369                 allLeAudioDevicesMutable.getValue().stream()
1370                         .filter(state -> state.device.getAddress().equals(device.getAddress()))
1371                         .findAny();
1372 
1373         if (!valid_device_opt.isPresent()) return false;
1374 
1375         LeAudioDeviceStateWrapper valid_device = valid_device_opt.get();
1376         LeAudioDeviceStateWrapper.HapData svc_data = valid_device.hapData;
1377 
1378         List current_presets = svc_data.hapPresetsMutable.getValue();
1379         if (current_presets == null) current_presets = new ArrayList<BluetoothHapPresetInfo>();
1380 
1381         // Remove old one and add back the new one
1382         ListIterator<BluetoothHapPresetInfo> iter = current_presets.listIterator();
1383         while (iter.hasNext()) {
1384             if (iter.next().getIndex() == new_preset.getIndex()) {
1385                 iter.remove();
1386             }
1387         }
1388         current_presets.add(new_preset);
1389 
1390         svc_data.hapPresetsMutable.postValue(current_presets);
1391         return true;
1392     }
1393 
hapSetActivePreset(BluetoothDevice device, int preset_index)1394     public boolean hapSetActivePreset(BluetoothDevice device, int preset_index) {
1395         if (bluetoothHapClient == null) return false;
1396 
1397         bluetoothHapClient.selectPreset(device, preset_index);
1398         return true;
1399     }
1400 
hapSetActivePresetForGroup(BluetoothDevice device, int preset_index)1401     public boolean hapSetActivePresetForGroup(BluetoothDevice device, int preset_index) {
1402         if (bluetoothHapClient == null) return false;
1403 
1404         int groupId = bluetoothLeAudio.getGroupId(device);
1405         bluetoothHapClient.selectPresetForGroup(groupId, preset_index);
1406         return true;
1407     }
1408 
hapChangePresetName(BluetoothDevice device, int preset_index, String name)1409     public boolean hapChangePresetName(BluetoothDevice device, int preset_index, String name) {
1410         if (bluetoothHapClient == null) return false;
1411 
1412         bluetoothHapClient.setPresetName(device, preset_index, name);
1413         return true;
1414     }
1415 
hapPreviousDevicePreset(BluetoothDevice device)1416     public boolean hapPreviousDevicePreset(BluetoothDevice device) {
1417         if (bluetoothHapClient == null) return false;
1418 
1419         // Use hidden API
1420         try {
1421             Method switchToPreviousPresetMethod =
1422                     BluetoothHapClient.class.getDeclaredMethod(
1423                             "switchToPreviousPreset", BluetoothDevice.class);
1424             switchToPreviousPresetMethod.setAccessible(true);
1425 
1426             switchToPreviousPresetMethod.invoke(bluetoothHapClient, device);
1427         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1428             return false;
1429         }
1430         return true;
1431     }
1432 
hapNextDevicePreset(BluetoothDevice device)1433     public boolean hapNextDevicePreset(BluetoothDevice device) {
1434         if (bluetoothHapClient == null) return false;
1435 
1436         // Use hidden API
1437         try {
1438             Method switchToNextPresetMethod =
1439                     BluetoothHapClient.class.getDeclaredMethod(
1440                             "switchToNextPreset", BluetoothDevice.class);
1441             switchToNextPresetMethod.setAccessible(true);
1442 
1443             switchToNextPresetMethod.invoke(bluetoothHapClient, device);
1444         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1445             return false;
1446         }
1447         return true;
1448     }
1449 
hapPreviousGroupPreset(int group_id)1450     public boolean hapPreviousGroupPreset(int group_id) {
1451         if (bluetoothHapClient == null) return false;
1452 
1453         // Use hidden API
1454         try {
1455             Method switchToPreviousPresetForGroupMethod =
1456                     BluetoothHapClient.class.getDeclaredMethod(
1457                             "switchToPreviousPresetForGroup", int.class);
1458             switchToPreviousPresetForGroupMethod.setAccessible(true);
1459 
1460             switchToPreviousPresetForGroupMethod.invoke(bluetoothHapClient, group_id);
1461         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1462             return false;
1463         }
1464         return true;
1465     }
1466 
hapNextGroupPreset(int group_id)1467     public boolean hapNextGroupPreset(int group_id) {
1468         if (bluetoothHapClient == null) return false;
1469 
1470         // Use hidden API
1471         try {
1472             Method switchToNextPresetForGroupMethod =
1473                     BluetoothHapClient.class.getDeclaredMethod(
1474                             "switchToNextPresetForGroup", int.class);
1475             switchToNextPresetForGroupMethod.setAccessible(true);
1476 
1477             switchToNextPresetForGroupMethod.invoke(bluetoothHapClient, group_id);
1478         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1479             return false;
1480         }
1481         return true;
1482     }
1483 
hapGetHapGroup(BluetoothDevice device)1484     public int hapGetHapGroup(BluetoothDevice device) {
1485         if (bluetoothHapClient == null) return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
1486 
1487         // Use hidden API
1488         try {
1489             Method getHapGroupMethod =
1490                     BluetoothHapClient.class.getDeclaredMethod(
1491                             "getHapGroup", BluetoothDevice.class);
1492             getHapGroupMethod.setAccessible(true);
1493 
1494             return (Integer) getHapGroupMethod.invoke(bluetoothHapClient, device);
1495         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1496             // Do nothing
1497         }
1498         return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
1499     }
1500 
initLeAudioBroadcastProxy()1501     private void initLeAudioBroadcastProxy() {
1502         if (!isLeAudioBroadcastSourceSupported()) return;
1503         if (mBluetoothLeBroadcast == null) {
1504             bluetoothAdapter.getProfileProxy(
1505                     this.application, profileListener, BluetoothProfile.LE_AUDIO_BROADCAST);
1506         }
1507     }
1508 
cleanupLeAudioBroadcastProxy()1509     private void cleanupLeAudioBroadcastProxy() {
1510         if (!isLeAudioBroadcastSourceSupported()) return;
1511         if (mBluetoothLeBroadcast != null) {
1512             bluetoothAdapter.closeProfileProxy(
1513                     BluetoothProfile.LE_AUDIO_BROADCAST, mBluetoothLeBroadcast);
1514         }
1515     }
1516 
getBroadcastUpdateMetadataLive()1517     public LiveData<BluetoothLeBroadcastMetadata> getBroadcastUpdateMetadataLive() {
1518         return mBroadcastUpdateMutableLive;
1519     }
1520 
1521     public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>>
getBroadcastPlaybackStartedMutableLive()1522             getBroadcastPlaybackStartedMutableLive() {
1523         return mBroadcastPlaybackStartedMutableLive;
1524     }
1525 
1526     public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>>
getBroadcastPlaybackStoppedMutableLive()1527             getBroadcastPlaybackStoppedMutableLive() {
1528         return mBroadcastPlaybackStoppedMutableLive;
1529     }
1530 
getBroadcastAddedMutableLive()1531     public LiveData<Integer /* broadcastId */> getBroadcastAddedMutableLive() {
1532         return mBroadcastAddedMutableLive;
1533     }
1534 
1535     public LiveData<Pair<Integer /* reason */, Integer /* broadcastId */>>
getBroadcastRemovedMutableLive()1536             getBroadcastRemovedMutableLive() {
1537         return mBroadcastRemovedMutableLive;
1538     }
1539 
getBroadcastStatusMutableLive()1540     public LiveData<String> getBroadcastStatusMutableLive() {
1541         return mBroadcastStatusMutableLive;
1542     }
1543 
startBroadcast(BluetoothLeBroadcastSettings settings)1544     public boolean startBroadcast(BluetoothLeBroadcastSettings settings) {
1545         if (mBluetoothLeBroadcast == null) return false;
1546         mBluetoothLeBroadcast.startBroadcast(settings);
1547         return true;
1548     }
1549 
stopBroadcast(int broadcastId)1550     public boolean stopBroadcast(int broadcastId) {
1551         if (mBluetoothLeBroadcast == null) return false;
1552         mBluetoothLeBroadcast.stopBroadcast(broadcastId);
1553         return true;
1554     }
1555 
getAllLocalBroadcasts()1556     public List<BluetoothLeBroadcastMetadata> getAllLocalBroadcasts() {
1557         if (mBluetoothLeBroadcast == null) return Collections.emptyList();
1558         return mBluetoothLeBroadcast.getAllBroadcastMetadata();
1559     }
1560 
updateBroadcast(int broadcastId, BluetoothLeBroadcastSettings settings)1561     public boolean updateBroadcast(int broadcastId, BluetoothLeBroadcastSettings settings) {
1562         if (mBluetoothLeBroadcast == null) return false;
1563 
1564         mBluetoothLeBroadcast.updateBroadcast(broadcastId, settings);
1565         return true;
1566     }
1567 
getMaximumNumberOfBroadcast()1568     public int getMaximumNumberOfBroadcast() {
1569         if (mBluetoothLeBroadcast == null) {
1570             Log.d("BluetoothProxy", "mBluetoothLeBroadcast is null");
1571             return 0;
1572         }
1573         return mBluetoothLeBroadcast.getMaximumNumberOfBroadcasts();
1574     }
1575 
isPlaying(int broadcastId)1576     public boolean isPlaying(int broadcastId) {
1577         if (mBluetoothLeBroadcast == null) return false;
1578         return mBluetoothLeBroadcast.isPlaying(broadcastId);
1579     }
1580 
isLeAudioUnicastSupported()1581     boolean isLeAudioUnicastSupported() {
1582         return (bluetoothAdapter.isLeAudioSupported() == BluetoothStatusCodes.FEATURE_SUPPORTED);
1583     }
1584 
isCoordinatedSetProfileSupported()1585     boolean isCoordinatedSetProfileSupported() {
1586         return isLeAudioUnicastSupported();
1587     }
1588 
isVolumeControlClientSupported()1589     boolean isVolumeControlClientSupported() {
1590         return isLeAudioUnicastSupported();
1591     }
1592 
isLeAudioHearingAccessClientSupported()1593     boolean isLeAudioHearingAccessClientSupported() {
1594         return isLeAudioUnicastSupported();
1595     }
1596 
isLeAudioBroadcastSourceSupported()1597     public boolean isLeAudioBroadcastSourceSupported() {
1598         return (bluetoothAdapter.isLeAudioBroadcastSourceSupported()
1599                 == BluetoothStatusCodes.FEATURE_SUPPORTED);
1600     }
1601 
isLeAudioBroadcastScanAssistanSupported()1602     public boolean isLeAudioBroadcastScanAssistanSupported() {
1603         return (bluetoothAdapter.isLeAudioBroadcastAssistantSupported()
1604                 == BluetoothStatusCodes.FEATURE_SUPPORTED);
1605     }
1606 
setOnBassEventListener(OnBassEventListener listener)1607     public void setOnBassEventListener(OnBassEventListener listener) {
1608         mBassEventListener = listener;
1609     }
1610 
1611     // Used by BroadcastScanViewModel
1612     public interface OnBassEventListener {
onSourceFound(BluetoothLeBroadcastMetadata source)1613         void onSourceFound(BluetoothLeBroadcastMetadata source);
1614 
onScanningStateChanged(boolean isScanning)1615         void onScanningStateChanged(boolean isScanning);
1616     }
1617 
setOnLocalBroadcastEventListener(OnLocalBroadcastEventListener listener)1618     public void setOnLocalBroadcastEventListener(OnLocalBroadcastEventListener listener) {
1619         mLocalBroadcastEventListener = listener;
1620     }
1621 
1622     // Used by BroadcastScanViewModel
1623     public interface OnLocalBroadcastEventListener {
1624         // TODO: Add arguments in methods
onBroadcastStarted(int broadcastId)1625         void onBroadcastStarted(int broadcastId);
1626 
onBroadcastStopped(int broadcastId)1627         void onBroadcastStopped(int broadcastId);
1628 
onBroadcastUpdated(int broadcastId)1629         void onBroadcastUpdated(int broadcastId);
1630 
onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata)1631         void onBroadcastMetadataChanged(int broadcastId, BluetoothLeBroadcastMetadata metadata);
1632     }
1633 }
1634