Refactor EmergencyCallHelper to better handle DSDS

Refactor EmergencyCallHelper to better handle DSDS radios by disabling
airplane mode and waiting for each Phone's ServiceState to change to
allow for Emergency Calling.

Also, some tests are added using AndroidJUnit4Runner

This is the first part in a series of changes to the Telephony code.

Bug: 28200728
Change-Id: I642615480090f0b047cea00ab9f3256f321dfaa1
diff --git a/src/com/android/services/telephony/EmergencyCallHelper.java b/src/com/android/services/telephony/EmergencyCallHelper.java
index c64a649..295f4f7 100644
--- a/src/com/android/services/telephony/EmergencyCallHelper.java
+++ b/src/com/android/services/telephony/EmergencyCallHelper.java
@@ -17,18 +17,17 @@
 package com.android.services.telephony;
 
 import android.content.Context;
-
 import android.content.Intent;
-import android.os.AsyncResult;
-import android.os.Handler;
-import android.os.Message;
 import android.os.UserHandle;
 import android.provider.Settings;
-import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
 
-import com.android.internal.os.SomeArgs;
 import com.android.internal.telephony.Phone;
-import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
 
 /**
  * Helper class that implements special behavior related to emergency calls. Specifically, this
@@ -36,220 +35,75 @@
  * (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.
  */
-public class EmergencyCallHelper {
-
-    /**
-     * Receives the result of the EmergencyCallHelper's attempt to turn on the radio.
-     */
-    interface Callback {
-        void onComplete(boolean isRadioReady);
-    }
-
-    // Number of times to retry the call, and time between retry attempts.
-    public static final int MAX_NUM_RETRIES = 5;
-    public static final long TIME_BETWEEN_RETRIES_MILLIS = 5000;  // msec
-
-    // Handler message codes; see handleMessage()
-    private static final int MSG_START_SEQUENCE = 1;
-    private static final int MSG_SERVICE_STATE_CHANGED = 2;
-    private static final int MSG_RETRY_TIMEOUT = 3;
+public class EmergencyCallHelper implements EmergencyCallStateListener.Callback {
 
     private final Context mContext;
+    private EmergencyCallStateListener.Callback mCallback;
+    private List<EmergencyCallStateListener> mListeners;
+    private List<EmergencyCallStateListener> mInProgressListeners;
+    private boolean mIsEmergencyCallingEnabled;
 
-    private final Handler mHandler = new Handler() {
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_START_SEQUENCE:
-                    SomeArgs args = (SomeArgs) msg.obj;
-                    Phone phone = (Phone) args.arg1;
-                    EmergencyCallHelper.Callback callback =
-                            (EmergencyCallHelper.Callback) args.arg2;
-                    args.recycle();
-
-                    startSequenceInternal(phone, callback);
-                    break;
-                case MSG_SERVICE_STATE_CHANGED:
-                    onServiceStateChanged((ServiceState) ((AsyncResult) msg.obj).result);
-                    break;
-                case MSG_RETRY_TIMEOUT:
-                    onRetryTimeout();
-                    break;
-                default:
-                    Log.wtf(this, "handleMessage: unexpected message: %d.", msg.what);
-                    break;
-            }
-        }
-    };
-
-
-    private Callback mCallback;  // The callback to notify upon completion.
-    private Phone mPhone;  // The phone that will attempt to place the call.
-    private int mNumRetriesSoFar;
 
     public EmergencyCallHelper(Context context) {
-        Log.d(this, "EmergencyCallHelper constructor.");
         mContext = context;
+        mInProgressListeners = new ArrayList<>(2);
     }
 
+    private void setupListeners() {
+        if (mListeners != null) {
+            return;
+        }
+        mListeners = new ArrayList<>(2);
+        for (int i = 0; i < TelephonyManager.getDefault().getPhoneCount(); i++) {
+            mListeners.add(new EmergencyCallStateListener());
+        }
+    }
     /**
      * Starts the "turn on radio" sequence. This is the (single) external API of the
      * EmergencyCallHelper class.
      *
      * This method kicks off the following sequence:
-     * - Power on the radio.
+     * - Power on the radio for each Phone
      * - Listen for the service state change event telling us the radio has come up.
-     * - Retry if we've gone {@link #TIME_BETWEEN_RETRIES_MILLIS} without any response from the
-     *   radio.
+     * - Retry if we've gone a significant amount of time without any response from the radio.
      * - Finally, clean up any leftover state.
      *
      * 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.)
+     * serialized, and runs on the main looper.)
      */
-    public void startTurnOnRadioSequence(Phone phone, Callback callback) {
-        Log.d(this, "startTurnOnRadioSequence");
-
-        SomeArgs args = SomeArgs.obtain();
-        args.arg1 = phone;
-        args.arg2 = callback;
-        mHandler.obtainMessage(MSG_START_SEQUENCE, args).sendToTarget();
-    }
-
-    /**
-     * Actual implementation of startTurnOnRadioSequence(), guaranteed to run on the handler thread.
-     * @see #startTurnOnRadioSequence
-     */
-    private void startSequenceInternal(Phone phone, Callback callback) {
-        Log.d(this, "startSequenceInternal()");
-
-        // First of all, clean up any state left over from a prior emergency call sequence. This
-        // ensures that we'll behave sanely if another startTurnOnRadioSequence() comes in while
-        // we're already in the middle of the sequence.
-        cleanup();
-
-        mPhone = phone;
+    public void enableEmergencyCalling(EmergencyCallStateListener.Callback callback) {
+        setupListeners();
         mCallback = callback;
+        mInProgressListeners.clear();
+        mIsEmergencyCallingEnabled = false;
+        for (int i = 0; i < TelephonyManager.getDefault().getPhoneCount(); i++) {
+            Phone phone = PhoneFactory.getPhone(i);
+            if (phone == null)
+                continue;
 
-
-        // No need to check the current service state here, since the only reason to invoke 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
-        // onServiceStateChanged(). 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();
-    }
-
-    /**
-     * 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(ServiceState state) {
-        Log.d(this, "onServiceStateChanged(), new state = %s.", 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)
-
-        if (isOkToCall(state.getState(), mPhone.getState())) {
-            // Woo hoo!  It's OK to actually place the call.
-            Log.d(this, "onServiceStateChanged: ok to call!");
-
-            onComplete(true);
-            cleanup();
-        } 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.)
-            Log.d(this, "onServiceStateChanged: not ready to call yet, keep waiting.");
+            mInProgressListeners.add(mListeners.get(i));
+            mListeners.get(i).waitForRadioOn(phone, this);
         }
+
+        powerOnRadio();
     }
-
-    private boolean isOkToCall(int serviceState, PhoneConstants.State phoneState) {
-        // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY, it's finally OK to place
-        // the emergency call.
-        return ((phoneState == PhoneConstants.State.OFFHOOK)
-                || (serviceState == ServiceState.STATE_IN_SERVICE)
-                || (serviceState == ServiceState.STATE_EMERGENCY_ONLY)) ||
-
-                // Allow STATE_OUT_OF_SERVICE if we are at the max number of retries.
-                (mNumRetriesSoFar == MAX_NUM_RETRIES &&
-                 serviceState == ServiceState.STATE_OUT_OF_SERVICE);
-    }
-
     /**
-     * Handles the retry timer expiring.
-     */
-    private void onRetryTimeout() {
-        PhoneConstants.State phoneState = mPhone.getState();
-        int serviceState = mPhone.getServiceState().getState();
-        Log.d(this, "onRetryTimeout():  phone state = %s, service state = %d, retries = %d.",
-               phoneState, serviceState, 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 (isOkToCall(serviceState, phoneState)) {
-            Log.d(this, "onRetryTimeout: Radio is on. Cleaning up.");
-
-            // Woo hoo -- we successfully got out of airplane mode.
-            onComplete(true);
-            cleanup();
-        } else {
-            // Uh oh; we've waited the full TIME_BETWEEN_RETRIES_MILLIS and the radio is still not
-            // powered-on.  Try again.
-
-            mNumRetriesSoFar++;
-            Log.d(this, "mNumRetriesSoFar is now " + mNumRetriesSoFar);
-
-            if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
-                Log.w(this, "Hit MAX_NUM_RETRIES; giving up.");
-                cleanup();
-            } else {
-                Log.d(this, "Trying (again) to turn on the radio.");
-                powerOnRadio();  // Again, we'll (hopefully) get an onServiceStateChanged() callback
-                                 // when the radio successfully comes up.
-                startRetryTimer();
-            }
-        }
-    }
-
-    /**
-     * 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.
+     * Attempt to power on the radio (i.e. take the device out of airplane mode). We'll eventually
+     * get an onServiceStateChanged() callback when the radio successfully comes up.
      */
     private void powerOnRadio() {
         Log.d(this, "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(mContext.getContentResolver(),
-                                   Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
+                Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
             Log.d(this, "==> Turning off airplane mode.");
 
             // Change the system setting
             Settings.Global.putInt(mContext.getContentResolver(),
-                                   Settings.Global.AIRPLANE_MODE_ON, 0);
+                    Settings.Global.AIRPLANE_MODE_ON, 0);
 
             // Post the broadcast intend for change in airplane mode
             // TODO: We really should not be in charge of sending this broadcast.
@@ -258,77 +112,19 @@
             Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
             intent.putExtra("state", false);
             mContext.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.
-            Log.d(this, "==> (Apparently) not in airplane mode; manually powering radio on.");
-            mPhone.setRadioPower(true);
         }
     }
 
     /**
-     * Clean up when done with the whole sequence: either after successfully turning on the radio,
-     * or after bailing out because of too many failures.
-     *
-     * The exact cleanup steps are:
-     * - Notify callback if we still hadn't sent it a response.
-     * - 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
-     *
-     * Basically this method guarantees that there will be no more activity from the
-     * EmergencyCallHelper until someone kicks off the whole sequence again with another call to
-     * {@link #startTurnOnRadioSequence}
-     *
-     * TODO: Do the work for the comment below:
-     * 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.
+     * This method is called from multiple Listeners on the Main Looper.
+     * Synchronization is not necessary.
      */
-    private void cleanup() {
-        Log.d(this, "cleanup()");
-
-        // This will send a failure call back if callback has yet to be invoked.  If the callback
-        // was already invoked, it's a no-op.
-        onComplete(false);
-
-        unregisterForServiceStateChanged();
-        cancelRetryTimer();
-
-        // Used for unregisterForServiceStateChanged() so we null it out here instead.
-        mPhone = null;
-        mNumRetriesSoFar = 0;
-    }
-
-    private void startRetryTimer() {
-        cancelRetryTimer();
-        mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
-    }
-
-    private void cancelRetryTimer() {
-        mHandler.removeMessages(MSG_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.)
-        unregisterForServiceStateChanged();
-        mPhone.registerForServiceStateChanged(mHandler, MSG_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(mHandler);  // Safe even if unnecessary
-        }
-        mHandler.removeMessages(MSG_SERVICE_STATE_CHANGED);  // Clean up any pending messages too
-    }
-
-    private void onComplete(boolean isRadioReady) {
-        if (mCallback != null) {
-            Callback tempCallback = mCallback;
-            mCallback = null;
-            tempCallback.onComplete(isRadioReady);
+    @Override
+    public void onComplete(EmergencyCallStateListener listener, boolean isRadioReady) {
+        mIsEmergencyCallingEnabled |= isRadioReady;
+        mInProgressListeners.remove(listener);
+        if (mCallback != null && mInProgressListeners.isEmpty()) {
+            mCallback.onComplete(null, mIsEmergencyCallingEnabled);
         }
     }
 }
diff --git a/src/com/android/services/telephony/EmergencyCallStateListener.java b/src/com/android/services/telephony/EmergencyCallStateListener.java
new file mode 100644
index 0000000..19b3d36
--- /dev/null
+++ b/src/com/android/services/telephony/EmergencyCallStateListener.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2016 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.services.telephony;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.telephony.ServiceState;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+
+/**
+ * Helper class that listens to a Phone's radio state and sends a callback when the radio state of
+ * that Phone is either "in service" or "emergency calls only."
+ */
+public class EmergencyCallStateListener {
+
+    /**
+     * Receives the result of the EmergencyCallStateListener's attempt to turn on the radio.
+     */
+    interface Callback {
+        void onComplete(EmergencyCallStateListener listener, boolean isRadioReady);
+    }
+
+    // Number of times to retry the call, and time between retry attempts.
+    private static int MAX_NUM_RETRIES = 5;
+    private static long TIME_BETWEEN_RETRIES_MILLIS = 5000;  // msec
+
+    // Handler message codes; see handleMessage()
+    @VisibleForTesting
+    public static final int MSG_START_SEQUENCE = 1;
+    @VisibleForTesting
+    public static final int MSG_SERVICE_STATE_CHANGED = 2;
+    @VisibleForTesting
+    public static final int MSG_RETRY_TIMEOUT = 3;
+
+    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_START_SEQUENCE:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        Phone phone = (Phone) args.arg1;
+                        EmergencyCallStateListener.Callback callback =
+                                (EmergencyCallStateListener.Callback) args.arg2;
+                        startSequenceInternal(phone, callback);
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                case MSG_SERVICE_STATE_CHANGED:
+                    onServiceStateChanged((ServiceState) ((AsyncResult) msg.obj).result);
+                    break;
+                case MSG_RETRY_TIMEOUT:
+                    onRetryTimeout();
+                    break;
+                default:
+                    Log.wtf(this, "handleMessage: unexpected message: %d.", msg.what);
+                    break;
+            }
+        }
+    };
+
+
+    private Callback mCallback;  // The callback to notify upon completion.
+    private Phone mPhone;  // The phone that will attempt to place the call.
+    private int mNumRetriesSoFar;
+
+    /**
+     * Starts the "wait for radio" sequence. This is the (single) external API of the
+     * EmergencyCallStateListener class.
+     *
+     * This method kicks off the following sequence:
+     * - Listen for the service state change event telling us the radio has come up.
+     * - Retry if we've gone {@link #TIME_BETWEEN_RETRIES_MILLIS} without any response from the
+     *   radio.
+     * - Finally, clean up any leftover state.
+     *
+     * This method is safe to call from any thread, since it simply posts a message to the
+     * EmergencyCallStateListener's handler (thus ensuring that the rest of the sequence is entirely
+     * serialized, and runs only on the handler thread.)
+     */
+    public void waitForRadioOn(Phone phone, Callback callback) {
+        Log.d(this, "waitForRadioOn: Phone " + phone.getPhoneId());
+
+        if (mPhone != null) {
+            // If there already is an ongoing request, ignore the new one!
+            return;
+        }
+
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = phone;
+        args.arg2 = callback;
+        mHandler.obtainMessage(MSG_START_SEQUENCE, args).sendToTarget();
+    }
+
+    /**
+     * Actual implementation of waitForRadioOn(), guaranteed to run on the handler thread.
+     *
+     * @see #waitForRadioOn
+     */
+    private void startSequenceInternal(Phone phone, Callback callback) {
+        Log.d(this, "startSequenceInternal: Phone " + phone.getPhoneId());
+
+        // First of all, clean up any state left over from a prior emergency call sequence. This
+        // ensures that we'll behave sanely if another startTurnOnRadioSequence() comes in while
+        // we're already in the middle of the sequence.
+        cleanup();
+
+        mPhone = phone;
+        mCallback = callback;
+
+        registerForServiceStateChanged();
+        // Next step: when the SERVICE_STATE_CHANGED event comes in, we'll retry the call; see
+        // onServiceStateChanged(). 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();
+    }
+
+    /**
+     * 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(ServiceState state) {
+        Log.d(this, "onServiceStateChanged(), new state = %s, Phone ", 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)
+
+        if (isOkToCall(state.getState())) {
+            // Woo hoo!  It's OK to actually place the call.
+            Log.d(this, "onServiceStateChanged: ok to call!");
+
+            onComplete(true);
+            cleanup();
+        } 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.)
+            Log.d(this, "onServiceStateChanged: not ready to call yet, keep waiting.");
+        }
+    }
+
+    private boolean isOkToCall(int serviceState) {
+        // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY, it's finally OK to place
+        // the emergency call.
+        return ((mPhone.getState() == PhoneConstants.State.OFFHOOK)
+                || (serviceState == ServiceState.STATE_IN_SERVICE)
+                || (serviceState == ServiceState.STATE_EMERGENCY_ONLY))
+                // STATE_EMERGENCY_ONLY currently is not used, so we must also check the service
+                // state for emergency only calling.
+                || (serviceState == ServiceState.STATE_OUT_OF_SERVICE &&
+                        mPhone.getServiceState().isEmergencyOnly())
+                // Allow STATE_OUT_OF_SERVICE if we are at the max number of retries.
+                || (mNumRetriesSoFar == MAX_NUM_RETRIES &&
+                        serviceState == ServiceState.STATE_OUT_OF_SERVICE);
+    }
+
+    /**
+     * Handles the retry timer expiring.
+     */
+    private void onRetryTimeout() {
+        int serviceState = mPhone.getServiceState().getState();
+        Log.d(this, "onRetryTimeout():  phone state = %s, service state = %d, retries = %d.",
+                mPhone.getState(), serviceState, 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 (isOkToCall(serviceState)) {
+            Log.d(this, "onRetryTimeout: Radio is on. Cleaning up.");
+
+            // Woo hoo -- we successfully got out of airplane mode.
+            onComplete(true);
+            cleanup();
+        } else {
+            // Uh oh; we've waited the full TIME_BETWEEN_RETRIES_MILLIS and the radio is still not
+            // powered-on.  Try again.
+
+            mNumRetriesSoFar++;
+            Log.d(this, "mNumRetriesSoFar is now " + mNumRetriesSoFar);
+
+            if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
+                Log.w(this, "Hit MAX_NUM_RETRIES; giving up.");
+                cleanup();
+            } else {
+                Log.d(this, "Trying (again) to turn on the radio.");
+                mPhone.setRadioPower(true);
+                startRetryTimer();
+            }
+        }
+    }
+
+    /**
+     * Clean up when done with the whole sequence: either after successfully turning on the radio,
+     * or after bailing out because of too many failures.
+     *
+     * The exact cleanup steps are:
+     * - Notify callback if we still hadn't sent it a response.
+     * - 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
+     *
+     * Basically this method guarantees that there will be no more activity from the
+     * EmergencyCallStateListener until someone kicks off the whole sequence again with another call
+     * to {@link #waitForRadioOn}
+     *
+     * TODO: Do the work for the comment below:
+     * 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() {
+        Log.d(this, "cleanup()");
+
+        // This will send a failure call back if callback has yet to be invoked.  If the callback
+        // was already invoked, it's a no-op.
+        onComplete(false);
+
+        unregisterForServiceStateChanged();
+        cancelRetryTimer();
+
+        // Used for unregisterForServiceStateChanged() so we null it out here instead.
+        mPhone = null;
+        mNumRetriesSoFar = 0;
+    }
+
+    private void startRetryTimer() {
+        cancelRetryTimer();
+        mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
+    }
+
+    private void cancelRetryTimer() {
+        mHandler.removeMessages(MSG_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.)
+        unregisterForServiceStateChanged();
+        mPhone.registerForServiceStateChanged(mHandler, MSG_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(mHandler);  // Safe even if unnecessary
+        }
+        mHandler.removeMessages(MSG_SERVICE_STATE_CHANGED);  // Clean up any pending messages too
+    }
+
+    private void onComplete(boolean isRadioReady) {
+        if (mCallback != null) {
+            Callback tempCallback = mCallback;
+            mCallback = null;
+            tempCallback.onComplete(this, isRadioReady);
+        }
+    }
+
+    @VisibleForTesting
+    public Handler getHandler() {
+        return mHandler;
+    }
+
+    @VisibleForTesting
+    public void setMaxNumRetries(int retries) {
+        MAX_NUM_RETRIES = retries;
+    }
+
+    @VisibleForTesting
+    public void setTimeBetweenRetriesMillis(long timeMs) {
+        TIME_BETWEEN_RETRIES_MILLIS = timeMs;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || !getClass().equals(o.getClass())) return false;
+
+        EmergencyCallStateListener that = (EmergencyCallStateListener) o;
+
+        if (mNumRetriesSoFar != that.mNumRetriesSoFar) {
+            return false;
+        }
+        if (mCallback != null ? !mCallback.equals(that.mCallback) : that.mCallback != null) {
+            return false;
+        }
+        return mPhone != null ? mPhone.equals(that.mPhone) : that.mPhone == null;
+
+    }
+}
diff --git a/src/com/android/services/telephony/TelephonyConnectionService.java b/src/com/android/services/telephony/TelephonyConnectionService.java
index a4434dd..0846c59 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -311,10 +311,10 @@
             if (mEmergencyCallHelper == null) {
                 mEmergencyCallHelper = new EmergencyCallHelper(this);
             }
-            mEmergencyCallHelper.startTurnOnRadioSequence(phone,
-                    new EmergencyCallHelper.Callback() {
+            mEmergencyCallHelper.enableEmergencyCalling(new EmergencyCallStateListener.Callback() {
                         @Override
-                        public void onComplete(boolean isRadioReady) {
+                        public void onComplete(EmergencyCallStateListener listener,
+                                boolean isRadioReady) {
                             if (connection.getState() == Connection.STATE_DISCONNECTED) {
                                 // If the connection has already been disconnected, do nothing.
                             } else if (isRadioReady) {
diff --git a/tests/Android.mk b/tests/Android.mk
index 6cc0355..e1b564f 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -25,6 +25,8 @@
 
 LOCAL_MODULE_TAGS := tests
 
+LOCAL_JAVA_LIBRARIES := telephony-common
+
 LOCAL_INSTRUMENTATION_FOR := TeleService
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 8900568..cae4c1b 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -62,17 +62,16 @@
     </application>
 
     <!--
-        The prefered way is to use 'runtest':
-           runtest phone-unit
+        To run all tests:
+            adb shell am instrument -w
+                com.android.phone.tests/android.support.test.runner.AndroidJUnitRunner
 
-         runtest is a wrapper around 'adb shell'. The low level shell command is:
-           adb shell am instrument -w com.android.phone.tests/android.test.InstrumentationTestRunner
+        To run a single class test:
+            adb shell am instrument -e class com.android.phone.unit.FooUnitTest
+                -w com.android.phone.tests/android.support.test.runner.AndroidJUnitRunner
 
-         To run a single test case:
-           adb shell am instrument -w com.android.phone.tests/android.test.InstrumentationTestRunner
-                                   -e com.android.phone.unit.FooUnitTest
     -->
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.phone"
         android:label="Phone application tests." />
 </manifest>
diff --git a/tests/src/com/android/TelephonyTestBase.java b/tests/src/com/android/TelephonyTestBase.java
new file mode 100644
index 0000000..6dee12b
--- /dev/null
+++ b/tests/src/com/android/TelephonyTestBase.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.content.Context;
+import android.os.Handler;
+import android.support.test.InstrumentationRegistry;
+
+import com.android.phone.MockitoHelper;
+
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper class to load Mockito Resources into a test.
+ */
+public class TelephonyTestBase {
+
+    protected Context mContext;
+    MockitoHelper mMockitoHelper = new MockitoHelper();
+
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mMockitoHelper.setUp(mContext, getClass());
+        MockitoAnnotations.initMocks(this);
+    }
+
+    public void tearDown() throws Exception {
+        mMockitoHelper.tearDown();
+    }
+
+    protected final void waitForHandlerAction(Handler h, long timeoutMillis) {
+        final CountDownLatch lock = new CountDownLatch(1);
+        h.post(lock::countDown);
+        while (lock.getCount() > 0) {
+            try {
+                lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+    }
+
+    protected final void waitForHandlerActionDelayed(Handler h, long timeoutMillis, long delayMs) {
+        final CountDownLatch lock = new CountDownLatch(1);
+        h.postDelayed(lock::countDown, delayMs);
+        while (lock.getCount() > 0) {
+            try {
+                lock.await(timeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (InterruptedException e) {
+                // do nothing
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/phone/MockitoHelper.java b/tests/src/com/android/phone/MockitoHelper.java
index 3da5d6e..7998030 100644
--- a/tests/src/com/android/phone/MockitoHelper.java
+++ b/tests/src/com/android/phone/MockitoHelper.java
@@ -16,6 +16,8 @@
 
 package com.android.phone;
 
+import android.content.Context;
+
 import com.android.services.telephony.Log;
 
 /**
@@ -24,6 +26,7 @@
 public final class MockitoHelper {
 
     private static final String TAG = "MockitoHelper";
+    private static final String DEXCACHE = "dexmaker.dexcache";
 
     private ClassLoader mOriginalClassLoader;
     private Thread mContextThread;
@@ -34,7 +37,7 @@
      *
      * @param packageClass test case class
      */
-    public void setUp(Class<?> packageClass) throws Exception {
+    public void setUp(Context context, Class<?> packageClass) throws Exception {
         // makes a copy of the context classloader
         mContextThread = Thread.currentThread();
         mOriginalClassLoader = mContextThread.getContextClassLoader();
@@ -42,6 +45,9 @@
         Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader
                 + " to " + newClassLoader);
         mContextThread.setContextClassLoader(newClassLoader);
+        String dexCache = context.getCacheDir().toString();
+        Log.v(this, "Setting property %s to %s", DEXCACHE, dexCache);
+        System.setProperty(DEXCACHE, dexCache);
     }
 
     /**
@@ -50,5 +56,6 @@
     public void tearDown() throws Exception {
         Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader);
         mContextThread.setContextClassLoader(mOriginalClassLoader);
+        System.clearProperty(DEXCACHE);
     }
 }
\ No newline at end of file
diff --git a/tests/src/com/android/phone/common/mail/MailTransportTest.java b/tests/src/com/android/phone/common/mail/MailTransportTest.java
index 6acd517..9eaef6b 100644
--- a/tests/src/com/android/phone/common/mail/MailTransportTest.java
+++ b/tests/src/com/android/phone/common/mail/MailTransportTest.java
@@ -61,7 +61,7 @@
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        mMokitoHelper.setUp(getClass());
+        mMokitoHelper.setUp(getContext(), getClass());
         MockitoAnnotations.initMocks(this);
     }
 
diff --git a/tests/src/com/android/services/telephony/EmergencyCallStateListenerTest.java b/tests/src/com/android/services/telephony/EmergencyCallStateListenerTest.java
new file mode 100644
index 0000000..64cf052
--- /dev/null
+++ b/tests/src/com/android/services/telephony/EmergencyCallStateListenerTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 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.services.telephony;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.telephony.ServiceState;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.TelephonyTestBase;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.isNull;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests the EmergencyCallStateListener, which listens to one Phone and waits until its service
+ * state changes to accepting emergency calls or in service. If it can not find a tower to camp onto
+ * for emergency calls, then it will fail after a timeout period.
+ */
+@RunWith(AndroidJUnit4.class)
+public class EmergencyCallStateListenerTest extends TelephonyTestBase {
+
+    private static final long TIMEOUT_MS = 100;
+
+    @Mock Phone mMockPhone;
+    @Mock EmergencyCallStateListener.Callback mCallback;
+    EmergencyCallStateListener mListener;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mListener = new EmergencyCallStateListener();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mListener.getHandler().removeCallbacksAndMessages(null);
+        super.tearDown();
+    }
+
+    @Test
+    public void testRegisterForCallback() {
+        mListener.waitForRadioOn(mMockPhone, mCallback);
+
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+
+        verify(mMockPhone).unregisterForServiceStateChanged(any(Handler.class));
+        verify(mMockPhone).registerForServiceStateChanged(any(Handler.class),
+                eq(EmergencyCallStateListener.MSG_SERVICE_STATE_CHANGED), isNull());
+    }
+
+    @Test
+    public void testPhoneChangeState_InService() {
+        ServiceState state = new ServiceState();
+        state.setState(ServiceState.STATE_IN_SERVICE);
+        when(mMockPhone.getState()).thenReturn(PhoneConstants.State.IDLE);
+        mListener.waitForRadioOn(mMockPhone, mCallback);
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+
+        mListener.getHandler().obtainMessage(EmergencyCallStateListener.MSG_SERVICE_STATE_CHANGED,
+                new AsyncResult(null, state, null)).sendToTarget();
+
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+        verify(mCallback).onComplete(eq(mListener), eq(true));
+    }
+
+    @Test
+    public void testPhoneChangeState_EmergencyCalls() {
+        ServiceState state = new ServiceState();
+        state.setState(ServiceState.STATE_OUT_OF_SERVICE);
+        state.setEmergencyOnly(true);
+        when(mMockPhone.getState()).thenReturn(PhoneConstants.State.IDLE);
+        when(mMockPhone.getServiceState()).thenReturn(state);
+        mListener.waitForRadioOn(mMockPhone, mCallback);
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+
+        mListener.getHandler().obtainMessage(EmergencyCallStateListener.MSG_SERVICE_STATE_CHANGED,
+                new AsyncResult(null, state, null)).sendToTarget();
+
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+        verify(mCallback).onComplete(eq(mListener), eq(true));
+    }
+
+    @Test
+    public void testPhoneChangeState_OutOfService() {
+        ServiceState state = new ServiceState();
+        state.setState(ServiceState.STATE_OUT_OF_SERVICE);
+        when(mMockPhone.getState()).thenReturn(PhoneConstants.State.IDLE);
+        when(mMockPhone.getServiceState()).thenReturn(state);
+        mListener.waitForRadioOn(mMockPhone, mCallback);
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+
+        // Don't expect any answer, since it is not the one that we want and the timeout for giving
+        // up hasn't expired yet.
+        mListener.getHandler().obtainMessage(EmergencyCallStateListener.MSG_SERVICE_STATE_CHANGED,
+                new AsyncResult(null, state, null)).sendToTarget();
+
+        waitForHandlerAction(mListener.getHandler(), TIMEOUT_MS);
+        verify(mCallback, never()).onComplete(any(EmergencyCallStateListener.class), anyBoolean());
+    }
+
+    @Test
+    public void testTimeout_EmergencyCalls() {
+        ServiceState state = new ServiceState();
+        state.setState(ServiceState.STATE_OUT_OF_SERVICE);
+        state.setEmergencyOnly(true);
+        when(mMockPhone.getState()).thenReturn(PhoneConstants.State.IDLE);
+        when(mMockPhone.getServiceState()).thenReturn(state);
+        mListener.waitForRadioOn(mMockPhone, mCallback);
+        mListener.setTimeBetweenRetriesMillis(500);
+
+        // Wait for the timer to expire and check state manually in onRetryTimeout
+        waitForHandlerActionDelayed(mListener.getHandler(), TIMEOUT_MS, 600);
+
+        verify(mCallback).onComplete(eq(mListener), eq(true));
+    }
+
+    @Test
+    public void testTimeout_RetryFailure() {
+        ServiceState state = new ServiceState();
+        state.setState(ServiceState.STATE_POWER_OFF);
+        when(mMockPhone.getState()).thenReturn(PhoneConstants.State.IDLE);
+        when(mMockPhone.getServiceState()).thenReturn(state);
+        mListener.waitForRadioOn(mMockPhone, mCallback);
+        mListener.setTimeBetweenRetriesMillis(100);
+        mListener.setMaxNumRetries(2);
+
+        // Wait for the timer to expire and check state manually in onRetryTimeout
+        waitForHandlerActionDelayed(mListener.getHandler(), TIMEOUT_MS, 600);
+
+        verify(mCallback).onComplete(eq(mListener), eq(false));
+        verify(mMockPhone, times(2)).setRadioPower(eq(true));
+    }
+
+}