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