PbapClientStateMachine.java revision 16159961f4060636b4f778b39ef2d006807fc803
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 */
42package com.android.bluetooth.pbapclient;
43
44import android.accounts.Account;
45import android.accounts.AccountManager;
46import android.bluetooth.BluetoothDevice;
47import android.bluetooth.BluetoothProfile;
48import android.bluetooth.BluetoothPbapClient;
49import android.content.Context;
50import android.content.Intent;
51import android.os.HandlerThread;
52import android.os.Message;
53import android.os.Process;
54import android.os.UserManager;
55import android.provider.CallLog;
56import android.util.Log;
57
58import com.android.bluetooth.btservice.ProfileService;
59import com.android.bluetooth.R;
60import com.android.internal.util.IState;
61import com.android.internal.util.State;
62import com.android.internal.util.StateMachine;
63
64import java.lang.IllegalStateException;
65import java.util.ArrayList;
66import java.util.List;
67
68final class PbapClientStateMachine extends StateMachine {
69    private static final boolean DBG = true;
70    private static final String TAG = "PbapClientStateMachine";
71
72    // Messages for handling connect/disconnect requests.
73    private static final int MSG_CONNECT = 1;
74    private static final int MSG_DISCONNECT = 2;
75
76    // Messages for handling error conditions.
77    private static final int MSG_CONNECT_TIMEOUT = 3;
78    private static final int MSG_DISCONNECT_TIMEOUT = 4;
79
80    // Messages for feedback from ConnectionHandler.
81    static final int MSG_CONNECTION_COMPLETE = 5;
82    static final int MSG_CONNECTION_FAILED = 6;
83    static final int MSG_CONNECTION_CLOSED = 7;
84    static final int MSG_RESUME_DOWNLOAD = 8;
85
86    static final int CONNECT_TIMEOUT = 6000;
87    static final int DISCONNECT_TIMEOUT = 3000;
88
89    private final Object mLock;
90    private State mDisconnected;
91    private State mConnecting;
92    private State mConnected;
93    private State mDisconnecting;
94
95    // mCurrentDevice may only be changed in Disconnected State.
96    private BluetoothDevice mCurrentDevice = null;
97    private PbapClientService mService;
98    private Context mContext;
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
106    PbapClientStateMachine(PbapClientService svc, Context context) {
107        super(TAG);
108
109        mService = svc;
110        mContext = context;
111        mLock = new Object();
112        mUserManager = UserManager.get(mContext);
113        mDisconnected = new Disconnected();
114        mConnecting = new Connecting();
115        mDisconnecting = new Disconnecting();
116        mConnected = new Connected();
117        removeUncleanAccounts();
118
119        addState(mDisconnected);
120        addState(mConnecting);
121        addState(mDisconnecting);
122        addState(mConnected);
123
124        setInitialState(mDisconnected);
125    }
126
127    class Disconnected extends State {
128        @Override
129        public void enter() {
130            Log.d(TAG,"Enter Disconnected: " + getCurrentMessage().what);
131            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
132                    BluetoothProfile.STATE_DISCONNECTED);
133            mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
134            synchronized (mLock) {
135                mCurrentDevice = null;
136            }
137
138        }
139
140        @Override
141        public boolean processMessage(Message message) {
142            if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName());
143            switch (message.what) {
144                case MSG_CONNECT:
145                    if (message.obj instanceof BluetoothDevice) {
146                        synchronized(mLock) {
147                            mCurrentDevice = (BluetoothDevice) message.obj;
148                        }
149                        transitionTo(mConnecting);
150                    } else {
151                        Log.w(TAG,"Received CONNECT without valid device");
152                        throw new IllegalStateException("invalid device");
153                    }
154                    break;
155
156                case MSG_DISCONNECT:
157                    Log.w(TAG,"Received unexpected disconnect while disconnected.");
158                    // It is possible if something crashed for others to think we are connected
159                    // already, just remind them.
160                    if (message.obj instanceof BluetoothDevice) {
161                        onConnectionStateChanged((BluetoothDevice) message.obj,
162                                BluetoothProfile.STATE_DISCONNECTED,
163                                BluetoothProfile.STATE_DISCONNECTED);
164                    }
165                    break;
166
167                case MSG_RESUME_DOWNLOAD:
168                    // Do nothing.
169                    break;
170
171                default:
172                    Log.w(TAG,"Received unexpected message while disconnected.");
173                    return NOT_HANDLED;
174            }
175            return HANDLED;
176        }
177    }
178
179    class Connecting extends State {
180        @Override
181        public void enter() {
182            if (DBG) Log.d(TAG,"Enter Connecting: " + getCurrentMessage().what);
183            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
184                    BluetoothProfile.STATE_CONNECTING);
185            mMostRecentState = BluetoothProfile.STATE_CONNECTING;
186            // Create a seperate handler instance and thread for performing
187            // connect/download/disconnect opperations as they may be timeconsuming and error prone.
188            mHandlerThread = new HandlerThread("PBAP PCE handler",
189                    Process.THREAD_PRIORITY_BACKGROUND);
190            mHandlerThread.start();
191            mConnectionHandler = new PbapClientConnectionHandler(mHandlerThread.getLooper(),
192                mContext, PbapClientStateMachine.this, mCurrentDevice);
193            mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT)
194                    .sendToTarget();
195            sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT);
196        }
197
198        @Override
199        public boolean processMessage(Message message) {
200            if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName());
201            switch (message.what) {
202                case MSG_DISCONNECT:
203                    if (message.obj instanceof BluetoothDevice &&
204                            ((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
205                        removeMessages(MSG_CONNECT_TIMEOUT);
206                        transitionTo(mDisconnecting);
207                    }
208                    break;
209
210                case MSG_CONNECTION_COMPLETE:
211                    removeMessages(MSG_CONNECT_TIMEOUT);
212                    transitionTo(mConnected);
213                    break;
214
215                case MSG_CONNECTION_FAILED:
216                case MSG_CONNECT_TIMEOUT:
217                    removeMessages(MSG_CONNECT_TIMEOUT);
218                    transitionTo(mDisconnecting);
219                    break;
220                case MSG_CONNECT:
221                    Log.w(TAG,"Connecting already in progress");
222                    break;
223
224                case MSG_RESUME_DOWNLOAD:
225                    // Do nothing.
226                    break;
227
228                default:
229                    Log.w(TAG,"Received unexpected message while Connecting");
230                    return NOT_HANDLED;
231            }
232            return HANDLED;
233        }
234    }
235
236    class Disconnecting extends State {
237        @Override
238        public void enter() {
239            Log.d(TAG,"Enter Disconnecting: " + getCurrentMessage().what);
240            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
241                    BluetoothProfile.STATE_DISCONNECTING);
242            mMostRecentState = BluetoothProfile.STATE_DISCONNECTING;
243            mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT)
244                    .sendToTarget();
245            sendMessageDelayed(MSG_DISCONNECT_TIMEOUT,DISCONNECT_TIMEOUT);
246        }
247
248        @Override
249        public boolean processMessage(Message message) {
250            if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName());
251            switch (message.what) {
252                case MSG_CONNECTION_CLOSED:
253                    removeMessages(MSG_DISCONNECT_TIMEOUT);
254                    mHandlerThread.quitSafely();
255                    transitionTo(mDisconnected);
256                    break;
257
258                case MSG_CONNECT:
259                case MSG_DISCONNECT:
260                    deferMessage(message);
261                    break;
262
263                case MSG_DISCONNECT_TIMEOUT:
264                    Log.w(TAG,"Disconnect Timeout, Forcing");
265                    mConnectionHandler.abort();
266                    break;
267
268                case MSG_RESUME_DOWNLOAD:
269                    // Do nothing.
270                    break;
271
272                default:
273                    Log.w(TAG,"Received unexpected message while Disconnecting");
274                    return NOT_HANDLED;
275            }
276            return HANDLED;
277        }
278    }
279
280    class Connected extends State {
281        @Override
282        public void enter() {
283            Log.d(TAG,"Enter Connected: " + getCurrentMessage().what);
284            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
285                    BluetoothProfile.STATE_CONNECTED);
286            mMostRecentState = BluetoothProfile.STATE_CONNECTED;
287            if (mUserManager.isUserUnlocked()) {
288                mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
289                        .sendToTarget();
290            }
291        }
292
293        @Override
294        public boolean processMessage(Message message) {
295            if (DBG) Log.d(TAG,"Processing MSG " + message.what + " from " + this.getName());
296            switch (message.what) {
297                case MSG_CONNECT:
298                    onConnectionStateChanged(mCurrentDevice, BluetoothProfile.STATE_CONNECTED,
299                            BluetoothProfile.STATE_CONNECTED);
300
301
302                    Log.w(TAG,"Received CONNECT while Connected, ignoring");
303                    break;
304
305                case MSG_DISCONNECT:
306                    if ((message.obj instanceof BluetoothDevice) &&
307                           ((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
308                        transitionTo(mDisconnecting);
309                    }
310                    break;
311
312                case MSG_RESUME_DOWNLOAD:
313                    mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
314                    .sendToTarget();
315                break;
316
317                default:
318                    Log.w(TAG,"Received unexpected message while Connected");
319                    return NOT_HANDLED;
320            }
321            return HANDLED;
322        }
323    }
324
325    private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
326        if (device == null) {
327            Log.w(TAG,"onConnectionStateChanged with invalid device");
328            return;
329        }
330        Log.d(TAG,"Connection state " + device + ": " + prevState + "->" + state);
331        Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
332        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
333        intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
334        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
335        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
336        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
337        mService.notifyProfileConnectionStateChanged(device, BluetoothProfile.PBAP_CLIENT, state,
338                prevState);
339    }
340
341    public void connect(BluetoothDevice device) {
342        Log.d(TAG, "Connect Request " + device.getAddress());
343        sendMessage(MSG_CONNECT, device);
344    }
345
346    public void disconnect(BluetoothDevice device) {
347        Log.d(TAG, "Disconnect Request "  + device);
348        sendMessage(MSG_DISCONNECT, device);
349    }
350
351    public void resumeDownload() {
352        removeUncleanAccounts();
353        sendMessage(MSG_RESUME_DOWNLOAD);
354    }
355
356    void doQuit() {
357        removeUncleanAccounts();
358        if (mHandlerThread != null) {
359            mHandlerThread.quitSafely();
360        }
361        quitNow();
362    }
363
364    public int getConnectionState() {
365        IState currentState = getCurrentState();
366        if (currentState instanceof Disconnected) {
367            return BluetoothProfile.STATE_DISCONNECTED;
368        } else if (currentState instanceof Connecting) {
369            return BluetoothProfile.STATE_CONNECTING;
370        } else if (currentState instanceof Connected) {
371            return BluetoothProfile.STATE_CONNECTED;
372        } else if (currentState instanceof Disconnecting) {
373            return BluetoothProfile.STATE_DISCONNECTING;
374        }
375        Log.w(TAG, "Unknown State");
376        return BluetoothProfile.STATE_DISCONNECTED;
377    }
378
379    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
380        int clientState  = -1;
381        BluetoothDevice currentDevice = null;
382        synchronized (mLock) {
383            clientState = getConnectionState();
384            currentDevice = getDevice();
385        }
386        List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
387        for (int state : states) {
388            if (clientState == state) {
389                if (currentDevice != null) {
390                    deviceList.add(currentDevice);
391                }
392            }
393        }
394        return deviceList;
395    }
396
397    public int getConnectionState(BluetoothDevice device) {
398        if (device == null) {
399            return BluetoothProfile.STATE_DISCONNECTED;
400        }
401        synchronized (mLock) {
402            if (device.equals(mCurrentDevice)) {
403                return getConnectionState();
404            }
405        }
406        return BluetoothProfile.STATE_DISCONNECTED;
407    }
408
409
410    public BluetoothDevice getDevice() {
411        /*
412         * Disconnected is the only state where device can change, and to prevent the race
413         * condition of reporting a valid device while disconnected fix the report here.  Note that
414         * Synchronization of the state and device is not possible with current state machine
415         * desingn since the actual Transition happens sometime after the transitionTo method.
416         */
417         if (getCurrentState() instanceof Disconnected) {
418            return null;
419        }
420        return mCurrentDevice;
421    }
422
423    Context getContext() {
424        return mContext;
425    }
426
427    private void removeUncleanAccounts() {
428        // Find all accounts that match the type "pbap" and delete them.
429        AccountManager accountManager = AccountManager.get(mContext);
430        Account[] accounts = accountManager.getAccountsByType(
431                mContext.getString(R.string.pbap_account_type));
432        Log.w(TAG, "Found " + accounts.length + " unclean accounts");
433        for (Account acc : accounts) {
434            Log.w(TAG, "Deleting " + acc);
435            // The device ID is the name of the account.
436            accountManager.removeAccountExplicitly(acc);
437        }
438        mContext.getContentResolver().delete(CallLog.Calls.CONTENT_URI, null, null);
439
440    }
441
442    public void dump(StringBuilder sb) {
443        ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
444        ProfileService.println(sb, "StateMachine: " + this.toString());
445    }
446}
447