• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.internal.util.IState;
63 import com.android.internal.util.State;
64 import com.android.internal.util.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 = true;
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             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                     }
228                     if (DBG) {
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             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             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, ProfileService.BLUETOOTH_PERM);
349     }
350 
disconnect(BluetoothDevice device)351     public void disconnect(BluetoothDevice device) {
352         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);
437         ProfileService.println(sb, "StateMachine: " + this.toString());
438     }
439 }
440