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