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 android.Manifest.permission.BLUETOOTH_CONNECT;
19 
20 import android.annotation.RequiresPermission;
21 import android.bluetooth.BluetoothA2dpSink;
22 import android.bluetooth.BluetoothAudioConfig;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothProfile;
25 import android.content.Intent;
26 import android.media.AudioFormat;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.util.Log;
30 
31 import com.android.bluetooth.BluetoothMetricsProto;
32 import com.android.bluetooth.Utils;
33 import com.android.bluetooth.btservice.MetricsLogger;
34 import com.android.bluetooth.btservice.ProfileService;
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.State;
37 import com.android.internal.util.StateMachine;
38 
39 class A2dpSinkStateMachine extends StateMachine {
40     private static final String TAG = A2dpSinkStateMachine.class.getSimpleName();
41 
42     // 0->99 Events from Outside
43     @VisibleForTesting static final int CONNECT = 1;
44     @VisibleForTesting static final int DISCONNECT = 2;
45 
46     // 100->199 Internal Events
47     @VisibleForTesting static final int CLEANUP = 100;
48     @VisibleForTesting static final int CONNECT_TIMEOUT = 101;
49 
50     // 200->299 Events from Native
51     @VisibleForTesting static final int STACK_EVENT = 200;
52 
53     static final int CONNECT_TIMEOUT_MS = 10000;
54 
55     protected final BluetoothDevice mDevice;
56     protected final byte[] mDeviceAddress;
57     protected final A2dpSinkService mService;
58     protected final A2dpSinkNativeInterface mNativeInterface;
59     protected final Disconnected mDisconnected;
60     protected final Connecting mConnecting;
61     protected final Connected mConnected;
62     protected final Disconnecting mDisconnecting;
63 
64     protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
65     protected BluetoothAudioConfig mAudioConfig = null;
66 
A2dpSinkStateMachine( Looper looper, BluetoothDevice device, A2dpSinkService service, A2dpSinkNativeInterface nativeInterface)67     A2dpSinkStateMachine(
68             Looper looper,
69             BluetoothDevice device,
70             A2dpSinkService service,
71             A2dpSinkNativeInterface nativeInterface) {
72         super(TAG, looper);
73         mDevice = device;
74         mDeviceAddress = Utils.getByteAddress(mDevice);
75         mService = service;
76         mNativeInterface = nativeInterface;
77 
78         mDisconnected = new Disconnected();
79         mConnecting = new Connecting();
80         mConnected = new Connected();
81         mDisconnecting = new Disconnecting();
82 
83         addState(mDisconnected);
84         addState(mConnecting);
85         addState(mConnected);
86         addState(mDisconnecting);
87 
88         setInitialState(mDisconnected);
89         Log.d(TAG, "[" + mDevice + "] State machine created");
90     }
91 
92     /**
93      * Get the current connection state
94      *
95      * @return current State
96      */
getState()97     public int getState() {
98         return mMostRecentState;
99     }
100 
101     /** get current audio config */
getAudioConfig()102     BluetoothAudioConfig getAudioConfig() {
103         return mAudioConfig;
104     }
105 
106     /**
107      * Get the underlying device tracked by this state machine
108      *
109      * @return device in focus
110      */
getDevice()111     public synchronized BluetoothDevice getDevice() {
112         return mDevice;
113     }
114 
115     /** send the Connect command asynchronously */
connect()116     final void connect() {
117         sendMessage(CONNECT);
118     }
119 
120     /** send the Disconnect command asynchronously */
disconnect()121     final void disconnect() {
122         sendMessage(DISCONNECT);
123     }
124 
125     /** send the stack event asynchronously */
onStackEvent(StackEvent event)126     final void onStackEvent(StackEvent event) {
127         sendMessage(STACK_EVENT, event);
128     }
129 
130     /**
131      * Dump the current State Machine to the string builder.
132      *
133      * @param sb output string
134      */
dump(StringBuilder sb)135     public void dump(StringBuilder sb) {
136         ProfileService.println(
137                 sb, "mDevice: " + mDevice + "(" + Utils.getName(mDevice) + ") " + this.toString());
138     }
139 
140     @Override
unhandledMessage(Message msg)141     protected void unhandledMessage(Message msg) {
142         Log.w(
143                 TAG,
144                 "["
145                         + mDevice
146                         + "] unhandledMessage state="
147                         + getCurrentState()
148                         + ", msg.what="
149                         + msg.what);
150     }
151 
152     class Disconnected extends State {
153         @Override
enter()154         public void enter() {
155             Log.d(TAG, "[" + mDevice + "] Enter Disconnected");
156             if (mMostRecentState != BluetoothProfile.STATE_DISCONNECTED) {
157                 sendMessage(CLEANUP);
158             }
159             onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTED);
160         }
161 
162         @Override
processMessage(Message message)163         public boolean processMessage(Message message) {
164             switch (message.what) {
165                 case STACK_EVENT:
166                     processStackEvent((StackEvent) message.obj);
167                     return true;
168                 case CONNECT:
169                     Log.d(TAG, "[" + mDevice + "] Connect");
170                     transitionTo(mConnecting);
171                     return true;
172                 case CLEANUP:
173                     mService.removeStateMachine(A2dpSinkStateMachine.this);
174                     return true;
175             }
176             return false;
177         }
178 
179         @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
processStackEvent(StackEvent event)180         void processStackEvent(StackEvent event) {
181             switch (event.mType) {
182                 case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
183                     switch (event.mState) {
184                         case StackEvent.CONNECTION_STATE_CONNECTING:
185                             if (mService.getConnectionPolicy(mDevice)
186                                     == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
187                                 Log.w(
188                                         TAG,
189                                         "["
190                                                 + mDevice
191                                                 + "] Ignore incoming connection, profile"
192                                                 + " is turned off");
193                                 mNativeInterface.disconnectA2dpSink(mDevice);
194                             } else {
195                                 mConnecting.mIncomingConnection = true;
196                                 transitionTo(mConnecting);
197                             }
198                             break;
199                         case StackEvent.CONNECTION_STATE_CONNECTED:
200                             transitionTo(mConnected);
201                             break;
202                         case StackEvent.CONNECTION_STATE_DISCONNECTED:
203                             sendMessage(CLEANUP);
204                             break;
205                     }
206             }
207         }
208     }
209 
210     class Connecting extends State {
211         boolean mIncomingConnection = false;
212 
213         @Override
enter()214         public void enter() {
215             Log.d(TAG, "[" + mDevice + "] Enter Connecting");
216             onConnectionStateChanged(BluetoothProfile.STATE_CONNECTING);
217             sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
218 
219             if (!mIncomingConnection) {
220                 mNativeInterface.connectA2dpSink(mDevice);
221             }
222 
223             super.enter();
224         }
225 
226         @Override
processMessage(Message message)227         public boolean processMessage(Message message) {
228             switch (message.what) {
229                 case STACK_EVENT:
230                     processStackEvent((StackEvent) message.obj);
231                     return true;
232                 case CONNECT_TIMEOUT:
233                     transitionTo(mDisconnected);
234                     return true;
235                 case DISCONNECT:
236                     Log.d(
237                             TAG,
238                             "["
239                                     + mDevice
240                                     + "] Received disconnect message while connecting."
241                                     + "deferred");
242                     deferMessage(message);
243                     return true;
244             }
245             return false;
246         }
247 
processStackEvent(StackEvent event)248         void processStackEvent(StackEvent event) {
249             switch (event.mType) {
250                 case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
251                     switch (event.mState) {
252                         case StackEvent.CONNECTION_STATE_CONNECTED:
253                             transitionTo(mConnected);
254                             break;
255                         case StackEvent.CONNECTION_STATE_DISCONNECTED:
256                             transitionTo(mDisconnected);
257                             break;
258                     }
259             }
260         }
261 
262         @Override
exit()263         public void exit() {
264             removeMessages(CONNECT_TIMEOUT);
265             mIncomingConnection = false;
266         }
267     }
268 
269     class Connected extends State {
270         @Override
enter()271         public void enter() {
272             Log.d(TAG, "[" + mDevice + "] Enter Connected");
273             onConnectionStateChanged(BluetoothProfile.STATE_CONNECTED);
274         }
275 
276         @Override
processMessage(Message message)277         public boolean processMessage(Message message) {
278             switch (message.what) {
279                 case DISCONNECT:
280                     transitionTo(mDisconnecting);
281                     mNativeInterface.disconnectA2dpSink(mDevice);
282                     return true;
283                 case STACK_EVENT:
284                     processStackEvent((StackEvent) message.obj);
285                     return true;
286             }
287             return false;
288         }
289 
processStackEvent(StackEvent event)290         void processStackEvent(StackEvent event) {
291             switch (event.mType) {
292                 case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
293                     switch (event.mState) {
294                         case StackEvent.CONNECTION_STATE_DISCONNECTING:
295                             transitionTo(mDisconnecting);
296                             break;
297                         case StackEvent.CONNECTION_STATE_DISCONNECTED:
298                             transitionTo(mDisconnected);
299                             break;
300                     }
301                     break;
302                 case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
303                     mAudioConfig =
304                             new BluetoothAudioConfig(
305                                     event.mSampleRate,
306                                     event.mChannelCount,
307                                     AudioFormat.ENCODING_PCM_16BIT);
308                     break;
309             }
310         }
311     }
312 
313     protected class Disconnecting extends State {
314         @Override
enter()315         public void enter() {
316             Log.d(TAG, "[" + mDevice + "] Enter Disconnecting");
317             onConnectionStateChanged(BluetoothProfile.STATE_DISCONNECTING);
318             transitionTo(mDisconnected);
319         }
320     }
321 
onConnectionStateChanged(int currentState)322     protected void onConnectionStateChanged(int currentState) {
323         if (mMostRecentState == currentState) {
324             return;
325         }
326         if (currentState == BluetoothProfile.STATE_CONNECTED) {
327             MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP_SINK);
328         }
329         Log.d(TAG, "[" + mDevice + "] Connection state: " + mMostRecentState + "->" + currentState);
330         Intent intent = new Intent(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
331         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, mMostRecentState);
332         intent.putExtra(BluetoothProfile.EXTRA_STATE, currentState);
333         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
334         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
335         mService.connectionStateChanged(mDevice, mMostRecentState, currentState);
336         mMostRecentState = currentState;
337         mService.sendBroadcast(
338                 intent, BLUETOOTH_CONNECT, Utils.getTempBroadcastOptions().toBundle());
339     }
340 }
341