Merge "Phone theme updates." into nyc-mr1-dev
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 783878c..808a5d6 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -637,8 +637,53 @@
notifier.updateCallNotifierRegistrationsAfterRadioTechnologyChange();
}
- private void handleAirplaneModeChange(int newMode) {
- if (newMode == AIRPLANE_ON) {
+ private void handleAirplaneModeChange(Context context, int newMode) {
+ int cellState = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.CELL_ON, PhoneConstants.CELL_ON_FLAG);
+ boolean isAirplaneNewlyOn = (newMode == 1);
+ switch (cellState) {
+ case PhoneConstants.CELL_OFF_FLAG:
+ // Airplane mode does not affect the cell radio if user
+ // has turned it off.
+ break;
+ case PhoneConstants.CELL_ON_FLAG:
+ maybeTurnCellOff(context, isAirplaneNewlyOn);
+ break;
+ case PhoneConstants.CELL_OFF_DUE_TO_AIRPLANE_MODE_FLAG:
+ maybeTurnCellOn(context, isAirplaneNewlyOn);
+ break;
+ }
+ }
+
+ /*
+ * Returns true if the radio must be turned off when entering airplane mode.
+ */
+ private boolean isCellOffInAirplaneMode(Context context) {
+ String airplaneModeRadios = Settings.Global.getString(context.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_RADIOS);
+ return airplaneModeRadios == null
+ || airplaneModeRadios.contains(Settings.Global.RADIO_CELL);
+ }
+
+ private void setRadioPowerOff(Context context) {
+ Log.i(LOG_TAG, "Turning radio off - airplane");
+ Settings.Global.putInt(context.getContentResolver(), Settings.Global.CELL_ON,
+ PhoneConstants.CELL_OFF_DUE_TO_AIRPLANE_MODE_FLAG);
+ Settings.Global.putInt(getContentResolver(), Settings.Global.ENABLE_CELLULAR_ON_BOOT, 0);
+ PhoneUtils.setRadioPower(false);
+ }
+
+ private void setRadioPowerOn(Context context) {
+ Log.i(LOG_TAG, "Turning radio on - airplane");
+ Settings.Global.putInt(context.getContentResolver(), Settings.Global.CELL_ON,
+ PhoneConstants.CELL_ON_FLAG);
+ Settings.Global.putInt(getContentResolver(), Settings.Global.ENABLE_CELLULAR_ON_BOOT,
+ 1);
+ PhoneUtils.setRadioPower(true);
+ }
+
+ private void maybeTurnCellOff(Context context, boolean isAirplaneNewlyOn) {
+ if (isAirplaneNewlyOn) {
// If we are trying to turn off the radio, make sure there are no active
// emergency calls. If there are, switch airplane mode back to off.
if (PhoneUtils.isInEmergencyCall(mCM)) {
@@ -647,13 +692,17 @@
Toast.makeText(this, R.string.radio_off_during_emergency_call, Toast.LENGTH_LONG)
.show();
Log.i(LOG_TAG, "Ignoring airplane mode: emergency call. Turning airplane off");
+ } else if (isCellOffInAirplaneMode(context)) {
+ setRadioPowerOff(context);
} else {
- Log.i(LOG_TAG, "Turning radio off - airplane");
- PhoneUtils.setRadioPower(false);
+ Log.i(LOG_TAG, "Ignoring airplane mode: settings prevent cell radio power off");
}
- } else {
- Log.i(LOG_TAG, "Turning radio on - airplane");
- PhoneUtils.setRadioPower(true);
+ }
+ }
+
+ private void maybeTurnCellOn(Context context, boolean isAirplaneNewlyOn) {
+ if (!isAirplaneNewlyOn) {
+ setRadioPowerOn(context);
}
}
@@ -671,7 +720,7 @@
if (airplaneMode != AIRPLANE_OFF) {
airplaneMode = AIRPLANE_ON;
}
- handleAirplaneModeChange(airplaneMode);
+ handleAirplaneModeChange(context, airplaneMode);
} else if (action.equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) {
int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
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..2346a7f
--- /dev/null
+++ b/src/com/android/services/telephony/EmergencyCallStateListener.java
@@ -0,0 +1,322 @@
+/*
+ * 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;
+import com.android.internal.telephony.SubscriptionController;
+
+/**
+ * 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 = %s", state,
+ mPhone.getPhoneId());
+
+ // 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 8b62653..9f252a2 100644
--- a/src/com/android/services/telephony/TelephonyConnectionService.java
+++ b/src/com/android/services/telephony/TelephonyConnectionService.java
@@ -175,10 +175,83 @@
}
}
- boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(this, number);
+ final boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(this, number);
- // Get the right phone object from the account data passed in.
- final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber);
+ if (isEmergencyNumber && !isRadioOn()) {
+ final Uri emergencyHandle = handle;
+ // By default, Connection based on the default Phone, since we need to return to Telecom
+ // now.
+ final int defaultPhoneType = PhoneFactory.getDefaultPhone().getPhoneType();
+ final Connection emergencyConnection = getTelephonyConnection(request, number,
+ isEmergencyNumber, emergencyHandle, PhoneFactory.getDefaultPhone());
+ if (mEmergencyCallHelper == null) {
+ mEmergencyCallHelper = new EmergencyCallHelper(this);
+ }
+ mEmergencyCallHelper.enableEmergencyCalling(new EmergencyCallStateListener.Callback() {
+ @Override
+ public void onComplete(EmergencyCallStateListener listener, boolean isRadioReady) {
+ if (isRadioReady) {
+ // Get the right phone object since the radio has been turned on
+ // successfully.
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(),
+ isEmergencyNumber);
+ // If the PhoneType of the Phone being used is different than the Default
+ // Phone, then we need create a new Connection using that PhoneType and
+ // replace it in Telecom.
+ if (phone.getPhoneType() != defaultPhoneType) {
+ Connection repConnection = getTelephonyConnection(request, number,
+ isEmergencyNumber, emergencyHandle, phone);
+ // If there was a failure, the resulting connection will not be a
+ // TelephonyConnection, so don't place the call, just return!
+ if (repConnection instanceof TelephonyConnection) {
+ placeOutgoingConnection((TelephonyConnection) repConnection, phone,
+ request);
+ }
+ // Notify Telecom of the new Connection type.
+ // TODO: Switch out the underlying connection instead of creating a new
+ // one and causing UI Jank.
+ addExistingConnection(PhoneUtils.makePstnPhoneAccountHandle(phone),
+ repConnection);
+ // Remove the old connection from Telecom after.
+ emergencyConnection.setDisconnected(
+ DisconnectCauseUtil.toTelecomDisconnectCause(
+ android.telephony.DisconnectCause.OUTGOING_CANCELED,
+ "Reconnecting outgoing Emergency Call."));
+ emergencyConnection.destroy();
+ } else {
+ placeOutgoingConnection((TelephonyConnection) emergencyConnection,
+ phone, request);
+ }
+ } else {
+ Log.w(this, "onCreateOutgoingConnection, failed to turn on radio");
+ emergencyConnection.setDisconnected(
+ DisconnectCauseUtil.toTelecomDisconnectCause(
+ android.telephony.DisconnectCause.POWER_OFF,
+ "Failed to turn on radio."));
+ emergencyConnection.destroy();
+ }
+ }
+ });
+ // Return the still unconnected GsmConnection and wait for the Radios to boot before
+ // connecting it to the underlying Phone.
+ return emergencyConnection;
+ } else {
+ // Get the right phone object from the account data passed in.
+ final Phone phone = getPhoneForAccount(request.getAccountHandle(), isEmergencyNumber);
+ Connection resultConnection = getTelephonyConnection(request, number, isEmergencyNumber,
+ handle, phone);
+ // If there was a failure, the resulting connection will not be a TelephonyConnection,
+ // so don't place the call!
+ if(resultConnection instanceof TelephonyConnection) {
+ placeOutgoingConnection((TelephonyConnection) resultConnection, phone, request);
+ }
+ return resultConnection;
+ }
+ }
+
+ private Connection getTelephonyConnection(final ConnectionRequest request, final String number,
+ boolean isEmergencyNumber, final Uri handle, Phone phone) {
+
if (phone == null) {
final Context context = getApplicationContext();
if (context.getResources().getBoolean(R.bool.config_checkSimStateBeforeOutgoingCall)) {
@@ -228,7 +301,6 @@
state = phone.getServiceState().getDataRegState();
}
}
- boolean useEmergencyCallHelper = false;
// If we're dialing a non-emergency number and the phone is in ECM mode, reject the call if
// carrier configuration specifies that we cannot make non-emergency calls in ECM mode.
@@ -250,11 +322,7 @@
}
}
- if (isEmergencyNumber) {
- if (!phone.isRadioOn()) {
- useEmergencyCallHelper = true;
- }
- } else {
+ if (!isEmergencyNumber) {
switch (state) {
case ServiceState.STATE_IN_SERVICE:
case ServiceState.STATE_EMERGENCY_ONLY:
@@ -309,34 +377,6 @@
connection.setInitializing();
connection.setVideoState(request.getVideoState());
- if (useEmergencyCallHelper) {
- if (mEmergencyCallHelper == null) {
- mEmergencyCallHelper = new EmergencyCallHelper(this);
- }
- mEmergencyCallHelper.startTurnOnRadioSequence(phone,
- new EmergencyCallHelper.Callback() {
- @Override
- public void onComplete(boolean isRadioReady) {
- if (connection.getState() == Connection.STATE_DISCONNECTED) {
- // If the connection has already been disconnected, do nothing.
- } else if (isRadioReady) {
- connection.setInitialized();
- placeOutgoingConnection(connection, phone, request);
- } else {
- Log.d(this, "onCreateOutgoingConnection, failed to turn on radio");
- connection.setDisconnected(
- DisconnectCauseUtil.toTelecomDisconnectCause(
- android.telephony.DisconnectCause.POWER_OFF,
- "Failed to turn on radio."));
- connection.destroy();
- }
- }
- });
-
- } else {
- placeOutgoingConnection(connection, phone, request);
- }
-
return connection;
}
@@ -514,6 +554,14 @@
}
+ private boolean isRadioOn() {
+ boolean result = false;
+ for (Phone phone : PhoneFactory.getPhones()) {
+ result |= phone.isRadioOn();
+ }
+ return result;
+ }
+
private void placeOutgoingConnection(
TelephonyConnection connection, Phone phone, ConnectionRequest request) {
String number = connection.getAddress().getSchemeSpecificPart();
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));
+ }
+
+}