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