1 /* 2 * Copyright (C) 2011 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.phone; 18 19 import com.android.internal.telephony.CallManager; 20 import com.android.internal.telephony.Connection; 21 import com.android.internal.telephony.Phone; 22 import com.android.phone.Constants.CallStatusCode; 23 import com.android.phone.InCallUiState.ProgressIndicationType; 24 25 import android.content.Context; 26 import android.content.Intent; 27 import android.os.AsyncResult; 28 import android.os.Handler; 29 import android.os.Message; 30 import android.os.PowerManager; 31 import android.provider.Settings; 32 import android.telephony.ServiceState; 33 import android.util.Log; 34 35 36 /** 37 * Helper class for the {@link CallController} that implements special 38 * behavior related to emergency calls. Specifically, this class handles 39 * the case of the user trying to dial an emergency number while the radio 40 * is off (i.e. the device is in airplane mode), by forcibly turning the 41 * radio back on, waiting for it to come up, and then retrying the 42 * emergency call. 43 * 44 * This class is instantiated lazily (the first time the user attempts to 45 * make an emergency call from airplane mode) by the the 46 * {@link CallController} singleton. 47 */ 48 public class EmergencyCallHelper extends Handler { 49 private static final String TAG = "EmergencyCallHelper"; 50 private static final boolean DBG = true; 51 52 // Number of times to retry the call, and time between retry attempts. 53 public static final int MAX_NUM_RETRIES = 6; 54 public static final long TIME_BETWEEN_RETRIES = 5000; // msec 55 56 // Timeout used with our wake lock (just as a safety valve to make 57 // sure we don't hold it forever). 58 public static final long WAKE_LOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in msec 59 60 // Handler message codes; see handleMessage() 61 private static final int START_SEQUENCE = 1; 62 private static final int SERVICE_STATE_CHANGED = 2; 63 private static final int DISCONNECT = 3; 64 private static final int RETRY_TIMEOUT = 4; 65 66 private CallController mCallController; 67 private PhoneApp mApp; 68 private CallManager mCM; 69 private Phone mPhone; 70 private String mNumber; // The emergency number we're trying to dial 71 private int mNumRetriesSoFar; 72 73 // Wake lock we hold while running the whole sequence 74 private PowerManager.WakeLock mPartialWakeLock; 75 EmergencyCallHelper(CallController callController)76 public EmergencyCallHelper(CallController callController) { 77 if (DBG) log("EmergencyCallHelper constructor..."); 78 mCallController = callController; 79 mApp = PhoneApp.getInstance(); 80 mCM = mApp.mCM; 81 } 82 83 @Override handleMessage(Message msg)84 public void handleMessage(Message msg) { 85 switch (msg.what) { 86 case START_SEQUENCE: 87 startSequenceInternal(msg); 88 break; 89 case SERVICE_STATE_CHANGED: 90 onServiceStateChanged(msg); 91 break; 92 case DISCONNECT: 93 onDisconnect(msg); 94 break; 95 case RETRY_TIMEOUT: 96 onRetryTimeout(); 97 break; 98 default: 99 Log.wtf(TAG, "handleMessage: unexpected message: " + msg); 100 break; 101 } 102 } 103 104 /** 105 * Starts the "emergency call from airplane mode" sequence. 106 * 107 * This is the (single) external API of the EmergencyCallHelper class. 108 * This method is called from the CallController placeCall() sequence 109 * if the user dials a valid emergency number, but the radio is 110 * powered-off (presumably due to airplane mode.) 111 * 112 * This method kicks off the following sequence: 113 * - Power on the radio 114 * - Listen for the service state change event telling us the radio has come up 115 * - Then launch the emergency call 116 * - Retry if the call fails with an OUT_OF_SERVICE error 117 * - Retry if we've gone 5 seconds without any response from the radio 118 * - Finally, clean up any leftover state (progress UI, wake locks, etc.) 119 * 120 * This method is safe to call from any thread, since it simply posts 121 * a message to the EmergencyCallHelper's handler (thus ensuring that 122 * the rest of the sequence is entirely serialized, and runs only on 123 * the handler thread.) 124 * 125 * This method does *not* force the in-call UI to come up; our caller 126 * is responsible for doing that (presumably by calling 127 * PhoneApp.displayCallScreen().) 128 */ startEmergencyCallFromAirplaneModeSequence(String number)129 public void startEmergencyCallFromAirplaneModeSequence(String number) { 130 if (DBG) log("startEmergencyCallFromAirplaneModeSequence('" + number + "')..."); 131 Message msg = obtainMessage(START_SEQUENCE, number); 132 sendMessage(msg); 133 } 134 135 /** 136 * Actual implementation of startEmergencyCallFromAirplaneModeSequence(), 137 * guaranteed to run on the handler thread. 138 * @see startEmergencyCallFromAirplaneModeSequence() 139 */ startSequenceInternal(Message msg)140 private void startSequenceInternal(Message msg) { 141 if (DBG) log("startSequenceInternal(): msg = " + msg); 142 143 // First of all, clean up any state (including mPartialWakeLock!) 144 // left over from a prior emergency call sequence. 145 // This ensures that we'll behave sanely if another 146 // startEmergencyCallFromAirplaneModeSequence() comes in while 147 // we're already in the middle of the sequence. 148 cleanup(); 149 150 mNumber = (String) msg.obj; 151 if (DBG) log("- startSequenceInternal: Got mNumber: '" + mNumber + "'"); 152 153 mNumRetriesSoFar = 0; 154 155 // Reset mPhone to whatever the current default phone is right now. 156 mPhone = mApp.mCM.getDefaultPhone(); 157 158 // Wake lock to make sure the processor doesn't go to sleep midway 159 // through the emergency call sequence. 160 PowerManager pm = (PowerManager) mApp.getSystemService(Context.POWER_SERVICE); 161 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 162 // Acquire with a timeout, just to be sure we won't hold the wake 163 // lock forever even if a logic bug (in this class) causes us to 164 // somehow never call cleanup(). 165 if (DBG) log("- startSequenceInternal: acquiring wake lock"); 166 mPartialWakeLock.acquire(WAKE_LOCK_TIMEOUT); 167 168 // No need to check the current service state here, since the only 169 // reason the CallController would call this method in the first 170 // place is if the radio is powered-off. 171 // 172 // So just go ahead and turn the radio on. 173 174 powerOnRadio(); // We'll get an onServiceStateChanged() callback 175 // when the radio successfully comes up. 176 177 // Next step: when the SERVICE_STATE_CHANGED event comes in, 178 // we'll retry the call; see placeEmergencyCall(); 179 // But also, just in case, start a timer to make sure we'll retry 180 // the call even if the SERVICE_STATE_CHANGED event never comes in 181 // for some reason. 182 startRetryTimer(); 183 184 // And finally, let the in-call UI know that we need to 185 // display the "Turning on radio..." progress indication. 186 mApp.inCallUiState.setProgressIndication(ProgressIndicationType.TURNING_ON_RADIO); 187 188 // (Our caller is responsible for calling mApp.displayCallScreen().) 189 } 190 191 /** 192 * Handles the SERVICE_STATE_CHANGED event. 193 * 194 * (Normally this event tells us that the radio has finally come 195 * up. In that case, it's now safe to actually place the 196 * emergency call.) 197 */ onServiceStateChanged(Message msg)198 private void onServiceStateChanged(Message msg) { 199 ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result; 200 if (DBG) log("onServiceStateChanged()... new state = " + state); 201 202 // Possible service states: 203 // - STATE_IN_SERVICE // Normal operation 204 // - STATE_OUT_OF_SERVICE // Still searching for an operator to register to, 205 // // or no radio signal 206 // - STATE_EMERGENCY_ONLY // Phone is locked; only emergency numbers are allowed 207 // - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode) 208 209 // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY, 210 // it's finally OK to place the emergency call. 211 boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE) 212 || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY); 213 214 if (okToCall) { 215 // Woo hoo! It's OK to actually place the call. 216 if (DBG) log("onServiceStateChanged: ok to call!"); 217 218 // Deregister for the service state change events. 219 unregisterForServiceStateChanged(); 220 221 // Take down the "Turning on radio..." indication. 222 mApp.inCallUiState.clearProgressIndication(); 223 224 placeEmergencyCall(); 225 226 // The in-call UI is probably still up at this point, 227 // but make sure of that: 228 mApp.displayCallScreen(); 229 } else { 230 // The service state changed, but we're still not ready to call yet. 231 // (This probably was the transition from STATE_POWER_OFF to 232 // STATE_OUT_OF_SERVICE, which happens immediately after powering-on 233 // the radio.) 234 // 235 // So just keep waiting; we'll probably get to either 236 // STATE_IN_SERVICE or STATE_EMERGENCY_ONLY very shortly. 237 // (Or even if that doesn't happen, we'll at least do another retry 238 // when the RETRY_TIMEOUT event fires.) 239 if (DBG) log("onServiceStateChanged: not ready to call yet, keep waiting..."); 240 } 241 } 242 243 /** 244 * Handles a DISCONNECT event from the telephony layer. 245 * 246 * Even after we successfully place an emergency call (after powering 247 * on the radio), it's still possible for the call to fail with the 248 * disconnect cause OUT_OF_SERVICE. If so, schedule a retry. 249 */ onDisconnect(Message msg)250 private void onDisconnect(Message msg) { 251 Connection conn = (Connection) ((AsyncResult) msg.obj).result; 252 Connection.DisconnectCause cause = conn.getDisconnectCause(); 253 if (DBG) log("onDisconnect: connection '" + conn 254 + "', addr '" + conn.getAddress() + "', cause = " + cause); 255 256 if (cause == Connection.DisconnectCause.OUT_OF_SERVICE) { 257 // Wait a bit more and try again (or just bail out totally if 258 // we've had too many failures.) 259 if (DBG) log("- onDisconnect: OUT_OF_SERVICE, need to retry..."); 260 scheduleRetryOrBailOut(); 261 } else { 262 // Any other disconnect cause means we're done. 263 // Either the emergency call succeeded *and* ended normally, 264 // or else there was some error that we can't retry. In either 265 // case, just clean up our internal state.) 266 267 if (DBG) log("==> Disconnect event; clean up..."); 268 cleanup(); 269 270 // Nothing else to do here. If the InCallScreen was visible, 271 // it would have received this disconnect event too (so it'll 272 // show the "Call ended" state and finish itself without any 273 // help from us.) 274 } 275 } 276 277 /** 278 * Handles the retry timer expiring. 279 */ onRetryTimeout()280 private void onRetryTimeout() { 281 Phone.State phoneState = mCM.getState(); 282 int serviceState = mPhone.getServiceState().getState(); 283 if (DBG) log("onRetryTimeout(): phone state " + phoneState 284 + ", service state " + serviceState 285 + ", mNumRetriesSoFar = " + mNumRetriesSoFar); 286 287 // - If we're actually in a call, we've succeeded. 288 // 289 // - Otherwise, if the radio is now on, that means we successfully got 290 // out of airplane mode but somehow didn't get the service state 291 // change event. In that case, try to place the call. 292 // 293 // - If the radio is still powered off, try powering it on again. 294 295 if (phoneState == Phone.State.OFFHOOK) { 296 if (DBG) log("- onRetryTimeout: Call is active! Cleaning up..."); 297 cleanup(); 298 return; 299 } 300 301 if (serviceState != ServiceState.STATE_POWER_OFF) { 302 // Woo hoo -- we successfully got out of airplane mode. 303 304 // Deregister for the service state change events; we don't need 305 // these any more now that the radio is powered-on. 306 unregisterForServiceStateChanged(); 307 308 // Take down the "Turning on radio..." indication. 309 mApp.inCallUiState.clearProgressIndication(); 310 311 placeEmergencyCall(); // If the call fails, placeEmergencyCall() 312 // will schedule a retry. 313 } else { 314 // Uh oh; we've waited the full TIME_BETWEEN_RETRIES and the 315 // radio is still not powered-on. Try again... 316 317 if (DBG) log("- Trying (again) to turn on the radio..."); 318 powerOnRadio(); // Again, we'll (hopefully) get an onServiceStateChanged() 319 // callback when the radio successfully comes up. 320 321 // ...and also set a fresh retry timer (or just bail out 322 // totally if we've had too many failures.) 323 scheduleRetryOrBailOut(); 324 } 325 326 // Finally, the in-call UI is probably still up at this point, 327 // but make sure of that: 328 mApp.displayCallScreen(); 329 } 330 331 /** 332 * Attempt to power on the radio (i.e. take the device out 333 * of airplane mode.) 334 * 335 * Additionally, start listening for service state changes; 336 * we'll eventually get an onServiceStateChanged() callback 337 * when the radio successfully comes up. 338 */ powerOnRadio()339 private void powerOnRadio() { 340 if (DBG) log("- powerOnRadio()..."); 341 342 // We're about to turn on the radio, so arrange to be notified 343 // when the sequence is complete. 344 registerForServiceStateChanged(); 345 346 // If airplane mode is on, we turn it off the same way that the 347 // Settings activity turns it off. 348 if (Settings.System.getInt(mApp.getContentResolver(), 349 Settings.System.AIRPLANE_MODE_ON, 0) > 0) { 350 if (DBG) log("==> Turning off airplane mode..."); 351 352 // Change the system setting 353 Settings.System.putInt(mApp.getContentResolver(), 354 Settings.System.AIRPLANE_MODE_ON, 0); 355 356 // Post the intent 357 Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); 358 intent.putExtra("state", false); 359 mApp.sendBroadcast(intent); 360 } else { 361 // Otherwise, for some strange reason the radio is off 362 // (even though the Settings database doesn't think we're 363 // in airplane mode.) In this case just turn the radio 364 // back on. 365 if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on..."); 366 mPhone.setRadioPower(true); 367 } 368 } 369 370 /** 371 * Actually initiate the outgoing emergency call. 372 * (We do this once the radio has successfully been powered-up.) 373 * 374 * If the call succeeds, we're done. 375 * If the call fails, schedule a retry of the whole sequence. 376 */ placeEmergencyCall()377 private void placeEmergencyCall() { 378 if (DBG) log("placeEmergencyCall()..."); 379 380 // Place an outgoing call to mNumber. 381 // Note we call PhoneUtils.placeCall() directly; we don't want any 382 // of the behavior from CallController.placeCallInternal() here. 383 // (Specifically, we don't want to start the "emergency call from 384 // airplane mode" sequence from the beginning again!) 385 386 registerForDisconnect(); // Get notified when this call disconnects 387 388 if (DBG) log("- placing call to '" + mNumber + "'..."); 389 int callStatus = PhoneUtils.placeCall(mApp, 390 mPhone, 391 mNumber, 392 null, // contactUri 393 true, // isEmergencyCall 394 null); // gatewayUri 395 if (DBG) log("- PhoneUtils.placeCall() returned status = " + callStatus); 396 397 boolean success; 398 // Note PhoneUtils.placeCall() returns one of the CALL_STATUS_* 399 // constants, not a CallStatusCode enum value. 400 switch (callStatus) { 401 case PhoneUtils.CALL_STATUS_DIALED: 402 success = true; 403 break; 404 405 case PhoneUtils.CALL_STATUS_DIALED_MMI: 406 case PhoneUtils.CALL_STATUS_FAILED: 407 default: 408 // Anything else is a failure, and we'll need to retry. 409 Log.w(TAG, "placeEmergencyCall(): placeCall() failed: callStatus = " + callStatus); 410 success = false; 411 break; 412 } 413 414 if (success) { 415 if (DBG) log("==> Success from PhoneUtils.placeCall()!"); 416 // Ok, the emergency call is (hopefully) under way. 417 418 // We're not done yet, though, so don't call cleanup() here. 419 // (It's still possible that this call will fail, and disconnect 420 // with cause==OUT_OF_SERVICE. If so, that will trigger a retry 421 // from the onDisconnect() method.) 422 } else { 423 if (DBG) log("==> Failure."); 424 // Wait a bit more and try again (or just bail out totally if 425 // we've had too many failures.) 426 scheduleRetryOrBailOut(); 427 } 428 } 429 430 /** 431 * Schedules a retry in response to some failure (either the radio 432 * failing to power on, or a failure when trying to place the call.) 433 * Or, if we've hit the retry limit, bail out of this whole sequence 434 * and display a failure message to the user. 435 */ scheduleRetryOrBailOut()436 private void scheduleRetryOrBailOut() { 437 mNumRetriesSoFar++; 438 if (DBG) log("scheduleRetryOrBailOut()... mNumRetriesSoFar is now " + mNumRetriesSoFar); 439 440 if (mNumRetriesSoFar > MAX_NUM_RETRIES) { 441 Log.w(TAG, "scheduleRetryOrBailOut: hit MAX_NUM_RETRIES; giving up..."); 442 cleanup(); 443 // ...and have the InCallScreen display a generic failure 444 // message. 445 mApp.inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED); 446 } else { 447 if (DBG) log("- Scheduling another retry..."); 448 startRetryTimer(); 449 mApp.inCallUiState.setProgressIndication(ProgressIndicationType.RETRYING); 450 } 451 } 452 453 /** 454 * Clean up when done with the whole sequence: either after 455 * successfully placing *and* ending the emergency call, or after 456 * bailing out because of too many failures. 457 * 458 * The exact cleanup steps are: 459 * - Take down any progress UI (and also ask the in-call UI to refresh itself, 460 * if it's still visible) 461 * - Double-check that we're not still registered for any telephony events 462 * - Clean up any extraneous handler messages (like retry timeouts) still in the queue 463 * - Make sure we're not still holding any wake locks 464 * 465 * Basically this method guarantees that there will be no more 466 * activity from the EmergencyCallHelper until the CallController 467 * kicks off the whole sequence again with another call to 468 * startEmergencyCallFromAirplaneModeSequence(). 469 * 470 * Note we don't call this method simply after a successful call to 471 * placeCall(), since it's still possible the call will disconnect 472 * very quickly with an OUT_OF_SERVICE error. 473 */ cleanup()474 private void cleanup() { 475 if (DBG) log("cleanup()..."); 476 477 // Take down the "Turning on radio..." indication. 478 mApp.inCallUiState.clearProgressIndication(); 479 480 unregisterForServiceStateChanged(); 481 unregisterForDisconnect(); 482 cancelRetryTimer(); 483 484 // Release / clean up the wake lock 485 if (mPartialWakeLock != null) { 486 if (mPartialWakeLock.isHeld()) { 487 if (DBG) log("- releasing wake lock"); 488 mPartialWakeLock.release(); 489 } 490 mPartialWakeLock = null; 491 } 492 493 // And finally, ask the in-call UI to refresh itself (to clean up the 494 // progress indication if necessary), if it's currently visible. 495 mApp.updateInCallScreen(); 496 } 497 startRetryTimer()498 private void startRetryTimer() { 499 removeMessages(RETRY_TIMEOUT); 500 sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES); 501 } 502 cancelRetryTimer()503 private void cancelRetryTimer() { 504 removeMessages(RETRY_TIMEOUT); 505 } 506 registerForServiceStateChanged()507 private void registerForServiceStateChanged() { 508 // Unregister first, just to make sure we never register ourselves 509 // twice. (We need this because Phone.registerForServiceStateChanged() 510 // does not prevent multiple registration of the same handler.) 511 mPhone.unregisterForServiceStateChanged(this); // Safe even if not currently registered 512 mPhone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null); 513 } 514 unregisterForServiceStateChanged()515 private void unregisterForServiceStateChanged() { 516 // This method is safe to call even if we haven't set mPhone yet. 517 if (mPhone != null) { 518 mPhone.unregisterForServiceStateChanged(this); // Safe even if unnecessary 519 } 520 removeMessages(SERVICE_STATE_CHANGED); // Clean up any pending messages too 521 } 522 registerForDisconnect()523 private void registerForDisconnect() { 524 // Note: no need to unregister first, since 525 // CallManager.registerForDisconnect() automatically prevents 526 // multiple registration of the same handler. 527 mCM.registerForDisconnect(this, DISCONNECT, null); 528 } 529 unregisterForDisconnect()530 private void unregisterForDisconnect() { 531 mCM.unregisterForDisconnect(this); // Safe even if not currently registered 532 removeMessages(DISCONNECT); // Clean up any pending messages too 533 } 534 535 536 // 537 // Debugging 538 // 539 log(String msg)540 private static void log(String msg) { 541 Log.d(TAG, msg); 542 } 543 } 544