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