1 /*
2  * Copyright 2020 HIMSA II K/S - www.himsa.com.
3  * Represented by EHIMA - www.ehima.com
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /**
19  * Bluetooth LeAudio StateMachine. There is one instance per remote device's ASE.
20  *  - "Disconnected" and "Connected" are steady states.
21  *  - "Connecting" and "Disconnecting" are transient states until the
22  *     connection / disconnection is completed.
23  *
24  *
25  *                        (Disconnected)
26  *                           |       ^
27  *                   CONNECT |       | DISCONNECTED
28  *                           V       |
29  *                 (Connecting)<--->(Disconnecting)
30  *                           |       ^
31  *                 CONNECTED |       | DISCONNECT
32  *                           V       |
33  *                          (Connected)
34  * NOTES:
35  *  - If state machine is in "Connecting" state and the remote device sends
36  *    DISCONNECT request, the state machine transitions to "Disconnecting" state.
37  *  - Similarly, if the state machine is in "Disconnecting" state and the remote device
38  *    sends CONNECT request, the state machine transitions to "Connecting" state.
39  *
40  *                    DISCONNECT
41  *    (Connecting) ---------------> (Disconnecting)
42  *                 <---------------
43  *                      CONNECT
44  *
45  */
46 
47 package com.android.bluetooth.le_audio;
48 
49 import android.bluetooth.BluetoothDevice;
50 import android.bluetooth.BluetoothLeAudio;
51 import android.bluetooth.BluetoothProfile;
52 import android.content.Intent;
53 import android.os.Looper;
54 import android.os.Message;
55 import android.util.Log;
56 import static android.Manifest.permission.BLUETOOTH_CONNECT;
57 
58 import android.annotation.RequiresPermission;
59 
60 import com.android.bluetooth.Utils;
61 import com.android.bluetooth.btservice.ProfileService;
62 import com.android.internal.annotations.VisibleForTesting;
63 import com.android.internal.util.State;
64 import com.android.internal.util.StateMachine;
65 
66 final class LeAudioStateMachine extends StateMachine {
67     private static final boolean DBG = false;
68     private static final String TAG = "LeAudioStateMachine";
69 
70     static final int CONNECT = 1;
71     static final int DISCONNECT = 2;
72     @VisibleForTesting
73     static final int STACK_EVENT = 101;
74     private static final int CONNECT_TIMEOUT = 201;
75 
76     @VisibleForTesting
77     static int sConnectTimeoutMs = 30000;        // 30s
78 
79     private Disconnected mDisconnected;
80     private Connecting mConnecting;
81     private Disconnecting mDisconnecting;
82     private Connected mConnected;
83 
84     private int mLastConnectionState = -1;
85 
86     private LeAudioService mService;
87     private LeAudioNativeInterface mNativeInterface;
88 
89     private final BluetoothDevice mDevice;
90 
LeAudioStateMachine(BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper)91     LeAudioStateMachine(BluetoothDevice device, LeAudioService svc,
92             LeAudioNativeInterface nativeInterface, Looper looper) {
93         super(TAG, looper);
94         mDevice = device;
95         mService = svc;
96         mNativeInterface = nativeInterface;
97 
98         mDisconnected = new Disconnected();
99         mConnecting = new Connecting();
100         mDisconnecting = new Disconnecting();
101         mConnected = new Connected();
102 
103         addState(mDisconnected);
104         addState(mConnecting);
105         addState(mDisconnecting);
106         addState(mConnected);
107 
108         setInitialState(mDisconnected);
109     }
110 
make(BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper)111     static LeAudioStateMachine make(BluetoothDevice device, LeAudioService svc,
112             LeAudioNativeInterface nativeInterface, Looper looper) {
113         Log.i(TAG, "make for device");
114         LeAudioStateMachine LeAudioSm = new LeAudioStateMachine(device, svc, nativeInterface, looper);
115         LeAudioSm.start();
116         return LeAudioSm;
117     }
118 
doQuit()119     public void doQuit() {
120         log("doQuit for device " + mDevice);
121         quitNow();
122     }
123 
cleanup()124     public void cleanup() {
125         log("cleanup for device " + mDevice);
126     }
127 
128     @VisibleForTesting
129     class Disconnected extends State {
130         @Override
enter()131         public void enter() {
132             Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString(
133                     getCurrentMessage().what));
134 
135             removeDeferredMessages(DISCONNECT);
136 
137             if (mLastConnectionState != -1) {
138                 // Don't broadcast during startup
139                 broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED,
140                         mLastConnectionState);
141             }
142         }
143 
144         @Override
exit()145         public void exit() {
146             log("Exit Disconnected(" + mDevice + "): " + messageWhatToString(
147                     getCurrentMessage().what));
148             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
149         }
150 
151         @Override
processMessage(Message message)152         public boolean processMessage(Message message) {
153             log("Disconnected process message(" + mDevice + "): " + messageWhatToString(
154                     message.what));
155 
156             switch (message.what) {
157                 case CONNECT:
158                     int groupId = message.arg1;
159                     log("Connecting to " + mDevice + " group " + groupId);
160                     if (!mNativeInterface.connectLeAudio(mDevice)) {
161                         Log.e(TAG, "Disconnected: error connecting to " + mDevice);
162                         break;
163                     }
164                     if (mService.okToConnect(mDevice)) {
165                         transitionTo(mConnecting);
166                     } else {
167                         // Reject the request and stay in Disconnected state
168                         Log.w(TAG, "Outgoing LeAudio Connecting request rejected: " + mDevice);
169                     }
170                     break;
171                 case DISCONNECT:
172                     Log.d(TAG, "Disconnected: " + mDevice);
173                     mNativeInterface.disconnectLeAudio(mDevice);
174                     break;
175                 case STACK_EVENT:
176                     LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
177                     if (DBG) {
178                         Log.d(TAG, "Disconnected: stack event: " + event);
179                     }
180                     if (!mDevice.equals(event.device)) {
181                         Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
182                     }
183                     switch (event.type) {
184                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
185                             processConnectionEvent(event.valueInt1, event.valueInt2);
186                             break;
187                         default:
188                             Log.e(TAG, "Disconnected: ignoring stack event: " + event);
189                             break;
190                     }
191                     break;
192                 default:
193                     return NOT_HANDLED;
194             }
195             return HANDLED;
196         }
197 
198         // in Disconnected state
processConnectionEvent(int state, int groupId)199         private void processConnectionEvent(int state, int groupId) {
200             switch (state) {
201                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
202                     Log.w(TAG, "Ignore LeAudio DISCONNECTED event: " + mDevice);
203                     break;
204                 case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
205                     if (mService.okToConnect(mDevice)) {
206                         Log.i(TAG, "Incoming LeAudio Connecting request accepted: " + mDevice);
207                         transitionTo(mConnecting);
208                     } else {
209                         // Reject the connection and stay in Disconnected state itself
210                         Log.w(TAG, "Incoming LeAudio Connecting request rejected: " + mDevice);
211                         mNativeInterface.disconnectLeAudio(mDevice);
212                     }
213                     break;
214                 case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
215                     Log.w(TAG, "LeAudio Connected from Disconnected state: " + mDevice);
216                     if (mService.okToConnect(mDevice)) {
217                         Log.i(TAG, "Incoming LeAudio Connected request accepted: " + mDevice);
218                         transitionTo(mConnected);
219                     } else {
220                         // Reject the connection and stay in Disconnected state itself
221                         Log.w(TAG, "Incoming LeAudio Connected request rejected: " + mDevice);
222                         mNativeInterface.disconnectLeAudio(mDevice);
223                     }
224                     break;
225                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
226                     Log.w(TAG, "Ignore LeAudio DISCONNECTING event: " + mDevice);
227                     break;
228                 default:
229                     Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice);
230                     break;
231             }
232         }
233     }
234 
235     @VisibleForTesting
236     class Connecting extends State {
237         @Override
enter()238         public void enter() {
239             Log.i(TAG, "Enter Connecting(" + mDevice + "): "
240                     + messageWhatToString(getCurrentMessage().what));
241             sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
242             broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState);
243         }
244 
245         @Override
exit()246         public void exit() {
247             log("Exit Connecting(" + mDevice + "): "
248                     + messageWhatToString(getCurrentMessage().what));
249             mLastConnectionState = BluetoothProfile.STATE_CONNECTING;
250             removeMessages(CONNECT_TIMEOUT);
251         }
252 
253         @Override
processMessage(Message message)254         public boolean processMessage(Message message) {
255             log("Connecting process message(" + mDevice + "): "
256                     + messageWhatToString(message.what));
257 
258             switch (message.what) {
259                 case CONNECT:
260                     deferMessage(message);
261                     break;
262                 case CONNECT_TIMEOUT:
263                     Log.w(TAG, "Connecting connection timeout: " + mDevice);
264                     mNativeInterface.disconnectLeAudio(mDevice);
265                     LeAudioStackEvent disconnectEvent =
266                             new LeAudioStackEvent(
267                                     LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
268                     disconnectEvent.device = mDevice;
269                     disconnectEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
270                     sendMessage(STACK_EVENT, disconnectEvent);
271                     break;
272                 case DISCONNECT:
273                     log("Connecting: connection canceled to " + mDevice);
274                     mNativeInterface.disconnectLeAudio(mDevice);
275                     transitionTo(mDisconnected);
276                     break;
277                 case STACK_EVENT:
278                     LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
279                     log("Connecting: stack event: " + event);
280                     if (!mDevice.equals(event.device)) {
281                         Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
282                     }
283                     switch (event.type) {
284                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
285                             processConnectionEvent(event.valueInt1, event.valueInt2);
286                             break;
287                         default:
288                             Log.e(TAG, "Connecting: ignoring stack event: " + event);
289                             break;
290                     }
291                     break;
292                 default:
293                     return NOT_HANDLED;
294             }
295             return HANDLED;
296         }
297 
298         // in Connecting state
processConnectionEvent(int state, int groupId)299         private void processConnectionEvent(int state, int groupId) {
300             switch (state) {
301                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
302                     Log.w(TAG, "Connecting device disconnected: " + mDevice);
303                     transitionTo(mDisconnected);
304                     break;
305                 case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
306                     transitionTo(mConnected);
307                     break;
308                 case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
309                     break;
310                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
311                     Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice);
312                     transitionTo(mDisconnecting);
313                     break;
314                 default:
315                     Log.e(TAG, "Incorrect state: " + state);
316                     break;
317             }
318         }
319     }
320 
321     @VisibleForTesting
322     class Disconnecting extends State {
323         @Override
enter()324         public void enter() {
325             Log.i(TAG, "Enter Disconnecting(" + mDevice + "): "
326                     + messageWhatToString(getCurrentMessage().what));
327             sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
328             broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState);
329         }
330 
331         @Override
exit()332         public void exit() {
333             log("Exit Disconnecting(" + mDevice + "): "
334                     + messageWhatToString(getCurrentMessage().what));
335             mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING;
336             removeMessages(CONNECT_TIMEOUT);
337         }
338 
339         @Override
processMessage(Message message)340         public boolean processMessage(Message message) {
341             log("Disconnecting process message(" + mDevice + "): "
342                     + messageWhatToString(message.what));
343 
344             switch (message.what) {
345                 case CONNECT:
346                     deferMessage(message);
347                     break;
348                 case CONNECT_TIMEOUT: {
349                     Log.w(TAG, "Disconnecting connection timeout: " + mDevice);
350                     mNativeInterface.disconnectLeAudio(mDevice);
351                     LeAudioStackEvent disconnectEvent =
352                             new LeAudioStackEvent(
353                                     LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
354                     disconnectEvent.device = mDevice;
355                     disconnectEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
356                     sendMessage(STACK_EVENT, disconnectEvent);
357                     break;
358                 }
359                 case DISCONNECT:
360                     deferMessage(message);
361                     break;
362                 case STACK_EVENT:
363                     LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
364                     log("Disconnecting: stack event: " + event);
365                     if (!mDevice.equals(event.device)) {
366                         Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
367                     }
368                     switch (event.type) {
369                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
370                             processConnectionEvent(event.valueInt1, event.valueInt2);
371                             break;
372                         default:
373                             Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
374                             break;
375                     }
376                     break;
377                 default:
378                     return NOT_HANDLED;
379             }
380             return HANDLED;
381         }
382 
383         // in Disconnecting state
processConnectionEvent(int state, int groupId)384         private void processConnectionEvent(int state, int groupId) {
385             switch (state) {
386                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
387                     Log.i(TAG, "Disconnected: " + mDevice);
388                     transitionTo(mDisconnected);
389                     break;
390                 case LeAudioStackEvent.CONNECTION_STATE_CONNECTED:
391                     if (mService.okToConnect(mDevice)) {
392                         Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice);
393                         transitionTo(mConnected);
394                     } else {
395                         // Reject the connection and stay in Disconnecting state
396                         Log.w(TAG, "Incoming LeAudio Connected request rejected: " + mDevice);
397                         mNativeInterface.disconnectLeAudio(mDevice);
398                     }
399                     break;
400                 case LeAudioStackEvent.CONNECTION_STATE_CONNECTING:
401                     if (mService.okToConnect(mDevice)) {
402                         Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice);
403                         transitionTo(mConnecting);
404                     } else {
405                         // Reject the connection and stay in Disconnecting state
406                         Log.w(TAG, "Incoming LeAudio Connecting request rejected: " + mDevice);
407                         mNativeInterface.disconnectLeAudio(mDevice);
408                     }
409                     break;
410                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
411                     break;
412                 default:
413                     Log.e(TAG, "Incorrect state: " + state);
414                     break;
415             }
416         }
417     }
418 
419     @VisibleForTesting
420     class Connected extends State {
421         @Override
enter()422         public void enter() {
423             Log.i(TAG, "Enter Connected(" + mDevice + "): "
424                     + messageWhatToString(getCurrentMessage().what));
425             removeDeferredMessages(CONNECT);
426             broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState);
427         }
428 
429         @Override
exit()430         public void exit() {
431             log("Exit Connected(" + mDevice + "): "
432                     + messageWhatToString(getCurrentMessage().what));
433             mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
434         }
435 
436         @Override
processMessage(Message message)437         public boolean processMessage(Message message) {
438             log("Connected process message(" + mDevice + "): "
439                     + messageWhatToString(message.what));
440 
441             switch (message.what) {
442                 case CONNECT:
443                     Log.w(TAG, "Connected: CONNECT ignored: " + mDevice);
444                     break;
445                 case DISCONNECT:
446                     log("Disconnecting from " + mDevice);
447                     if (!mNativeInterface.disconnectLeAudio(mDevice)) {
448                         // If error in the native stack, transition directly to Disconnected state.
449                         Log.e(TAG, "Connected: error disconnecting from " + mDevice);
450                         transitionTo(mDisconnected);
451                         break;
452                     }
453                     transitionTo(mDisconnecting);
454                     break;
455                 case STACK_EVENT:
456                     LeAudioStackEvent event = (LeAudioStackEvent) message.obj;
457                     log("Connected: stack event: " + event);
458                     if (!mDevice.equals(event.device)) {
459                         Log.wtfStack(TAG, "Device(" + mDevice + "): event mismatch: " + event);
460                     }
461                     switch (event.type) {
462                         case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
463                             processConnectionEvent(event.valueInt1, event.valueInt2);
464                             break;
465                         default:
466                             Log.e(TAG, "Connected: ignoring stack event: " + event);
467                             break;
468                     }
469                     break;
470                 default:
471                     return NOT_HANDLED;
472             }
473             return HANDLED;
474         }
475 
476         // in Connected state
processConnectionEvent(int state, int groupId)477         private void processConnectionEvent(int state, int groupId) {
478             switch (state) {
479                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
480                     Log.i(TAG, "Disconnected from " + mDevice);
481                     transitionTo(mDisconnected);
482                     break;
483                 case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTING:
484                     Log.i(TAG, "Disconnecting from " + mDevice);
485                     transitionTo(mDisconnecting);
486                     break;
487                 default:
488                     Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state);
489                     break;
490             }
491         }
492     }
493 
getConnectionState()494     int getConnectionState() {
495         String currentState = getCurrentState().getName();
496         switch (currentState) {
497             case "Disconnected":
498                 return BluetoothProfile.STATE_DISCONNECTED;
499             case "Connecting":
500                 return BluetoothProfile.STATE_CONNECTING;
501             case "Connected":
502                 return BluetoothProfile.STATE_CONNECTED;
503             case "Disconnecting":
504                 return BluetoothProfile.STATE_DISCONNECTING;
505             default:
506                 Log.e(TAG, "Bad currentState: " + currentState);
507                 return BluetoothProfile.STATE_DISCONNECTED;
508         }
509     }
510 
getDevice()511     BluetoothDevice getDevice() {
512         return mDevice;
513     }
514 
isConnected()515     synchronized boolean isConnected() {
516         return getCurrentState() == mConnected;
517     }
518 
519     // This method does not check for error condition (newState == prevState)
broadcastConnectionState(int newState, int prevState)520     private void broadcastConnectionState(int newState, int prevState) {
521         log("Connection state " + mDevice + ": " + profileStateToString(prevState)
522                     + "->" + profileStateToString(newState));
523 
524         Intent intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
525         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
526         intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
527         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
528         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
529                         | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
530         mService.sendBroadcast(intent, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions());
531     }
532 
messageWhatToString(int what)533     private static String messageWhatToString(int what) {
534         switch (what) {
535             case CONNECT:
536                 return "CONNECT";
537             case DISCONNECT:
538                 return "DISCONNECT";
539             case STACK_EVENT:
540                 return "STACK_EVENT";
541             case CONNECT_TIMEOUT:
542                 return "CONNECT_TIMEOUT";
543             default:
544                 break;
545         }
546         return Integer.toString(what);
547     }
548 
profileStateToString(int state)549     private static String profileStateToString(int state) {
550         switch (state) {
551             case BluetoothProfile.STATE_DISCONNECTED:
552                 return "DISCONNECTED";
553             case BluetoothProfile.STATE_CONNECTING:
554                 return "CONNECTING";
555             case BluetoothProfile.STATE_CONNECTED:
556                 return "CONNECTED";
557             case BluetoothProfile.STATE_DISCONNECTING:
558                 return "DISCONNECTING";
559             default:
560                 break;
561         }
562         return Integer.toString(state);
563     }
564 
565     @Override
log(String msg)566     protected void log(String msg) {
567         if (DBG) {
568             super.log(msg);
569         }
570     }
571 }
572