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