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