Blanket copy of PhoneApp to services/Telephony.

First phase of splitting out InCallUI from PhoneApp.

Change-Id: I237341c4ff00e96c677caa4580b251ef3432931b
diff --git a/src/com/android/phone/EmergencyCallHelper.java b/src/com/android/phone/EmergencyCallHelper.java
new file mode 100644
index 0000000..7f5b0d2
--- /dev/null
+++ b/src/com/android/phone/EmergencyCallHelper.java
@@ -0,0 +1,545 @@
+/*
+ * 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.internal.telephony.PhoneConstants;
+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.os.UserHandle;
+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 = false;
+
+    // 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 PhoneGlobals 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 = PhoneGlobals.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() {
+        PhoneConstants.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 == PhoneConstants.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.Global.getInt(mApp.getContentResolver(),
+                                   Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
+            if (DBG) log("==> Turning off airplane mode...");
+
+            // Change the system setting
+            Settings.Global.putInt(mApp.getContentResolver(),
+                                   Settings.Global.AIRPLANE_MODE_ON, 0);
+
+            // Post the intent
+            Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+            intent.putExtra("state", false);
+            mApp.sendBroadcastAsUser(intent, UserHandle.ALL);
+        } 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);
+    }
+}