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