1 /*
2  * Copyright (C) 2016 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 Pbap PCE StateMachine
19  *                      (Disconnected)
20  *                           |    ^
21  *                   CONNECT |    | DISCONNECTED
22  *                           V    |
23  *                 (Connecting) (Disconnecting)
24  *                           |    ^
25  *                 CONNECTED |    | DISCONNECT
26  *                           V    |
27  *                        (Connected)
28  *
29  * Valid Transitions:
30  * State + Event -> Transition:
31  *
32  * Disconnected + CONNECT -> Connecting
33  * Connecting + CONNECTED -> Connected
34  * Connecting + TIMEOUT -> Disconnecting
35  * Connecting + DISCONNECT -> Disconnecting
36  * Connected + DISCONNECT -> Disconnecting
37  * Disconnecting + DISCONNECTED -> (Safe) Disconnected
38  * Disconnecting + TIMEOUT -> (Force) Disconnected
39  * Disconnecting + CONNECT : Defer Message
40  *
41  */
42 package com.android.bluetooth.pbapclient;
43 
44 import android.bluetooth.BluetoothDevice;
45 import android.bluetooth.BluetoothPbapClient;
46 import android.bluetooth.BluetoothProfile;
47 import android.bluetooth.BluetoothUuid;
48 import android.content.BroadcastReceiver;
49 import android.content.Context;
50 import android.content.Intent;
51 import android.content.IntentFilter;
52 import android.os.HandlerThread;
53 import android.os.Message;
54 import android.os.ParcelUuid;
55 import android.os.Process;
56 import android.os.UserManager;
57 import android.util.Log;
58 import static android.Manifest.permission.BLUETOOTH_CONNECT;
59 
60 import com.android.bluetooth.BluetoothMetricsProto;
61 import com.android.bluetooth.Utils;
62 import com.android.bluetooth.btservice.MetricsLogger;
63 import com.android.bluetooth.btservice.ProfileService;
64 import com.android.bluetooth.statemachine.IState;
65 import com.android.bluetooth.statemachine.State;
66 import com.android.bluetooth.statemachine.StateMachine;
67 
68 import java.util.ArrayList;
69 import java.util.List;
70 
71 final class PbapClientStateMachine extends StateMachine {
72     private static final boolean DBG = false; //Utils.DBG;
73     private static final String TAG = "PbapClientStateMachine";
74 
75     // Messages for handling connect/disconnect requests.
76     private static final int MSG_DISCONNECT = 2;
77     private static final int MSG_SDP_COMPLETE = 9;
78 
79     // Messages for handling error conditions.
80     private static final int MSG_CONNECT_TIMEOUT = 3;
81     private static final int MSG_DISCONNECT_TIMEOUT = 4;
82 
83     // Messages for feedback from ConnectionHandler.
84     static final int MSG_CONNECTION_COMPLETE = 5;
85     static final int MSG_CONNECTION_FAILED = 6;
86     static final int MSG_CONNECTION_CLOSED = 7;
87     static final int MSG_RESUME_DOWNLOAD = 8;
88 
89     static final int CONNECT_TIMEOUT = 10000;
90     static final int DISCONNECT_TIMEOUT = 3000;
91 
92     private final Object mLock;
93     private State mDisconnected;
94     private State mConnecting;
95     private State mConnected;
96     private State mDisconnecting;
97 
98     // mCurrentDevice may only be changed in Disconnected State.
99     private final BluetoothDevice mCurrentDevice;
100     private PbapClientService mService;
101     private PbapClientConnectionHandler mConnectionHandler;
102     private HandlerThread mHandlerThread = null;
103     private UserManager mUserManager = null;
104 
105     // mMostRecentState maintains previous state for broadcasting transitions.
106     private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
107 
PbapClientStateMachine(PbapClientService svc, BluetoothDevice device)108     PbapClientStateMachine(PbapClientService svc, BluetoothDevice device) {
109         super(TAG);
110 
111         mService = svc;
112         mCurrentDevice = device;
113         mLock = new Object();
114         mUserManager = UserManager.get(mService);
115         mDisconnected = new Disconnected();
116         mConnecting = new Connecting();
117         mDisconnecting = new Disconnecting();
118         mConnected = new Connected();
119 
120         addState(mDisconnected);
121         addState(mConnecting);
122         addState(mDisconnecting);
123         addState(mConnected);
124 
125         setInitialState(mConnecting);
126     }
127 
128     class Disconnected extends State {
129         @Override
enter()130         public void enter() {
131             if (DBG) Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what);
132             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
133                     BluetoothProfile.STATE_DISCONNECTED);
134             mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
135             quit();
136         }
137     }
138 
139     class Connecting extends State {
140         private SDPBroadcastReceiver mSdpReceiver;
141 
142         @Override
enter()143         public void enter() {
144             if (DBG) {
145                 Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what);
146             }
147             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
148                     BluetoothProfile.STATE_CONNECTING);
149             mSdpReceiver = new SDPBroadcastReceiver();
150             mSdpReceiver.register();
151             mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
152             mMostRecentState = BluetoothProfile.STATE_CONNECTING;
153 
154             // Create a separate handler instance and thread for performing
155             // connect/download/disconnect operations as they may be time consuming and error prone.
156             mHandlerThread =
157                     new HandlerThread("PBAP PCE handler", Process.THREAD_PRIORITY_BACKGROUND);
158             mHandlerThread.start();
159             mConnectionHandler =
160                     new PbapClientConnectionHandler.Builder().setLooper(mHandlerThread.getLooper())
161                             .setContext(mService)
162                             .setClientSM(PbapClientStateMachine.this)
163                             .setRemoteDevice(mCurrentDevice)
164                             .build();
165 
166             sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT);
167         }
168 
169         @Override
processMessage(Message message)170         public boolean processMessage(Message message) {
171             if (DBG) {
172                 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
173             }
174             switch (message.what) {
175                 case MSG_DISCONNECT:
176                     if (message.obj instanceof BluetoothDevice && message.obj.equals(
177                             mCurrentDevice)) {
178                         removeMessages(MSG_CONNECT_TIMEOUT);
179                         transitionTo(mDisconnecting);
180                     }
181                     break;
182 
183                 case MSG_CONNECTION_COMPLETE:
184                     removeMessages(MSG_CONNECT_TIMEOUT);
185                     transitionTo(mConnected);
186                     break;
187 
188                 case MSG_CONNECTION_FAILED:
189                 case MSG_CONNECT_TIMEOUT:
190                     removeMessages(MSG_CONNECT_TIMEOUT);
191                     transitionTo(mDisconnecting);
192                     break;
193 
194                 case MSG_SDP_COMPLETE:
195                     mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT,
196                             message.obj).sendToTarget();
197                     break;
198 
199                 default:
200                     Log.w(TAG, "Received unexpected message while Connecting");
201                     return NOT_HANDLED;
202             }
203             return HANDLED;
204         }
205 
206         @Override
exit()207         public void exit() {
208             mSdpReceiver.unregister();
209             mSdpReceiver = null;
210         }
211 
212         private class SDPBroadcastReceiver extends BroadcastReceiver {
213             @Override
onReceive(Context context, Intent intent)214             public void onReceive(Context context, Intent intent) {
215                 String action = intent.getAction();
216                 if (DBG) {
217                     Log.v(TAG, "onReceive" + action);
218                 }
219                 if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
220                     BluetoothDevice device =
221                             intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
222                     if (!device.equals(getDevice())) {
223                         Log.w(TAG, "SDP Record fetched for different device - Ignore");
224                         return;
225                     }
226                     ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
227                     if (DBG) {
228                         Log.v(TAG, "Received UUID: " + uuid.toString());
229                         Log.v(TAG, "expected UUID: " + BluetoothUuid.PBAP_PSE.toString());
230                     }
231                     if (uuid.equals(BluetoothUuid.PBAP_PSE)) {
232                         sendMessage(MSG_SDP_COMPLETE,
233                                 intent.getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD));
234                     }
235                 }
236             }
237 
register()238             public void register() {
239                 IntentFilter filter = new IntentFilter();
240                 filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
241                 mService.registerReceiver(this, filter);
242             }
243 
unregister()244             public void unregister() {
245                 mService.unregisterReceiver(this);
246             }
247         }
248     }
249 
250     class Disconnecting extends State {
251         @Override
enter()252         public void enter() {
253             if (DBG) Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what);
254             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
255                     BluetoothProfile.STATE_DISCONNECTING);
256             mMostRecentState = BluetoothProfile.STATE_DISCONNECTING;
257             mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT)
258                     .sendToTarget();
259             sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT);
260         }
261 
262         @Override
processMessage(Message message)263         public boolean processMessage(Message message) {
264             if (DBG) {
265                 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
266             }
267             switch (message.what) {
268                 case MSG_CONNECTION_CLOSED:
269                     removeMessages(MSG_DISCONNECT_TIMEOUT);
270                     mHandlerThread.quitSafely();
271                     transitionTo(mDisconnected);
272                     break;
273 
274                 case MSG_DISCONNECT:
275                     deferMessage(message);
276                     break;
277 
278                 case MSG_DISCONNECT_TIMEOUT:
279                     Log.w(TAG, "Disconnect Timeout, Forcing");
280                     mConnectionHandler.abort();
281                     break;
282 
283                 case MSG_RESUME_DOWNLOAD:
284                     // Do nothing.
285                     break;
286 
287                 default:
288                     Log.w(TAG, "Received unexpected message while Disconnecting");
289                     return NOT_HANDLED;
290             }
291             return HANDLED;
292         }
293     }
294 
295     class Connected extends State {
296         @Override
enter()297         public void enter() {
298             if (DBG) Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
299             onConnectionStateChanged(mCurrentDevice, mMostRecentState,
300                     BluetoothProfile.STATE_CONNECTED);
301             mMostRecentState = BluetoothProfile.STATE_CONNECTED;
302             if (mUserManager.isUserUnlocked()) {
303                 mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
304                         .sendToTarget();
305             }
306         }
307 
308         @Override
processMessage(Message message)309         public boolean processMessage(Message message) {
310             if (DBG) {
311                 Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
312             }
313             switch (message.what) {
314                 case MSG_DISCONNECT:
315                     if ((message.obj instanceof BluetoothDevice)
316                             && ((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
317                         transitionTo(mDisconnecting);
318                     }
319                     break;
320 
321                 case MSG_RESUME_DOWNLOAD:
322                     mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
323                             .sendToTarget();
324                     break;
325 
326                 default:
327                     Log.w(TAG, "Received unexpected message while Connected");
328                     return NOT_HANDLED;
329             }
330             return HANDLED;
331         }
332     }
333 
onConnectionStateChanged(BluetoothDevice device, int prevState, int state)334     private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
335         if (device == null) {
336             Log.w(TAG, "onConnectionStateChanged with invalid device");
337             return;
338         }
339         if (prevState != state && state == BluetoothProfile.STATE_CONNECTED) {
340             MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.PBAP_CLIENT);
341         }
342         Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state);
343         Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
344         intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
345         intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
346         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
347         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
348         mService.sendBroadcast(intent, BLUETOOTH_CONNECT, Utils.getTempAllowlistBroadcastOptions());
349     }
350 
disconnect(BluetoothDevice device)351     public void disconnect(BluetoothDevice device) {
352         if (DBG) Log.d(TAG, "Disconnect Request " + device);
353         sendMessage(MSG_DISCONNECT, device);
354     }
355 
resumeDownload()356     public void resumeDownload() {
357         sendMessage(MSG_RESUME_DOWNLOAD);
358     }
359 
doQuit()360     void doQuit() {
361         if (mHandlerThread != null) {
362             mHandlerThread.quitSafely();
363         }
364         quitNow();
365     }
366 
367     @Override
onQuitting()368     protected void onQuitting() {
369         mService.cleanupDevice(mCurrentDevice);
370     }
371 
getConnectionState()372     public int getConnectionState() {
373         IState currentState = getCurrentState();
374         if (currentState instanceof Disconnected) {
375             return BluetoothProfile.STATE_DISCONNECTED;
376         } else if (currentState instanceof Connecting) {
377             return BluetoothProfile.STATE_CONNECTING;
378         } else if (currentState instanceof Connected) {
379             return BluetoothProfile.STATE_CONNECTED;
380         } else if (currentState instanceof Disconnecting) {
381             return BluetoothProfile.STATE_DISCONNECTING;
382         }
383         Log.w(TAG, "Unknown State");
384         return BluetoothProfile.STATE_DISCONNECTED;
385     }
386 
getDevicesMatchingConnectionStates(int[] states)387     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
388         int clientState;
389         BluetoothDevice currentDevice;
390         synchronized (mLock) {
391             clientState = getConnectionState();
392             currentDevice = getDevice();
393         }
394         List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
395         for (int state : states) {
396             if (clientState == state) {
397                 if (currentDevice != null) {
398                     deviceList.add(currentDevice);
399                 }
400             }
401         }
402         return deviceList;
403     }
404 
getConnectionState(BluetoothDevice device)405     public int getConnectionState(BluetoothDevice device) {
406         if (device == null) {
407             return BluetoothProfile.STATE_DISCONNECTED;
408         }
409         synchronized (mLock) {
410             if (device.equals(mCurrentDevice)) {
411                 return getConnectionState();
412             }
413         }
414         return BluetoothProfile.STATE_DISCONNECTED;
415     }
416 
417 
getDevice()418     public BluetoothDevice getDevice() {
419         /*
420          * Disconnected is the only state where device can change, and to prevent the race
421          * condition of reporting a valid device while disconnected fix the report here.  Note that
422          * Synchronization of the state and device is not possible with current state machine
423          * desingn since the actual Transition happens sometime after the transitionTo method.
424          */
425         if (getCurrentState() instanceof Disconnected) {
426             return null;
427         }
428         return mCurrentDevice;
429     }
430 
getContext()431     Context getContext() {
432         return mService;
433     }
434 
dump(StringBuilder sb)435     public void dump(StringBuilder sb) {
436         ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice.getAddress() + "("
437                 + Utils.getName(mCurrentDevice) + ") " + this.toString());
438     }
439 }
440