• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.annotation.RequiresPermission;
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothAudioConfig;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothProfile;
23 import android.bluetooth.IBluetoothA2dpSink;
24 import android.content.AttributionSource;
25 import android.media.AudioManager;
26 import android.sysprop.BluetoothProperties;
27 import android.util.Log;
28 
29 import com.android.bluetooth.Utils;
30 import com.android.bluetooth.btservice.AdapterService;
31 import com.android.bluetooth.btservice.ProfileService;
32 import com.android.bluetooth.btservice.storage.DatabaseManager;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.modules.utils.SynchronousResultReceiver;
35 
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.Objects;
41 import java.util.concurrent.ConcurrentHashMap;
42 
43 /**
44  * Provides Bluetooth A2DP Sink profile, as a service in the Bluetooth application.
45  * @hide
46  */
47 public class A2dpSinkService extends ProfileService {
48     private static final String TAG = "A2dpSinkService";
49     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
50     private int mMaxConnectedAudioDevices;
51 
52     private AdapterService mAdapterService;
53     private DatabaseManager mDatabaseManager;
54     private Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
55             new ConcurrentHashMap<>(1);
56 
57     private final Object mStreamHandlerLock = new Object();
58 
59     private final Object mActiveDeviceLock = new Object();
60     private BluetoothDevice mActiveDevice = null;
61 
62     private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
63     private static A2dpSinkService sService;
64 
65     A2dpSinkNativeInterface mNativeInterface;
66 
isEnabled()67     public static boolean isEnabled() {
68         return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false);
69     }
70 
71     @Override
start()72     protected boolean start() {
73         mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
74                 "AdapterService cannot be null when A2dpSinkService starts");
75         mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
76                 "DatabaseManager cannot be null when A2dpSinkService starts");
77         mNativeInterface = A2dpSinkNativeInterface.getInstance();
78 
79         mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
80         mNativeInterface.init(mMaxConnectedAudioDevices);
81 
82         synchronized (mStreamHandlerLock) {
83             mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface);
84         }
85 
86         setA2dpSinkService(this);
87         BluetoothDevice activeDevice = getActiveDevice();
88         String deviceAddress = activeDevice != null ?
89                 activeDevice.getAddress() :
90                 AdapterService.ACTIVITY_ATTRIBUTION_NO_ACTIVE_DEVICE_ADDRESS;
91         mAdapterService.notifyActivityAttributionInfo(getAttributionSource(), deviceAddress);
92         return true;
93     }
94 
95     @Override
stop()96     protected boolean stop() {
97         BluetoothDevice activeDevice = getActiveDevice();
98         String deviceAddress = activeDevice != null ?
99                 activeDevice.getAddress() :
100                 AdapterService.ACTIVITY_ATTRIBUTION_NO_ACTIVE_DEVICE_ADDRESS;
101         mAdapterService.notifyActivityAttributionInfo(getAttributionSource(), deviceAddress);
102         setA2dpSinkService(null);
103         mNativeInterface.cleanup();
104         for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
105             stateMachine.quitNow();
106         }
107         mDeviceStateMap.clear();
108         synchronized (mStreamHandlerLock) {
109             if (mA2dpSinkStreamHandler != null) {
110                 mA2dpSinkStreamHandler.cleanup();
111                 mA2dpSinkStreamHandler = null;
112             }
113         }
114         return true;
115     }
116 
getA2dpSinkService()117     public static synchronized A2dpSinkService getA2dpSinkService() {
118         return sService;
119     }
120 
121     /**
122      * Testing API to inject a mockA2dpSinkService.
123      * @hide
124      */
125     @VisibleForTesting
setA2dpSinkService(A2dpSinkService service)126     public static synchronized void setA2dpSinkService(A2dpSinkService service) {
127         sService = service;
128     }
129 
130 
A2dpSinkService()131     public A2dpSinkService() {}
132 
133     /**
134      * Set the device that should be allowed to actively stream
135      */
setActiveDevice(BluetoothDevice device)136     public boolean setActiveDevice(BluetoothDevice device) {
137         synchronized (mActiveDeviceLock) {
138             if (mNativeInterface.setActiveDevice(device)) {
139                 mActiveDevice = device;
140                 return true;
141             }
142             return false;
143         }
144     }
145 
146     /**
147      * Get the device that is allowed to be actively streaming
148      */
getActiveDevice()149     public BluetoothDevice getActiveDevice() {
150         synchronized (mActiveDeviceLock) {
151             return mActiveDevice;
152         }
153     }
154 
155     /**
156      * Request audio focus such that the designated device can stream audio
157      */
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, SynchronousResultReceiver receiver)221         public void connect(BluetoothDevice device, AttributionSource source,
222                 SynchronousResultReceiver receiver) {
223             try {
224                 A2dpSinkService service = getService(source);
225                 boolean result = false;
226                 if (service != null) {
227                     result = service.connect(device);
228                 }
229                 receiver.send(result);
230             } catch (RuntimeException e) {
231                 receiver.propagateException(e);
232             }
233         }
234 
235         @Override
disconnect(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)236         public void disconnect(BluetoothDevice device, AttributionSource source,
237                 SynchronousResultReceiver receiver) {
238             try {
239                 A2dpSinkService service = getService(source);
240                 boolean result = false;
241                 if (service != null) {
242                     result = service.disconnect(device);
243                 }
244                 receiver.send(result);
245             } catch (RuntimeException e) {
246                 receiver.propagateException(e);
247             }
248         }
249 
250         @Override
getConnectedDevices(AttributionSource source, SynchronousResultReceiver receiver)251         public void getConnectedDevices(AttributionSource source,
252                 SynchronousResultReceiver receiver) {
253             try {
254                 A2dpSinkService service = getService(source);
255                 List<BluetoothDevice> result = new ArrayList<BluetoothDevice>(0);
256                 if (service != null) {
257                     result = service.getConnectedDevices();
258                 }
259                 receiver.send(result);
260             } catch (RuntimeException e) {
261                 receiver.propagateException(e);
262             }
263         }
264 
265         @Override
getDevicesMatchingConnectionStates(int[] states, AttributionSource source, SynchronousResultReceiver receiver)266         public void getDevicesMatchingConnectionStates(int[] states,
267                 AttributionSource source, SynchronousResultReceiver receiver) {
268             try {
269                 A2dpSinkService service = getService(source);
270                 List<BluetoothDevice> result = new ArrayList<BluetoothDevice>(0);
271                 if (service != null) {
272                     result = service.getDevicesMatchingConnectionStates(states);
273                 }
274                 receiver.send(result);
275             } catch (RuntimeException e) {
276                 receiver.propagateException(e);
277             }
278         }
279 
280         @Override
getConnectionState(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)281         public void getConnectionState(BluetoothDevice device, AttributionSource source,
282                 SynchronousResultReceiver receiver) {
283             try {
284                 A2dpSinkService service = getService(source);
285                 int result = BluetoothProfile.STATE_DISCONNECTED;
286                 if (service != null) {
287                     result = service.getConnectionState(device);
288                 }
289                 receiver.send(result);
290             } catch (RuntimeException e) {
291                 receiver.propagateException(e);
292             }
293         }
294 
295         @Override
setConnectionPolicy(BluetoothDevice device, int connectionPolicy, AttributionSource source, SynchronousResultReceiver receiver)296         public void setConnectionPolicy(BluetoothDevice device, int connectionPolicy,
297                 AttributionSource source, SynchronousResultReceiver receiver) {
298             try {
299                 A2dpSinkService service = getService(source);
300                 boolean result = false;
301                 if (service != null) {
302                     result = service.setConnectionPolicy(device, connectionPolicy);
303                 }
304                 receiver.send(result);
305             } catch (RuntimeException e) {
306                 receiver.propagateException(e);
307             }
308         }
309 
310         @Override
getConnectionPolicy(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)311         public void getConnectionPolicy(BluetoothDevice device, AttributionSource source,
312                 SynchronousResultReceiver receiver) {
313             try {
314                 A2dpSinkService service = getService(source);
315                 int result = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
316                 if (service != null) {
317                     result = service.getConnectionPolicy(device);
318                 }
319                 receiver.send(result);
320             } catch (RuntimeException e) {
321                 receiver.propagateException(e);
322             }
323         }
324 
325         @Override
isA2dpPlaying(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)326         public void isA2dpPlaying(BluetoothDevice device, AttributionSource source,
327                 SynchronousResultReceiver receiver) {
328             try {
329                 A2dpSinkService service = getService(source);
330                 boolean result = false;
331                 if (service != null) {
332                     result = service.isA2dpPlaying(device);
333                 }
334                 receiver.send(result);
335             } catch (RuntimeException e) {
336                 receiver.propagateException(e);
337             }
338         }
339 
340         @Override
getAudioConfig(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)341         public void getAudioConfig(BluetoothDevice device,
342                 AttributionSource source, SynchronousResultReceiver receiver) {
343             try {
344                 A2dpSinkService service = getService(source);
345                 BluetoothAudioConfig result = null;
346                 if (service != null) {
347                     result = service.getAudioConfig(device);
348                 }
349                 receiver.send(result);
350             } catch (RuntimeException e) {
351                 receiver.propagateException(e);
352             }
353         }
354     }
355 
356     /* Generic Profile Code */
357 
358     /**
359      * Connect the given Bluetooth device.
360      *
361      * @return true if connection is successful, false otherwise.
362      */
363     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
connect(BluetoothDevice device)364     public boolean connect(BluetoothDevice device) {
365         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
366                 "Need BLUETOOTH_PRIVILEGED permission");
367         if (device == null) {
368             throw new IllegalArgumentException("Null device");
369         }
370         if (DBG) {
371             StringBuilder sb = new StringBuilder();
372             dump(sb);
373             Log.d(TAG, " connect device: " + device
374                     + ", InstanceMap start state: " + sb.toString());
375         }
376         if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
377             Log.w(TAG, "Connection not allowed: <" + device
378                     + "> is CONNECTION_POLICY_FORBIDDEN");
379             return false;
380         }
381 
382         A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
383         if (stateMachine != null) {
384             stateMachine.connect();
385             return true;
386         } else {
387             // a state machine instance doesn't exist yet, and the max has been reached.
388             Log.e(TAG, "Maxed out on the number of allowed A2DP Sink connections. "
389                     + "Connect request rejected on " + device);
390             return false;
391         }
392     }
393 
394     /**
395      * Disconnect the given Bluetooth device.
396      *
397      * @return true if disconnect is successful, false otherwise.
398      */
disconnect(BluetoothDevice device)399     public boolean disconnect(BluetoothDevice device) {
400         if (DBG) {
401             StringBuilder sb = new StringBuilder();
402             dump(sb);
403             Log.d(TAG, "A2DP disconnect device: " + device
404                     + ", InstanceMap start state: " + sb.toString());
405         }
406 
407         if (device == null) {
408             throw new IllegalArgumentException("Null device");
409         }
410 
411         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
412         // a state machine instance doesn't exist. maybe it is already gone?
413         if (stateMachine == null) {
414             return false;
415         }
416         int connectionState = stateMachine.getState();
417         if (connectionState == BluetoothProfile.STATE_DISCONNECTED
418                 || connectionState == BluetoothProfile.STATE_DISCONNECTING) {
419             return false;
420         }
421         // upon completion of disconnect, the state machine will remove itself from the available
422         // devices map
423         stateMachine.disconnect();
424         return true;
425     }
426 
427     /**
428      * Remove a device's state machine.
429      *
430      * Called by the state machines when they disconnect.
431      *
432      * Visible for testing so it can be mocked and verified on.
433      */
434     @VisibleForTesting
removeStateMachine(A2dpSinkStateMachine stateMachine)435     public void removeStateMachine(A2dpSinkStateMachine stateMachine) {
436         mDeviceStateMap.remove(stateMachine.getDevice());
437     }
438 
getConnectedDevices()439     public List<BluetoothDevice> getConnectedDevices() {
440         return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED});
441     }
442 
getOrCreateStateMachine(BluetoothDevice device)443     protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
444         A2dpSinkStateMachine newStateMachine =
445                 new A2dpSinkStateMachine(device, this, mNativeInterface);
446         A2dpSinkStateMachine existingStateMachine =
447                 mDeviceStateMap.putIfAbsent(device, newStateMachine);
448         // Given null is not a valid value in our map, ConcurrentHashMap will return null if the
449         // key was absent and our new value was added. We should then start and return it.
450         if (existingStateMachine == null) {
451             newStateMachine.start();
452             return newStateMachine;
453         }
454         return existingStateMachine;
455     }
456 
457     @VisibleForTesting
getStateMachineForDevice(BluetoothDevice device)458     protected A2dpSinkStateMachine getStateMachineForDevice(BluetoothDevice device) {
459         return mDeviceStateMap.get(device);
460     }
461 
getDevicesMatchingConnectionStates(int[] states)462     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
463         if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
464         List<BluetoothDevice> deviceList = new ArrayList<>();
465         BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
466         int connectionState;
467         for (BluetoothDevice device : bondedDevices) {
468             connectionState = getConnectionState(device);
469             if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState);
470             for (int i = 0; i < states.length; i++) {
471                 if (connectionState == states[i]) {
472                     deviceList.add(device);
473                 }
474             }
475         }
476         if (DBG) Log.d(TAG, deviceList.toString());
477         Log.d(TAG, "GetDevicesDone");
478         return deviceList;
479     }
480 
481     /**
482      * Get the current connection state of the profile
483      *
484      * @param device is the remote bluetooth device
485      * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected,
486      * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected,
487      * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
488      * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
489      */
getConnectionState(BluetoothDevice device)490     public int getConnectionState(BluetoothDevice device) {
491         if (device == null) return BluetoothProfile.STATE_DISCONNECTED;
492         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
493         return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
494                 : stateMachine.getState();
495     }
496 
497     /**
498      * Set connection policy of the profile and connects it if connectionPolicy is
499      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
500      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
501      *
502      * <p> The device should already be paired.
503      * Connection policy can be one of:
504      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
505      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
506      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
507      *
508      * @param device Paired bluetooth device
509      * @param connectionPolicy is the connection policy to set to for this profile
510      * @return true if connectionPolicy is set, false on error
511      */
512     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)513     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
514         enforceCallingOrSelfPermission(
515                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
516         if (DBG) {
517             Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
518         }
519 
520         if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK,
521                   connectionPolicy)) {
522             return false;
523         }
524         if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
525             connect(device);
526         } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
527             disconnect(device);
528         }
529         return true;
530     }
531 
532     /**
533      * Get the connection policy of the profile.
534      *
535      * @param device the remote device
536      * @return connection policy of the specified device
537      */
538     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(BluetoothDevice device)539     public int getConnectionPolicy(BluetoothDevice device) {
540         enforceCallingOrSelfPermission(
541                 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission");
542         return mDatabaseManager
543                 .getProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK);
544     }
545 
546 
547     @Override
dump(StringBuilder sb)548     public void dump(StringBuilder sb) {
549         super.dump(sb);
550         ProfileService.println(sb, "Active Device = " + getActiveDevice());
551         ProfileService.println(sb, "Max Connected Devices = " + mMaxConnectedAudioDevices);
552         ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size());
553         for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
554             ProfileService.println(sb,
555                     "==== StateMachine for " + stateMachine.getDevice() + " ====");
556             stateMachine.dump(sb);
557         }
558     }
559 
getAudioConfig(BluetoothDevice device)560     BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
561         if (device == null) return null;
562         A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
563         // a state machine instance doesn't exist. maybe it is already gone?
564         if (stateMachine == null) {
565             return null;
566         }
567         return stateMachine.getAudioConfig();
568     }
569 
570     /**
571      * Receive and route a stack event from the JNI
572      */
messageFromNative(StackEvent event)573     protected void messageFromNative(StackEvent event) {
574         switch (event.mType) {
575             case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
576                 onConnectionStateChanged(event);
577                 return;
578             case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
579                 onAudioStateChanged(event);
580                 return;
581             case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
582                 onAudioConfigChanged(event);
583                 return;
584             default:
585                 Log.e(TAG, "Received unknown stack event of type " + event.mType);
586                 return;
587         }
588     }
589 
onConnectionStateChanged(StackEvent event)590     private void onConnectionStateChanged(StackEvent event) {
591         BluetoothDevice device = event.mDevice;
592         if (device == null) {
593             return;
594         }
595         A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
596         stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
597     }
598 
onAudioStateChanged(StackEvent event)599     private void onAudioStateChanged(StackEvent event) {
600         int state = event.mState;
601         synchronized (mStreamHandlerLock) {
602             if (mA2dpSinkStreamHandler == null) {
603                 Log.e(TAG, "Received audio state change before we've been started");
604                 return;
605             } else if (state == StackEvent.AUDIO_STATE_STARTED) {
606                 mA2dpSinkStreamHandler.obtainMessage(
607                         A2dpSinkStreamHandler.SRC_STR_START).sendToTarget();
608             } else if (state == StackEvent.AUDIO_STATE_STOPPED
609                     || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) {
610                 mA2dpSinkStreamHandler.obtainMessage(
611                         A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
612             } else {
613                 Log.w(TAG, "Unhandled audio state change, state=" + state);
614             }
615         }
616     }
617 
onAudioConfigChanged(StackEvent event)618     private void onAudioConfigChanged(StackEvent event) {
619         BluetoothDevice device = event.mDevice;
620         if (device == null) {
621             return;
622         }
623         A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
624         stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
625     }
626 }
627