BluetoothDeviceProfileState.java revision d50ac2966edd9c58d8026a624fd795bf54311158
1/*
2 * Copyright (C) 2010 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
17package android.bluetooth;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.os.Message;
24import android.server.BluetoothA2dpService;
25import android.server.BluetoothService;
26import android.util.Log;
27
28import com.android.internal.util.HierarchicalState;
29import com.android.internal.util.HierarchicalStateMachine;
30
31/**
32 * This class is the Profile connection state machine associated with a remote
33 * device. When the device bonds an instance of this class is created.
34 * This tracks incoming and outgoing connections of all the profiles. Incoming
35 * connections are preferred over outgoing connections and HFP preferred over
36 * A2DP. When the device is unbonded, the instance is removed.
37 *
38 * States:
39 * {@link BondedDevice}: This state represents a bonded device. When in this
40 * state none of the profiles are in transition states.
41 *
42 * {@link OutgoingHandsfree}: Handsfree profile connection is in a transition
43 * state because of a outgoing Connect or Disconnect.
44 *
45 * {@link IncomingHandsfree}: Handsfree profile connection is in a transition
46 * state because of a incoming Connect or Disconnect.
47 *
48 * {@link IncomingA2dp}: A2dp profile connection is in a transition
49 * state because of a incoming Connect or Disconnect.
50 *
51 * {@link OutgoingA2dp}: A2dp profile connection is in a transition
52 * state because of a outgoing Connect or Disconnect.
53 *
54 * Todo(): Write tests for this class, when the Android Mock support is completed.
55 * @hide
56 */
57public final class BluetoothDeviceProfileState extends HierarchicalStateMachine {
58    private static final String TAG = "BluetoothDeviceProfileState";
59    private static final boolean DBG = false;
60
61    public static final int CONNECT_HFP_OUTGOING = 1;
62    public static final int CONNECT_HFP_INCOMING = 2;
63    public static final int CONNECT_A2DP_OUTGOING = 3;
64    public static final int CONNECT_A2DP_INCOMING = 4;
65
66    public static final int DISCONNECT_HFP_OUTGOING = 5;
67    private static final int DISCONNECT_HFP_INCOMING = 6;
68    public static final int DISCONNECT_A2DP_OUTGOING = 7;
69    public static final int DISCONNECT_A2DP_INCOMING = 8;
70    public static final int DISCONNECT_PBAP_OUTGOING = 9;
71
72    public static final int UNPAIR = 100;
73    public static final int AUTO_CONNECT_PROFILES = 101;
74    public static final int TRANSITION_TO_STABLE = 102;
75    public static final int CONNECT_OTHER_PROFILES = 103;
76
77    private static final int AUTO_CONNECT_DELAY = 6000; // 6 secs
78
79    private BondedDevice mBondedDevice = new BondedDevice();
80    private OutgoingHandsfree mOutgoingHandsfree = new OutgoingHandsfree();
81    private IncomingHandsfree mIncomingHandsfree = new IncomingHandsfree();
82    private IncomingA2dp mIncomingA2dp = new IncomingA2dp();
83    private OutgoingA2dp mOutgoingA2dp = new OutgoingA2dp();
84
85    private Context mContext;
86    private BluetoothService mService;
87    private BluetoothA2dpService mA2dpService;
88    private BluetoothHeadset  mHeadsetService;
89    private BluetoothPbap     mPbapService;
90    private boolean mHeadsetServiceConnected;
91    private boolean mPbapServiceConnected;
92
93    private BluetoothDevice mDevice;
94    private int mHeadsetState;
95    private int mA2dpState;
96
97    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
98        @Override
99        public void onReceive(Context context, Intent intent) {
100            String action = intent.getAction();
101            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
102            if (!device.equals(mDevice)) return;
103
104            if (action.equals(BluetoothHeadset.ACTION_STATE_CHANGED)) {
105                int newState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, 0);
106                int oldState = intent.getIntExtra(BluetoothHeadset.EXTRA_PREVIOUS_STATE, 0);
107                int initiator = intent.getIntExtra(
108                    BluetoothHeadset.EXTRA_DISCONNECT_INITIATOR,
109                    BluetoothHeadset.LOCAL_DISCONNECT);
110                mHeadsetState = newState;
111                if (newState == BluetoothHeadset.STATE_DISCONNECTED &&
112                    initiator == BluetoothHeadset.REMOTE_DISCONNECT) {
113                    sendMessage(DISCONNECT_HFP_INCOMING);
114                }
115                if (newState == BluetoothHeadset.STATE_CONNECTED ||
116                    newState == BluetoothHeadset.STATE_DISCONNECTED) {
117                    sendMessage(TRANSITION_TO_STABLE);
118                }
119            } else if (action.equals(BluetoothA2dp.ACTION_SINK_STATE_CHANGED)) {
120                int newState = intent.getIntExtra(BluetoothA2dp.EXTRA_SINK_STATE, 0);
121                int oldState = intent.getIntExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, 0);
122                mA2dpState = newState;
123                if ((oldState == BluetoothA2dp.STATE_CONNECTED ||
124                           oldState == BluetoothA2dp.STATE_PLAYING) &&
125                           newState == BluetoothA2dp.STATE_DISCONNECTED) {
126                    sendMessage(DISCONNECT_A2DP_INCOMING);
127                }
128                if (newState == BluetoothA2dp.STATE_CONNECTED ||
129                    newState == BluetoothA2dp.STATE_DISCONNECTED) {
130                    sendMessage(TRANSITION_TO_STABLE);
131                }
132            } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
133                // This is technically not needed, but we can get stuck sometimes.
134                // For example, if incoming A2DP fails, we are not informed by Bluez
135                sendMessage(TRANSITION_TO_STABLE);
136            }
137      }
138    };
139
140    private boolean isPhoneDocked(BluetoothDevice autoConnectDevice) {
141      // This works only because these broadcast intents are "sticky"
142      Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
143      if (i != null) {
144          int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
145          if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
146              BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
147              if (device != null && autoConnectDevice.equals(device)) {
148                  return true;
149              }
150          }
151      }
152      return false;
153  }
154
155    public BluetoothDeviceProfileState(Context context, String address,
156          BluetoothService service, BluetoothA2dpService a2dpService) {
157        super(address);
158        mContext = context;
159        mDevice = new BluetoothDevice(address);
160        mService = service;
161        mA2dpService = a2dpService;
162
163        addState(mBondedDevice);
164        addState(mOutgoingHandsfree);
165        addState(mIncomingHandsfree);
166        addState(mIncomingA2dp);
167        addState(mOutgoingA2dp);
168        setInitialState(mBondedDevice);
169
170        IntentFilter filter = new IntentFilter();
171        // Fine-grained state broadcasts
172        filter.addAction(BluetoothA2dp.ACTION_SINK_STATE_CHANGED);
173        filter.addAction(BluetoothHeadset.ACTION_STATE_CHANGED);
174        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
175        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
176
177        mContext.registerReceiver(mBroadcastReceiver, filter);
178
179        HeadsetServiceListener l = new HeadsetServiceListener();
180        PbapServiceListener p = new PbapServiceListener();
181    }
182
183    private class HeadsetServiceListener implements BluetoothHeadset.ServiceListener {
184        public HeadsetServiceListener() {
185            mHeadsetService = new BluetoothHeadset(mContext, this);
186        }
187        public void onServiceConnected() {
188            synchronized(BluetoothDeviceProfileState.this) {
189                mHeadsetServiceConnected = true;
190            }
191        }
192        public void onServiceDisconnected() {
193            synchronized(BluetoothDeviceProfileState.this) {
194                mHeadsetServiceConnected = false;
195            }
196        }
197    }
198
199    private class PbapServiceListener implements BluetoothPbap.ServiceListener {
200        public PbapServiceListener() {
201            mPbapService = new BluetoothPbap(mContext, this);
202        }
203        public void onServiceConnected() {
204            synchronized(BluetoothDeviceProfileState.this) {
205                mPbapServiceConnected = true;
206            }
207        }
208        public void onServiceDisconnected() {
209            synchronized(BluetoothDeviceProfileState.this) {
210                mPbapServiceConnected = false;
211            }
212        }
213    }
214
215    private class BondedDevice extends HierarchicalState {
216        @Override
217        protected void enter() {
218            Log.i(TAG, "Entering ACL Connected state with: " + getCurrentMessage().what);
219            Message m = new Message();
220            m.copyFrom(getCurrentMessage());
221            sendMessageAtFrontOfQueue(m);
222        }
223        @Override
224        protected boolean processMessage(Message message) {
225            log("ACL Connected State -> Processing Message: " + message.what);
226            switch(message.what) {
227                case CONNECT_HFP_OUTGOING:
228                case DISCONNECT_HFP_OUTGOING:
229                    transitionTo(mOutgoingHandsfree);
230                    break;
231                case CONNECT_HFP_INCOMING:
232                    transitionTo(mIncomingHandsfree);
233                    break;
234                case DISCONNECT_HFP_INCOMING:
235                    transitionTo(mIncomingHandsfree);
236                    break;
237                case CONNECT_A2DP_OUTGOING:
238                case DISCONNECT_A2DP_OUTGOING:
239                    transitionTo(mOutgoingA2dp);
240                    break;
241                case CONNECT_A2DP_INCOMING:
242                case DISCONNECT_A2DP_INCOMING:
243                    transitionTo(mIncomingA2dp);
244                    break;
245                case DISCONNECT_PBAP_OUTGOING:
246                    processCommand(DISCONNECT_PBAP_OUTGOING);
247                    break;
248                case UNPAIR:
249                    if (mHeadsetState != BluetoothHeadset.STATE_DISCONNECTED) {
250                        sendMessage(DISCONNECT_HFP_OUTGOING);
251                        deferMessage(message);
252                        break;
253                    } else if (mA2dpState != BluetoothA2dp.STATE_DISCONNECTED) {
254                        sendMessage(DISCONNECT_A2DP_OUTGOING);
255                        deferMessage(message);
256                        break;
257                    }
258                    processCommand(UNPAIR);
259                    break;
260                case AUTO_CONNECT_PROFILES:
261                    if (isPhoneDocked(mDevice)) {
262                        // Don't auto connect to docks.
263                        break;
264                    } else if (!mHeadsetServiceConnected) {
265                        deferMessage(message);
266                    } else {
267                        if (mHeadsetService.getPriority(mDevice) ==
268                              BluetoothHeadset.PRIORITY_AUTO_CONNECT &&
269                              !mHeadsetService.isConnected(mDevice)) {
270                            Log.i(TAG, "Headset:Auto Connect Profiles");
271                            mHeadsetService.connectHeadset(mDevice);
272                        }
273                        if (mA2dpService != null &&
274                              mA2dpService.getSinkPriority(mDevice) ==
275                              BluetoothA2dp.PRIORITY_AUTO_CONNECT &&
276                              mA2dpService.getConnectedSinks().length == 0) {
277                            Log.i(TAG, "A2dp:Auto Connect Profiles");
278                            mA2dpService.connectSink(mDevice);
279                        }
280                    }
281                    break;
282                case CONNECT_OTHER_PROFILES:
283                    if (isPhoneDocked(mDevice)) {
284                       break;
285                    }
286                    if (message.arg1 == CONNECT_A2DP_OUTGOING) {
287                        if (mA2dpService != null &&
288                            mA2dpService.getConnectedSinks().length == 0) {
289                            Log.i(TAG, "A2dp:Connect Other Profiles");
290                            mA2dpService.connectSink(mDevice);
291                        }
292                    } else if (message.arg1 == CONNECT_HFP_OUTGOING) {
293                        if (!mHeadsetServiceConnected) {
294                            deferMessage(message);
295                        } else {
296                            if (!mHeadsetService.isConnected(mDevice)) {
297                                Log.i(TAG, "Headset:Connect Other Profiles");
298                                mHeadsetService.connectHeadset(mDevice);
299                            }
300                        }
301                    }
302                    break;
303                case TRANSITION_TO_STABLE:
304                    // ignore.
305                    break;
306                default:
307                    return NOT_HANDLED;
308            }
309            return HANDLED;
310        }
311    }
312
313    private class OutgoingHandsfree extends HierarchicalState {
314        private boolean mStatus = false;
315        private int mCommand;
316
317        @Override
318        protected void enter() {
319            Log.i(TAG, "Entering OutgoingHandsfree state with: " + getCurrentMessage().what);
320            mCommand = getCurrentMessage().what;
321            if (mCommand != CONNECT_HFP_OUTGOING &&
322                mCommand != DISCONNECT_HFP_OUTGOING) {
323                Log.e(TAG, "Error: OutgoingHandsfree state with command:" + mCommand);
324            }
325            mStatus = processCommand(mCommand);
326            if (!mStatus) {
327                sendMessage(TRANSITION_TO_STABLE);
328                mService.sendProfileStateMessage(BluetoothProfileState.HFP,
329                                                 BluetoothProfileState.TRANSITION_TO_STABLE);
330            }
331        }
332
333        @Override
334        protected boolean processMessage(Message message) {
335            log("OutgoingHandsfree State -> Processing Message: " + message.what);
336            Message deferMsg = new Message();
337            int command = message.what;
338            switch(command) {
339                case CONNECT_HFP_OUTGOING:
340                    if (command != mCommand) {
341                        // Disconnect followed by a connect - defer
342                        deferMessage(message);
343                    }
344                    break;
345                case CONNECT_HFP_INCOMING:
346                    if (mCommand == CONNECT_HFP_OUTGOING) {
347                        // Cancel outgoing connect, accept incoming
348                        cancelCommand(CONNECT_HFP_OUTGOING);
349                        transitionTo(mIncomingHandsfree);
350                    } else {
351                        // We have done the disconnect but we are not
352                        // sure which state we are in at this point.
353                        deferMessage(message);
354                    }
355                    break;
356                case CONNECT_A2DP_INCOMING:
357                    // accept incoming A2DP, retry HFP_OUTGOING
358                    transitionTo(mIncomingA2dp);
359
360                    if (mStatus) {
361                        deferMsg.what = mCommand;
362                        deferMessage(deferMsg);
363                    }
364                    break;
365                case CONNECT_A2DP_OUTGOING:
366                    deferMessage(message);
367                    break;
368                case DISCONNECT_HFP_OUTGOING:
369                    if (mCommand == CONNECT_HFP_OUTGOING) {
370                        // Cancel outgoing connect
371                        cancelCommand(CONNECT_HFP_OUTGOING);
372                        processCommand(DISCONNECT_HFP_OUTGOING);
373                    }
374                    // else ignore
375                    break;
376                case DISCONNECT_HFP_INCOMING:
377                    // When this happens the socket would be closed and the headset
378                    // state moved to DISCONNECTED, cancel the outgoing thread.
379                    // if it still is in CONNECTING state
380                    cancelCommand(CONNECT_HFP_OUTGOING);
381                    break;
382                case DISCONNECT_A2DP_OUTGOING:
383                    deferMessage(message);
384                    break;
385                case DISCONNECT_A2DP_INCOMING:
386                    // Bluez will handle the disconnect. If because of this the outgoing
387                    // handsfree connection has failed, then retry.
388                    if (mStatus) {
389                       deferMsg.what = mCommand;
390                       deferMessage(deferMsg);
391                    }
392                    break;
393                case DISCONNECT_PBAP_OUTGOING:
394                case UNPAIR:
395                case AUTO_CONNECT_PROFILES:
396                case CONNECT_OTHER_PROFILES:
397                    deferMessage(message);
398                    break;
399                case TRANSITION_TO_STABLE:
400                    transitionTo(mBondedDevice);
401                    break;
402                default:
403                    return NOT_HANDLED;
404            }
405            return HANDLED;
406        }
407    }
408
409    private class IncomingHandsfree extends HierarchicalState {
410        private boolean mStatus = false;
411        private int mCommand;
412
413        @Override
414        protected void enter() {
415            Log.i(TAG, "Entering IncomingHandsfree state with: " + getCurrentMessage().what);
416            mCommand = getCurrentMessage().what;
417            if (mCommand != CONNECT_HFP_INCOMING &&
418                mCommand != DISCONNECT_HFP_INCOMING) {
419                Log.e(TAG, "Error: IncomingHandsfree state with command:" + mCommand);
420            }
421            mStatus = processCommand(mCommand);
422            if (!mStatus) {
423                sendMessage(TRANSITION_TO_STABLE);
424                mService.sendProfileStateMessage(BluetoothProfileState.HFP,
425                                                 BluetoothProfileState.TRANSITION_TO_STABLE);
426            }
427        }
428
429        @Override
430        protected boolean processMessage(Message message) {
431            log("IncomingHandsfree State -> Processing Message: " + message.what);
432            switch(message.what) {
433                case CONNECT_HFP_OUTGOING:
434                    deferMessage(message);
435                    break;
436                case CONNECT_HFP_INCOMING:
437                    // Ignore
438                    Log.e(TAG, "Error: Incoming connection with a pending incoming connection");
439                    break;
440                case CONNECT_A2DP_INCOMING:
441                    // Serialize the commands.
442                    deferMessage(message);
443                    break;
444                case CONNECT_A2DP_OUTGOING:
445                    deferMessage(message);
446                    break;
447                case DISCONNECT_HFP_OUTGOING:
448                    // We don't know at what state we are in the incoming HFP connection state.
449                    // We can be changing from DISCONNECTED to CONNECTING, or
450                    // from CONNECTING to CONNECTED, so serializing this command is
451                    // the safest option.
452                    deferMessage(message);
453                    break;
454                case DISCONNECT_HFP_INCOMING:
455                    // Nothing to do here, we will already be DISCONNECTED
456                    // by this point.
457                    break;
458                case DISCONNECT_A2DP_OUTGOING:
459                    deferMessage(message);
460                    break;
461                case DISCONNECT_A2DP_INCOMING:
462                    // Bluez handles incoming A2DP disconnect.
463                    // If this causes incoming HFP to fail, it is more of a headset problem
464                    // since both connections are incoming ones.
465                    break;
466                case DISCONNECT_PBAP_OUTGOING:
467                case UNPAIR:
468                case AUTO_CONNECT_PROFILES:
469                case CONNECT_OTHER_PROFILES:
470                    deferMessage(message);
471                    break;
472                case TRANSITION_TO_STABLE:
473                    transitionTo(mBondedDevice);
474                    break;
475                default:
476                    return NOT_HANDLED;
477            }
478            return HANDLED;
479        }
480    }
481
482    private class OutgoingA2dp extends HierarchicalState {
483        private boolean mStatus = false;
484        private int mCommand;
485
486        @Override
487        protected void enter() {
488            Log.i(TAG, "Entering OutgoingA2dp state with: " + getCurrentMessage().what);
489            mCommand = getCurrentMessage().what;
490            if (mCommand != CONNECT_A2DP_OUTGOING &&
491                mCommand != DISCONNECT_A2DP_OUTGOING) {
492                Log.e(TAG, "Error: OutgoingA2DP state with command:" + mCommand);
493            }
494            mStatus = processCommand(mCommand);
495            if (!mStatus) {
496                sendMessage(TRANSITION_TO_STABLE);
497                mService.sendProfileStateMessage(BluetoothProfileState.A2DP,
498                                                 BluetoothProfileState.TRANSITION_TO_STABLE);
499            }
500        }
501
502        @Override
503        protected boolean processMessage(Message message) {
504            log("OutgoingA2dp State->Processing Message: " + message.what);
505            Message deferMsg = new Message();
506            switch(message.what) {
507                case CONNECT_HFP_OUTGOING:
508                    processCommand(CONNECT_HFP_OUTGOING);
509
510                    // Don't cancel A2DP outgoing as there is no guarantee it
511                    // will get canceled.
512                    // It might already be connected but we might not have got the
513                    // A2DP_SINK_STATE_CHANGE. Hence, no point disconnecting here.
514                    // The worst case, the connection will fail, retry.
515                    // The same applies to Disconnecting an A2DP connection.
516                    if (mStatus) {
517                        deferMsg.what = mCommand;
518                        deferMessage(deferMsg);
519                    }
520                    break;
521                case CONNECT_HFP_INCOMING:
522                    processCommand(CONNECT_HFP_INCOMING);
523
524                    // Don't cancel A2DP outgoing as there is no guarantee
525                    // it will get canceled.
526                    // The worst case, the connection will fail, retry.
527                    if (mStatus) {
528                        deferMsg.what = mCommand;
529                        deferMessage(deferMsg);
530                    }
531                    break;
532                case CONNECT_A2DP_INCOMING:
533                    // Bluez will take care of conflicts between incoming and outgoing
534                    // connections.
535                    transitionTo(mIncomingA2dp);
536                    break;
537                case CONNECT_A2DP_OUTGOING:
538                    // Ignore
539                    break;
540                case DISCONNECT_HFP_OUTGOING:
541                    deferMessage(message);
542                    break;
543                case DISCONNECT_HFP_INCOMING:
544                    // At this point, we are already disconnected
545                    // with HFP. Sometimes A2DP connection can
546                    // fail due to the disconnection of HFP. So add a retry
547                    // for the A2DP.
548                    if (mStatus) {
549                        deferMsg.what = mCommand;
550                        deferMessage(deferMsg);
551                    }
552                    break;
553                case DISCONNECT_A2DP_OUTGOING:
554                    deferMessage(message);
555                    break;
556                case DISCONNECT_A2DP_INCOMING:
557                    // Ignore, will be handled by Bluez
558                    break;
559                case DISCONNECT_PBAP_OUTGOING:
560                case UNPAIR:
561                case AUTO_CONNECT_PROFILES:
562                case CONNECT_OTHER_PROFILES:
563                    deferMessage(message);
564                    break;
565                case TRANSITION_TO_STABLE:
566                    transitionTo(mBondedDevice);
567                    break;
568                default:
569                    return NOT_HANDLED;
570            }
571            return HANDLED;
572        }
573    }
574
575    private class IncomingA2dp extends HierarchicalState {
576        private boolean mStatus = false;
577        private int mCommand;
578
579        @Override
580        protected void enter() {
581            Log.i(TAG, "Entering IncomingA2dp state with: " + getCurrentMessage().what);
582            mCommand = getCurrentMessage().what;
583            if (mCommand != CONNECT_A2DP_INCOMING &&
584                mCommand != DISCONNECT_A2DP_INCOMING) {
585                Log.e(TAG, "Error: IncomingA2DP state with command:" + mCommand);
586            }
587            mStatus = processCommand(mCommand);
588            if (!mStatus) {
589                sendMessage(TRANSITION_TO_STABLE);
590                mService.sendProfileStateMessage(BluetoothProfileState.A2DP,
591                                                 BluetoothProfileState.TRANSITION_TO_STABLE);
592            }
593        }
594
595        @Override
596        protected boolean processMessage(Message message) {
597            log("IncomingA2dp State->Processing Message: " + message.what);
598            Message deferMsg = new Message();
599            switch(message.what) {
600                case CONNECT_HFP_OUTGOING:
601                    deferMessage(message);
602                    break;
603                case CONNECT_HFP_INCOMING:
604                    // Shouldn't happen, but serialize the commands.
605                    deferMessage(message);
606                    break;
607                case CONNECT_A2DP_INCOMING:
608                    // ignore
609                    break;
610                case CONNECT_A2DP_OUTGOING:
611                    // Defer message and retry
612                    deferMessage(message);
613                    break;
614                case DISCONNECT_HFP_OUTGOING:
615                    deferMessage(message);
616                    break;
617                case DISCONNECT_HFP_INCOMING:
618                    // Shouldn't happen but if does, we can handle it.
619                    // Depends if the headset can handle it.
620                    // Incoming A2DP will be handled by Bluez, Disconnect HFP
621                    // the socket would have already been closed.
622                    // ignore
623                    break;
624                case DISCONNECT_A2DP_OUTGOING:
625                    deferMessage(message);
626                    break;
627                case DISCONNECT_A2DP_INCOMING:
628                    // Ignore, will be handled by Bluez
629                    break;
630                case DISCONNECT_PBAP_OUTGOING:
631                case UNPAIR:
632                case AUTO_CONNECT_PROFILES:
633                case CONNECT_OTHER_PROFILES:
634                    deferMessage(message);
635                    break;
636                case TRANSITION_TO_STABLE:
637                    transitionTo(mBondedDevice);
638                    break;
639                default:
640                    return NOT_HANDLED;
641            }
642            return HANDLED;
643        }
644    }
645
646
647
648    synchronized void cancelCommand(int command) {
649        if (command == CONNECT_HFP_OUTGOING ) {
650            // Cancel the outgoing thread.
651            if (mHeadsetServiceConnected) {
652                mHeadsetService.cancelConnectThread();
653            }
654            // HeadsetService is down. Phone process most likely crashed.
655            // The thread would have got killed.
656        }
657    }
658
659    synchronized void deferProfileServiceMessage(int command) {
660        Message msg = new Message();
661        msg.what = command;
662        deferMessage(msg);
663    }
664
665    synchronized boolean processCommand(int command) {
666        Log.i(TAG, "Processing command:" + command);
667        switch(command) {
668            case  CONNECT_HFP_OUTGOING:
669                if (mHeadsetService != null) {
670                    return mHeadsetService.connectHeadsetInternal(mDevice);
671                }
672                break;
673            case CONNECT_HFP_INCOMING:
674                if (!mHeadsetServiceConnected) {
675                    deferProfileServiceMessage(command);
676                } else if (mHeadsetState == BluetoothHeadset.STATE_CONNECTING) {
677                    return mHeadsetService.acceptIncomingConnect(mDevice);
678                } else if (mHeadsetState == BluetoothHeadset.STATE_DISCONNECTED) {
679                    handleConnectionOfOtherProfiles(command);
680                    return mHeadsetService.createIncomingConnect(mDevice);
681                }
682                break;
683            case CONNECT_A2DP_OUTGOING:
684                if (mA2dpService != null) {
685                    return mA2dpService.connectSinkInternal(mDevice);
686                }
687                break;
688            case CONNECT_A2DP_INCOMING:
689                handleConnectionOfOtherProfiles(command);
690                // ignore, Bluez takes care
691                return true;
692            case DISCONNECT_HFP_OUTGOING:
693                if (!mHeadsetServiceConnected) {
694                    deferProfileServiceMessage(command);
695                } else {
696                    // Disconnect PBAP
697                    // TODO(): Add PBAP to the state machine.
698                    Message m = new Message();
699                    m.what = DISCONNECT_PBAP_OUTGOING;
700                    deferMessage(m);
701                    if (mHeadsetService.getPriority(mDevice) ==
702                        BluetoothHeadset.PRIORITY_AUTO_CONNECT) {
703                        mHeadsetService.setPriority(mDevice, BluetoothHeadset.PRIORITY_ON);
704                    }
705                    return mHeadsetService.disconnectHeadsetInternal(mDevice);
706                }
707                break;
708            case DISCONNECT_HFP_INCOMING:
709                // ignore
710                return true;
711            case DISCONNECT_A2DP_INCOMING:
712                // ignore
713                return true;
714            case DISCONNECT_A2DP_OUTGOING:
715                if (mA2dpService != null) {
716                    if (mA2dpService.getSinkPriority(mDevice) ==
717                        BluetoothA2dp.PRIORITY_AUTO_CONNECT) {
718                        mA2dpService.setSinkPriority(mDevice, BluetoothHeadset.PRIORITY_ON);
719                    }
720                    return mA2dpService.disconnectSinkInternal(mDevice);
721                }
722                break;
723            case DISCONNECT_PBAP_OUTGOING:
724                if (!mPbapServiceConnected) {
725                    deferProfileServiceMessage(command);
726                } else {
727                    return mPbapService.disconnect();
728                }
729                break;
730            case UNPAIR:
731                return mService.removeBondInternal(mDevice.getAddress());
732            default:
733                Log.e(TAG, "Error: Unknown Command");
734        }
735        return false;
736    }
737
738    private void handleConnectionOfOtherProfiles(int command) {
739        // The white paper recommendations mentions that when there is a
740        // link loss, it is the responsibility of the remote device to connect.
741        // Many connect only 1 profile - and they connect the second profile on
742        // some user action (like play being pressed) and so we need this code.
743        // Auto Connect code only connects to the last connected device - which
744        // is useful in cases like when the phone reboots. But consider the
745        // following case:
746        // User is connected to the car's phone and  A2DP profile.
747        // User comes to the desk  and places the phone in the dock
748        // (or any speaker or music system or even another headset) and thus
749        // gets connected to the A2DP profile.  User goes back to the car.
750        // Ideally the car's system is supposed to send incoming connections
751        // from both Handsfree and A2DP profile. But they don't. The Auto
752        // connect code, will not work here because we only auto connect to the
753        // last connected device for that profile which in this case is the dock.
754        // Now suppose a user is using 2 headsets simultaneously, one for the
755        // phone profile one for the A2DP profile. If this is the use case, we
756        // expect the user to use the preference to turn off the A2DP profile in
757        // the Settings screen for the first headset. Else, after link loss,
758        // there can be an incoming connection from the first headset which
759        // might result in the connection of the A2DP profile (if the second
760        // headset is slower) and thus the A2DP profile on the second headset
761        // will never get connected.
762        //
763        // TODO(): Handle other profiles here.
764        switch (command) {
765            case CONNECT_HFP_INCOMING:
766                // Connect A2DP if there is no incoming connection
767                // If the priority is OFF - don't auto connect.
768                // If the priority is AUTO_CONNECT, auto connect code takes care.
769                if (mA2dpService.getSinkPriority(mDevice) == BluetoothA2dp.PRIORITY_ON) {
770                    Message msg = new Message();
771                    msg.what = CONNECT_OTHER_PROFILES;
772                    msg.arg1 = CONNECT_A2DP_OUTGOING;
773                    sendMessageDelayed(msg, AUTO_CONNECT_DELAY);
774                }
775                break;
776            case CONNECT_A2DP_INCOMING:
777                // This is again against spec. HFP incoming connections should be made
778                // before A2DP, so we should not hit this case. But many devices
779                // don't follow this.
780                if (mHeadsetService.getPriority(mDevice) == BluetoothHeadset.PRIORITY_ON) {
781                    Message msg = new Message();
782                    msg.what = CONNECT_OTHER_PROFILES;
783                    msg.arg1 = CONNECT_HFP_OUTGOING;
784                    sendMessageDelayed(msg, AUTO_CONNECT_DELAY);
785                }
786                break;
787            default:
788                break;
789        }
790
791    }
792
793    /*package*/ BluetoothDevice getDevice() {
794        return mDevice;
795    }
796
797    private void log(String message) {
798        if (DBG) {
799            Log.i(TAG, "Device:" + mDevice + " Message:" + message);
800        }
801    }
802}
803