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