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.bluetooth.BluetoothUuid;
48import android.content.BroadcastReceiver;
49import android.content.Context;
50import android.content.Intent;
51import android.content.IntentFilter;
52import android.os.HandlerThread;
53import android.os.Message;
54import android.os.ParcelUuid;
55import android.os.Process;
56import android.os.UserManager;
57import android.util.Log;
58
59import com.android.bluetooth.btservice.ProfileService;
60import com.android.bluetooth.R;
61import com.android.internal.util.IState;
62import com.android.internal.util.State;
63import com.android.internal.util.StateMachine;
64
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_DISCONNECT = 2;
74    private static final int MSG_SDP_COMPLETE = 9;
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 = 10000;
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 final BluetoothDevice mCurrentDevice;
97    private PbapClientService mService;
98    private PbapClientConnectionHandler mConnectionHandler;
99    private HandlerThread mHandlerThread = null;
100    private UserManager mUserManager = null;
101
102    // mMostRecentState maintains previous state for broadcasting transitions.
103    private int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
104
105    PbapClientStateMachine(PbapClientService svc, BluetoothDevice device) {
106        super(TAG);
107
108        mService = svc;
109        mCurrentDevice = device;
110        mLock = new Object();
111        mUserManager = UserManager.get(mService);
112        mDisconnected = new Disconnected();
113        mConnecting = new Connecting();
114        mDisconnecting = new Disconnecting();
115        mConnected = new Connected();
116
117        addState(mDisconnected);
118        addState(mConnecting);
119        addState(mDisconnecting);
120        addState(mConnected);
121
122        setInitialState(mConnecting);
123    }
124
125    class Disconnected extends State {
126        @Override
127        public void enter() {
128            Log.d(TAG, "Enter Disconnected: " + getCurrentMessage().what);
129            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
130                    BluetoothProfile.STATE_DISCONNECTED);
131            mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
132            quit();
133        }
134    }
135
136    class Connecting extends State {
137        private SDPBroadcastReceiver mSdpReceiver;
138
139        @Override
140        public void enter() {
141            if (DBG) Log.d(TAG, "Enter Connecting: " + getCurrentMessage().what);
142            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
143                    BluetoothProfile.STATE_CONNECTING);
144            mSdpReceiver = new SDPBroadcastReceiver();
145            mSdpReceiver.register();
146            mCurrentDevice.sdpSearch(BluetoothUuid.PBAP_PSE);
147            mMostRecentState = BluetoothProfile.STATE_CONNECTING;
148
149            // Create a separate handler instance and thread for performing
150            // connect/download/disconnect operations as they may be time consuming and error prone.
151            mHandlerThread = new HandlerThread("PBAP PCE handler",
152                    Process.THREAD_PRIORITY_BACKGROUND);
153            mHandlerThread.start();
154            mConnectionHandler = new PbapClientConnectionHandler.Builder()
155                                         .setLooper(mHandlerThread.getLooper())
156                                         .setContext(mService)
157                                         .setClientSM(PbapClientStateMachine.this)
158                                         .setRemoteDevice(mCurrentDevice)
159                                         .build();
160
161            sendMessageDelayed(MSG_CONNECT_TIMEOUT, CONNECT_TIMEOUT);
162        }
163
164        @Override
165        public boolean processMessage(Message message) {
166            if (DBG) Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
167            switch (message.what) {
168                case MSG_DISCONNECT:
169                    if (message.obj instanceof BluetoothDevice
170                            && message.obj.equals(mCurrentDevice)) {
171                        removeMessages(MSG_CONNECT_TIMEOUT);
172                        transitionTo(mDisconnecting);
173                    }
174                    break;
175
176                case MSG_CONNECTION_COMPLETE:
177                    removeMessages(MSG_CONNECT_TIMEOUT);
178                    transitionTo(mConnected);
179                    break;
180
181                case MSG_CONNECTION_FAILED:
182                case MSG_CONNECT_TIMEOUT:
183                    removeMessages(MSG_CONNECT_TIMEOUT);
184                    transitionTo(mDisconnecting);
185                    break;
186
187                case MSG_SDP_COMPLETE:
188                    mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_CONNECT,
189                            message.obj).sendToTarget();
190                    break;
191
192                default:
193                    Log.w(TAG, "Received unexpected message while Connecting");
194                    return NOT_HANDLED;
195            }
196            return HANDLED;
197        }
198
199        @Override
200        public void exit() {
201            mSdpReceiver.unregister();
202            mSdpReceiver = null;
203        }
204
205        private class SDPBroadcastReceiver extends BroadcastReceiver {
206            @Override
207            public void onReceive(Context context, Intent intent) {
208                String action = intent.getAction();
209                if (DBG) Log.v(TAG, "onReceive" + action);
210                if (action.equals(BluetoothDevice.ACTION_SDP_RECORD)) {
211                    BluetoothDevice device = intent.getParcelableExtra(
212                            BluetoothDevice.EXTRA_DEVICE);
213                    if (!device.equals(getDevice())) {
214                        Log.w(TAG, "SDP Record fetched for different device - Ignore");
215                        return;
216                    }
217                    ParcelUuid uuid = intent.getParcelableExtra(BluetoothDevice.EXTRA_UUID);
218                    if (DBG) Log.v(TAG, "Received UUID: " + uuid.toString());
219                    if (DBG) Log.v(TAG, "expected UUID: " +
220                            BluetoothUuid.PBAP_PSE.toString());
221                    if (uuid.equals(BluetoothUuid.PBAP_PSE)) {
222                        sendMessage(MSG_SDP_COMPLETE, intent
223                                .getParcelableExtra(BluetoothDevice.EXTRA_SDP_RECORD));
224                    }
225                }
226            }
227
228            public void register() {
229                IntentFilter filter = new IntentFilter();
230                filter.addAction(BluetoothDevice.ACTION_SDP_RECORD);
231                mService.registerReceiver(this, filter);
232            }
233
234            public void unregister() {
235                mService.unregisterReceiver(this);
236            }
237        }
238    }
239
240    class Disconnecting extends State {
241        @Override
242        public void enter() {
243            Log.d(TAG, "Enter Disconnecting: " + getCurrentMessage().what);
244            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
245                    BluetoothProfile.STATE_DISCONNECTING);
246            mMostRecentState = BluetoothProfile.STATE_DISCONNECTING;
247            mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DISCONNECT)
248                    .sendToTarget();
249            sendMessageDelayed(MSG_DISCONNECT_TIMEOUT, DISCONNECT_TIMEOUT);
250        }
251
252        @Override
253        public boolean processMessage(Message message) {
254            if (DBG) Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
255            switch (message.what) {
256                case MSG_CONNECTION_CLOSED:
257                    removeMessages(MSG_DISCONNECT_TIMEOUT);
258                    mHandlerThread.quitSafely();
259                    transitionTo(mDisconnected);
260                    break;
261
262                case MSG_DISCONNECT:
263                    deferMessage(message);
264                    break;
265
266                case MSG_DISCONNECT_TIMEOUT:
267                    Log.w(TAG, "Disconnect Timeout, Forcing");
268                    mConnectionHandler.abort();
269                    break;
270
271                case MSG_RESUME_DOWNLOAD:
272                    // Do nothing.
273                    break;
274
275                default:
276                    Log.w(TAG, "Received unexpected message while Disconnecting");
277                    return NOT_HANDLED;
278            }
279            return HANDLED;
280        }
281    }
282
283    class Connected extends State {
284        @Override
285        public void enter() {
286            Log.d(TAG, "Enter Connected: " + getCurrentMessage().what);
287            onConnectionStateChanged(mCurrentDevice, mMostRecentState,
288                    BluetoothProfile.STATE_CONNECTED);
289            mMostRecentState = BluetoothProfile.STATE_CONNECTED;
290            if (mUserManager.isUserUnlocked()) {
291                mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
292                        .sendToTarget();
293            }
294        }
295
296        @Override
297        public boolean processMessage(Message message) {
298            if (DBG) Log.d(TAG, "Processing MSG " + message.what + " from " + this.getName());
299            switch (message.what) {
300                case MSG_DISCONNECT:
301                    if ((message.obj instanceof BluetoothDevice) &&
302                            ((BluetoothDevice) message.obj).equals(mCurrentDevice)) {
303                        transitionTo(mDisconnecting);
304                    }
305                    break;
306
307                case MSG_RESUME_DOWNLOAD:
308                    mConnectionHandler.obtainMessage(PbapClientConnectionHandler.MSG_DOWNLOAD)
309                            .sendToTarget();
310                    break;
311
312                default:
313                    Log.w(TAG, "Received unexpected message while Connected");
314                    return NOT_HANDLED;
315            }
316            return HANDLED;
317        }
318    }
319
320    private void onConnectionStateChanged(BluetoothDevice device, int prevState, int state) {
321        if (device == null) {
322            Log.w(TAG, "onConnectionStateChanged with invalid device");
323            return;
324        }
325        Log.d(TAG, "Connection state " + device + ": " + prevState + "->" + state);
326        Intent intent = new Intent(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
327        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
328        intent.putExtra(BluetoothProfile.EXTRA_STATE, state);
329        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
330        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
331        mService.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
332    }
333
334    public void disconnect(BluetoothDevice device) {
335        Log.d(TAG, "Disconnect Request " + device);
336        sendMessage(MSG_DISCONNECT, device);
337    }
338
339    public void resumeDownload() {
340        sendMessage(MSG_RESUME_DOWNLOAD);
341    }
342
343    void doQuit() {
344        if (mHandlerThread != null) {
345            mHandlerThread.quitSafely();
346        }
347        quitNow();
348    }
349
350    @Override
351    protected void onQuitting() {
352        mService.cleanupDevice(mCurrentDevice);
353    }
354
355    public int getConnectionState() {
356        IState currentState = getCurrentState();
357        if (currentState instanceof Disconnected) {
358            return BluetoothProfile.STATE_DISCONNECTED;
359        } else if (currentState instanceof Connecting) {
360            return BluetoothProfile.STATE_CONNECTING;
361        } else if (currentState instanceof Connected) {
362            return BluetoothProfile.STATE_CONNECTED;
363        } else if (currentState instanceof Disconnecting) {
364            return BluetoothProfile.STATE_DISCONNECTING;
365        }
366        Log.w(TAG, "Unknown State");
367        return BluetoothProfile.STATE_DISCONNECTED;
368    }
369
370    public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
371        int clientState;
372        BluetoothDevice currentDevice;
373        synchronized (mLock) {
374            clientState = getConnectionState();
375            currentDevice = getDevice();
376        }
377        List<BluetoothDevice> deviceList = new ArrayList<BluetoothDevice>();
378        for (int state : states) {
379            if (clientState == state) {
380                if (currentDevice != null) {
381                    deviceList.add(currentDevice);
382                }
383            }
384        }
385        return deviceList;
386    }
387
388    public int getConnectionState(BluetoothDevice device) {
389        if (device == null) {
390            return BluetoothProfile.STATE_DISCONNECTED;
391        }
392        synchronized (mLock) {
393            if (device.equals(mCurrentDevice)) {
394                return getConnectionState();
395            }
396        }
397        return BluetoothProfile.STATE_DISCONNECTED;
398    }
399
400
401    public BluetoothDevice getDevice() {
402        /*
403         * Disconnected is the only state where device can change, and to prevent the race
404         * condition of reporting a valid device while disconnected fix the report here.  Note that
405         * Synchronization of the state and device is not possible with current state machine
406         * desingn since the actual Transition happens sometime after the transitionTo method.
407         */
408        if (getCurrentState() instanceof Disconnected) {
409            return null;
410        }
411        return mCurrentDevice;
412    }
413
414    Context getContext() {
415        return mService;
416    }
417
418    public void dump(StringBuilder sb) {
419        ProfileService.println(sb, "mCurrentDevice: " + mCurrentDevice);
420        ProfileService.println(sb, "StateMachine: " + this.toString());
421    }
422}
423