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