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