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