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