HeadsetService.java revision 970d01527efa5606701c5b1f4942a56c47814eab
1/*
2 * Copyright (C) 2012 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 com.android.bluetooth.hfp;
18
19import android.bluetooth.BluetoothDevice;
20import android.bluetooth.BluetoothHeadset;
21import android.bluetooth.BluetoothProfile;
22import android.bluetooth.IBluetoothHeadset;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.media.AudioManager;
28import android.os.BatteryManager;
29import android.os.HandlerThread;
30import android.os.Looper;
31import android.os.Message;
32import android.provider.Settings;
33import android.util.Log;
34
35import com.android.bluetooth.Utils;
36import com.android.bluetooth.btservice.ProfileService;
37
38import java.util.ArrayList;
39import java.util.List;
40
41/**
42 * Provides Bluetooth Headset and Handsfree profile, as a service in the Bluetooth application.
43 */
44public class HeadsetService extends ProfileService {
45    private static final String TAG = "HeadsetService";
46    private static final boolean DBG = false;
47    private static final String MODIFY_PHONE_STATE = android.Manifest.permission.MODIFY_PHONE_STATE;
48    private static final int MAX_HEADSET_CONNECTIONS = 1;
49
50    private HandlerThread mStateMachinesThread;
51    private HeadsetStateMachine mStateMachine;
52    private HeadsetNativeInterface mNativeInterface;
53    private HeadsetSystemInterface mSystemInterface;
54    private boolean mStarted;
55    private boolean mCreated;
56    private static HeadsetService sHeadsetService;
57
58    @Override
59    protected String getName() {
60        return TAG;
61    }
62
63    @Override
64    public synchronized IProfileServiceBinder initBinder() {
65        return new BluetoothHeadsetBinder(this);
66    }
67
68    @Override
69    protected synchronized void create() {
70        mCreated = true;
71    }
72
73    @Override
74    protected synchronized boolean start() {
75        Log.i(TAG, "start()");
76        // Step 1: Start handler thread for state machines
77        mStateMachinesThread = new HandlerThread("HeadsetService.StateMachines");
78        mStateMachinesThread.start();
79        // Step 2: Initialize system interface
80        mSystemInterface = new HeadsetSystemInterface(this);
81        mSystemInterface.init();
82        // Step 3: Initialize native interface
83        mNativeInterface = HeadsetNativeInterface.getInstance();
84        mNativeInterface.init(MAX_HEADSET_CONNECTIONS,
85                BluetoothHeadset.isInbandRingingSupported(this));
86        // Step 4: Initialize state machine
87        mStateMachine =
88                HeadsetStateMachine.make(mStateMachinesThread.getLooper(), this, mNativeInterface,
89                        mSystemInterface);
90        // Step 5: Setup broadcast receivers
91        IntentFilter filter = new IntentFilter();
92        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
93        filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
94        filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
95        registerReceiver(mHeadsetReceiver, filter);
96        // Step 6: Mark service as started
97        setHeadsetService(this);
98        mStarted = true;
99        return true;
100    }
101
102    @Override
103    protected synchronized boolean stop() {
104        Log.i(TAG, "stop()");
105        if (!mStarted) {
106            Log.w(TAG, "stop() called before start()");
107            // Still return true because it is considered "stopped"
108            return true;
109        }
110        // Step 6: Mark service as stopped
111        mStarted = false;
112        // Step 5: Tear down broadcast receivers
113        unregisterReceiver(mHeadsetReceiver);
114        // Step 4: Destroy state machine
115        HeadsetStateMachine.destroy(mStateMachine);
116        mStateMachine = null;
117        // Step 3: Destroy native interface
118        mNativeInterface.cleanup();
119        // Step 2: Destroy system interface
120        mSystemInterface.stop();
121        // Step 1: Stop handler thread
122        mStateMachinesThread.quitSafely();
123        mStateMachinesThread = null;
124        setHeadsetService(null);
125        return true;
126    }
127
128    @Override
129    protected synchronized void cleanup() {
130        Log.i(TAG, "cleanup");
131        if (!mCreated) {
132            Log.w(TAG, "cleanup() called before create()");
133        }
134        mCreated = false;
135    }
136
137    /**
138     * Checks if this service object is able to accept binder calls
139     * @return True if the object can accept binder calls, False otherwise
140     */
141    public synchronized boolean isAlive() {
142        return isAvailable() && mCreated && mStarted;
143    }
144
145    synchronized Looper getStateMachinesThreadLooper() {
146        return mStateMachinesThread.getLooper();
147    }
148
149    synchronized void onDeviceStateChanged(HeadsetDeviceState deviceState) {
150        mStateMachine.sendMessage(HeadsetStateMachine.DEVICE_STATE_CHANGED, deviceState);
151    }
152
153    /**
154     * Handle messages from native (JNI) to Java. This needs to be synchronized to avoid posting
155     * messages to state machine before start() is done
156     *
157     * @param stackEvent event from native stack
158     */
159    synchronized void messageFromNative(HeadsetStackEvent stackEvent) {
160        mStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT, stackEvent);
161    }
162
163    private final BroadcastReceiver mHeadsetReceiver = new BroadcastReceiver() {
164        @Override
165        public void onReceive(Context context, Intent intent) {
166            String action = intent.getAction();
167            if (action == null) {
168                Log.w(TAG, "mHeadsetReceiver, action is null");
169                return;
170            }
171            switch (action) {
172                case Intent.ACTION_BATTERY_CHANGED: {
173                    int batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
174                    int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
175                    if (batteryLevel < 0 || scale <= 0) {
176                        Log.e(TAG, "Bad Battery Changed intent: batteryLevel=" + batteryLevel
177                                + ", scale=" + scale);
178                        return;
179                    }
180                    batteryLevel = batteryLevel * 5 / scale;
181                    mSystemInterface.getHeadsetPhoneState().setCindBatteryCharge(batteryLevel);
182                    return;
183                }
184                case AudioManager.VOLUME_CHANGED_ACTION: {
185                    int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
186                    if (streamType == AudioManager.STREAM_BLUETOOTH_SCO) {
187                        mStateMachine.sendMessage(HeadsetStateMachine.INTENT_SCO_VOLUME_CHANGED,
188                                intent);
189                    }
190                    return;
191                }
192                case BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY: {
193                    int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
194                            BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
195                    if (requestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) {
196                        if (DBG) {
197                            Log.d(TAG, "Received BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS");
198                        }
199                        mStateMachine.sendMessage(
200                                HeadsetStateMachine.INTENT_CONNECTION_ACCESS_REPLY, intent);
201                    }
202                    return;
203                }
204                default:
205                    Log.w(TAG, "Unknown action " + action);
206            }
207        }
208    };
209
210    /**
211     * Handlers for incoming service calls
212     */
213    private static class BluetoothHeadsetBinder extends IBluetoothHeadset.Stub
214            implements IProfileServiceBinder {
215        private volatile HeadsetService mService;
216
217        BluetoothHeadsetBinder(HeadsetService svc) {
218            mService = svc;
219        }
220
221        @Override
222        public void cleanup() {
223            mService = null;
224        }
225
226        private HeadsetService getService() {
227            final HeadsetService service = mService;
228            if (!Utils.checkCallerAllowManagedProfiles(service)) {
229                Log.w(TAG, "Headset call not allowed for non-active user");
230                return null;
231            }
232            if (service == null) {
233                Log.w(TAG, "Service is null");
234                return null;
235            }
236            if (!service.isAlive()) {
237                Log.w(TAG, "Service is not alive");
238                return null;
239            }
240            return service;
241        }
242
243        @Override
244        public boolean connect(BluetoothDevice device) {
245            HeadsetService service = getService();
246            if (service == null) {
247                return false;
248            }
249            return service.connect(device);
250        }
251
252        @Override
253        public boolean disconnect(BluetoothDevice device) {
254            HeadsetService service = getService();
255            if (service == null) {
256                return false;
257            }
258            if (DBG) {
259                Log.d(TAG, "disconnect in HeadsetService");
260            }
261            return service.disconnect(device);
262        }
263
264        @Override
265        public List<BluetoothDevice> getConnectedDevices() {
266            HeadsetService service = getService();
267            if (service == null) {
268                return new ArrayList<BluetoothDevice>(0);
269            }
270            return service.getConnectedDevices();
271        }
272
273        @Override
274        public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
275            HeadsetService service = getService();
276            if (service == null) {
277                return new ArrayList<BluetoothDevice>(0);
278            }
279            return service.getDevicesMatchingConnectionStates(states);
280        }
281
282        @Override
283        public int getConnectionState(BluetoothDevice device) {
284            HeadsetService service = getService();
285            if (service == null) {
286                return BluetoothProfile.STATE_DISCONNECTED;
287            }
288            return service.getConnectionState(device);
289        }
290
291        @Override
292        public boolean setPriority(BluetoothDevice device, int priority) {
293            HeadsetService service = getService();
294            if (service == null) {
295                return false;
296            }
297            return service.setPriority(device, priority);
298        }
299
300        @Override
301        public int getPriority(BluetoothDevice device) {
302            HeadsetService service = getService();
303            if (service == null) {
304                return BluetoothProfile.PRIORITY_UNDEFINED;
305            }
306            return service.getPriority(device);
307        }
308
309        @Override
310        public boolean startVoiceRecognition(BluetoothDevice device) {
311            HeadsetService service = getService();
312            if (service == null) {
313                return false;
314            }
315            return service.startVoiceRecognition(device);
316        }
317
318        @Override
319        public boolean stopVoiceRecognition(BluetoothDevice device) {
320            HeadsetService service = getService();
321            if (service == null) {
322                return false;
323            }
324            return service.stopVoiceRecognition(device);
325        }
326
327        @Override
328        public boolean isAudioOn() {
329            HeadsetService service = getService();
330            if (service == null) {
331                return false;
332            }
333            return service.isAudioOn();
334        }
335
336        @Override
337        public boolean isAudioConnected(BluetoothDevice device) {
338            HeadsetService service = getService();
339            if (service == null) {
340                return false;
341            }
342            return service.isAudioConnected(device);
343        }
344
345        @Override
346        public int getAudioState(BluetoothDevice device) {
347            HeadsetService service = getService();
348            if (service == null) {
349                return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
350            }
351            return service.getAudioState(device);
352        }
353
354        @Override
355        public boolean connectAudio() {
356            HeadsetService service = getService();
357            if (service == null) {
358                return false;
359            }
360            return service.connectAudio();
361        }
362
363        @Override
364        public boolean disconnectAudio() {
365            HeadsetService service = getService();
366            if (service == null) {
367                return false;
368            }
369            return service.disconnectAudio();
370        }
371
372        @Override
373        public void setAudioRouteAllowed(boolean allowed) {
374            HeadsetService service = getService();
375            if (service == null) {
376                return;
377            }
378            service.setAudioRouteAllowed(allowed);
379        }
380
381        @Override
382        public boolean getAudioRouteAllowed() {
383            HeadsetService service = getService();
384            if (service != null) {
385                return service.getAudioRouteAllowed();
386            }
387            return false;
388        }
389
390        @Override
391        public void setForceScoAudio(boolean forced) {
392            HeadsetService service = getService();
393            if (service == null) {
394                return;
395            }
396            service.setForceScoAudio(forced);
397        }
398
399        @Override
400        public boolean startScoUsingVirtualVoiceCall(BluetoothDevice device) {
401            HeadsetService service = getService();
402            if (service == null) {
403                return false;
404            }
405            return service.startScoUsingVirtualVoiceCall(device);
406        }
407
408        @Override
409        public boolean stopScoUsingVirtualVoiceCall(BluetoothDevice device) {
410            HeadsetService service = getService();
411            if (service == null) {
412                return false;
413            }
414            return service.stopScoUsingVirtualVoiceCall(device);
415        }
416
417        @Override
418        public void phoneStateChanged(int numActive, int numHeld, int callState, String number,
419                int type) {
420            HeadsetService service = getService();
421            if (service == null) {
422                return;
423            }
424            service.phoneStateChanged(numActive, numHeld, callState, number, type);
425        }
426
427        @Override
428        public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
429                String number, int type) {
430            HeadsetService service = getService();
431            if (service == null) {
432                return;
433            }
434            service.clccResponse(index, direction, status, mode, mpty, number, type);
435        }
436
437        @Override
438        public boolean sendVendorSpecificResultCode(BluetoothDevice device, String command,
439                String arg) {
440            HeadsetService service = getService();
441            if (service == null) {
442                return false;
443            }
444            return service.sendVendorSpecificResultCode(device, command, arg);
445        }
446    }
447
448    // API methods
449    public static synchronized HeadsetService getHeadsetService() {
450        if (sHeadsetService != null && sHeadsetService.isAvailable()) {
451            if (DBG) {
452                Log.d(TAG, "getHeadsetService(): returning " + sHeadsetService);
453            }
454            return sHeadsetService;
455        }
456        if (DBG) {
457            if (sHeadsetService == null) {
458                Log.d(TAG, "getHeadsetService(): service is NULL");
459            } else if (!(sHeadsetService.isAvailable())) {
460                Log.d(TAG, "getHeadsetService(): service is not available");
461            }
462        }
463        return null;
464    }
465
466    private static synchronized void setHeadsetService(HeadsetService instance) {
467        if (DBG) {
468            Log.d(TAG, "setHeadsetService(): set to: " + instance);
469        }
470        sHeadsetService = instance;
471    }
472
473    public boolean connect(BluetoothDevice device) {
474        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
475        Log.d(TAG, "connect: device=" + device);
476        if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
477            Log.w(TAG, "connect: PRIORITY_OFF, device=" + device);
478            return false;
479        }
480        int connectionState = mStateMachine.getConnectionState(device);
481        if (connectionState == BluetoothProfile.STATE_CONNECTED
482                || connectionState == BluetoothProfile.STATE_CONNECTING) {
483            Log.w(TAG, "connect: already connected/connecting, connectionState=" + connectionState
484                    + ", device=" + device);
485            return false;
486        }
487        mStateMachine.sendMessage(HeadsetStateMachine.CONNECT, device);
488        return true;
489    }
490
491    boolean disconnect(BluetoothDevice device) {
492        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
493        Log.d(TAG, "disconnect: device=" + device);
494        int connectionState = mStateMachine.getConnectionState(device);
495        if (connectionState != BluetoothProfile.STATE_CONNECTED
496                && connectionState != BluetoothProfile.STATE_CONNECTING) {
497            Log.w(TAG, "disconnect: not connected/connecting, connectionState=" + connectionState
498                    + ", device=" + device);
499            return false;
500        }
501        mStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT, device);
502        return true;
503    }
504
505    public List<BluetoothDevice> getConnectedDevices() {
506        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
507        return mStateMachine.getConnectedDevices();
508    }
509
510    private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
511        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
512        return mStateMachine.getDevicesMatchingConnectionStates(states);
513    }
514
515    public int getConnectionState(BluetoothDevice device) {
516        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
517        return mStateMachine.getConnectionState(device);
518    }
519
520    public boolean setPriority(BluetoothDevice device, int priority) {
521        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
522        Settings.Global.putInt(getContentResolver(),
523                Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()), priority);
524        if (DBG) {
525            Log.d(TAG, "Saved priority " + device + " = " + priority);
526        }
527        return true;
528    }
529
530    public int getPriority(BluetoothDevice device) {
531        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
532        return Settings.Global.getInt(getContentResolver(),
533                Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
534                BluetoothProfile.PRIORITY_UNDEFINED);
535    }
536
537    boolean startVoiceRecognition(BluetoothDevice device) {
538        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
539        int connectionState = mStateMachine.getConnectionState(device);
540        if (connectionState != BluetoothProfile.STATE_CONNECTED
541                && connectionState != BluetoothProfile.STATE_CONNECTING) {
542            return false;
543        }
544        mStateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_START, device);
545        return true;
546    }
547
548    boolean stopVoiceRecognition(BluetoothDevice device) {
549        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
550        // It seem that we really need to check the AudioOn state.
551        // But since we allow startVoiceRecognition in STATE_CONNECTED and
552        // STATE_CONNECTING state, we do these 2 in this method
553        int connectionState = mStateMachine.getConnectionState(device);
554        if (connectionState != BluetoothProfile.STATE_CONNECTED
555                && connectionState != BluetoothProfile.STATE_CONNECTING) {
556            return false;
557        }
558        mStateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_STOP, device);
559        return true;
560    }
561
562    boolean isAudioOn() {
563        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
564        return mStateMachine.getAudioState() != BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
565    }
566
567    boolean isAudioConnected(BluetoothDevice device) {
568        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
569        return mStateMachine.getAudioState() == BluetoothHeadset.STATE_AUDIO_CONNECTED;
570    }
571
572    int getAudioState(BluetoothDevice device) {
573        return mStateMachine.getAudioState();
574    }
575
576    public void setAudioRouteAllowed(boolean allowed) {
577        mStateMachine.setAudioRouteAllowed(allowed);
578    }
579
580    public boolean getAudioRouteAllowed() {
581        return mStateMachine.getAudioRouteAllowed();
582    }
583
584    public void setForceScoAudio(boolean forced) {
585        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
586        mStateMachine.setForceScoAudio(forced);
587    }
588
589    boolean connectAudio() {
590        // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission
591        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
592        if (mStateMachine.getConnectionState(mStateMachine.getCurrentDevice())
593                != BluetoothProfile.STATE_CONNECTED) {
594            Log.w(TAG, "connectAudio: profile not connected");
595            return false;
596        }
597        if (isAudioOn()) {
598            Log.w(TAG, "connectAudio: audio is not idle, current state "
599                    + mStateMachine.getAudioState());
600            return false;
601        }
602        mStateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO,
603                mStateMachine.getCurrentDevice());
604        return true;
605    }
606
607    boolean disconnectAudio() {
608        // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission
609        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
610        if (mStateMachine.getAudioState() != BluetoothHeadset.STATE_AUDIO_CONNECTED) {
611            Log.w(TAG, "disconnectAudio, audio is not connected");
612            return false;
613        }
614        mStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO,
615                mStateMachine.getCurrentDevice());
616        return true;
617    }
618
619    boolean startScoUsingVirtualVoiceCall(BluetoothDevice device) {
620        /* Do not ignore request if HSM state is still Disconnected or
621           Pending, it will be processed when transitioned to Connected */
622        mStateMachine.sendMessage(HeadsetStateMachine.VIRTUAL_CALL_START, device);
623        return true;
624    }
625
626    boolean stopScoUsingVirtualVoiceCall(BluetoothDevice device) {
627        int connectionState = mStateMachine.getConnectionState(device);
628        if (connectionState != BluetoothProfile.STATE_CONNECTED
629                && connectionState != BluetoothProfile.STATE_CONNECTING) {
630            return false;
631        }
632        mStateMachine.sendMessage(HeadsetStateMachine.VIRTUAL_CALL_STOP, device);
633        return true;
634    }
635
636    private void phoneStateChanged(int numActive, int numHeld, int callState, String number,
637            int type) {
638        enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
639        Message msg = mStateMachine.obtainMessage(HeadsetStateMachine.CALL_STATE_CHANGED);
640        msg.obj = new HeadsetCallState(numActive, numHeld, callState, number, type);
641        msg.arg1 = 0; // false
642        mStateMachine.sendMessage(msg);
643    }
644
645    private void clccResponse(int index, int direction, int status, int mode, boolean mpty,
646            String number, int type) {
647        enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
648        mStateMachine.sendMessage(HeadsetStateMachine.SEND_CCLC_RESPONSE,
649                new HeadsetClccResponse(index, direction, status, mode, mpty, number, type));
650    }
651
652    private boolean sendVendorSpecificResultCode(BluetoothDevice device, String command,
653            String arg) {
654        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
655        int connectionState = mStateMachine.getConnectionState(device);
656        if (connectionState != BluetoothProfile.STATE_CONNECTED) {
657            return false;
658        }
659        // Currently we support only "+ANDROID".
660        if (!command.equals(BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID)) {
661            Log.w(TAG, "Disallowed unsolicited result code command: " + command);
662            return false;
663        }
664        mStateMachine.sendMessage(HeadsetStateMachine.SEND_VENDOR_SPECIFIC_RESULT_CODE,
665                new HeadsetVendorSpecificResultCode(device, command, arg));
666        return true;
667    }
668
669    @Override
670    public void dump(StringBuilder sb) {
671        super.dump(sb);
672        if (mStateMachine != null) {
673            mStateMachine.dump(sb);
674        }
675    }
676}
677