1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 /**
18  * Bluetooth A2DP StateMachine. There is one instance per remote device.
19  *  - "Disconnected" and "Connected" are steady states.
20  *  - "Connecting" and "Disconnecting" are transient states until the
21  *     connection / disconnection is completed.
22  *
23  *
24  *                        (Disconnected)
25  *                           |       ^
26  *                   CONNECT |       | DISCONNECTED
27  *                           V       |
28  *                 (Connecting)<--->(Disconnecting)
29  *                           |       ^
30  *                 CONNECTED |       | DISCONNECT
31  *                           V       |
32  *                          (Connected)
33  * NOTES:
34  *  - If state machine is in "Connecting" state and the remote device sends
35  *    DISCONNECT request, the state machine transitions to "Disconnecting" state.
36  *  - Similarly, if the state machine is in "Disconnecting" state and the remote device
37  *    sends CONNECT request, the state machine transitions to "Connecting" state.
38  *
39  *                    DISCONNECT
40  *    (Connecting) ---------------> (Disconnecting)
41  *                 <---------------
42  *                      CONNECT
43  *
44  */
45 
46 package com.android.bluetooth.a2dp;
47 
48 import static android.Manifest.permission.BLUETOOTH_CONNECT;
49 
50 import android.annotation.RequiresPermission;
51 import android.bluetooth.BluetoothA2dp;
52 import android.bluetooth.BluetoothCodecConfig;
53 import android.bluetooth.BluetoothCodecStatus;
54 import android.bluetooth.BluetoothDevice;
55 import android.bluetooth.BluetoothProfile;
56 import android.content.Intent;
57 import android.os.Looper;
58 import android.os.Message;
59 import android.util.Log;
60 
61 import com.android.bluetooth.Utils;
62 import com.android.bluetooth.btservice.ProfileService;
63 import com.android.bluetooth.statemachine.State;
64 import com.android.bluetooth.statemachine.StateMachine;
65 import com.android.internal.annotations.VisibleForTesting;
66 
67 import java.io.FileDescriptor;
68 import java.io.PrintWriter;
69 import java.io.StringWriter;
70 import java.util.Objects;
71 import java.util.Scanner;
72 
73 final class A2dpStateMachine extends StateMachine {
74     private static final boolean DBG = true;
75     private static final String TAG = "A2dpStateMachine";
76 
77     static final int CONNECT = 1;
78     static final int DISCONNECT = 2;
79     @VisibleForTesting
80     static final int STACK_EVENT = 101;
81     private static final int CONNECT_TIMEOUT = 201;
82 
83     // NOTE: the value is not "final" - it is modified in the unit tests
84     @VisibleForTesting
85     static int sConnectTimeoutMs = 30000;        // 30s
86 
87     private Disconnected mDisconnected;
88     private Connecting mConnecting;
89     private Disconnecting mDisconnecting;
90     private Connected mConnected;
91     private int mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
92     private int mLastConnectionState = -1;
93 
94     private A2dpService mA2dpService;
95     private A2dpNativeInterface mA2dpNativeInterface;
96     @VisibleForTesting
97     boolean mA2dpOffloadEnabled = false;
98     private final BluetoothDevice mDevice;
99     private boolean mIsPlaying = false;
100     private BluetoothCodecStatus mCodecStatus;
101 
A2dpStateMachine(BluetoothDevice device, A2dpService a2dpService, A2dpNativeInterface a2dpNativeInterface, Looper looper)102     A2dpStateMachine(BluetoothDevice device, A2dpService a2dpService,
103                      A2dpNativeInterface a2dpNativeInterface, Looper looper) {
104         super(TAG, looper);
105         setDbg(DBG);
106         mDevice = device;
107         mA2dpService = a2dpService;
108         mA2dpNativeInterface = a2dpNativeInterface;
109 
110         mDisconnected = new Disconnected();
111         mConnecting = new Connecting();
112         mDisconnecting = new Disconnecting();
113         mConnected = new Connected();
114 
115         addState(mDisconnected);
116         addState(mConnecting);
117         addState(mDisconnecting);
118         addState(mConnected);
119         mA2dpOffloadEnabled = mA2dpService.mA2dpOffloadEnabled;
120 
121         setInitialState(mDisconnected);
122     }
123 
make(BluetoothDevice device, A2dpService a2dpService, A2dpNativeInterface a2dpNativeInterface, Looper looper)124     static A2dpStateMachine make(BluetoothDevice device, A2dpService a2dpService,
125                                  A2dpNativeInterface a2dpNativeInterface, Looper looper) {
126         Log.i(TAG, "make for device " + device);
127         A2dpStateMachine a2dpSm = new A2dpStateMachine(device, a2dpService, a2dpNativeInterface,
128                                                        looper);
129         a2dpSm.start();
130         return a2dpSm;
131     }
132 
doQuit()133     public void doQuit() {
134         log("doQuit for device " + mDevice);
135         if (mIsPlaying) {
136             // Stop if auido is still playing
137             log("doQuit: stopped playing " + mDevice);
138             mIsPlaying = false;
139             broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
140                                 BluetoothA2dp.STATE_PLAYING);
141         }
142         quitNow();
143     }
144 
cleanup()145     public void cleanup() {
146         log("cleanup for device " + mDevice);
147     }
148 
149     @VisibleForTesting
150     class Disconnected extends State {
151         @Override
enter()152         public void enter() {
153             Message currentMessage = getCurrentMessage();
154             Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + (currentMessage == null ? "null"
155                     : messageWhatToString(currentMessage.what)));
156             mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
157 
158             removeDeferredMessages(DISCONNECT);
159 
160             if (mLastConnectionState != -1) {
161                 // Don't broadcast during startup
162                 broadcastConnectionState(mConnectionState, mLastConnectionState);
163                 if (mIsPlaying) {
164                     Log.i(TAG, "Disconnected: stopped playing: " + mDevice);
165                     mIsPlaying = false;
166                     broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
167                                         BluetoothA2dp.STATE_PLAYING);
168                 }
169             }
170         }
171 
172         @Override
exit()173         public void exit() {
174             Message currentMessage = getCurrentMessage();
175             log("Exit Disconnected(" + mDevice + "): " + (currentMessage == null ? "null"
176                     : messageWhatToString(currentMessage.what)));
177             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
178         }
179 
180         @Override
processMessage(Message message)181         public boolean processMessage(Message message) {
182             log("Disconnected process message(" + mDevice + "): "
183                     + messageWhatToString(message.what));
184 
185             switch (message.what) {
186                 case CONNECT:
187                     Log.i(TAG, "Connecting to " + mDevice);
188                     if (!mA2dpNativeInterface.connectA2dp(mDevice)) {
189                         Log.e(TAG, "Disconnected: error connecting to " + mDevice);
190                         break;
191                     }
192                     if (mA2dpService.okToConnect(mDevice, true)) {
193                         transitionTo(mConnecting);
194                     } else {
195                         // Reject the request and stay in Disconnected state
196                         Log.w(TAG, "Outgoing A2DP Connecting request rejected: " + mDevice);
197                     }
198                     break;
199                 case DISCONNECT:
200                     Log.w(TAG, "Disconnected: DISCONNECT ignored: " + mDevice);
201                     break;
202                 case STACK_EVENT:
203                     A2dpStackEvent event = (A2dpStackEvent) message.obj;
204                     log("Disconnected: stack event: " + event);
205                     if (!mDevice.equals(event.device)) {
206                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
207                     }
208                     switch (event.type) {
209                         case A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
210                             processConnectionEvent(event.valueInt);
211                             break;
212                         case A2dpStackEvent.EVENT_TYPE_CODEC_CONFIG_CHANGED:
213                             processCodecConfigEvent(event.codecStatus);
214                             break;
215                         default:
216                             Log.e(TAG, "Disconnected: ignoring stack event: " + event);
217                             break;
218                     }
219                     break;
220                 default:
221                     return NOT_HANDLED;
222             }
223             return HANDLED;
224         }
225 
226         // in Disconnected state
processConnectionEvent(int event)227         private void processConnectionEvent(int event) {
228             switch (event) {
229                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTED:
230                     Log.w(TAG, "Ignore A2DP DISCONNECTED event: " + mDevice);
231                     break;
232                 case A2dpStackEvent.CONNECTION_STATE_CONNECTING:
233                     if (mA2dpService.okToConnect(mDevice, false)) {
234                         Log.i(TAG, "Incoming A2DP Connecting request accepted: " + mDevice);
235                         transitionTo(mConnecting);
236                     } else {
237                         // Reject the connection and stay in Disconnected state itself
238                         Log.w(TAG, "Incoming A2DP Connecting request rejected: " + mDevice);
239                         mA2dpNativeInterface.disconnectA2dp(mDevice);
240                     }
241                     break;
242                 case A2dpStackEvent.CONNECTION_STATE_CONNECTED:
243                     Log.w(TAG, "A2DP Connected from Disconnected state: " + mDevice);
244                     if (mA2dpService.okToConnect(mDevice, false)) {
245                         Log.i(TAG, "Incoming A2DP Connected request accepted: " + mDevice);
246                         transitionTo(mConnected);
247                     } else {
248                         // Reject the connection and stay in Disconnected state itself
249                         Log.w(TAG, "Incoming A2DP Connected request rejected: " + mDevice);
250                         mA2dpNativeInterface.disconnectA2dp(mDevice);
251                     }
252                     break;
253                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTING:
254                     Log.w(TAG, "Ignore A2DP DISCONNECTING event: " + mDevice);
255                     break;
256                 default:
257                     Log.e(TAG, "Incorrect event: " + event + " device: " + mDevice);
258                     break;
259             }
260         }
261     }
262 
263     @VisibleForTesting
264     class Connecting extends State {
265         @Override
enter()266         public void enter() {
267             Message currentMessage = getCurrentMessage();
268             Log.i(TAG, "Enter Connecting(" + mDevice + "): " + (currentMessage == null ? "null"
269                     : messageWhatToString(currentMessage.what)));
270             sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
271             mConnectionState = BluetoothProfile.STATE_CONNECTING;
272             broadcastConnectionState(mConnectionState, mLastConnectionState);
273         }
274 
275         @Override
exit()276         public void exit() {
277             Message currentMessage = getCurrentMessage();
278             log("Exit Connecting(" + mDevice + "): " + (currentMessage == null ? "null"
279                     : messageWhatToString(currentMessage.what)));
280             mLastConnectionState = BluetoothProfile.STATE_CONNECTING;
281             removeMessages(CONNECT_TIMEOUT);
282         }
283 
284         @Override
processMessage(Message message)285         public boolean processMessage(Message message) {
286             log("Connecting process message(" + mDevice + "): "
287                     + messageWhatToString(message.what));
288 
289             switch (message.what) {
290                 case CONNECT:
291                     deferMessage(message);
292                     break;
293                 case CONNECT_TIMEOUT: {
294                     Log.w(TAG, "Connecting connection timeout: " + mDevice);
295                     mA2dpNativeInterface.disconnectA2dp(mDevice);
296                     A2dpStackEvent event =
297                             new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
298                     event.device = mDevice;
299                     event.valueInt = A2dpStackEvent.CONNECTION_STATE_DISCONNECTED;
300                     sendMessage(STACK_EVENT, event);
301                     break;
302                 }
303                 case DISCONNECT:
304                     // Cancel connection
305                     Log.i(TAG, "Connecting: connection canceled to " + mDevice);
306                     mA2dpNativeInterface.disconnectA2dp(mDevice);
307                     transitionTo(mDisconnected);
308                     break;
309                 case STACK_EVENT:
310                     A2dpStackEvent event = (A2dpStackEvent) message.obj;
311                     log("Connecting: stack event: " + event);
312                     if (!mDevice.equals(event.device)) {
313                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
314                     }
315                     switch (event.type) {
316                         case A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
317                             processConnectionEvent(event.valueInt);
318                             break;
319                         case A2dpStackEvent.EVENT_TYPE_CODEC_CONFIG_CHANGED:
320                             processCodecConfigEvent(event.codecStatus);
321                             break;
322                         case A2dpStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
323                             break;
324                         default:
325                             Log.e(TAG, "Connecting: ignoring stack event: " + event);
326                             break;
327                     }
328                     break;
329                 default:
330                     return NOT_HANDLED;
331             }
332             return HANDLED;
333         }
334 
335         // in Connecting state
processConnectionEvent(int event)336         private void processConnectionEvent(int event) {
337             switch (event) {
338                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTED:
339                     Log.w(TAG, "Connecting device disconnected: " + mDevice);
340                     transitionTo(mDisconnected);
341                     break;
342                 case A2dpStackEvent.CONNECTION_STATE_CONNECTED:
343                     transitionTo(mConnected);
344                     break;
345                 case A2dpStackEvent.CONNECTION_STATE_CONNECTING:
346                     // Ignored - probably an event that the outgoing connection was initiated
347                     break;
348                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTING:
349                     Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice);
350                     transitionTo(mDisconnecting);
351                     break;
352                 default:
353                     Log.e(TAG, "Incorrect event: " + event);
354                     break;
355             }
356         }
357     }
358 
359     @VisibleForTesting
360     class Disconnecting extends State {
361         @Override
enter()362         public void enter() {
363             Message currentMessage = getCurrentMessage();
364             Log.i(TAG, "Enter Disconnecting(" + mDevice + "): " + (currentMessage == null ? "null"
365                     : messageWhatToString(currentMessage.what)));
366             sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
367             mConnectionState = BluetoothProfile.STATE_DISCONNECTING;
368             broadcastConnectionState(mConnectionState, mLastConnectionState);
369         }
370 
371         @Override
exit()372         public void exit() {
373             Message currentMessage = getCurrentMessage();
374             log("Exit Disconnecting(" + mDevice + "): " + (currentMessage == null ? "null"
375                     : messageWhatToString(currentMessage.what)));
376             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING;
377             removeMessages(CONNECT_TIMEOUT);
378         }
379 
380         @Override
processMessage(Message message)381         public boolean processMessage(Message message) {
382             log("Disconnecting process message(" + mDevice + "): "
383                     + messageWhatToString(message.what));
384 
385             switch (message.what) {
386                 case CONNECT:
387                     deferMessage(message);
388                     break;
389                 case CONNECT_TIMEOUT: {
390                     Log.w(TAG, "Disconnecting connection timeout: " + mDevice);
391                     mA2dpNativeInterface.disconnectA2dp(mDevice);
392                     A2dpStackEvent event =
393                             new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
394                     event.device = mDevice;
395                     event.valueInt = A2dpStackEvent.CONNECTION_STATE_DISCONNECTED;
396                     sendMessage(STACK_EVENT, event);
397                     break;
398                 }
399                 case DISCONNECT:
400                     deferMessage(message);
401                     break;
402                 case STACK_EVENT:
403                     A2dpStackEvent event = (A2dpStackEvent) message.obj;
404                     log("Disconnecting: stack event: " + event);
405                     if (!mDevice.equals(event.device)) {
406                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
407                     }
408                     switch (event.type) {
409                         case A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
410                             processConnectionEvent(event.valueInt);
411                             break;
412                         case A2dpStackEvent.EVENT_TYPE_CODEC_CONFIG_CHANGED:
413                             processCodecConfigEvent(event.codecStatus);
414                             break;
415                         case A2dpStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
416                         default:
417                             Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
418                             break;
419                     }
420                     break;
421                 default:
422                     return NOT_HANDLED;
423             }
424             return HANDLED;
425         }
426 
427         // in Disconnecting state
processConnectionEvent(int event)428         private void processConnectionEvent(int event) {
429             switch (event) {
430                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTED:
431                     Log.i(TAG, "Disconnected: " + mDevice);
432                     transitionTo(mDisconnected);
433                     break;
434                 case A2dpStackEvent.CONNECTION_STATE_CONNECTED:
435                     if (mA2dpService.okToConnect(mDevice, false)) {
436                         Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice);
437                         transitionTo(mConnected);
438                     } else {
439                         // Reject the connection and stay in Disconnecting state
440                         Log.w(TAG, "Incoming A2DP Connected request rejected: " + mDevice);
441                         mA2dpNativeInterface.disconnectA2dp(mDevice);
442                     }
443                     break;
444                 case A2dpStackEvent.CONNECTION_STATE_CONNECTING:
445                     if (mA2dpService.okToConnect(mDevice, false)) {
446                         Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice);
447                         transitionTo(mConnecting);
448                     } else {
449                         // Reject the connection and stay in Disconnecting state
450                         Log.w(TAG, "Incoming A2DP Connecting request rejected: " + mDevice);
451                         mA2dpNativeInterface.disconnectA2dp(mDevice);
452                     }
453                     break;
454                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTING:
455                     // We are already disconnecting, do nothing
456                     break;
457                 default:
458                     Log.e(TAG, "Incorrect event: " + event);
459                     break;
460             }
461         }
462     }
463 
464     @VisibleForTesting
465     class Connected extends State {
466         @Override
enter()467         public void enter() {
468             Message currentMessage = getCurrentMessage();
469             Log.i(TAG, "Enter Connected(" + mDevice + "): " + (currentMessage == null ? "null"
470                     : messageWhatToString(currentMessage.what)));
471             mConnectionState = BluetoothProfile.STATE_CONNECTED;
472 
473             removeDeferredMessages(CONNECT);
474 
475             // Each time a device connects, we want to re-check if it supports optional
476             // codecs (perhaps it's had a firmware update, etc.) and save that state if
477             // it differs from what we had saved before.
478             mA2dpService.updateOptionalCodecsSupport(mDevice);
479             broadcastConnectionState(mConnectionState, mLastConnectionState);
480             // Upon connected, the audio starts out as stopped
481             broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
482                                 BluetoothA2dp.STATE_PLAYING);
483         }
484 
485         @Override
exit()486         public void exit() {
487             Message currentMessage = getCurrentMessage();
488             log("Exit Connected(" + mDevice + "): " + (currentMessage == null ? "null"
489                     : messageWhatToString(currentMessage.what)));
490             mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
491         }
492 
493         @Override
processMessage(Message message)494         public boolean processMessage(Message message) {
495             log("Connected process message(" + mDevice + "): " + messageWhatToString(message.what));
496 
497             switch (message.what) {
498                 case CONNECT:
499                     Log.w(TAG, "Connected: CONNECT ignored: " + mDevice);
500                     break;
501                 case DISCONNECT: {
502                     Log.i(TAG, "Disconnecting from " + mDevice);
503                     if (!mA2dpNativeInterface.disconnectA2dp(mDevice)) {
504                         // If error in the native stack, transition directly to Disconnected state.
505                         Log.e(TAG, "Connected: error disconnecting from " + mDevice);
506                         transitionTo(mDisconnected);
507                         break;
508                     }
509                     transitionTo(mDisconnecting);
510                 }
511                 break;
512                 case STACK_EVENT:
513                     A2dpStackEvent event = (A2dpStackEvent) message.obj;
514                     log("Connected: stack event: " + event);
515                     if (!mDevice.equals(event.device)) {
516                         Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
517                     }
518                     switch (event.type) {
519                         case A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
520                             processConnectionEvent(event.valueInt);
521                             break;
522                         case A2dpStackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
523                             processAudioStateEvent(event.valueInt);
524                             break;
525                         case A2dpStackEvent.EVENT_TYPE_CODEC_CONFIG_CHANGED:
526                             processCodecConfigEvent(event.codecStatus);
527                             break;
528                         default:
529                             Log.e(TAG, "Connected: ignoring stack event: " + event);
530                             break;
531                     }
532                     break;
533                 default:
534                     return NOT_HANDLED;
535             }
536             return HANDLED;
537         }
538 
539         // in Connected state
processConnectionEvent(int event)540         private void processConnectionEvent(int event) {
541             switch (event) {
542                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTED:
543                     Log.i(TAG, "Disconnected from " + mDevice);
544                     transitionTo(mDisconnected);
545                     break;
546                 case A2dpStackEvent.CONNECTION_STATE_CONNECTED:
547                     Log.w(TAG, "Ignore A2DP CONNECTED event: " + mDevice);
548                     break;
549                 case A2dpStackEvent.CONNECTION_STATE_CONNECTING:
550                     Log.w(TAG, "Ignore A2DP CONNECTING event: " + mDevice);
551                     break;
552                 case A2dpStackEvent.CONNECTION_STATE_DISCONNECTING:
553                     Log.i(TAG, "Disconnecting from " + mDevice);
554                     transitionTo(mDisconnecting);
555                     break;
556                 default:
557                     Log.e(TAG, "Connection State Device: " + mDevice + " bad event: " + event);
558                     break;
559             }
560         }
561 
562         // in Connected state
processAudioStateEvent(int state)563         private void processAudioStateEvent(int state) {
564             switch (state) {
565                 case A2dpStackEvent.AUDIO_STATE_STARTED:
566                     synchronized (this) {
567                         if (!mIsPlaying) {
568                             Log.i(TAG, "Connected: started playing: " + mDevice);
569                             mIsPlaying = true;
570                             broadcastAudioState(BluetoothA2dp.STATE_PLAYING,
571                                                 BluetoothA2dp.STATE_NOT_PLAYING);
572                         }
573                     }
574                     break;
575                 case A2dpStackEvent.AUDIO_STATE_REMOTE_SUSPEND:
576                 case A2dpStackEvent.AUDIO_STATE_STOPPED:
577                     synchronized (this) {
578                         if (mIsPlaying) {
579                             Log.i(TAG, "Connected: stopped playing: " + mDevice);
580                             mIsPlaying = false;
581                             broadcastAudioState(BluetoothA2dp.STATE_NOT_PLAYING,
582                                                 BluetoothA2dp.STATE_PLAYING);
583                         }
584                     }
585                     break;
586                 default:
587                     Log.e(TAG, "Audio State Device: " + mDevice + " bad state: " + state);
588                     break;
589             }
590         }
591     }
592 
getConnectionState()593     int getConnectionState() {
594         return mConnectionState;
595     }
596 
getDevice()597     BluetoothDevice getDevice() {
598         return mDevice;
599     }
600 
isConnected()601     boolean isConnected() {
602         synchronized (this) {
603             return (getConnectionState() == BluetoothProfile.STATE_CONNECTED);
604         }
605     }
606 
isPlaying()607     boolean isPlaying() {
608         synchronized (this) {
609             return mIsPlaying;
610         }
611     }
612 
getCodecStatus()613     BluetoothCodecStatus getCodecStatus() {
614         synchronized (this) {
615             return mCodecStatus;
616         }
617     }
618 
619     // NOTE: This event is processed in any state
620     @VisibleForTesting
processCodecConfigEvent(BluetoothCodecStatus newCodecStatus)621     void processCodecConfigEvent(BluetoothCodecStatus newCodecStatus) {
622         BluetoothCodecConfig prevCodecConfig = null;
623         BluetoothCodecStatus prevCodecStatus = mCodecStatus;
624 
625         synchronized (this) {
626             if (mCodecStatus != null) {
627                 prevCodecConfig = mCodecStatus.getCodecConfig();
628             }
629             mCodecStatus = newCodecStatus;
630         }
631 
632         if (DBG) {
633             Log.d(TAG, "A2DP Codec Config: " + prevCodecConfig + "->"
634                     + newCodecStatus.getCodecConfig());
635             for (BluetoothCodecConfig codecConfig :
636                      newCodecStatus.getCodecsLocalCapabilities()) {
637                 Log.d(TAG, "A2DP Codec Local Capability: " + codecConfig);
638             }
639             for (BluetoothCodecConfig codecConfig :
640                      newCodecStatus.getCodecsSelectableCapabilities()) {
641                 Log.d(TAG, "A2DP Codec Selectable Capability: " + codecConfig);
642             }
643         }
644 
645         if (isConnected() && !sameSelectableCodec(prevCodecStatus, mCodecStatus)) {
646             // Remote selectable codec could be changed if codec config changed
647             // in connected state, we need to re-check optional codec status
648             // for this codec change event.
649             mA2dpService.updateOptionalCodecsSupport(mDevice);
650         }
651         if (mA2dpOffloadEnabled) {
652             boolean update = false;
653             BluetoothCodecConfig newCodecConfig = mCodecStatus.getCodecConfig();
654             if ((prevCodecConfig != null)
655                     && (prevCodecConfig.getCodecType() != newCodecConfig.getCodecType())) {
656                 update = true;
657             } else if (!newCodecConfig.sameAudioFeedingParameters(prevCodecConfig)) {
658                 update = true;
659             } else if ((newCodecConfig.getCodecType()
660                         == BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC)
661                     && (prevCodecConfig != null)
662                     && (prevCodecConfig.getCodecSpecific1()
663                         != newCodecConfig.getCodecSpecific1())) {
664                 update = true;
665             }
666             if (update) {
667                 mA2dpService.codecConfigUpdated(mDevice, mCodecStatus, false);
668             }
669             return;
670         }
671 
672         boolean sameAudioFeedingParameters =
673                 newCodecStatus.getCodecConfig().sameAudioFeedingParameters(prevCodecConfig);
674         mA2dpService.codecConfigUpdated(mDevice, mCodecStatus, sameAudioFeedingParameters);
675     }
676 
677     // This method does not check for error conditon (newState == prevState)
broadcastConnectionState(int newState, int prevState)678     private void broadcastConnectionState(int newState, int prevState) {
679         log("Connection state " + mDevice + ": " + profileStateToString(prevState)
680                     + "->" + profileStateToString(newState));
681 
682         Intent intent = new Intent(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
683         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
684         intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
685         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
686         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
687                         | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
688         mA2dpService.sendBroadcast(intent, BLUETOOTH_CONNECT,
689                 Utils.getTempAllowlistBroadcastOptions());
690     }
691 
broadcastAudioState(int newState, int prevState)692     private void broadcastAudioState(int newState, int prevState) {
693         log("A2DP Playing state : device: " + mDevice + " State:" + audioStateToString(prevState)
694                 + "->" + audioStateToString(newState));
695         Intent intent = new Intent(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED);
696         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
697         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
698         intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
699         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
700         mA2dpService.sendBroadcast(intent, BLUETOOTH_CONNECT,
701                 Utils.getTempAllowlistBroadcastOptions());
702     }
703 
704     @Override
getLogRecString(Message msg)705     protected String getLogRecString(Message msg) {
706         StringBuilder builder = new StringBuilder();
707         builder.append(messageWhatToString(msg.what));
708         builder.append(": ");
709         builder.append("arg1=")
710                 .append(msg.arg1)
711                 .append(", arg2=")
712                 .append(msg.arg2)
713                 .append(", obj=")
714                 .append(msg.obj);
715         return builder.toString();
716     }
717 
sameSelectableCodec(BluetoothCodecStatus prevCodecStatus, BluetoothCodecStatus newCodecStatus)718     private static boolean sameSelectableCodec(BluetoothCodecStatus prevCodecStatus,
719             BluetoothCodecStatus newCodecStatus) {
720         if (prevCodecStatus == null) {
721             return false;
722         }
723         return BluetoothCodecStatus.sameCapabilities(
724                 prevCodecStatus.getCodecsSelectableCapabilities(),
725                 newCodecStatus.getCodecsSelectableCapabilities());
726     }
727 
messageWhatToString(int what)728     private static String messageWhatToString(int what) {
729         switch (what) {
730             case CONNECT:
731                 return "CONNECT";
732             case DISCONNECT:
733                 return "DISCONNECT";
734             case STACK_EVENT:
735                 return "STACK_EVENT";
736             case CONNECT_TIMEOUT:
737                 return "CONNECT_TIMEOUT";
738             default:
739                 break;
740         }
741         return Integer.toString(what);
742     }
743 
profileStateToString(int state)744     private static String profileStateToString(int state) {
745         switch (state) {
746             case BluetoothProfile.STATE_DISCONNECTED:
747                 return "DISCONNECTED";
748             case BluetoothProfile.STATE_CONNECTING:
749                 return "CONNECTING";
750             case BluetoothProfile.STATE_CONNECTED:
751                 return "CONNECTED";
752             case BluetoothProfile.STATE_DISCONNECTING:
753                 return "DISCONNECTING";
754             default:
755                 break;
756         }
757         return Integer.toString(state);
758     }
759 
audioStateToString(int state)760     private static String audioStateToString(int state) {
761         switch (state) {
762             case BluetoothA2dp.STATE_PLAYING:
763                 return "PLAYING";
764             case BluetoothA2dp.STATE_NOT_PLAYING:
765                 return "NOT_PLAYING";
766             default:
767                 break;
768         }
769         return Integer.toString(state);
770     }
771 
dump(StringBuilder sb)772     public void dump(StringBuilder sb) {
773         boolean isActive = Objects.equals(mDevice, mA2dpService.getActiveDevice());
774         ProfileService.println(sb,
775                 "=== A2dpStateMachine for " + mDevice + (isActive ? " (Active) ===" : " ==="));
776         ProfileService.println(sb,
777                 "  getConnectionPolicy: " + mA2dpService.getConnectionPolicy(mDevice));
778         ProfileService.println(sb, "  mConnectionState: " + profileStateToString(mConnectionState)
779                 + ", mLastConnectionState: " + profileStateToString(mLastConnectionState));
780         ProfileService.println(sb, "  mIsPlaying: " + mIsPlaying);
781         ProfileService.println(sb,
782                 "  getSupportsOptionalCodecs: " + mA2dpService.getSupportsOptionalCodecs(mDevice)
783                 + ", getOptionalCodecsEnabled: " + mA2dpService.getOptionalCodecsEnabled(mDevice));
784         synchronized (this) {
785             if (mCodecStatus != null) {
786                 ProfileService.println(sb, "  mCodecConfig: " + mCodecStatus.getCodecConfig());
787                 ProfileService.println(sb, "  mCodecsSelectableCapabilities:");
788                 for (BluetoothCodecConfig config : mCodecStatus.getCodecsSelectableCapabilities()) {
789                     ProfileService.println(sb, "    " + config);
790                 }
791             }
792         }
793         ProfileService.println(sb, "  StateMachine: " + this.toString());
794         // Dump the state machine logs
795         StringWriter stringWriter = new StringWriter();
796         PrintWriter printWriter = new PrintWriter(stringWriter);
797         super.dump(new FileDescriptor(), printWriter, new String[]{});
798         printWriter.flush();
799         stringWriter.flush();
800         ProfileService.println(sb, "  StateMachineLog:");
801         Scanner scanner = new Scanner(stringWriter.toString());
802         while (scanner.hasNextLine()) {
803             String line = scanner.nextLine();
804             ProfileService.println(sb, "    " + line);
805         }
806         scanner.close();
807     }
808 
809     @Override
log(String msg)810     protected void log(String msg) {
811         if (DBG) {
812             super.log(msg);
813         }
814     }
815 }
816