blob: c64a6498a6ecb77f7cb3c5354d051e09e793a817 [file] [log] [blame]
/*
* Copyright (C) 2014 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.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 com.android.internal.os.SomeArgs;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
/**
* Helper class that implements special behavior related to emergency calls. Specifically, this
* class handles the case of the user trying to dial an emergency number while the radio is off
* (i.e. the device is in airplane mode), by forcibly turning the radio back on, waiting for it to
* come up, and then retrying the emergency call.
*/
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;
private final Context mContext;
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;
}
/**
* 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.
* - 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
* EmergencyCallHelper's handler (thus ensuring that the rest of the sequence is entirely
* serialized, and runs only on the handler thread.)
*/
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;
mCallback = callback;
// 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.");
}
}
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.
*/
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) {
Log.d(this, "==> Turning off airplane mode.");
// Change the system setting
Settings.Global.putInt(mContext.getContentResolver(),
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.
// If changing the setting is sufficent to trigger all of the rest of the logic,
// then that should also trigger the broadcast intent.
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.
*/
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);
}
}
}