/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.phone; import com.android.internal.telephony.CallManager; import com.android.internal.telephony.Connection; import com.android.internal.telephony.Phone; import com.android.phone.Constants.CallStatusCode; import com.android.phone.InCallUiState.ProgressIndicationType; import android.content.Context; import android.content.Intent; import android.os.AsyncResult; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.provider.Settings; import android.telephony.ServiceState; import android.util.Log; /** * Helper class for the {@link CallController} that implements special * behavior related to emergency calls. Specifically, this class handles * the case of the user trying to dial an emergency number while the radio * is off (i.e. the device is in airplane mode), by forcibly turning the * radio back on, waiting for it to come up, and then retrying the * emergency call. * * This class is instantiated lazily (the first time the user attempts to * make an emergency call from airplane mode) by the the * {@link CallController} singleton. */ public class EmergencyCallHelper extends Handler { private static final String TAG = "EmergencyCallHelper"; private static final boolean DBG = true; // Number of times to retry the call, and time between retry attempts. public static final int MAX_NUM_RETRIES = 6; public static final long TIME_BETWEEN_RETRIES = 5000; // msec // Timeout used with our wake lock (just as a safety valve to make // sure we don't hold it forever). public static final long WAKE_LOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in msec // Handler message codes; see handleMessage() private static final int START_SEQUENCE = 1; private static final int SERVICE_STATE_CHANGED = 2; private static final int DISCONNECT = 3; private static final int RETRY_TIMEOUT = 4; private CallController mCallController; private PhoneApp mApp; private CallManager mCM; private Phone mPhone; private String mNumber; // The emergency number we're trying to dial private int mNumRetriesSoFar; // Wake lock we hold while running the whole sequence private PowerManager.WakeLock mPartialWakeLock; public EmergencyCallHelper(CallController callController) { if (DBG) log("EmergencyCallHelper constructor..."); mCallController = callController; mApp = PhoneApp.getInstance(); mCM = mApp.mCM; } @Override public void handleMessage(Message msg) { switch (msg.what) { case START_SEQUENCE: startSequenceInternal(msg); break; case SERVICE_STATE_CHANGED: onServiceStateChanged(msg); break; case DISCONNECT: onDisconnect(msg); break; case RETRY_TIMEOUT: onRetryTimeout(); break; default: Log.wtf(TAG, "handleMessage: unexpected message: " + msg); break; } } /** * Starts the "emergency call from airplane mode" sequence. * * This is the (single) external API of the EmergencyCallHelper class. * This method is called from the CallController placeCall() sequence * if the user dials a valid emergency number, but the radio is * powered-off (presumably due to airplane mode.) * * This method kicks off the following sequence: * - Power on the radio * - Listen for the service state change event telling us the radio has come up * - Then launch the emergency call * - Retry if the call fails with an OUT_OF_SERVICE error * - Retry if we've gone 5 seconds without any response from the radio * - Finally, clean up any leftover state (progress UI, wake locks, etc.) * * This method is safe to call from any thread, since it simply posts * a message to the EmergencyCallHelper's handler (thus ensuring that * the rest of the sequence is entirely serialized, and runs only on * the handler thread.) * * This method does *not* force the in-call UI to come up; our caller * is responsible for doing that (presumably by calling * PhoneApp.displayCallScreen().) */ public void startEmergencyCallFromAirplaneModeSequence(String number) { if (DBG) log("startEmergencyCallFromAirplaneModeSequence('" + number + "')..."); Message msg = obtainMessage(START_SEQUENCE, number); sendMessage(msg); } /** * Actual implementation of startEmergencyCallFromAirplaneModeSequence(), * guaranteed to run on the handler thread. * @see startEmergencyCallFromAirplaneModeSequence() */ private void startSequenceInternal(Message msg) { if (DBG) log("startSequenceInternal(): msg = " + msg); // First of all, clean up any state (including mPartialWakeLock!) // left over from a prior emergency call sequence. // This ensures that we'll behave sanely if another // startEmergencyCallFromAirplaneModeSequence() comes in while // we're already in the middle of the sequence. cleanup(); mNumber = (String) msg.obj; if (DBG) log("- startSequenceInternal: Got mNumber: '" + mNumber + "'"); mNumRetriesSoFar = 0; // Reset mPhone to whatever the current default phone is right now. mPhone = mApp.mCM.getDefaultPhone(); // Wake lock to make sure the processor doesn't go to sleep midway // through the emergency call sequence. PowerManager pm = (PowerManager) mApp.getSystemService(Context.POWER_SERVICE); mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); // Acquire with a timeout, just to be sure we won't hold the wake // lock forever even if a logic bug (in this class) causes us to // somehow never call cleanup(). if (DBG) log("- startSequenceInternal: acquiring wake lock"); mPartialWakeLock.acquire(WAKE_LOCK_TIMEOUT); // No need to check the current service state here, since the only // reason the CallController would call this method in the first // place is if the radio is powered-off. // // So just go ahead and turn the radio on. powerOnRadio(); // We'll get an onServiceStateChanged() callback // when the radio successfully comes up. // Next step: when the SERVICE_STATE_CHANGED event comes in, // we'll retry the call; see placeEmergencyCall(); // But also, just in case, start a timer to make sure we'll retry // the call even if the SERVICE_STATE_CHANGED event never comes in // for some reason. startRetryTimer(); // And finally, let the in-call UI know that we need to // display the "Turning on radio..." progress indication. mApp.inCallUiState.setProgressIndication(ProgressIndicationType.TURNING_ON_RADIO); // (Our caller is responsible for calling mApp.displayCallScreen().) } /** * Handles the SERVICE_STATE_CHANGED event. * * (Normally this event tells us that the radio has finally come * up. In that case, it's now safe to actually place the * emergency call.) */ private void onServiceStateChanged(Message msg) { ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result; if (DBG) log("onServiceStateChanged()... new state = " + state); // Possible service states: // - STATE_IN_SERVICE // Normal operation // - STATE_OUT_OF_SERVICE // Still searching for an operator to register to, // // or no radio signal // - STATE_EMERGENCY_ONLY // Phone is locked; only emergency numbers are allowed // - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode) // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY, // it's finally OK to place the emergency call. boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE) || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY); if (okToCall) { // Woo hoo! It's OK to actually place the call. if (DBG) log("onServiceStateChanged: ok to call!"); // Deregister for the service state change events. unregisterForServiceStateChanged(); // Take down the "Turning on radio..." indication. mApp.inCallUiState.clearProgressIndication(); placeEmergencyCall(); // The in-call UI is probably still up at this point, // but make sure of that: mApp.displayCallScreen(); } else { // The service state changed, but we're still not ready to call yet. // (This probably was the transition from STATE_POWER_OFF to // STATE_OUT_OF_SERVICE, which happens immediately after powering-on // the radio.) // // So just keep waiting; we'll probably get to either // STATE_IN_SERVICE or STATE_EMERGENCY_ONLY very shortly. // (Or even if that doesn't happen, we'll at least do another retry // when the RETRY_TIMEOUT event fires.) if (DBG) log("onServiceStateChanged: not ready to call yet, keep waiting..."); } } /** * Handles a DISCONNECT event from the telephony layer. * * Even after we successfully place an emergency call (after powering * on the radio), it's still possible for the call to fail with the * disconnect cause OUT_OF_SERVICE. If so, schedule a retry. */ private void onDisconnect(Message msg) { Connection conn = (Connection) ((AsyncResult) msg.obj).result; Connection.DisconnectCause cause = conn.getDisconnectCause(); if (DBG) log("onDisconnect: connection '" + conn + "', addr '" + conn.getAddress() + "', cause = " + cause); if (cause == Connection.DisconnectCause.OUT_OF_SERVICE) { // Wait a bit more and try again (or just bail out totally if // we've had too many failures.) if (DBG) log("- onDisconnect: OUT_OF_SERVICE, need to retry..."); scheduleRetryOrBailOut(); } else { // Any other disconnect cause means we're done. // Either the emergency call succeeded *and* ended normally, // or else there was some error that we can't retry. In either // case, just clean up our internal state.) if (DBG) log("==> Disconnect event; clean up..."); cleanup(); // Nothing else to do here. If the InCallScreen was visible, // it would have received this disconnect event too (so it'll // show the "Call ended" state and finish itself without any // help from us.) } } /** * Handles the retry timer expiring. */ private void onRetryTimeout() { Phone.State phoneState = mCM.getState(); int serviceState = mPhone.getServiceState().getState(); if (DBG) log("onRetryTimeout(): phone state " + phoneState + ", service state " + serviceState + ", mNumRetriesSoFar = " + mNumRetriesSoFar); // - If we're actually in a call, we've succeeded. // // - Otherwise, if the radio is now on, that means we successfully got // out of airplane mode but somehow didn't get the service state // change event. In that case, try to place the call. // // - If the radio is still powered off, try powering it on again. if (phoneState == Phone.State.OFFHOOK) { if (DBG) log("- onRetryTimeout: Call is active! Cleaning up..."); cleanup(); return; } if (serviceState != ServiceState.STATE_POWER_OFF) { // Woo hoo -- we successfully got out of airplane mode. // Deregister for the service state change events; we don't need // these any more now that the radio is powered-on. unregisterForServiceStateChanged(); // Take down the "Turning on radio..." indication. mApp.inCallUiState.clearProgressIndication(); placeEmergencyCall(); // If the call fails, placeEmergencyCall() // will schedule a retry. } else { // Uh oh; we've waited the full TIME_BETWEEN_RETRIES and the // radio is still not powered-on. Try again... if (DBG) log("- Trying (again) to turn on the radio..."); powerOnRadio(); // Again, we'll (hopefully) get an onServiceStateChanged() // callback when the radio successfully comes up. // ...and also set a fresh retry timer (or just bail out // totally if we've had too many failures.) scheduleRetryOrBailOut(); } // Finally, the in-call UI is probably still up at this point, // but make sure of that: mApp.displayCallScreen(); } /** * Attempt to power on the radio (i.e. take the device out * of airplane mode.) * * Additionally, start listening for service state changes; * we'll eventually get an onServiceStateChanged() callback * when the radio successfully comes up. */ private void powerOnRadio() { if (DBG) log("- powerOnRadio()..."); // We're about to turn on the radio, so arrange to be notified // when the sequence is complete. registerForServiceStateChanged(); // If airplane mode is on, we turn it off the same way that the // Settings activity turns it off. if (Settings.System.getInt(mApp.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) > 0) { if (DBG) log("==> Turning off airplane mode..."); // Change the system setting Settings.System.putInt(mApp.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0); // Post the intent Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); intent.putExtra("state", false); mApp.sendBroadcast(intent); } else { // Otherwise, for some strange reason the radio is off // (even though the Settings database doesn't think we're // in airplane mode.) In this case just turn the radio // back on. if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on..."); mPhone.setRadioPower(true); } } /** * Actually initiate the outgoing emergency call. * (We do this once the radio has successfully been powered-up.) * * If the call succeeds, we're done. * If the call fails, schedule a retry of the whole sequence. */ private void placeEmergencyCall() { if (DBG) log("placeEmergencyCall()..."); // Place an outgoing call to mNumber. // Note we call PhoneUtils.placeCall() directly; we don't want any // of the behavior from CallController.placeCallInternal() here. // (Specifically, we don't want to start the "emergency call from // airplane mode" sequence from the beginning again!) registerForDisconnect(); // Get notified when this call disconnects if (DBG) log("- placing call to '" + mNumber + "'..."); int callStatus = PhoneUtils.placeCall(mApp, mPhone, mNumber, null, // contactUri true, // isEmergencyCall null); // gatewayUri if (DBG) log("- PhoneUtils.placeCall() returned status = " + callStatus); boolean success; // Note PhoneUtils.placeCall() returns one of the CALL_STATUS_* // constants, not a CallStatusCode enum value. switch (callStatus) { case PhoneUtils.CALL_STATUS_DIALED: success = true; break; case PhoneUtils.CALL_STATUS_DIALED_MMI: case PhoneUtils.CALL_STATUS_FAILED: default: // Anything else is a failure, and we'll need to retry. Log.w(TAG, "placeEmergencyCall(): placeCall() failed: callStatus = " + callStatus); success = false; break; } if (success) { if (DBG) log("==> Success from PhoneUtils.placeCall()!"); // Ok, the emergency call is (hopefully) under way. // We're not done yet, though, so don't call cleanup() here. // (It's still possible that this call will fail, and disconnect // with cause==OUT_OF_SERVICE. If so, that will trigger a retry // from the onDisconnect() method.) } else { if (DBG) log("==> Failure."); // Wait a bit more and try again (or just bail out totally if // we've had too many failures.) scheduleRetryOrBailOut(); } } /** * Schedules a retry in response to some failure (either the radio * failing to power on, or a failure when trying to place the call.) * Or, if we've hit the retry limit, bail out of this whole sequence * and display a failure message to the user. */ private void scheduleRetryOrBailOut() { mNumRetriesSoFar++; if (DBG) log("scheduleRetryOrBailOut()... mNumRetriesSoFar is now " + mNumRetriesSoFar); if (mNumRetriesSoFar > MAX_NUM_RETRIES) { Log.w(TAG, "scheduleRetryOrBailOut: hit MAX_NUM_RETRIES; giving up..."); cleanup(); // ...and have the InCallScreen display a generic failure // message. mApp.inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED); } else { if (DBG) log("- Scheduling another retry..."); startRetryTimer(); mApp.inCallUiState.setProgressIndication(ProgressIndicationType.RETRYING); } } /** * Clean up when done with the whole sequence: either after * successfully placing *and* ending the emergency call, or after * bailing out because of too many failures. * * The exact cleanup steps are: * - Take down any progress UI (and also ask the in-call UI to refresh itself, * if it's still visible) * - Double-check that we're not still registered for any telephony events * - Clean up any extraneous handler messages (like retry timeouts) still in the queue * - Make sure we're not still holding any wake locks * * Basically this method guarantees that there will be no more * activity from the EmergencyCallHelper until the CallController * kicks off the whole sequence again with another call to * startEmergencyCallFromAirplaneModeSequence(). * * Note we don't call this method simply after a successful call to * placeCall(), since it's still possible the call will disconnect * very quickly with an OUT_OF_SERVICE error. */ private void cleanup() { if (DBG) log("cleanup()..."); // Take down the "Turning on radio..." indication. mApp.inCallUiState.clearProgressIndication(); unregisterForServiceStateChanged(); unregisterForDisconnect(); cancelRetryTimer(); // Release / clean up the wake lock if (mPartialWakeLock != null) { if (mPartialWakeLock.isHeld()) { if (DBG) log("- releasing wake lock"); mPartialWakeLock.release(); } mPartialWakeLock = null; } // And finally, ask the in-call UI to refresh itself (to clean up the // progress indication if necessary), if it's currently visible. mApp.updateInCallScreen(); } private void startRetryTimer() { removeMessages(RETRY_TIMEOUT); sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES); } private void cancelRetryTimer() { removeMessages(RETRY_TIMEOUT); } private void registerForServiceStateChanged() { // Unregister first, just to make sure we never register ourselves // twice. (We need this because Phone.registerForServiceStateChanged() // does not prevent multiple registration of the same handler.) mPhone.unregisterForServiceStateChanged(this); // Safe even if not currently registered mPhone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null); } private void unregisterForServiceStateChanged() { // This method is safe to call even if we haven't set mPhone yet. if (mPhone != null) { mPhone.unregisterForServiceStateChanged(this); // Safe even if unnecessary } removeMessages(SERVICE_STATE_CHANGED); // Clean up any pending messages too } private void registerForDisconnect() { // Note: no need to unregister first, since // CallManager.registerForDisconnect() automatically prevents // multiple registration of the same handler. mCM.registerForDisconnect(this, DISCONNECT, null); } private void unregisterForDisconnect() { mCM.unregisterForDisconnect(this); // Safe even if not currently registered removeMessages(DISCONNECT); // Clean up any pending messages too } // // Debugging // private static void log(String msg) { Log.d(TAG, msg); } }