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