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