1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.bluetooth.a2dpsink;
17 
18 import static java.util.Objects.requireNonNull;
19 
20 import android.annotation.RequiresPermission;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothAudioConfig;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothProfile;
25 import android.bluetooth.IBluetoothA2dpSink;
26 import android.content.AttributionSource;
27 import android.content.Context;
28 import android.media.AudioManager;
29 import android.os.Looper;
30 import android.sysprop.BluetoothProperties;
31 import android.util.Log;
32 
33 import com.android.bluetooth.Utils;
34 import com.android.bluetooth.btservice.AdapterService;
35 import com.android.bluetooth.btservice.ProfileService;
36 import com.android.bluetooth.btservice.storage.DatabaseManager;
37 import com.android.internal.annotations.GuardedBy;
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Collections;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.concurrent.ConcurrentHashMap;
46 
47 /** Provides Bluetooth A2DP Sink profile, as a service in the Bluetooth application. */
48 public class A2dpSinkService extends ProfileService {
49     private static final String TAG = A2dpSinkService.class.getSimpleName();
50 
51     private final Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
52             new ConcurrentHashMap<>(1);
53 
54     private final A2dpSinkNativeInterface mNativeInterface;
55     private final Looper mLooper;
56 
57     private final Object mActiveDeviceLock = new Object();
58 
59     @GuardedBy("mActiveDeviceLock")
60     private BluetoothDevice mActiveDevice = null;
61 
62     private final Object mStreamHandlerLock = new Object();
63 
64     @GuardedBy("mStreamHandlerLock")
65     private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
66 
67     private static A2dpSinkService sService;
68 
69     private int mMaxConnectedAudioDevices;
70 
71     private AdapterService mAdapterService;
72     private DatabaseManager mDatabaseManager;
73 
A2dpSinkService(Context ctx)74     public A2dpSinkService(Context ctx) {
75         super(ctx);
76         mNativeInterface = requireNonNull(A2dpSinkNativeInterface.getInstance());
77         mLooper = Looper.getMainLooper();
78     }
79 
80     @VisibleForTesting
A2dpSinkService(Context ctx, A2dpSinkNativeInterface nativeInterface, Looper looper)81     A2dpSinkService(Context ctx, A2dpSinkNativeInterface nativeInterface, Looper looper) {
82         super(ctx);
83         mNativeInterface = requireNonNull(nativeInterface);
84         mLooper = looper;
85     }
86 
isEnabled()87     public static boolean isEnabled() {
88         return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false);
89     }
90 
91     @Override
start()92     public void start() {
93         mAdapterService =
94                 requireNonNull(
95                         AdapterService.getAdapterService(),
96                         "AdapterService cannot be null when A2dpSinkService starts");
97         mDatabaseManager =
98                 requireNonNull(
99                         AdapterService.getAdapterService().getDatabase(),
100                         "DatabaseManager cannot be null when A2dpSinkService starts");
101 
102         mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
103         mNativeInterface.init(mMaxConnectedAudioDevices);
104 
105         synchronized (mStreamHandlerLock) {
106             mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface);
107         }
108 
109         setA2dpSinkService(this);
110     }
111 
112     @Override
stop()113     public void stop() {
114         setA2dpSinkService(null);
115         mNativeInterface.cleanup();
116         for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
117             stateMachine.quitNow();
118         }
119         mDeviceStateMap.clear();
120         synchronized (mStreamHandlerLock) {
121             if (mA2dpSinkStreamHandler != null) {
122                 mA2dpSinkStreamHandler.cleanup();
123                 mA2dpSinkStreamHandler = null;
124             }
125         }
126     }
127 
getA2dpSinkService()128     public static synchronized A2dpSinkService getA2dpSinkService() {
129         return sService;
130     }
131 
132     /** Testing API to inject a mockA2dpSinkService. */
133     @VisibleForTesting
setA2dpSinkService(A2dpSinkService service)134     public static synchronized void setA2dpSinkService(A2dpSinkService service) {
135         sService = service;
136     }
137 
138     /** Set the device that should be allowed to actively stream */
setActiveDevice(BluetoothDevice device)139     public boolean setActiveDevice(BluetoothDevice device) {
140         Log.i(TAG, "setActiveDevice(device=" + device + ")");
141         synchronized (mActiveDeviceLock) {
142             if (mNativeInterface.setActiveDevice(device)) {
143                 mActiveDevice = device;
144                 return true;
145             }
146             return false;
147         }
148     }
149 
150     /** Get the device that is allowed to be actively streaming */
getActiveDevice()151     public BluetoothDevice getActiveDevice() {
152         synchronized (mActiveDeviceLock) {
153             return mActiveDevice;
154         }
155     }
156 
157     /** Request audio focus such that the designated device can stream audio */
requestAudioFocus(BluetoothDevice device, boolean request)158     public void requestAudioFocus(BluetoothDevice device, boolean request) {
159         synchronized (mStreamHandlerLock) {
160             if (mA2dpSinkStreamHandler == null) return;
161             mA2dpSinkStreamHandler.requestAudioFocus(request);
162         }
163     }
164 
165     /**
166      * Get the current Bluetooth Audio focus state
167      *
168      * @return AudioManger.AUDIOFOCUS_* states on success, or AudioManager.ERROR on error
169      */
getFocusState()170     public int getFocusState() {
171         synchronized (mStreamHandlerLock) {
172             if (mA2dpSinkStreamHandler == null) return AudioManager.ERROR;
173             return mA2dpSinkStreamHandler.getFocusState();
174         }
175     }
176 
177     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
isA2dpPlaying(BluetoothDevice device)178     boolean isA2dpPlaying(BluetoothDevice device) {
179         enforceCallingOrSelfPermission(
180                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
181         synchronized (mStreamHandlerLock) {
182             if (mA2dpSinkStreamHandler == null) return false;
183             return mA2dpSinkStreamHandler.isPlaying();
184         }
185     }
186 
187     @Override
initBinder()188     protected IProfileServiceBinder initBinder() {
189         return new A2dpSinkServiceBinder(this);
190     }
191 
192     // Binder object: Must be static class or memory leak may occur
193     @VisibleForTesting
194     static class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub
195             implements IProfileServiceBinder {
196         private A2dpSinkService mService;
197 
198         @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getService(AttributionSource source)199         private A2dpSinkService getService(AttributionSource source) {
200             if (Utils.isInstrumentationTestMode()) {
201                 return mService;
202             }
203             if (!Utils.checkServiceAvailable(mService, TAG)
204                     || !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG)
205                     || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) {
206                 return null;
207             }
208             return mService;
209         }
210 
A2dpSinkServiceBinder(A2dpSinkService svc)211         A2dpSinkServiceBinder(A2dpSinkService svc) {
212             mService = svc;
213         }
214 
215         @Override
cleanup()216         public void cleanup() {
217             mService = null;
218         }
219 
220         @Override
connect(BluetoothDevice device, AttributionSource source)221         public boolean connect(BluetoothDevice device, AttributionSource source) {
222             A2dpSinkService service = getService(source);
223             if (service == null) {
224                 return false;
225             }
226             return service.connect(device);
227         }
228 
229         @Override
disconnect(BluetoothDevice device, AttributionSource source)230         public boolean disconnect(BluetoothDevice device, AttributionSource source) {
231             A2dpSinkService service = getService(source);
232             if (service == null) {
233                 return false;
234             }
235             return service.disconnect(device);
236         }
237 
238         @Override
getConnectedDevices(AttributionSource source)239         public List<BluetoothDevice> getConnectedDevices(AttributionSource source) {
240             A2dpSinkService service = getService(source);
241             if (service == null) {
242                 return Collections.emptyList();
243             }
244             return service.getConnectedDevices();
245         }
246 
247         @Override
getDevicesMatchingConnectionStates( int[] states, AttributionSource source)248         public List<BluetoothDevice> getDevicesMatchingConnectionStates(
249                 int[] states, AttributionSource source) {
250             A2dpSinkService service = getService(source);
251             if (service == null) {
252                 return Collections.emptyList();
253             }
254             return service.getDevicesMatchingConnectionStates(states);
255         }
256 
257         @Override
getConnectionState(BluetoothDevice device, AttributionSource source)258         public int getConnectionState(BluetoothDevice device, AttributionSource source) {
259             A2dpSinkService service = getService(source);
260             if (service == null) {
261                 return BluetoothProfile.STATE_DISCONNECTED;
262             }
263             return service.getConnectionState(device);
264         }
265 
266         @Override
setConnectionPolicy( BluetoothDevice device, int connectionPolicy, AttributionSource source)267         public boolean setConnectionPolicy(
268                 BluetoothDevice device, int connectionPolicy, AttributionSource source) {
269             A2dpSinkService service = getService(source);
270             if (service == null) {
271                 return false;
272             }
273             return service.setConnectionPolicy(device, connectionPolicy);
274         }
275 
276         @Override
getConnectionPolicy(BluetoothDevice device, AttributionSource source)277         public int getConnectionPolicy(BluetoothDevice device, AttributionSource source) {
278             A2dpSinkService service = getService(source);
279             if (service == null) {
280                 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
281             }
282             return service.getConnectionPolicy(device);
283         }
284 
285         @Override
isA2dpPlaying(BluetoothDevice device, AttributionSource source)286         public boolean isA2dpPlaying(BluetoothDevice device, AttributionSource source) {
287             A2dpSinkService service = getService(source);
288             if (service == null) {
289                 return false;
290             }
291             return service.isA2dpPlaying(device);
292         }
293 
294         @Override
getAudioConfig( BluetoothDevice device, AttributionSource source)295         public BluetoothAudioConfig getAudioConfig(
296                 BluetoothDevice device, AttributionSource source) {
297             A2dpSinkService service = getService(source);
298             if (service == null) {
299                 return null;
300             }
301             return service.getAudioConfig(device);
302         }
303     }
304 
305     /* Generic Profile Code */
306 
307     /**
308      * Connect the given Bluetooth device.
309      *
310      * @return true if connection is successful, false otherwise.
311      */
312     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
connect(BluetoothDevice device)313     public boolean connect(BluetoothDevice device) {
314         Log.d(TAG, "connect device=" + device);
315         enforceCallingOrSelfPermission(
316                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
317         if (device == null) {
318             throw new IllegalArgumentException("Null device");
319         }
320         if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
321             Log.w(TAG, "Connection not allowed: <" + device + "> is CONNECTION_POLICY_FORBIDDEN");
322             return false;
323         }
324 
325         A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
326         if (stateMachine != null) {
327             stateMachine.connect();
328             return true;
329         } else {
330             // a state machine instance doesn't exist yet, and the max has been reached.
331             Log.e(
332                     TAG,
333                     "Maxed out on the number of allowed A2DP Sink connections. "
334                             + "Connect request rejected on "
335                             + device);
336             return false;
337         }
338     }
339 
340     /**
341      * Disconnect the given Bluetooth device.
342      *
343      * @return true if disconnect is successful, false otherwise.
344      */
disconnect(BluetoothDevice device)345     public boolean disconnect(BluetoothDevice device) {
346         Log.d(TAG, "disconnect device=" + device);
347         if (device == null) {
348             throw new IllegalArgumentException("Null device");
349         }
350 
351         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
352         // a state machine instance doesn't exist. maybe it is already gone?
353         if (stateMachine == null) {
354             return false;
355         }
356         int connectionState = stateMachine.getState();
357         if (connectionState == BluetoothProfile.STATE_DISCONNECTED
358                 || connectionState == BluetoothProfile.STATE_DISCONNECTING) {
359             return false;
360         }
361         // upon completion of disconnect, the state machine will remove itself from the available
362         // devices map
363         stateMachine.disconnect();
364         return true;
365     }
366 
367     /**
368      * Remove a device's state machine.
369      *
370      * <p>Called by the state machines when they disconnect.
371      *
372      * <p>Visible for testing so it can be mocked and verified on.
373      */
removeStateMachine(A2dpSinkStateMachine stateMachine)374     void removeStateMachine(A2dpSinkStateMachine stateMachine) {
375         if (stateMachine == null) {
376             return;
377         }
378         mDeviceStateMap.remove(stateMachine.getDevice());
379         stateMachine.quitNow();
380     }
381 
getConnectedDevices()382     public List<BluetoothDevice> getConnectedDevices() {
383         return getDevicesMatchingConnectionStates(new int[] {BluetoothAdapter.STATE_CONNECTED});
384     }
385 
getOrCreateStateMachine(BluetoothDevice device)386     protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
387         A2dpSinkStateMachine newStateMachine =
388                 new A2dpSinkStateMachine(mLooper, device, this, mNativeInterface);
389         A2dpSinkStateMachine existingStateMachine =
390                 mDeviceStateMap.putIfAbsent(device, newStateMachine);
391         // Given null is not a valid value in our map, ConcurrentHashMap will return null if the
392         // key was absent and our new value was added. We should then start and return it. Else
393         // we quit the new one so we don't leak a thread
394         if (existingStateMachine == null) {
395             newStateMachine.start();
396             return newStateMachine;
397         }
398         return existingStateMachine;
399     }
400 
401     @VisibleForTesting
getStateMachineForDevice(BluetoothDevice device)402     protected A2dpSinkStateMachine getStateMachineForDevice(BluetoothDevice device) {
403         return mDeviceStateMap.get(device);
404     }
405 
getDevicesMatchingConnectionStates(int[] states)406     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
407         Log.d(TAG, "getDevicesMatchingConnectionStates(states=" + Arrays.toString(states) + ")");
408         List<BluetoothDevice> deviceList = new ArrayList<>();
409         BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
410         int connectionState;
411         for (BluetoothDevice device : bondedDevices) {
412             connectionState = getConnectionState(device);
413             Log.d(TAG, "Device: " + device + "State: " + connectionState);
414             for (int i = 0; i < states.length; i++) {
415                 if (connectionState == states[i]) {
416                     deviceList.add(device);
417                 }
418             }
419         }
420         Log.d(
421                 TAG,
422                 "getDevicesMatchingConnectionStates("
423                         + Arrays.toString(states)
424                         + "): Found "
425                         + deviceList.toString());
426         return deviceList;
427     }
428 
429     /**
430      * Get the current connection state of the profile
431      *
432      * @param device is the remote bluetooth device
433      * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected, {@link
434      *     BluetoothProfile#STATE_CONNECTING} if this profile is being connected, {@link
435      *     BluetoothProfile#STATE_CONNECTED} if this profile is connected, or {@link
436      *     BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
437      */
getConnectionState(BluetoothDevice device)438     public int getConnectionState(BluetoothDevice device) {
439         if (device == null) return BluetoothProfile.STATE_DISCONNECTED;
440         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
441         return (stateMachine == null)
442                 ? BluetoothProfile.STATE_DISCONNECTED
443                 : stateMachine.getState();
444     }
445 
446     /**
447      * Set connection policy of the profile and connects it if connectionPolicy is {@link
448      * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link
449      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
450      *
451      * <p>The device should already be paired. Connection policy can be one of: {@link
452      * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link
453      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
454      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
455      *
456      * @param device Paired bluetooth device
457      * @param connectionPolicy is the connection policy to set to for this profile
458      * @return true if connectionPolicy is set, false on error
459      */
460     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)461     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
462         enforceCallingOrSelfPermission(
463                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
464         Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
465 
466         if (!mDatabaseManager.setProfileConnectionPolicy(
467                 device, BluetoothProfile.A2DP_SINK, connectionPolicy)) {
468             return false;
469         }
470         if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
471             connect(device);
472         } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
473             disconnect(device);
474         }
475         return true;
476     }
477 
478     /**
479      * Get the connection policy of the profile.
480      *
481      * @param device the remote device
482      * @return connection policy of the specified device
483      */
484     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(BluetoothDevice device)485     public int getConnectionPolicy(BluetoothDevice device) {
486         enforceCallingOrSelfPermission(
487                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
488         return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK);
489     }
490 
491     @Override
dump(StringBuilder sb)492     public void dump(StringBuilder sb) {
493         super.dump(sb);
494         ProfileService.println(sb, "Active Device = " + getActiveDevice());
495         ProfileService.println(sb, "Max Connected Devices = " + mMaxConnectedAudioDevices);
496         ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size());
497         for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
498             ProfileService.println(
499                     sb, "==== StateMachine for " + stateMachine.getDevice() + " ====");
500             stateMachine.dump(sb);
501         }
502     }
503 
getAudioConfig(BluetoothDevice device)504     BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
505         if (device == null) return null;
506         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
507         // a state machine instance doesn't exist. maybe it is already gone?
508         if (stateMachine == null) {
509             return null;
510         }
511         return stateMachine.getAudioConfig();
512     }
513 
514     /** Receive and route a stack event from the JNI */
messageFromNative(StackEvent event)515     protected void messageFromNative(StackEvent event) {
516         switch (event.mType) {
517             case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
518                 onConnectionStateChanged(event);
519                 return;
520             case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
521                 onAudioStateChanged(event);
522                 return;
523             case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
524                 onAudioConfigChanged(event);
525                 return;
526             default:
527                 Log.e(TAG, "Received unknown stack event of type " + event.mType);
528                 return;
529         }
530     }
531 
onConnectionStateChanged(StackEvent event)532     private void onConnectionStateChanged(StackEvent event) {
533         BluetoothDevice device = event.mDevice;
534         if (device == null) {
535             return;
536         }
537         A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
538         stateMachine.onStackEvent(event);
539     }
540 
onAudioStateChanged(StackEvent event)541     private void onAudioStateChanged(StackEvent event) {
542         int state = event.mState;
543         synchronized (mStreamHandlerLock) {
544             if (mA2dpSinkStreamHandler == null) {
545                 Log.e(TAG, "Received audio state change before we've been started");
546                 return;
547             } else if (state == StackEvent.AUDIO_STATE_STARTED) {
548                 mA2dpSinkStreamHandler
549                         .obtainMessage(A2dpSinkStreamHandler.SRC_STR_START)
550                         .sendToTarget();
551             } else if (state == StackEvent.AUDIO_STATE_STOPPED
552                     || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) {
553                 mA2dpSinkStreamHandler
554                         .obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP)
555                         .sendToTarget();
556             } else {
557                 Log.w(TAG, "Unhandled audio state change, state=" + state);
558             }
559         }
560     }
561 
onAudioConfigChanged(StackEvent event)562     private void onAudioConfigChanged(StackEvent event) {
563         BluetoothDevice device = event.mDevice;
564         if (device == null) {
565             return;
566         }
567         A2dpSinkStateMachine stateMachine = getStateMachineForDevice(device);
568         if (stateMachine == null) {
569             Log.w(
570                     TAG,
571                     "Received audio config changed event for an unconnected device, device="
572                             + device);
573             return;
574         }
575         stateMachine.onStackEvent(event);
576     }
577 
connectionStateChanged(BluetoothDevice device, int fromState, int toState)578     void connectionStateChanged(BluetoothDevice device, int fromState, int toState) {
579         mAdapterService.notifyProfileConnectionStateChangeToGatt(
580                 BluetoothProfile.A2DP_SINK, fromState, toState);
581         mAdapterService.updateProfileConnectionAdapterProperties(
582                 device, BluetoothProfile.A2DP_SINK, toState, fromState);
583     }
584 }
585