BluetoothHandsfree.java revision b1164d370e8d83a8a4f3cbdc73dffc087254cabd
1/* 2 * Copyright (C) 2008 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.phone; 18 19import android.bluetooth.AtCommandHandler; 20import android.bluetooth.AtCommandResult; 21import android.bluetooth.AtParser; 22import android.bluetooth.BluetoothDevice; 23import android.bluetooth.BluetoothHeadset; 24import android.bluetooth.BluetoothIntent; 25import android.bluetooth.HeadsetBase; 26import android.bluetooth.ScoSocket; 27import android.content.ActivityNotFoundException; 28import android.content.BroadcastReceiver; 29import android.content.Context; 30import android.content.Intent; 31import android.content.IntentFilter; 32import android.media.AudioManager; 33import android.net.Uri; 34import android.os.AsyncResult; 35import android.os.Bundle; 36import android.os.Handler; 37import android.os.Message; 38import android.os.PowerManager; 39import android.os.PowerManager.WakeLock; 40import android.os.SystemProperties; 41import android.telephony.PhoneNumberUtils; 42import android.telephony.ServiceState; 43import android.telephony.SignalStrength; 44import android.util.Log; 45 46import com.android.internal.telephony.Call; 47import com.android.internal.telephony.Connection; 48import com.android.internal.telephony.Phone; 49import com.android.internal.telephony.TelephonyIntents; 50 51import java.util.LinkedList; 52/** 53 * Bluetooth headset manager for the Phone app. 54 * @hide 55 */ 56public class BluetoothHandsfree { 57 private static final String TAG = "BT HS/HF"; 58 private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2); 59 private static final boolean VDBG = false; // even more logging 60 61 public static final int TYPE_UNKNOWN = 0; 62 public static final int TYPE_HEADSET = 1; 63 public static final int TYPE_HANDSFREE = 2; 64 65 private final Context mContext; 66 private final Phone mPhone; 67 private ServiceState mServiceState; 68 private HeadsetBase mHeadset; // null when not connected 69 private int mHeadsetType; 70 private boolean mAudioPossible; 71 private ScoSocket mIncomingSco; 72 private ScoSocket mOutgoingSco; 73 private ScoSocket mConnectedSco; 74 75 private Call mForegroundCall; 76 private Call mBackgroundCall; 77 private Call mRingingCall; 78 79 private AudioManager mAudioManager; 80 private PowerManager mPowerManager; 81 82 private boolean mUserWantsAudio; 83 private WakeLock mStartCallWakeLock; // held while waiting for the intent to start call 84 private WakeLock mStartVoiceRecognitionWakeLock; // held while waiting for voice recognition 85 86 // AT command state 87 private static final int MAX_CONNECTIONS = 6; // Max connections allowed by GSM 88 89 private long mBgndEarliestConnectionTime = 0; 90 private boolean mClip = false; // Calling Line Information Presentation 91 private boolean mIndicatorsEnabled = false; 92 private boolean mCmee = false; // Extended Error reporting 93 private long[] mClccTimestamps; // Timestamps associated with each clcc index 94 private boolean[] mClccUsed; // Is this clcc index in use 95 private boolean mWaitingForCallStart; 96 private boolean mWaitingForVoiceRecognition; 97 // do not connect audio until service connection is established 98 // for 3-way supported devices, this is after AT+CHLD 99 // for non-3-way supported devices, this is after AT+CMER (see spec) 100 private boolean mServiceConnectionEstablished; 101 private final BluetoothPhoneState mBluetoothPhoneState; // for CIND and CIEV updates 102 private final BluetoothAtPhonebook mPhonebook; 103 private Phone.State mPhoneState = Phone.State.IDLE; 104 105 private DebugThread mDebugThread; 106 private int mScoGain = Integer.MIN_VALUE; 107 108 private static Intent sVoiceCommandIntent; 109 110 // Audio parameters 111 private static final String HEADSET_NREC = "bt_headset_nrec"; 112 private static final String HEADSET_NAME = "bt_headset_name"; 113 114 private int mRemoteBrsf = 0; 115 private int mLocalBrsf = 0; 116 117 /* Constants from Bluetooth Specification Hands-Free profile version 1.5 */ 118 private static final int BRSF_AG_THREE_WAY_CALLING = 1 << 0; 119 private static final int BRSF_AG_EC_NR = 1 << 1; 120 private static final int BRSF_AG_VOICE_RECOG = 1 << 2; 121 private static final int BRSF_AG_IN_BAND_RING = 1 << 3; 122 private static final int BRSF_AG_VOICE_TAG_NUMBE = 1 << 4; 123 private static final int BRSF_AG_REJECT_CALL = 1 << 5; 124 private static final int BRSF_AG_ENHANCED_CALL_STATUS = 1 << 6; 125 private static final int BRSF_AG_ENHANCED_CALL_CONTROL = 1 << 7; 126 private static final int BRSF_AG_ENHANCED_ERR_RESULT_CODES = 1 << 8; 127 128 private static final int BRSF_HF_EC_NR = 1 << 0; 129 private static final int BRSF_HF_CW_THREE_WAY_CALLING = 1 << 1; 130 private static final int BRSF_HF_CLIP = 1 << 2; 131 private static final int BRSF_HF_VOICE_REG_ACT = 1 << 3; 132 private static final int BRSF_HF_REMOTE_VOL_CONTROL = 1 << 4; 133 private static final int BRSF_HF_ENHANCED_CALL_STATUS = 1 << 5; 134 private static final int BRSF_HF_ENHANCED_CALL_CONTROL = 1 << 6; 135 136 public static String typeToString(int type) { 137 switch (type) { 138 case TYPE_UNKNOWN: 139 return "unknown"; 140 case TYPE_HEADSET: 141 return "headset"; 142 case TYPE_HANDSFREE: 143 return "handsfree"; 144 } 145 return null; 146 } 147 148 public BluetoothHandsfree(Context context, Phone phone) { 149 mPhone = phone; 150 mContext = context; 151 BluetoothDevice bluetooth = 152 (BluetoothDevice)context.getSystemService(Context.BLUETOOTH_SERVICE); 153 boolean bluetoothCapable = (bluetooth != null); 154 mHeadset = null; // nothing connected yet 155 156 mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 157 mStartCallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 158 TAG + ":StartCall"); 159 mStartCallWakeLock.setReferenceCounted(false); 160 mStartVoiceRecognitionWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, 161 TAG + ":VoiceRecognition"); 162 mStartVoiceRecognitionWakeLock.setReferenceCounted(false); 163 164 mLocalBrsf = BRSF_AG_THREE_WAY_CALLING | 165 BRSF_AG_EC_NR | 166 BRSF_AG_REJECT_CALL | 167 BRSF_AG_ENHANCED_CALL_STATUS; 168 169 if (sVoiceCommandIntent == null) { 170 sVoiceCommandIntent = new Intent(Intent.ACTION_VOICE_COMMAND); 171 sVoiceCommandIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 172 } 173 if (mContext.getPackageManager().resolveActivity(sVoiceCommandIntent, 0) != null && 174 !BluetoothHeadset.DISABLE_BT_VOICE_DIALING) { 175 mLocalBrsf |= BRSF_AG_VOICE_RECOG; 176 } 177 178 if (bluetoothCapable) { 179 resetAtState(); 180 } 181 182 mRingingCall = mPhone.getRingingCall(); 183 mForegroundCall = mPhone.getForegroundCall(); 184 mBackgroundCall = mPhone.getBackgroundCall(); 185 mBluetoothPhoneState = new BluetoothPhoneState(); 186 mUserWantsAudio = true; 187 mPhonebook = new BluetoothAtPhonebook(mContext, this); 188 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 189 } 190 191 /* package */ synchronized void onBluetoothEnabled() { 192 /* Bluez has a bug where it will always accept and then orphan 193 * incoming SCO connections, regardless of whether we have a listening 194 * SCO socket. So the best thing to do is always run a listening socket 195 * while bluetooth is on so that at least we can diconnect it 196 * immediately when we don't want it. 197 */ 198 if (mIncomingSco == null) { 199 mIncomingSco = createScoSocket(); 200 mIncomingSco.accept(); 201 } 202 } 203 204 /* package */ synchronized void onBluetoothDisabled() { 205 if (mConnectedSco != null) { 206 mAudioManager.setBluetoothScoOn(false); 207 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED); 208 mConnectedSco.close(); 209 mConnectedSco = null; 210 } 211 if (mOutgoingSco != null) { 212 mOutgoingSco.close(); 213 mOutgoingSco = null; 214 } 215 if (mIncomingSco != null) { 216 mIncomingSco.close(); 217 mIncomingSco = null; 218 } 219 } 220 221 private boolean isHeadsetConnected() { 222 if (mHeadset == null) { 223 return false; 224 } 225 return mHeadset.isConnected(); 226 } 227 228 /* package */ void connectHeadset(HeadsetBase headset, int headsetType) { 229 mHeadset = headset; 230 mHeadsetType = headsetType; 231 if (mHeadsetType == TYPE_HEADSET) { 232 initializeHeadsetAtParser(); 233 } else { 234 initializeHandsfreeAtParser(); 235 } 236 headset.startEventThread(); 237 configAudioParameters(); 238 239 if (inDebug()) { 240 startDebug(); 241 } 242 243 if (isIncallAudio()) { 244 audioOn(); 245 } 246 } 247 248 /* returns true if there is some kind of in-call audio we may wish to route 249 * bluetooth to */ 250 private boolean isIncallAudio() { 251 Call.State state = mForegroundCall.getState(); 252 253 return (state == Call.State.ACTIVE || state == Call.State.ALERTING); 254 } 255 256 /* package */ void disconnectHeadset() { 257 mHeadset = null; 258 stopDebug(); 259 resetAtState(); 260 } 261 262 private void resetAtState() { 263 mClip = false; 264 mIndicatorsEnabled = false; 265 mServiceConnectionEstablished = false; 266 mCmee = false; 267 mClccTimestamps = new long[MAX_CONNECTIONS]; 268 mClccUsed = new boolean[MAX_CONNECTIONS]; 269 for (int i = 0; i < MAX_CONNECTIONS; i++) { 270 mClccUsed[i] = false; 271 } 272 mRemoteBrsf = 0; 273 } 274 275 private void configAudioParameters() { 276 String name = mHeadset.getName(); 277 if (name == null) { 278 name = "<unknown>"; 279 } 280 mAudioManager.setParameter(HEADSET_NAME, name); 281 mAudioManager.setParameter(HEADSET_NREC, "on"); 282 } 283 284 285 /** Represents the data that we send in a +CIND or +CIEV command to the HF 286 */ 287 private class BluetoothPhoneState { 288 // 0: no service 289 // 1: service 290 private int mService; 291 292 // 0: no active call 293 // 1: active call (where active means audio is routed - not held call) 294 private int mCall; 295 296 // 0: not in call setup 297 // 1: incoming call setup 298 // 2: outgoing call setup 299 // 3: remote party being alerted in an outgoing call setup 300 private int mCallsetup; 301 302 // 0: no calls held 303 // 1: held call and active call 304 // 2: held call only 305 private int mCallheld; 306 307 // cellular signal strength of AG: 0-5 308 private int mSignal; 309 310 // cellular signal strength in CSQ rssi scale 311 private int mRssi; // for CSQ 312 313 // 0: roaming not active (home) 314 // 1: roaming active 315 private int mRoam; 316 317 // battery charge of AG: 0-5 318 private int mBattchg; 319 320 // 0: not registered 321 // 1: registered, home network 322 // 5: registered, roaming 323 private int mStat; // for CREG 324 325 private String mRingingNumber; // Context for in-progress RING's 326 private int mRingingType; 327 private boolean mIgnoreRing = false; 328 329 private static final int SERVICE_STATE_CHANGED = 1; 330 private static final int PHONE_STATE_CHANGED = 2; 331 private static final int RING = 3; 332 private static final int PHONE_CDMA_CALL_WAITING = 4; 333 334 private Handler mStateChangeHandler = new Handler() { 335 @Override 336 public void handleMessage(Message msg) { 337 switch(msg.what) { 338 case RING: 339 AtCommandResult result = ring(); 340 if (result != null) { 341 sendURC(result.toString()); 342 } 343 break; 344 case SERVICE_STATE_CHANGED: 345 ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result; 346 updateServiceState(sendUpdate(), state); 347 break; 348 case PHONE_STATE_CHANGED: 349 case PHONE_CDMA_CALL_WAITING: 350 Connection connection = null; 351 if (((AsyncResult) msg.obj).result instanceof Connection) { 352 connection = (Connection) ((AsyncResult) msg.obj).result; 353 } 354 updatePhoneState(sendUpdate(), connection); 355 break; 356 } 357 } 358 }; 359 360 private BluetoothPhoneState() { 361 // init members 362 updateServiceState(false, mPhone.getServiceState()); 363 updatePhoneState(false, null); 364 mBattchg = 5; // There is currently no API to get battery level 365 // on demand, so set to 5 and wait for an update 366 mSignal = asuToSignal(mPhone.getSignalStrength()); 367 368 // register for updates 369 mPhone.registerForServiceStateChanged(mStateChangeHandler, 370 SERVICE_STATE_CHANGED, null); 371 mPhone.registerForPhoneStateChanged(mStateChangeHandler, 372 PHONE_STATE_CHANGED, null); 373 if (mPhone.getPhoneName().equals("CDMA")) { 374 mPhone.registerForCallWaiting(mStateChangeHandler, 375 PHONE_CDMA_CALL_WAITING, null); 376 } 377 IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); 378 filter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED); 379 mContext.registerReceiver(mStateReceiver, filter); 380 } 381 382 private void updateBtPhoneStateAfterRadioTechnologyChange() { 383 if(DBG) Log.d(TAG, "updateBtPhoneStateAfterRadioTechnologyChange..."); 384 385 //Unregister all events from the old obsolete phone 386 mPhone.unregisterForServiceStateChanged(mStateChangeHandler); 387 mPhone.unregisterForPhoneStateChanged(mStateChangeHandler); 388 mPhone.unregisterForCallWaiting(mStateChangeHandler); 389 390 //Register all events new to the new active phone 391 mPhone.registerForServiceStateChanged(mStateChangeHandler, SERVICE_STATE_CHANGED, null); 392 mPhone.registerForPhoneStateChanged(mStateChangeHandler, PHONE_STATE_CHANGED, null); 393 if (mPhone.getPhoneName().equals("CDMA")) { 394 mPhone.registerForCallWaiting(mStateChangeHandler, 395 PHONE_CDMA_CALL_WAITING, null); 396 } 397 } 398 399 private boolean sendUpdate() { 400 return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mIndicatorsEnabled; 401 } 402 403 private boolean sendClipUpdate() { 404 return isHeadsetConnected() && mHeadsetType == TYPE_HANDSFREE && mClip; 405 } 406 407 /* convert [0,31] ASU signal strength to the [0,5] expected by 408 * bluetooth devices. Scale is similar to status bar policy 409 */ 410 private int gsmAsuToSignal(SignalStrength signalStrength) { 411 int asu = signalStrength.getGsmSignalStrength(); 412 if (asu >= 16) return 5; 413 else if (asu >= 8) return 4; 414 else if (asu >= 4) return 3; 415 else if (asu >= 2) return 2; 416 else if (asu >= 1) return 1; 417 else return 0; 418 } 419 420 /** 421 * Convert the cdma / evdo db levels to appropriate icon level. 422 * The scale is similar to the one used in status bar policy. 423 * 424 * @param signalStrength 425 * @return the icon level 426 */ 427 private int cdmaDbmEcioToSignal(SignalStrength signalStrength) { 428 int levelDbm = 0; 429 int levelEcio = 0; 430 int cdmaIconLevel = 0; 431 int evdoIconLevel = 0; 432 int cdmaDbm = signalStrength.getCdmaDbm(); 433 int cdmaEcio = signalStrength.getCdmaEcio(); 434 435 if (cdmaDbm >= -75) levelDbm = 4; 436 else if (cdmaDbm >= -85) levelDbm = 3; 437 else if (cdmaDbm >= -95) levelDbm = 2; 438 else if (cdmaDbm >= -100) levelDbm = 1; 439 else levelDbm = 0; 440 441 // Ec/Io are in dB*10 442 if (cdmaEcio >= -90) levelEcio = 4; 443 else if (cdmaEcio >= -110) levelEcio = 3; 444 else if (cdmaEcio >= -130) levelEcio = 2; 445 else if (cdmaEcio >= -150) levelEcio = 1; 446 else levelEcio = 0; 447 448 cdmaIconLevel = (levelDbm < levelEcio) ? levelDbm : levelEcio; 449 450 if (mServiceState != null && 451 (mServiceState.getRadioTechnology() == ServiceState.RADIO_TECHNOLOGY_EVDO_0 || 452 mServiceState.getRadioTechnology() == ServiceState.RADIO_TECHNOLOGY_EVDO_A)) { 453 int evdoEcio = signalStrength.getEvdoEcio(); 454 int evdoSnr = signalStrength.getEvdoSnr(); 455 int levelEvdoEcio = 0; 456 int levelEvdoSnr = 0; 457 458 // Ec/Io are in dB*10 459 if (evdoEcio >= -650) levelEvdoEcio = 4; 460 else if (evdoEcio >= -750) levelEvdoEcio = 3; 461 else if (evdoEcio >= -900) levelEvdoEcio = 2; 462 else if (evdoEcio >= -1050) levelEvdoEcio = 1; 463 else levelEvdoEcio = 0; 464 465 if (evdoSnr > 7) levelEvdoSnr = 4; 466 else if (evdoSnr > 5) levelEvdoSnr = 3; 467 else if (evdoSnr > 3) levelEvdoSnr = 2; 468 else if (evdoSnr > 1) levelEvdoSnr = 1; 469 else levelEvdoSnr = 0; 470 471 evdoIconLevel = (levelEvdoEcio < levelEvdoSnr) ? levelEvdoEcio : levelEvdoSnr; 472 } 473 // TODO(): There is a bug open regarding what should be sent. 474 return (cdmaIconLevel > evdoIconLevel) ? cdmaIconLevel : evdoIconLevel; 475 476 } 477 478 479 private int asuToSignal(SignalStrength signalStrength) { 480 if (signalStrength.isGsm()) { 481 return gsmAsuToSignal(signalStrength); 482 } else { 483 return cdmaDbmEcioToSignal(signalStrength); 484 } 485 } 486 487 488 /* convert [0,5] signal strength to a rssi signal strength for CSQ 489 * which is [0,31]. Despite the same scale, this is not the same value 490 * as ASU. 491 */ 492 private int signalToRssi(int signal) { 493 // using C4A suggested values 494 switch (signal) { 495 case 0: return 0; 496 case 1: return 4; 497 case 2: return 8; 498 case 3: return 13; 499 case 4: return 19; 500 case 5: return 31; 501 } 502 return 0; 503 } 504 505 506 private final BroadcastReceiver mStateReceiver = new BroadcastReceiver() { 507 @Override 508 public void onReceive(Context context, Intent intent) { 509 if (intent.getAction().equals(Intent.ACTION_BATTERY_CHANGED)) { 510 updateBatteryState(intent); 511 } else if (intent.getAction().equals(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED)) { 512 updateSignalState(intent); 513 } 514 } 515 }; 516 517 private synchronized void updateBatteryState(Intent intent) { 518 int batteryLevel = intent.getIntExtra("level", -1); 519 int scale = intent.getIntExtra("scale", -1); 520 if (batteryLevel == -1 || scale == -1) { 521 return; // ignore 522 } 523 batteryLevel = batteryLevel * 5 / scale; 524 if (mBattchg != batteryLevel) { 525 mBattchg = batteryLevel; 526 if (sendUpdate()) { 527 sendURC("+CIEV: 7," + mBattchg); 528 } 529 } 530 } 531 532 private synchronized void updateSignalState(Intent intent) { 533 // NOTE this function is called by the BroadcastReceiver mStateReceiver after intent 534 // ACTION_SIGNAL_STRENGTH_CHANGED and by the DebugThread mDebugThread 535 SignalStrength signalStrength = SignalStrength.newFromBundle(intent.getExtras()); 536 int signal; 537 538 if (signalStrength != null) { 539 signal = asuToSignal(signalStrength); 540 mRssi = signalToRssi(signal); // no unsolicited CSQ 541 if (signal != mSignal) { 542 mSignal = signal; 543 if (sendUpdate()) { 544 sendURC("+CIEV: 5," + mSignal); 545 } 546 } 547 } else { 548 Log.e(TAG, "Signal Strength null"); 549 } 550 } 551 552 private synchronized void updateServiceState(boolean sendUpdate, ServiceState state) { 553 int service = state.getState() == ServiceState.STATE_IN_SERVICE ? 1 : 0; 554 int roam = state.getRoaming() ? 1 : 0; 555 int stat; 556 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); 557 mServiceState = state; 558 if (service == 0) { 559 stat = 0; 560 } else { 561 stat = (roam == 1) ? 5 : 1; 562 } 563 564 if (service != mService) { 565 mService = service; 566 if (sendUpdate) { 567 result.addResponse("+CIEV: 1," + mService); 568 } 569 } 570 if (roam != mRoam) { 571 mRoam = roam; 572 if (sendUpdate) { 573 result.addResponse("+CIEV: 6," + mRoam); 574 } 575 } 576 if (stat != mStat) { 577 mStat = stat; 578 if (sendUpdate) { 579 result.addResponse(toCregString()); 580 } 581 } 582 583 sendURC(result.toString()); 584 } 585 586 private synchronized void updatePhoneState(boolean sendUpdate, Connection connection) { 587 int call = 0; 588 int callsetup = 0; 589 int callheld = 0; 590 int prevCallsetup = mCallsetup; 591 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); 592 593 if (DBG) log("updatePhoneState()"); 594 595 Phone.State newState = mPhone.getState(); 596 if (newState != mPhoneState) { 597 mPhoneState = newState; 598 switch (mPhoneState) { 599 case IDLE: 600 mUserWantsAudio = true; // out of call - reset state 601 audioOff(); 602 break; 603 default: 604 callStarted(); 605 } 606 } 607 608 switch(mForegroundCall.getState()) { 609 case ACTIVE: 610 call = 1; 611 mAudioPossible = true; 612 break; 613 case DIALING: 614 callsetup = 2; 615 mAudioPossible = false; 616 break; 617 case ALERTING: 618 callsetup = 3; 619 // Open the SCO channel for the outgoing call. 620 audioOn(); 621 mAudioPossible = true; 622 break; 623 default: 624 mAudioPossible = false; 625 } 626 627 switch(mRingingCall.getState()) { 628 case INCOMING: 629 case WAITING: 630 callsetup = 1; 631 break; 632 } 633 634 switch(mBackgroundCall.getState()) { 635 case HOLDING: 636 if (call == 1) { 637 callheld = 1; 638 } else { 639 call = 1; 640 callheld = 2; 641 } 642 break; 643 } 644 645 if (mCall != call) { 646 if (call == 1) { 647 // This means that a call has transitioned from NOT ACTIVE to ACTIVE. 648 // Switch on audio. 649 audioOn(); 650 } 651 mCall = call; 652 if (sendUpdate) { 653 result.addResponse("+CIEV: 2," + mCall); 654 } 655 } 656 if (mCallsetup != callsetup) { 657 mCallsetup = callsetup; 658 if (sendUpdate) { 659 // If mCall = 0, send CIEV 660 // mCall = 1, mCallsetup = 0, send CIEV 661 // mCall = 1, mCallsetup = 1, send CIEV after CCWA, 662 // if 3 way supported. 663 // mCall = 1, mCallsetup = 2 / 3 -> send CIEV, 664 // if 3 way is supported 665 if (mCall != 1 || mCallsetup == 0 || 666 mCallsetup != 1 && (mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) { 667 result.addResponse("+CIEV: 3," + mCallsetup); 668 } 669 } 670 } 671 672 boolean callsSwitched = 673 (callheld == 1 && ! (mBackgroundCall.getEarliestConnectTime() == 674 mBgndEarliestConnectionTime)); 675 676 mBgndEarliestConnectionTime = mBackgroundCall.getEarliestConnectTime(); 677 678 if (mCallheld != callheld || callsSwitched) { 679 mCallheld = callheld; 680 if (sendUpdate) { 681 result.addResponse("+CIEV: 4," + mCallheld); 682 } 683 } 684 685 if (callsetup == 1 && callsetup != prevCallsetup) { 686 // new incoming call 687 String number = null; 688 int type = 128; 689 // find incoming phone number and type 690 if (connection == null) { 691 connection = mRingingCall.getEarliestConnection(); 692 if (connection == null) { 693 Log.e(TAG, "Could not get a handle on Connection object for new " + 694 "incoming call"); 695 } 696 } 697 if (connection != null) { 698 number = connection.getAddress(); 699 if (number != null) { 700 type = PhoneNumberUtils.toaFromString(number); 701 } 702 } 703 if (number == null) { 704 number = ""; 705 } 706 if ((call != 0 || callheld != 0) && sendUpdate) { 707 // call waiting 708 if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) != 0x0) { 709 result.addResponse("+CCWA: \"" + number + "\"," + type); 710 result.addResponse("+CIEV: 3," + callsetup); 711 } 712 } else { 713 // regular new incoming call 714 mRingingNumber = number; 715 mRingingType = type; 716 mIgnoreRing = false; 717 718 if ((mLocalBrsf & BRSF_AG_IN_BAND_RING) == 0x1) { 719 audioOn(); 720 } 721 result.addResult(ring()); 722 } 723 } 724 sendURC(result.toString()); 725 } 726 727 private AtCommandResult ring() { 728 if (!mIgnoreRing && mRingingCall.isRinging()) { 729 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); 730 result.addResponse("RING"); 731 if (sendClipUpdate()) { 732 result.addResponse("+CLIP: \"" + mRingingNumber + "\"," + mRingingType); 733 } 734 735 Message msg = mStateChangeHandler.obtainMessage(RING); 736 mStateChangeHandler.sendMessageDelayed(msg, 3000); 737 return result; 738 } 739 return null; 740 } 741 742 private synchronized String toCregString() { 743 return new String("+CREG: 1," + mStat); 744 } 745 746 private synchronized AtCommandResult toCindResult() { 747 AtCommandResult result = new AtCommandResult(AtCommandResult.OK); 748 String status = "+CIND: " + mService + "," + mCall + "," + mCallsetup + "," + 749 mCallheld + "," + mSignal + "," + mRoam + "," + mBattchg; 750 result.addResponse(status); 751 return result; 752 } 753 754 private synchronized AtCommandResult toCsqResult() { 755 AtCommandResult result = new AtCommandResult(AtCommandResult.OK); 756 String status = "+CSQ: " + mRssi + ",99"; 757 result.addResponse(status); 758 return result; 759 } 760 761 762 private synchronized AtCommandResult getCindTestResult() { 763 return new AtCommandResult("+CIND: (\"service\",(0-1))," + "(\"call\",(0-1))," + 764 "(\"callsetup\",(0-3)),(\"callheld\",(0-2)),(\"signal\",(0-5))," + 765 "(\"roam\",(0-1)),(\"battchg\",(0-5))"); 766 } 767 768 private synchronized void ignoreRing() { 769 mCallsetup = 0; 770 mIgnoreRing = true; 771 if (sendUpdate()) { 772 sendURC("+CIEV: 3," + mCallsetup); 773 } 774 } 775 776 }; 777 778 private static final int SCO_ACCEPTED = 1; 779 private static final int SCO_CONNECTED = 2; 780 private static final int SCO_CLOSED = 3; 781 private static final int CHECK_CALL_STARTED = 4; 782 private static final int CHECK_VOICE_RECOGNITION_STARTED = 5; 783 784 private final Handler mHandler = new Handler() { 785 @Override 786 public synchronized void handleMessage(Message msg) { 787 switch (msg.what) { 788 case SCO_ACCEPTED: 789 if (msg.arg1 == ScoSocket.STATE_CONNECTED) { 790 if (isHeadsetConnected() && (mAudioPossible || allowAudioAnytime()) && 791 mConnectedSco == null) { 792 Log.i(TAG, "Routing audio for incoming SCO connection"); 793 mConnectedSco = (ScoSocket)msg.obj; 794 mAudioManager.setBluetoothScoOn(true); 795 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED); 796 } else { 797 Log.i(TAG, "Rejecting incoming SCO connection"); 798 ((ScoSocket)msg.obj).close(); 799 } 800 } // else error trying to accept, try again 801 mIncomingSco = createScoSocket(); 802 mIncomingSco.accept(); 803 break; 804 case SCO_CONNECTED: 805 if (msg.arg1 == ScoSocket.STATE_CONNECTED && isHeadsetConnected() && 806 mConnectedSco == null) { 807 if (DBG) log("Routing audio for outgoing SCO conection"); 808 mConnectedSco = (ScoSocket)msg.obj; 809 mAudioManager.setBluetoothScoOn(true); 810 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_CONNECTED); 811 } else if (msg.arg1 == ScoSocket.STATE_CONNECTED) { 812 if (DBG) log("Rejecting new connected outgoing SCO socket"); 813 ((ScoSocket)msg.obj).close(); 814 mOutgoingSco.close(); 815 } 816 mOutgoingSco = null; 817 break; 818 case SCO_CLOSED: 819 if (mConnectedSco == (ScoSocket)msg.obj) { 820 mConnectedSco = null; 821 mAudioManager.setBluetoothScoOn(false); 822 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED); 823 } else if (mOutgoingSco == (ScoSocket)msg.obj) { 824 mOutgoingSco = null; 825 } else if (mIncomingSco == (ScoSocket)msg.obj) { 826 mIncomingSco = null; 827 } 828 break; 829 case CHECK_CALL_STARTED: 830 if (mWaitingForCallStart) { 831 mWaitingForCallStart = false; 832 Log.e(TAG, "Timeout waiting for call to start"); 833 sendURC("ERROR"); 834 if (mStartCallWakeLock.isHeld()) { 835 mStartCallWakeLock.release(); 836 } 837 } 838 break; 839 case CHECK_VOICE_RECOGNITION_STARTED: 840 if (mWaitingForVoiceRecognition) { 841 mWaitingForVoiceRecognition = false; 842 Log.e(TAG, "Timeout waiting for voice recognition to start"); 843 sendURC("ERROR"); 844 } 845 break; 846 } 847 } 848 }; 849 850 private ScoSocket createScoSocket() { 851 return new ScoSocket(mPowerManager, mHandler, SCO_ACCEPTED, SCO_CONNECTED, SCO_CLOSED); 852 } 853 854 private void broadcastAudioStateIntent(int state) { 855 if (VDBG) log("broadcastAudioStateIntent(" + state + ")"); 856 Intent intent = new Intent(BluetoothIntent.HEADSET_AUDIO_STATE_CHANGED_ACTION); 857 intent.putExtra(BluetoothIntent.HEADSET_AUDIO_STATE, state); 858 mContext.sendBroadcast(intent, android.Manifest.permission.BLUETOOTH); 859 } 860 861 862 void updateBtHandsfreeAfterRadioTechnologyChange() { 863 if(DBG) Log.d(TAG, "updateBtHandsfreeAfterRadioTechnologyChange..."); 864 865 //Get the Call references from the new active phone again 866 mRingingCall = mPhone.getRingingCall(); 867 mForegroundCall = mPhone.getForegroundCall(); 868 mBackgroundCall = mPhone.getBackgroundCall(); 869 870 mBluetoothPhoneState.updateBtPhoneStateAfterRadioTechnologyChange(); 871 } 872 873 /** Request to establish SCO (audio) connection to bluetooth 874 * headset/handsfree, if one is connected. Does not block. 875 * Returns false if the user has requested audio off, or if there 876 * is some other immediate problem that will prevent BT audio. 877 */ 878 /* package */ synchronized boolean audioOn() { 879 if (VDBG) log("audioOn()"); 880 if (!isHeadsetConnected()) { 881 if (DBG) log("audioOn(): headset is not connected!"); 882 return false; 883 } 884 if (mHeadsetType == TYPE_HANDSFREE && !mServiceConnectionEstablished) { 885 if (DBG) log("audioOn(): service connection not yet established!"); 886 return false; 887 } 888 889 if (mConnectedSco != null) { 890 if (DBG) log("audioOn(): audio is already connected"); 891 return true; 892 } 893 894 if (!mUserWantsAudio) { 895 if (DBG) log("audioOn(): user requested no audio, ignoring"); 896 return false; 897 } 898 899 if (mOutgoingSco != null) { 900 if (DBG) log("audioOn(): outgoing SCO already in progress"); 901 return true; 902 } 903 mOutgoingSco = createScoSocket(); 904 if (!mOutgoingSco.connect(mHeadset.getAddress())) { 905 mOutgoingSco = null; 906 } 907 908 return true; 909 } 910 911 /** Used to indicate the user requested BT audio on. 912 * This will establish SCO (BT audio), even if the user requested it off 913 * previously on this call. 914 */ 915 /* package */ synchronized void userWantsAudioOn() { 916 mUserWantsAudio = true; 917 audioOn(); 918 } 919 /** Used to indicate the user requested BT audio off. 920 * This will prevent us from establishing BT audio again during this call 921 * if audioOn() is called. 922 */ 923 /* package */ synchronized void userWantsAudioOff() { 924 mUserWantsAudio = false; 925 audioOff(); 926 } 927 928 /** Request to disconnect SCO (audio) connection to bluetooth 929 * headset/handsfree, if one is connected. Does not block. 930 */ 931 /* package */ synchronized void audioOff() { 932 if (VDBG) log("audioOff()"); 933 934 if (mConnectedSco != null) { 935 mAudioManager.setBluetoothScoOn(false); 936 broadcastAudioStateIntent(BluetoothHeadset.AUDIO_STATE_DISCONNECTED); 937 mConnectedSco.close(); 938 mConnectedSco = null; 939 } 940 if (mOutgoingSco != null) { 941 mOutgoingSco.close(); 942 mOutgoingSco = null; 943 } 944 } 945 946 /* package */ boolean isAudioOn() { 947 return (mConnectedSco != null); 948 } 949 950 /* package */ void ignoreRing() { 951 mBluetoothPhoneState.ignoreRing(); 952 } 953 954 private void sendURC(String urc) { 955 if (isHeadsetConnected()) { 956 mHeadset.sendURC(urc); 957 } 958 } 959 960 /** helper to redial last dialled number */ 961 private AtCommandResult redial() { 962 String number = mPhonebook.getLastDialledNumber(); 963 if (number == null) { 964 // spec seems to suggest sending ERROR if we dont have a 965 // number to redial 966 if (DBG) log("Bluetooth redial requested (+BLDN), but no previous " + 967 "outgoing calls found. Ignoring"); 968 return new AtCommandResult(AtCommandResult.ERROR); 969 } 970 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 971 Uri.fromParts("tel", number, null)); 972 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 973 mContext.startActivity(intent); 974 975 // We do not immediately respond OK, wait until we get a phone state 976 // update. If we return OK now and the handsfree immeidately requests 977 // our phone state it will say we are not in call yet which confuses 978 // some devices 979 expectCallStart(); 980 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing 981 } 982 983 /** Build the +CLCC result 984 * The complexity arises from the fact that we need to maintain the same 985 * CLCC index even as a call moves between states. */ 986 private synchronized AtCommandResult getClccResult() { 987 // Collect all known connections 988 Connection[] clccConnections = new Connection[MAX_CONNECTIONS]; // indexed by CLCC index 989 LinkedList<Connection> newConnections = new LinkedList<Connection>(); 990 LinkedList<Connection> connections = new LinkedList<Connection>(); 991 if (mRingingCall.getState().isAlive()) { 992 connections.addAll(mRingingCall.getConnections()); 993 } 994 if (mForegroundCall.getState().isAlive()) { 995 connections.addAll(mForegroundCall.getConnections()); 996 } 997 if (mBackgroundCall.getState().isAlive()) { 998 connections.addAll(mBackgroundCall.getConnections()); 999 } 1000 1001 // Mark connections that we already known about 1002 boolean clccUsed[] = new boolean[MAX_CONNECTIONS]; 1003 for (int i = 0; i < MAX_CONNECTIONS; i++) { 1004 clccUsed[i] = mClccUsed[i]; 1005 mClccUsed[i] = false; 1006 } 1007 for (Connection c : connections) { 1008 boolean found = false; 1009 long timestamp = c.getCreateTime(); 1010 for (int i = 0; i < MAX_CONNECTIONS; i++) { 1011 if (clccUsed[i] && timestamp == mClccTimestamps[i]) { 1012 mClccUsed[i] = true; 1013 found = true; 1014 clccConnections[i] = c; 1015 break; 1016 } 1017 } 1018 if (!found) { 1019 newConnections.add(c); 1020 } 1021 } 1022 1023 // Find a CLCC index for new connections 1024 while (!newConnections.isEmpty()) { 1025 // Find lowest empty index 1026 int i = 0; 1027 while (mClccUsed[i]) i++; 1028 // Find earliest connection 1029 long earliestTimestamp = newConnections.get(0).getCreateTime(); 1030 Connection earliestConnection = newConnections.get(0); 1031 for (int j = 0; j < newConnections.size(); j++) { 1032 long timestamp = newConnections.get(j).getCreateTime(); 1033 if (timestamp < earliestTimestamp) { 1034 earliestTimestamp = timestamp; 1035 earliestConnection = newConnections.get(j); 1036 } 1037 } 1038 1039 // update 1040 mClccUsed[i] = true; 1041 mClccTimestamps[i] = earliestTimestamp; 1042 clccConnections[i] = earliestConnection; 1043 newConnections.remove(earliestConnection); 1044 } 1045 1046 // Build CLCC 1047 AtCommandResult result = new AtCommandResult(AtCommandResult.OK); 1048 for (int i = 0; i < clccConnections.length; i++) { 1049 if (mClccUsed[i]) { 1050 String clccEntry = connectionToClccEntry(i, clccConnections[i]); 1051 if (clccEntry != null) { 1052 result.addResponse(clccEntry); 1053 } 1054 } 1055 } 1056 1057 return result; 1058 } 1059 1060 /** Convert a Connection object into a single +CLCC result */ 1061 private String connectionToClccEntry(int index, Connection c) { 1062 int state; 1063 switch (c.getState()) { 1064 case ACTIVE: 1065 state = 0; 1066 break; 1067 case HOLDING: 1068 state = 1; 1069 break; 1070 case DIALING: 1071 state = 2; 1072 break; 1073 case ALERTING: 1074 state = 3; 1075 break; 1076 case INCOMING: 1077 state = 4; 1078 break; 1079 case WAITING: 1080 state = 5; 1081 break; 1082 default: 1083 return null; // bad state 1084 } 1085 1086 int mpty = 0; 1087 Call call = c.getCall(); 1088 if (call != null) { 1089 mpty = call.isMultiparty() ? 1 : 0; 1090 } 1091 1092 int direction = c.isIncoming() ? 1 : 0; 1093 1094 String number = c.getAddress(); 1095 int type = -1; 1096 if (number != null) { 1097 type = PhoneNumberUtils.toaFromString(number); 1098 } 1099 1100 String result = "+CLCC: " + (index + 1) + "," + direction + "," + state + ",0," + mpty; 1101 if (number != null) { 1102 result += ",\"" + number + "\"," + type; 1103 } 1104 return result; 1105 } 1106 /** 1107 * Register AT Command handlers to implement the Headset profile 1108 */ 1109 private void initializeHeadsetAtParser() { 1110 if (DBG) log("Registering Headset AT commands"); 1111 AtParser parser = mHeadset.getAtParser(); 1112 // Headset's usually only have one button, which is meant to cause the 1113 // HS to send us AT+CKPD=200 or AT+CKPD. 1114 parser.register("+CKPD", new AtCommandHandler() { 1115 private AtCommandResult headsetButtonPress() { 1116 if (mRingingCall.isRinging()) { 1117 // Answer the call 1118 PhoneUtils.answerCall(mPhone); 1119 // If in-band ring tone is supported, SCO connection will already 1120 // be up and the following call will just return. 1121 audioOn(); 1122 } else if (mForegroundCall.getState().isAlive()) { 1123 if (!isAudioOn()) { 1124 // Transfer audio from AG to HS 1125 audioOn(); 1126 } else { 1127 if (mHeadset.getDirection() == HeadsetBase.DIRECTION_INCOMING && 1128 (System.currentTimeMillis() - mHeadset.getConnectTimestamp()) < 5000) { 1129 // Headset made a recent ACL connection to us - and 1130 // made a mandatory AT+CKPD request to connect 1131 // audio which races with our automatic audio 1132 // setup. ignore 1133 } else { 1134 // Hang up the call 1135 audioOff(); 1136 PhoneUtils.hangup(mPhone); 1137 } 1138 } 1139 } else { 1140 // No current call - redial last number 1141 return redial(); 1142 } 1143 return new AtCommandResult(AtCommandResult.OK); 1144 } 1145 @Override 1146 public AtCommandResult handleActionCommand() { 1147 return headsetButtonPress(); 1148 } 1149 @Override 1150 public AtCommandResult handleSetCommand(Object[] args) { 1151 return headsetButtonPress(); 1152 } 1153 }); 1154 } 1155 1156 /** 1157 * Register AT Command handlers to implement the Handsfree profile 1158 */ 1159 private void initializeHandsfreeAtParser() { 1160 if (DBG) log("Registering Handsfree AT commands"); 1161 AtParser parser = mHeadset.getAtParser(); 1162 1163 // Answer 1164 parser.register('A', new AtCommandHandler() { 1165 @Override 1166 public AtCommandResult handleBasicCommand(String args) { 1167 PhoneUtils.answerCall(mPhone); 1168 return new AtCommandResult(AtCommandResult.OK); 1169 } 1170 }); 1171 parser.register('D', new AtCommandHandler() { 1172 @Override 1173 public AtCommandResult handleBasicCommand(String args) { 1174 if (args.length() > 0) { 1175 if (args.charAt(0) == '>') { 1176 // Yuck - memory dialling requested. 1177 // Just dial last number for now 1178 if (args.startsWith(">9999")) { // for PTS test 1179 return new AtCommandResult(AtCommandResult.ERROR); 1180 } 1181 return redial(); 1182 } else { 1183 // Remove trailing ';' 1184 if (args.charAt(args.length() - 1) == ';') { 1185 args = args.substring(0, args.length() - 1); 1186 } 1187 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 1188 Uri.fromParts("tel", args, null)); 1189 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1190 mContext.startActivity(intent); 1191 1192 expectCallStart(); 1193 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing 1194 } 1195 } 1196 return new AtCommandResult(AtCommandResult.ERROR); 1197 } 1198 }); 1199 1200 // Hang-up command 1201 parser.register("+CHUP", new AtCommandHandler() { 1202 @Override 1203 public AtCommandResult handleActionCommand() { 1204 if (!mForegroundCall.isIdle()) { 1205 PhoneUtils.hangup(mForegroundCall); 1206 } else if (!mRingingCall.isIdle()) { 1207 PhoneUtils.hangup(mRingingCall); 1208 } else if (!mBackgroundCall.isIdle()) { 1209 PhoneUtils.hangup(mBackgroundCall); 1210 } 1211 return new AtCommandResult(AtCommandResult.OK); 1212 } 1213 }); 1214 1215 // Bluetooth Retrieve Supported Features command 1216 parser.register("+BRSF", new AtCommandHandler() { 1217 private AtCommandResult sendBRSF() { 1218 return new AtCommandResult("+BRSF: " + mLocalBrsf); 1219 } 1220 @Override 1221 public AtCommandResult handleSetCommand(Object[] args) { 1222 // AT+BRSF=<handsfree supported features bitmap> 1223 // Handsfree is telling us which features it supports. We 1224 // send the features we support 1225 if (args.length == 1 && (args[0] instanceof Integer)) { 1226 mRemoteBrsf = (Integer) args[0]; 1227 } else { 1228 Log.w(TAG, "HF didn't sent BRSF assuming 0"); 1229 } 1230 return sendBRSF(); 1231 } 1232 @Override 1233 public AtCommandResult handleActionCommand() { 1234 // This seems to be out of spec, but lets do the nice thing 1235 return sendBRSF(); 1236 } 1237 @Override 1238 public AtCommandResult handleReadCommand() { 1239 // This seems to be out of spec, but lets do the nice thing 1240 return sendBRSF(); 1241 } 1242 }); 1243 1244 // Call waiting notification on/off 1245 parser.register("+CCWA", new AtCommandHandler() { 1246 @Override 1247 public AtCommandResult handleActionCommand() { 1248 // Seems to be out of spec, but lets return nicely 1249 return new AtCommandResult(AtCommandResult.OK); 1250 } 1251 @Override 1252 public AtCommandResult handleReadCommand() { 1253 // Call waiting is always on 1254 return new AtCommandResult("+CCWA: 1"); 1255 } 1256 @Override 1257 public AtCommandResult handleSetCommand(Object[] args) { 1258 // AT+CCWA=<n> 1259 // Handsfree is trying to enable/disable call waiting. We 1260 // cannot disable in the current implementation. 1261 return new AtCommandResult(AtCommandResult.OK); 1262 } 1263 @Override 1264 public AtCommandResult handleTestCommand() { 1265 // Request for range of supported CCWA paramters 1266 return new AtCommandResult("+CCWA: (\"n\",(1))"); 1267 } 1268 }); 1269 1270 // Mobile Equipment Event Reporting enable/disable command 1271 // Of the full 3GPP syntax paramters (mode, keyp, disp, ind, bfr) we 1272 // only support paramter ind (disable/enable evert reporting using 1273 // +CDEV) 1274 parser.register("+CMER", new AtCommandHandler() { 1275 @Override 1276 public AtCommandResult handleReadCommand() { 1277 return new AtCommandResult( 1278 "+CMER: 3,0,0," + (mIndicatorsEnabled ? "1" : "0")); 1279 } 1280 @Override 1281 public AtCommandResult handleSetCommand(Object[] args) { 1282 if (args.length < 4) { 1283 // This is a syntax error 1284 return new AtCommandResult(AtCommandResult.ERROR); 1285 } else if (args[0].equals(3) && args[1].equals(0) && 1286 args[2].equals(0)) { 1287 boolean valid = false; 1288 if (args[3].equals(0)) { 1289 mIndicatorsEnabled = false; 1290 valid = true; 1291 } else if (args[3].equals(1)) { 1292 mIndicatorsEnabled = true; 1293 valid = true; 1294 } 1295 if (valid) { 1296 if ((mRemoteBrsf & BRSF_HF_CW_THREE_WAY_CALLING) == 0x0) { 1297 mServiceConnectionEstablished = true; 1298 sendURC("OK"); // send immediately, then initiate audio 1299 if (isIncallAudio()) { 1300 audioOn(); 1301 } 1302 // only send OK once 1303 return new AtCommandResult(AtCommandResult.UNSOLICITED); 1304 } else { 1305 return new AtCommandResult(AtCommandResult.OK); 1306 } 1307 } 1308 } 1309 return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED); 1310 } 1311 @Override 1312 public AtCommandResult handleTestCommand() { 1313 return new AtCommandResult("+CMER: (3),(0),(0),(0-1)"); 1314 } 1315 }); 1316 1317 // Mobile Equipment Error Reporting enable/disable 1318 parser.register("+CMEE", new AtCommandHandler() { 1319 @Override 1320 public AtCommandResult handleActionCommand() { 1321 // out of spec, assume they want to enable 1322 mCmee = true; 1323 return new AtCommandResult(AtCommandResult.OK); 1324 } 1325 @Override 1326 public AtCommandResult handleReadCommand() { 1327 return new AtCommandResult("+CMEE: " + (mCmee ? "1" : "0")); 1328 } 1329 @Override 1330 public AtCommandResult handleSetCommand(Object[] args) { 1331 // AT+CMEE=<n> 1332 if (args.length == 0) { 1333 // <n> ommitted - default to 0 1334 mCmee = false; 1335 return new AtCommandResult(AtCommandResult.OK); 1336 } else if (!(args[0] instanceof Integer)) { 1337 // Syntax error 1338 return new AtCommandResult(AtCommandResult.ERROR); 1339 } else { 1340 mCmee = ((Integer)args[0] == 1); 1341 return new AtCommandResult(AtCommandResult.OK); 1342 } 1343 } 1344 @Override 1345 public AtCommandResult handleTestCommand() { 1346 // Probably not required but spec, but no harm done 1347 return new AtCommandResult("+CMEE: (0-1)"); 1348 } 1349 }); 1350 1351 // Bluetooth Last Dialled Number 1352 parser.register("+BLDN", new AtCommandHandler() { 1353 @Override 1354 public AtCommandResult handleActionCommand() { 1355 return redial(); 1356 } 1357 }); 1358 1359 // Indicator Update command 1360 parser.register("+CIND", new AtCommandHandler() { 1361 @Override 1362 public AtCommandResult handleReadCommand() { 1363 return mBluetoothPhoneState.toCindResult(); 1364 } 1365 @Override 1366 public AtCommandResult handleTestCommand() { 1367 return mBluetoothPhoneState.getCindTestResult(); 1368 } 1369 }); 1370 1371 // Query Signal Quality (legacy) 1372 parser.register("+CSQ", new AtCommandHandler() { 1373 @Override 1374 public AtCommandResult handleActionCommand() { 1375 return mBluetoothPhoneState.toCsqResult(); 1376 } 1377 }); 1378 1379 // Query network registration state 1380 parser.register("+CREG", new AtCommandHandler() { 1381 @Override 1382 public AtCommandResult handleReadCommand() { 1383 return new AtCommandResult(mBluetoothPhoneState.toCregString()); 1384 } 1385 }); 1386 1387 // Send DTMF. I don't know if we are also expected to play the DTMF tone 1388 // locally, right now we don't 1389 parser.register("+VTS", new AtCommandHandler() { 1390 @Override 1391 public AtCommandResult handleSetCommand(Object[] args) { 1392 if (args.length >= 1) { 1393 char c; 1394 if (args[0] instanceof Integer) { 1395 c = ((Integer) args[0]).toString().charAt(0); 1396 } else { 1397 c = ((String) args[0]).charAt(0); 1398 } 1399 if (isValidDtmf(c)) { 1400 mPhone.sendDtmf(c); 1401 return new AtCommandResult(AtCommandResult.OK); 1402 } 1403 } 1404 return new AtCommandResult(AtCommandResult.ERROR); 1405 } 1406 private boolean isValidDtmf(char c) { 1407 switch (c) { 1408 case '#': 1409 case '*': 1410 return true; 1411 default: 1412 if (Character.digit(c, 14) != -1) { 1413 return true; // 0-9 and A-D 1414 } 1415 return false; 1416 } 1417 } 1418 }); 1419 1420 // List calls 1421 parser.register("+CLCC", new AtCommandHandler() { 1422 @Override 1423 public AtCommandResult handleActionCommand() { 1424 return getClccResult(); 1425 } 1426 }); 1427 1428 // Call Hold and Multiparty Handling command 1429 parser.register("+CHLD", new AtCommandHandler() { 1430 @Override 1431 public AtCommandResult handleSetCommand(Object[] args) { 1432 if (args.length >= 1) { 1433 if (args[0].equals(0)) { 1434 boolean result; 1435 if (mRingingCall.isRinging()) { 1436 result = PhoneUtils.hangupRingingCall(mPhone); 1437 } else { 1438 result = PhoneUtils.hangupHoldingCall(mPhone); 1439 } 1440 if (result) { 1441 return new AtCommandResult(AtCommandResult.OK); 1442 } else { 1443 return new AtCommandResult(AtCommandResult.ERROR); 1444 } 1445 } else if (args[0].equals(1)) { 1446 if (mPhone.getPhoneName().equals("CDMA")) { 1447 // For CDMA, there is no answerAndEndActive, so we can behave 1448 // the same way as CHLD=2 here 1449 PhoneUtils.answerCall(mPhone); 1450 PhoneUtils.setMute(mPhone, false); 1451 return new AtCommandResult(AtCommandResult.OK); 1452 } else { 1453 // Hangup active call, answer held call 1454 if (PhoneUtils.answerAndEndActive(mPhone)) { 1455 return new AtCommandResult(AtCommandResult.OK); 1456 } else { 1457 return new AtCommandResult(AtCommandResult.ERROR); 1458 } 1459 } 1460 } else if (args[0].equals(2)) { 1461 if (mPhone.getPhoneName().equals("CDMA")) { 1462 // For CDMA, the way we switch to a new incoming call is by 1463 // calling PhoneUtils.answerCall(). switchAndHoldActive() won't 1464 // properly update the call state within telephony. 1465 PhoneUtils.answerCall(mPhone); 1466 PhoneUtils.setMute(mPhone, false); 1467 } else { 1468 PhoneUtils.switchHoldingAndActive(mPhone); 1469 } 1470 return new AtCommandResult(AtCommandResult.OK); 1471 } else if (args[0].equals(3)) { 1472 if (mForegroundCall.getState().isAlive() && 1473 mBackgroundCall.getState().isAlive()) { 1474 PhoneUtils.mergeCalls(mPhone); 1475 } 1476 return new AtCommandResult(AtCommandResult.OK); 1477 } 1478 } 1479 return new AtCommandResult(AtCommandResult.ERROR); 1480 } 1481 @Override 1482 public AtCommandResult handleTestCommand() { 1483 mServiceConnectionEstablished = true; 1484 sendURC("+CHLD: (0,1,2,3)"); 1485 sendURC("OK"); // send reply first, then connect audio 1486 if (isIncallAudio()) { 1487 audioOn(); 1488 } 1489 // already replied 1490 return new AtCommandResult(AtCommandResult.UNSOLICITED); 1491 } 1492 }); 1493 1494 // Get Network operator name 1495 parser.register("+COPS", new AtCommandHandler() { 1496 @Override 1497 public AtCommandResult handleReadCommand() { 1498 String operatorName = mPhone.getServiceState().getOperatorAlphaLong(); 1499 if (operatorName != null) { 1500 if (operatorName.length() > 16) { 1501 operatorName = operatorName.substring(0, 16); 1502 } 1503 return new AtCommandResult( 1504 "+COPS: 0,0,\"" + operatorName + "\""); 1505 } else { 1506 return new AtCommandResult( 1507 "+COPS: 0,0,\"UNKNOWN\",0"); 1508 } 1509 } 1510 @Override 1511 public AtCommandResult handleSetCommand(Object[] args) { 1512 // Handsfree only supports AT+COPS=3,0 1513 if (args.length != 2 || !(args[0] instanceof Integer) 1514 || !(args[1] instanceof Integer)) { 1515 // syntax error 1516 return new AtCommandResult(AtCommandResult.ERROR); 1517 } else if ((Integer)args[0] != 3 || (Integer)args[1] != 0) { 1518 return reportCmeError(BluetoothCmeError.OPERATION_NOT_SUPPORTED); 1519 } else { 1520 return new AtCommandResult(AtCommandResult.OK); 1521 } 1522 } 1523 @Override 1524 public AtCommandResult handleTestCommand() { 1525 // Out of spec, but lets be friendly 1526 return new AtCommandResult("+COPS: (3),(0)"); 1527 } 1528 }); 1529 1530 // Mobile PIN 1531 // AT+CPIN is not in the handsfree spec (although it is in 3GPP) 1532 parser.register("+CPIN", new AtCommandHandler() { 1533 @Override 1534 public AtCommandResult handleReadCommand() { 1535 return new AtCommandResult("+CPIN: READY"); 1536 } 1537 }); 1538 1539 // Bluetooth Response and Hold 1540 // Only supported on PDC (Japan) and CDMA networks. 1541 parser.register("+BTRH", new AtCommandHandler() { 1542 @Override 1543 public AtCommandResult handleReadCommand() { 1544 // Replying with just OK indicates no response and hold 1545 // features in use now 1546 return new AtCommandResult(AtCommandResult.OK); 1547 } 1548 @Override 1549 public AtCommandResult handleSetCommand(Object[] args) { 1550 // Neeed PDC or CDMA 1551 return new AtCommandResult(AtCommandResult.ERROR); 1552 } 1553 }); 1554 1555 // Request International Mobile Subscriber Identity (IMSI) 1556 // Not in bluetooth handset spec 1557 parser.register("+CIMI", new AtCommandHandler() { 1558 @Override 1559 public AtCommandResult handleActionCommand() { 1560 // AT+CIMI 1561 String imsi = mPhone.getSubscriberId(); 1562 if (imsi == null || imsi.length() == 0) { 1563 return reportCmeError(BluetoothCmeError.SIM_FAILURE); 1564 } else { 1565 return new AtCommandResult(imsi); 1566 } 1567 } 1568 }); 1569 1570 // Calling Line Identification Presentation 1571 parser.register("+CLIP", new AtCommandHandler() { 1572 @Override 1573 public AtCommandResult handleReadCommand() { 1574 // Currently assumes the network is provisioned for CLIP 1575 return new AtCommandResult("+CLIP: " + (mClip ? "1" : "0") + ",1"); 1576 } 1577 @Override 1578 public AtCommandResult handleSetCommand(Object[] args) { 1579 // AT+CLIP=<n> 1580 if (args.length >= 1 && (args[0].equals(0) || args[0].equals(1))) { 1581 mClip = args[0].equals(1); 1582 return new AtCommandResult(AtCommandResult.OK); 1583 } else { 1584 return new AtCommandResult(AtCommandResult.ERROR); 1585 } 1586 } 1587 @Override 1588 public AtCommandResult handleTestCommand() { 1589 return new AtCommandResult("+CLIP: (0-1)"); 1590 } 1591 }); 1592 1593 // AT+CGSN - Returns the device IMEI number. 1594 parser.register("+CGSN", new AtCommandHandler() { 1595 @Override 1596 public AtCommandResult handleActionCommand() { 1597 // Get the IMEI of the device. 1598 // mPhone will not be NULL at this point. 1599 return new AtCommandResult("+CGSN: " + mPhone.getDeviceId()); 1600 } 1601 }); 1602 1603 // AT+CGMM - Query Model Information 1604 parser.register("+CGMM", new AtCommandHandler() { 1605 @Override 1606 public AtCommandResult handleActionCommand() { 1607 // Return the Model Information. 1608 String model = SystemProperties.get("ro.product.model"); 1609 if (model != null) { 1610 return new AtCommandResult("+CGMM: " + model); 1611 } else { 1612 return new AtCommandResult(AtCommandResult.ERROR); 1613 } 1614 } 1615 }); 1616 1617 // AT+CGMI - Query Manufacturer Information 1618 parser.register("+CGMI", new AtCommandHandler() { 1619 @Override 1620 public AtCommandResult handleActionCommand() { 1621 // Return the Model Information. 1622 String manuf = SystemProperties.get("ro.product.manufacturer"); 1623 if (manuf != null) { 1624 return new AtCommandResult("+CGMI: " + manuf); 1625 } else { 1626 return new AtCommandResult(AtCommandResult.ERROR); 1627 } 1628 } 1629 }); 1630 1631 // Noise Reduction and Echo Cancellation control 1632 parser.register("+NREC", new AtCommandHandler() { 1633 @Override 1634 public AtCommandResult handleSetCommand(Object[] args) { 1635 if (args[0].equals(0)) { 1636 mAudioManager.setParameter(HEADSET_NREC, "off"); 1637 return new AtCommandResult(AtCommandResult.OK); 1638 } else if (args[0].equals(1)) { 1639 mAudioManager.setParameter(HEADSET_NREC, "on"); 1640 return new AtCommandResult(AtCommandResult.OK); 1641 } 1642 return new AtCommandResult(AtCommandResult.ERROR); 1643 } 1644 }); 1645 1646 // Voice recognition (dialing) 1647 parser.register("+BVRA", new AtCommandHandler() { 1648 @Override 1649 public AtCommandResult handleSetCommand(Object[] args) { 1650 if (BluetoothHeadset.DISABLE_BT_VOICE_DIALING) { 1651 return new AtCommandResult(AtCommandResult.ERROR); 1652 } 1653 if (args.length >= 1 && args[0].equals(1)) { 1654 synchronized (BluetoothHandsfree.this) { 1655 if (!mWaitingForVoiceRecognition) { 1656 try { 1657 mContext.startActivity(sVoiceCommandIntent); 1658 } catch (ActivityNotFoundException e) { 1659 return new AtCommandResult(AtCommandResult.ERROR); 1660 } 1661 expectVoiceRecognition(); 1662 } 1663 } 1664 return new AtCommandResult(AtCommandResult.UNSOLICITED); // send nothing yet 1665 } else if (args.length >= 1 && args[0].equals(0)) { 1666 audioOff(); 1667 return new AtCommandResult(AtCommandResult.OK); 1668 } 1669 return new AtCommandResult(AtCommandResult.ERROR); 1670 } 1671 @Override 1672 public AtCommandResult handleTestCommand() { 1673 return new AtCommandResult("+BVRA: (0-1)"); 1674 } 1675 }); 1676 1677 // Retrieve Subscriber Number 1678 parser.register("+CNUM", new AtCommandHandler() { 1679 @Override 1680 public AtCommandResult handleActionCommand() { 1681 String number = mPhone.getLine1Number(); 1682 if (number == null) { 1683 return new AtCommandResult(AtCommandResult.OK); 1684 } 1685 return new AtCommandResult("+CNUM: ,\"" + number + "\"," + 1686 PhoneNumberUtils.toaFromString(number) + ",,4"); 1687 } 1688 }); 1689 1690 // Microphone Gain 1691 parser.register("+VGM", new AtCommandHandler() { 1692 @Override 1693 public AtCommandResult handleSetCommand(Object[] args) { 1694 // AT+VGM=<gain> in range [0,15] 1695 // Headset/Handsfree is reporting its current gain setting 1696 return new AtCommandResult(AtCommandResult.OK); 1697 } 1698 }); 1699 1700 // Speaker Gain 1701 parser.register("+VGS", new AtCommandHandler() { 1702 @Override 1703 public AtCommandResult handleSetCommand(Object[] args) { 1704 // AT+VGS=<gain> in range [0,15] 1705 if (args.length != 1 || !(args[0] instanceof Integer)) { 1706 return new AtCommandResult(AtCommandResult.ERROR); 1707 } 1708 mScoGain = (Integer) args[0]; 1709 int flag = mAudioManager.isBluetoothScoOn() ? AudioManager.FLAG_SHOW_UI:0; 1710 1711 mAudioManager.setStreamVolume(AudioManager.STREAM_BLUETOOTH_SCO, mScoGain, flag); 1712 return new AtCommandResult(AtCommandResult.OK); 1713 } 1714 }); 1715 1716 // Phone activity status 1717 parser.register("+CPAS", new AtCommandHandler() { 1718 @Override 1719 public AtCommandResult handleActionCommand() { 1720 int status = 0; 1721 switch (mPhone.getState()) { 1722 case IDLE: 1723 status = 0; 1724 break; 1725 case RINGING: 1726 status = 3; 1727 break; 1728 case OFFHOOK: 1729 status = 4; 1730 break; 1731 } 1732 return new AtCommandResult("+CPAS: " + status); 1733 } 1734 }); 1735 mPhonebook.register(parser); 1736 } 1737 1738 public void sendScoGainUpdate(int gain) { 1739 if (mScoGain != gain && (mRemoteBrsf & BRSF_HF_REMOTE_VOL_CONTROL) != 0x0) { 1740 sendURC("+VGS:" + gain); 1741 mScoGain = gain; 1742 } 1743 } 1744 1745 public AtCommandResult reportCmeError(int error) { 1746 if (mCmee) { 1747 AtCommandResult result = new AtCommandResult(AtCommandResult.UNSOLICITED); 1748 result.addResponse("+CME ERROR: " + error); 1749 return result; 1750 } else { 1751 return new AtCommandResult(AtCommandResult.ERROR); 1752 } 1753 } 1754 1755 private static final int START_CALL_TIMEOUT = 10000; // ms 1756 1757 private synchronized void expectCallStart() { 1758 mWaitingForCallStart = true; 1759 Message msg = Message.obtain(mHandler, CHECK_CALL_STARTED); 1760 mHandler.sendMessageDelayed(msg, START_CALL_TIMEOUT); 1761 if (!mStartCallWakeLock.isHeld()) { 1762 mStartCallWakeLock.acquire(START_CALL_TIMEOUT); 1763 } 1764 } 1765 1766 private synchronized void callStarted() { 1767 if (mWaitingForCallStart) { 1768 mWaitingForCallStart = false; 1769 sendURC("OK"); 1770 if (mStartCallWakeLock.isHeld()) { 1771 mStartCallWakeLock.release(); 1772 } 1773 } 1774 } 1775 1776 private static final int START_VOICE_RECOGNITION_TIMEOUT = 5000; // ms 1777 1778 private synchronized void expectVoiceRecognition() { 1779 mWaitingForVoiceRecognition = true; 1780 Message msg = Message.obtain(mHandler, CHECK_VOICE_RECOGNITION_STARTED); 1781 mHandler.sendMessageDelayed(msg, START_VOICE_RECOGNITION_TIMEOUT); 1782 if (!mStartVoiceRecognitionWakeLock.isHeld()) { 1783 mStartVoiceRecognitionWakeLock.acquire(START_VOICE_RECOGNITION_TIMEOUT); 1784 } 1785 } 1786 1787 /* package */ synchronized boolean startVoiceRecognition() { 1788 if (mWaitingForVoiceRecognition) { 1789 // HF initiated 1790 mWaitingForVoiceRecognition = false; 1791 sendURC("OK"); 1792 } else { 1793 // AG initiated 1794 sendURC("+BVRA: 1"); 1795 } 1796 boolean ret = audioOn(); 1797 if (mStartVoiceRecognitionWakeLock.isHeld()) { 1798 mStartVoiceRecognitionWakeLock.release(); 1799 } 1800 return ret; 1801 } 1802 1803 /* package */ synchronized boolean stopVoiceRecognition() { 1804 sendURC("+BVRA: 0"); 1805 audioOff(); 1806 return true; 1807 } 1808 1809 private boolean inDebug() { 1810 return DBG && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE, false); 1811 } 1812 1813 private boolean allowAudioAnytime() { 1814 return inDebug() && SystemProperties.getBoolean(DebugThread.DEBUG_HANDSFREE_AUDIO_ANYTIME, 1815 false); 1816 } 1817 1818 private void startDebug() { 1819 if (DBG && mDebugThread == null) { 1820 mDebugThread = new DebugThread(); 1821 mDebugThread.start(); 1822 } 1823 } 1824 1825 private void stopDebug() { 1826 if (mDebugThread != null) { 1827 mDebugThread.interrupt(); 1828 mDebugThread = null; 1829 } 1830 } 1831 1832 /** Debug thread to read debug properties - runs when debug.bt.hfp is true 1833 * at the time a bluetooth handsfree device is connected. Debug properties 1834 * are polled and mock updates sent every 1 second */ 1835 private class DebugThread extends Thread { 1836 /** Turns on/off handsfree profile debugging mode */ 1837 private static final String DEBUG_HANDSFREE = "debug.bt.hfp"; 1838 1839 /** Mock battery level change - use 0 to 5 */ 1840 private static final String DEBUG_HANDSFREE_BATTERY = "debug.bt.hfp.battery"; 1841 1842 /** Mock no cellular service when false */ 1843 private static final String DEBUG_HANDSFREE_SERVICE = "debug.bt.hfp.service"; 1844 1845 /** Mock cellular roaming when true */ 1846 private static final String DEBUG_HANDSFREE_ROAM = "debug.bt.hfp.roam"; 1847 1848 /** false to true transition will force an audio (SCO) connection to 1849 * be established. true to false will force audio to be disconnected 1850 */ 1851 private static final String DEBUG_HANDSFREE_AUDIO = "debug.bt.hfp.audio"; 1852 1853 /** true allows incoming SCO connection out of call. 1854 */ 1855 private static final String DEBUG_HANDSFREE_AUDIO_ANYTIME = "debug.bt.hfp.audio_anytime"; 1856 1857 /** Mock signal strength change in ASU - use 0 to 31 */ 1858 private static final String DEBUG_HANDSFREE_SIGNAL = "debug.bt.hfp.signal"; 1859 1860 /** Debug AT+CLCC: print +CLCC result */ 1861 private static final String DEBUG_HANDSFREE_CLCC = "debug.bt.hfp.clcc"; 1862 1863 /** Debug AT+BSIR - Send In Band Ringtones Unsolicited AT command. 1864 * debug.bt.unsol.inband = 0 => AT+BSIR = 0 sent by the AG 1865 * debug.bt.unsol.inband = 1 => AT+BSIR = 0 sent by the AG 1866 * Other values are ignored. 1867 */ 1868 1869 private static final String DEBUG_UNSOL_INBAND_RINGTONE = 1870 "debug.bt.unsol.inband"; 1871 1872 @Override 1873 public void run() { 1874 boolean oldService = true; 1875 boolean oldRoam = false; 1876 boolean oldAudio = false; 1877 1878 while (!isInterrupted() && inDebug()) { 1879 int batteryLevel = SystemProperties.getInt(DEBUG_HANDSFREE_BATTERY, -1); 1880 if (batteryLevel >= 0 && batteryLevel <= 5) { 1881 Intent intent = new Intent(); 1882 intent.putExtra("level", batteryLevel); 1883 intent.putExtra("scale", 5); 1884 mBluetoothPhoneState.updateBatteryState(intent); 1885 } 1886 1887 boolean serviceStateChanged = false; 1888 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_SERVICE, true) != oldService) { 1889 oldService = !oldService; 1890 serviceStateChanged = true; 1891 } 1892 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_ROAM, false) != oldRoam) { 1893 oldRoam = !oldRoam; 1894 serviceStateChanged = true; 1895 } 1896 if (serviceStateChanged) { 1897 Bundle b = new Bundle(); 1898 b.putInt("state", oldService ? 0 : 1); 1899 b.putBoolean("roaming", oldRoam); 1900 mBluetoothPhoneState.updateServiceState(true, ServiceState.newFromBundle(b)); 1901 } 1902 1903 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_AUDIO, false) != oldAudio) { 1904 oldAudio = !oldAudio; 1905 if (oldAudio) { 1906 audioOn(); 1907 } else { 1908 audioOff(); 1909 } 1910 } 1911 1912 int signalLevel = SystemProperties.getInt(DEBUG_HANDSFREE_SIGNAL, -1); 1913 if (signalLevel >= 0 && signalLevel <= 31) { 1914 SignalStrength signalStrength = new SignalStrength(signalLevel, -1, -1, -1, 1915 -1, -1, -1, true); 1916 Intent intent = new Intent(); 1917 Bundle data = new Bundle(); 1918 signalStrength.fillInNotifierBundle(data); 1919 intent.putExtras(data); 1920 mBluetoothPhoneState.updateSignalState(intent); 1921 } 1922 1923 if (SystemProperties.getBoolean(DEBUG_HANDSFREE_CLCC, false)) { 1924 log(getClccResult().toString()); 1925 } 1926 try { 1927 sleep(1000); // 1 second 1928 } catch (InterruptedException e) { 1929 break; 1930 } 1931 1932 int inBandRing = 1933 SystemProperties.getInt(DEBUG_UNSOL_INBAND_RINGTONE, -1); 1934 if (inBandRing == 0 || inBandRing == 1) { 1935 AtCommandResult result = 1936 new AtCommandResult(AtCommandResult.UNSOLICITED); 1937 result.addResponse("+BSIR: " + inBandRing); 1938 sendURC(result.toString()); 1939 } 1940 } 1941 } 1942 } 1943 1944 private static void log(String msg) { 1945 Log.d(TAG, msg); 1946 } 1947} 1948 1949 1950 1951