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 
17 package com.android.bluetooth.hfp;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothHeadset;
21 import android.bluetooth.BluetoothProfile;
22 import android.bluetooth.IBluetoothHeadset;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.pm.PackageManager;
28 import android.media.AudioManager;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.provider.Settings;
32 import android.util.Log;
33 import com.android.bluetooth.btservice.ProfileService;
34 import com.android.bluetooth.Utils;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Iterator;
38 import java.util.Map;
39 
40 /**
41  * Provides Bluetooth Headset and Handsfree profile, as a service in
42  * the Bluetooth application.
43  * @hide
44  */
45 public class HeadsetService extends ProfileService {
46     private static final boolean DBG = false;
47     private static final String TAG = "HeadsetService";
48     private static final String MODIFY_PHONE_STATE = android.Manifest.permission.MODIFY_PHONE_STATE;
49 
50     private HeadsetStateMachine mStateMachine;
51     private static HeadsetService sHeadsetService;
52 
getName()53     protected String getName() {
54         return TAG;
55     }
56 
initBinder()57     public IProfileServiceBinder initBinder() {
58         return new BluetoothHeadsetBinder(this);
59     }
60 
start()61     protected boolean start() {
62         mStateMachine = HeadsetStateMachine.make(this);
63         IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
64         filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
65         filter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY);
66         try {
67             registerReceiver(mHeadsetReceiver, filter);
68         } catch (Exception e) {
69             Log.w(TAG,"Unable to register headset receiver",e);
70         }
71         setHeadsetService(this);
72         return true;
73     }
74 
stop()75     protected boolean stop() {
76         try {
77             unregisterReceiver(mHeadsetReceiver);
78         } catch (Exception e) {
79             Log.w(TAG,"Unable to unregister headset receiver",e);
80         }
81         if (mStateMachine != null) {
82             mStateMachine.doQuit();
83         }
84         return true;
85     }
86 
cleanup()87     protected boolean cleanup() {
88         if (mStateMachine != null) {
89             mStateMachine.cleanup();
90         }
91         clearHeadsetService();
92         return true;
93     }
94 
95     private final BroadcastReceiver mHeadsetReceiver = new BroadcastReceiver() {
96         @Override
97         public void onReceive(Context context, Intent intent) {
98             String action = intent.getAction();
99             if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
100                 mStateMachine.sendMessage(HeadsetStateMachine.INTENT_BATTERY_CHANGED, intent);
101             } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
102                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
103                 if (streamType == AudioManager.STREAM_BLUETOOTH_SCO) {
104                     mStateMachine.sendMessage(HeadsetStateMachine.INTENT_SCO_VOLUME_CHANGED,
105                                               intent);
106                 }
107             }
108             else if (action.equals(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY)) {
109                 int requestType = intent.getIntExtra(BluetoothDevice.EXTRA_ACCESS_REQUEST_TYPE,
110                                                BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS);
111                 if (requestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) {
112                     Log.v(TAG, "Received BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY");
113                     mStateMachine.handleAccessPermissionResult(intent);
114                 }
115             }
116         }
117     };
118 
119     /**
120      * Handlers for incoming service calls
121      */
122     private static class BluetoothHeadsetBinder extends IBluetoothHeadset.Stub implements IProfileServiceBinder {
123         private HeadsetService mService;
124 
BluetoothHeadsetBinder(HeadsetService svc)125         public BluetoothHeadsetBinder(HeadsetService svc) {
126             mService = svc;
127         }
cleanup()128         public boolean cleanup() {
129             mService = null;
130             return true;
131         }
132 
getService()133         private HeadsetService getService() {
134             if (!Utils.checkCallerAllowManagedProfiles(mService)) {
135                 Log.w(TAG,"Headset call not allowed for non-active user");
136                 return null;
137             }
138 
139             if (mService  != null && mService.isAvailable()) {
140                 return mService;
141             }
142             return null;
143         }
144 
connect(BluetoothDevice device)145         public boolean connect(BluetoothDevice device) {
146             HeadsetService service = getService();
147             if (service == null) return false;
148             return service.connect(device);
149         }
150 
disconnect(BluetoothDevice device)151         public boolean disconnect(BluetoothDevice device) {
152             HeadsetService service = getService();
153             if (service == null) return false;
154             if (DBG) Log.d(TAG, "disconnect in HeadsetService");
155             return service.disconnect(device);
156         }
157 
getConnectedDevices()158         public List<BluetoothDevice> getConnectedDevices() {
159             HeadsetService service = getService();
160             if (service == null) return new ArrayList<BluetoothDevice>(0);
161             return service.getConnectedDevices();
162         }
163 
getDevicesMatchingConnectionStates(int[] states)164         public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
165             HeadsetService service = getService();
166             if (service == null) return new ArrayList<BluetoothDevice>(0);
167             return service.getDevicesMatchingConnectionStates(states);
168         }
169 
getConnectionState(BluetoothDevice device)170         public int getConnectionState(BluetoothDevice device) {
171             HeadsetService service = getService();
172             if (service == null) return BluetoothProfile.STATE_DISCONNECTED;
173             return service.getConnectionState(device);
174         }
175 
setPriority(BluetoothDevice device, int priority)176         public boolean setPriority(BluetoothDevice device, int priority) {
177             HeadsetService service = getService();
178             if (service == null) return false;
179             return service.setPriority(device, priority);
180         }
181 
getPriority(BluetoothDevice device)182         public int getPriority(BluetoothDevice device) {
183             HeadsetService service = getService();
184             if (service == null) return BluetoothProfile.PRIORITY_UNDEFINED;
185             return service.getPriority(device);
186         }
187 
startVoiceRecognition(BluetoothDevice device)188         public boolean startVoiceRecognition(BluetoothDevice device) {
189             HeadsetService service = getService();
190             if (service == null) return false;
191             return service.startVoiceRecognition(device);
192         }
193 
stopVoiceRecognition(BluetoothDevice device)194         public boolean stopVoiceRecognition(BluetoothDevice device) {
195             HeadsetService service = getService();
196             if (service == null) return false;
197             return service.stopVoiceRecognition(device);
198         }
199 
isAudioOn()200         public boolean isAudioOn() {
201             HeadsetService service = getService();
202             if (service == null) return false;
203             return service.isAudioOn();
204         }
205 
isAudioConnected(BluetoothDevice device)206         public boolean isAudioConnected(BluetoothDevice device) {
207             HeadsetService service = getService();
208             if (service == null) return false;
209             return service.isAudioConnected(device);
210         }
211 
getBatteryUsageHint(BluetoothDevice device)212         public int getBatteryUsageHint(BluetoothDevice device) {
213             HeadsetService service = getService();
214             if (service == null) return 0;
215             return service.getBatteryUsageHint(device);
216         }
217 
acceptIncomingConnect(BluetoothDevice device)218         public boolean acceptIncomingConnect(BluetoothDevice device) {
219             HeadsetService service = getService();
220             if (service == null) return false;
221             return service.acceptIncomingConnect(device);
222         }
223 
rejectIncomingConnect(BluetoothDevice device)224         public boolean rejectIncomingConnect(BluetoothDevice device) {
225             HeadsetService service = getService();
226             if (service == null) return false;
227             return service.rejectIncomingConnect(device);
228         }
229 
getAudioState(BluetoothDevice device)230         public int getAudioState(BluetoothDevice device) {
231             HeadsetService service = getService();
232             if (service == null) return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
233             return service.getAudioState(device);
234         }
235 
connectAudio()236         public boolean connectAudio() {
237             HeadsetService service = getService();
238             if (service == null) return false;
239             return service.connectAudio();
240         }
241 
disconnectAudio()242         public boolean disconnectAudio() {
243             HeadsetService service = getService();
244             if (service == null) return false;
245             return service.disconnectAudio();
246         }
247 
setAudioRouteAllowed(boolean allowed)248         public void setAudioRouteAllowed(boolean allowed) {
249             HeadsetService service = getService();
250             if (service == null) return;
251             service.setAudioRouteAllowed(allowed);
252         }
253 
getAudioRouteAllowed()254         public boolean getAudioRouteAllowed() {
255             HeadsetService service = getService();
256             if (service != null) {
257                 return service.getAudioRouteAllowed();
258             }
259 
260             return false;
261         }
262 
startScoUsingVirtualVoiceCall(BluetoothDevice device)263         public boolean startScoUsingVirtualVoiceCall(BluetoothDevice device) {
264             HeadsetService service = getService();
265             if (service == null) return false;
266             return service.startScoUsingVirtualVoiceCall(device);
267         }
268 
stopScoUsingVirtualVoiceCall(BluetoothDevice device)269         public boolean stopScoUsingVirtualVoiceCall(BluetoothDevice device) {
270             HeadsetService service = getService();
271             if (service == null) return false;
272             return service.stopScoUsingVirtualVoiceCall(device);
273         }
274 
phoneStateChanged(int numActive, int numHeld, int callState, String number, int type)275         public void phoneStateChanged(int numActive, int numHeld, int callState,
276                                       String number, int type) {
277             HeadsetService service = getService();
278             if (service == null) return;
279             service.phoneStateChanged(numActive, numHeld, callState, number, type);
280         }
281 
clccResponse(int index, int direction, int status, int mode, boolean mpty, String number, int type)282         public void clccResponse(int index, int direction, int status, int mode, boolean mpty,
283                                  String number, int type) {
284             HeadsetService service = getService();
285             if (service == null) return;
286             service.clccResponse(index, direction, status, mode, mpty, number, type);
287         }
288 
sendVendorSpecificResultCode(BluetoothDevice device, String command, String arg)289         public boolean sendVendorSpecificResultCode(BluetoothDevice device,
290                                                     String command,
291                                                     String arg) {
292             HeadsetService service = getService();
293             if (service == null) {
294                 return false;
295             }
296             return service.sendVendorSpecificResultCode(device, command, arg);
297         }
298 
enableWBS()299         public boolean enableWBS() {
300             HeadsetService service = getService();
301             if (service == null) return false;
302             return service.enableWBS();
303         }
304 
disableWBS()305         public boolean disableWBS() {
306             HeadsetService service = getService();
307             if (service == null) return false;
308             return service.disableWBS();
309         }
310     };
311 
312     //API methods
getHeadsetService()313     public static synchronized HeadsetService getHeadsetService(){
314         if (sHeadsetService != null && sHeadsetService.isAvailable()) {
315             if (DBG) Log.d(TAG, "getHeadsetService(): returning " + sHeadsetService);
316             return sHeadsetService;
317         }
318         if (DBG)  {
319             if (sHeadsetService == null) {
320                 Log.d(TAG, "getHeadsetService(): service is NULL");
321             } else if (!(sHeadsetService.isAvailable())) {
322                 Log.d(TAG,"getHeadsetService(): service is not available");
323             }
324         }
325         return null;
326     }
327 
setHeadsetService(HeadsetService instance)328     private static synchronized void setHeadsetService(HeadsetService instance) {
329         if (instance != null && instance.isAvailable()) {
330             if (DBG) Log.d(TAG, "setHeadsetService(): set to: " + sHeadsetService);
331             sHeadsetService = instance;
332         } else {
333             if (DBG)  {
334                 if (sHeadsetService == null) {
335                     Log.d(TAG, "setHeadsetService(): service not available");
336                 } else if (!sHeadsetService.isAvailable()) {
337                     Log.d(TAG,"setHeadsetService(): service is cleaning up");
338                 }
339             }
340         }
341     }
342 
clearHeadsetService()343     private static synchronized void clearHeadsetService() {
344         sHeadsetService = null;
345     }
346 
connect(BluetoothDevice device)347     public boolean connect(BluetoothDevice device) {
348         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
349                                        "Need BLUETOOTH ADMIN permission");
350 
351         if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
352             return false;
353         }
354 
355         int connectionState = mStateMachine.getConnectionState(device);
356         Log.d(TAG,"connectionState = " + connectionState);
357         if (connectionState == BluetoothProfile.STATE_CONNECTED ||
358             connectionState == BluetoothProfile.STATE_CONNECTING) {
359             return false;
360         }
361 
362         mStateMachine.sendMessage(HeadsetStateMachine.CONNECT, device);
363         return true;
364     }
365 
disconnect(BluetoothDevice device)366     boolean disconnect(BluetoothDevice device) {
367         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
368                                        "Need BLUETOOTH ADMIN permission");
369         int connectionState = mStateMachine.getConnectionState(device);
370         if (connectionState != BluetoothProfile.STATE_CONNECTED &&
371             connectionState != BluetoothProfile.STATE_CONNECTING) {
372             return false;
373         }
374 
375         mStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT, device);
376         return true;
377     }
378 
getConnectedDevices()379     public List<BluetoothDevice> getConnectedDevices() {
380         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
381         return mStateMachine.getConnectedDevices();
382     }
383 
getDevicesMatchingConnectionStates(int[] states)384     private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
385         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
386         return mStateMachine.getDevicesMatchingConnectionStates(states);
387     }
388 
getConnectionState(BluetoothDevice device)389     int getConnectionState(BluetoothDevice device) {
390         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
391         return mStateMachine.getConnectionState(device);
392     }
393 
setPriority(BluetoothDevice device, int priority)394     public boolean setPriority(BluetoothDevice device, int priority) {
395         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
396                                        "Need BLUETOOTH_ADMIN permission");
397         Settings.Global.putInt(getContentResolver(),
398             Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
399             priority);
400         if (DBG) Log.d(TAG, "Saved priority " + device + " = " + priority);
401         return true;
402     }
403 
getPriority(BluetoothDevice device)404     public int getPriority(BluetoothDevice device) {
405         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM,
406                                        "Need BLUETOOTH_ADMIN permission");
407         int priority = Settings.Global.getInt(getContentResolver(),
408             Settings.Global.getBluetoothHeadsetPriorityKey(device.getAddress()),
409             BluetoothProfile.PRIORITY_UNDEFINED);
410         return priority;
411     }
412 
startVoiceRecognition(BluetoothDevice device)413     boolean startVoiceRecognition(BluetoothDevice device) {
414         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
415         int connectionState = mStateMachine.getConnectionState(device);
416         if (connectionState != BluetoothProfile.STATE_CONNECTED &&
417             connectionState != BluetoothProfile.STATE_CONNECTING) {
418             return false;
419         }
420         mStateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_START);
421         return true;
422     }
423 
stopVoiceRecognition(BluetoothDevice device)424     boolean stopVoiceRecognition(BluetoothDevice device) {
425         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
426         // It seem that we really need to check the AudioOn state.
427         // But since we allow startVoiceRecognition in STATE_CONNECTED and
428         // STATE_CONNECTING state, we do these 2 in this method
429         int connectionState = mStateMachine.getConnectionState(device);
430         if (connectionState != BluetoothProfile.STATE_CONNECTED &&
431             connectionState != BluetoothProfile.STATE_CONNECTING) {
432             return false;
433         }
434         mStateMachine.sendMessage(HeadsetStateMachine.VOICE_RECOGNITION_STOP);
435         // TODO is this return correct when the voice recognition is not on?
436         return true;
437     }
438 
isAudioOn()439     boolean isAudioOn() {
440         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
441         return mStateMachine.isAudioOn();
442     }
443 
isAudioConnected(BluetoothDevice device)444     boolean isAudioConnected(BluetoothDevice device) {
445         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
446         return mStateMachine.isAudioConnected(device);
447     }
448 
getBatteryUsageHint(BluetoothDevice device)449     int getBatteryUsageHint(BluetoothDevice device) {
450         // TODO(BT) ask for BT stack support?
451         return 0;
452     }
453 
acceptIncomingConnect(BluetoothDevice device)454     boolean acceptIncomingConnect(BluetoothDevice device) {
455         // TODO(BT) remove it if stack does access control
456         return false;
457     }
458 
rejectIncomingConnect(BluetoothDevice device)459     boolean rejectIncomingConnect(BluetoothDevice device) {
460         // TODO(BT) remove it if stack does access control
461         return false;
462     }
463 
getAudioState(BluetoothDevice device)464     int getAudioState(BluetoothDevice device) {
465         return mStateMachine.getAudioState(device);
466     }
467 
setAudioRouteAllowed(boolean allowed)468     public void setAudioRouteAllowed(boolean allowed) {
469         mStateMachine.setAudioRouteAllowed(allowed);
470     }
471 
getAudioRouteAllowed()472     public boolean getAudioRouteAllowed() {
473         return mStateMachine.getAudioRouteAllowed();
474     }
475 
connectAudio()476     boolean connectAudio() {
477         // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission
478         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
479         if (!mStateMachine.isConnected()) {
480             return false;
481         }
482         if (mStateMachine.isAudioOn()) {
483             return false;
484         }
485         mStateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO);
486         return true;
487     }
488 
disconnectAudio()489     boolean disconnectAudio() {
490         // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission
491         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
492         if (!mStateMachine.isAudioOn()) {
493             return false;
494         }
495         mStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO);
496         return true;
497     }
498 
startScoUsingVirtualVoiceCall(BluetoothDevice device)499     boolean startScoUsingVirtualVoiceCall(BluetoothDevice device) {
500         /* Do not ignore request if HSM state is still Disconnected or
501            Pending, it will be processed when transitioned to Connected */
502         mStateMachine.sendMessage(HeadsetStateMachine.VIRTUAL_CALL_START, device);
503         return true;
504     }
505 
stopScoUsingVirtualVoiceCall(BluetoothDevice device)506     boolean stopScoUsingVirtualVoiceCall(BluetoothDevice device) {
507         int connectionState = mStateMachine.getConnectionState(device);
508         if (connectionState != BluetoothProfile.STATE_CONNECTED &&
509             connectionState != BluetoothProfile.STATE_CONNECTING) {
510             return false;
511         }
512         mStateMachine.sendMessage(HeadsetStateMachine.VIRTUAL_CALL_STOP, device);
513         return true;
514     }
515 
phoneStateChanged(int numActive, int numHeld, int callState, String number, int type)516     private void phoneStateChanged(int numActive, int numHeld, int callState,
517                                   String number, int type) {
518         enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
519         Message msg = mStateMachine.obtainMessage(HeadsetStateMachine.CALL_STATE_CHANGED);
520         msg.obj = new HeadsetCallState(numActive, numHeld, callState, number, type);
521         msg.arg1 = 0; // false
522         mStateMachine.sendMessage(msg);
523     }
524 
clccResponse(int index, int direction, int status, int mode, boolean mpty, String number, int type)525     private void clccResponse(int index, int direction, int status, int mode, boolean mpty,
526                              String number, int type) {
527         enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
528         mStateMachine.sendMessage(HeadsetStateMachine.SEND_CCLC_RESPONSE,
529             new HeadsetClccResponse(index, direction, status, mode, mpty, number, type));
530     }
531 
sendVendorSpecificResultCode(BluetoothDevice device, String command, String arg)532     private boolean sendVendorSpecificResultCode(BluetoothDevice device,
533                                                  String command,
534                                                  String arg) {
535         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
536         int connectionState = mStateMachine.getConnectionState(device);
537         if (connectionState != BluetoothProfile.STATE_CONNECTED) {
538             return false;
539         }
540         // Currently we support only "+ANDROID".
541         if (!command.equals(BluetoothHeadset.VENDOR_RESULT_CODE_COMMAND_ANDROID)) {
542             Log.w(TAG, "Disallowed unsolicited result code command: " + command);
543             return false;
544         }
545         mStateMachine.sendMessage(HeadsetStateMachine.SEND_VENDOR_SPECIFIC_RESULT_CODE,
546                 new HeadsetVendorSpecificResultCode(device, command, arg));
547         return true;
548     }
549 
enableWBS()550     boolean enableWBS() {
551         // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission
552         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
553         if (!mStateMachine.isConnected()) {
554             return false;
555         }
556         if (mStateMachine.isAudioOn()) {
557             return false;
558         }
559 
560         for (BluetoothDevice device: getConnectedDevices()) {
561             mStateMachine.sendMessage(HeadsetStateMachine.ENABLE_WBS,device);
562         }
563 
564         return true;
565     }
566 
disableWBS()567     boolean disableWBS() {
568         // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission
569         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
570         if (!mStateMachine.isConnected()) {
571             return false;
572         }
573         if (mStateMachine.isAudioOn()) {
574             return false;
575         }
576         for (BluetoothDevice device: getConnectedDevices()) {
577             mStateMachine.sendMessage(HeadsetStateMachine.DISABLE_WBS,device);
578         }
579         return true;
580     }
581 
582     @Override
dump(StringBuilder sb)583     public void dump(StringBuilder sb) {
584         super.dump(sb);
585         if (mStateMachine != null) {
586             mStateMachine.dump(sb);
587         }
588     }
589 }
590