Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.phone;
     18 
     19 import com.android.internal.telephony.CallManager;
     20 import com.android.internal.telephony.Connection;
     21 import com.android.internal.telephony.Phone;
     22 import com.android.internal.telephony.PhoneConstants;
     23 
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.os.AsyncResult;
     27 import android.os.Handler;
     28 import android.os.Message;
     29 import android.os.PowerManager;
     30 import android.os.UserHandle;
     31 import android.provider.Settings;
     32 import android.telephony.DisconnectCause;
     33 import android.telephony.ServiceState;
     34 import android.util.Log;
     35 
     36 
     37 /**
     38  * Helper class for the {@link CallController} that implements special
     39  * behavior related to emergency calls.  Specifically, this class handles
     40  * the case of the user trying to dial an emergency number while the radio
     41  * is off (i.e. the device is in airplane mode), by forcibly turning the
     42  * radio back on, waiting for it to come up, and then retrying the
     43  * emergency call.
     44  *
     45  * This class is instantiated lazily (the first time the user attempts to
     46  * make an emergency call from airplane mode) by the the
     47  * {@link CallController} singleton.
     48  */
     49 public class EmergencyCallHelper extends Handler {
     50     private static final String TAG = "EmergencyCallHelper";
     51     private static final boolean DBG = false;
     52 
     53     // Number of times to retry the call, and time between retry attempts.
     54     public static final int MAX_NUM_RETRIES = 6;
     55     public static final long TIME_BETWEEN_RETRIES = 5000;  // msec
     56 
     57     // Timeout used with our wake lock (just as a safety valve to make
     58     // sure we don't hold it forever).
     59     public static final long WAKE_LOCK_TIMEOUT = 5 * 60 * 1000;  // 5 minutes in msec
     60 
     61     // Handler message codes; see handleMessage()
     62     private static final int START_SEQUENCE = 1;
     63     private static final int SERVICE_STATE_CHANGED = 2;
     64     private static final int DISCONNECT = 3;
     65     private static final int RETRY_TIMEOUT = 4;
     66 
     67     private CallController mCallController;
     68     private PhoneGlobals mApp;
     69     private CallManager mCM;
     70     private String mNumber;  // The emergency number we're trying to dial
     71     private int mNumRetriesSoFar;
     72 
     73     // Wake lock we hold while running the whole sequence
     74     private PowerManager.WakeLock mPartialWakeLock;
     75 
     76     public EmergencyCallHelper(CallController callController) {
     77         if (DBG) log("EmergencyCallHelper constructor...");
     78         mCallController = callController;
     79         mApp = PhoneGlobals.getInstance();
     80         mCM =  mApp.mCM;
     81     }
     82 
     83     @Override
     84     public void handleMessage(Message msg) {
     85         switch (msg.what) {
     86             case START_SEQUENCE:
     87                 startSequenceInternal(msg);
     88                 break;
     89             case SERVICE_STATE_CHANGED:
     90                 onServiceStateChanged(msg);
     91                 break;
     92             case DISCONNECT:
     93                 onDisconnect(msg);
     94                 break;
     95             case RETRY_TIMEOUT:
     96                 onRetryTimeout();
     97                 break;
     98             default:
     99                 Log.wtf(TAG, "handleMessage: unexpected message: " + msg);
    100                 break;
    101         }
    102     }
    103 
    104     /**
    105      * Starts the "emergency call from airplane mode" sequence.
    106      *
    107      * This is the (single) external API of the EmergencyCallHelper class.
    108      * This method is called from the CallController placeCall() sequence
    109      * if the user dials a valid emergency number, but the radio is
    110      * powered-off (presumably due to airplane mode.)
    111      *
    112      * This method kicks off the following sequence:
    113      * - Power on the radio
    114      * - Listen for the service state change event telling us the radio has come up
    115      * - Then launch the emergency call
    116      * - Retry if the call fails with an OUT_OF_SERVICE error
    117      * - Retry if we've gone 5 seconds without any response from the radio
    118      * - Finally, clean up any leftover state (progress UI, wake locks, etc.)
    119      *
    120      * This method is safe to call from any thread, since it simply posts
    121      * a message to the EmergencyCallHelper's handler (thus ensuring that
    122      * the rest of the sequence is entirely serialized, and runs only on
    123      * the handler thread.)
    124      *
    125      * This method does *not* force the in-call UI to come up; our caller
    126      * is responsible for doing that (presumably by calling
    127      * PhoneApp.displayCallScreen().)
    128      */
    129     public void startEmergencyCallFromAirplaneModeSequence(String number) {
    130         if (DBG) log("startEmergencyCallFromAirplaneModeSequence('" + number + "')...");
    131         Message msg = obtainMessage(START_SEQUENCE, number);
    132         sendMessage(msg);
    133     }
    134 
    135     /**
    136      * Actual implementation of startEmergencyCallFromAirplaneModeSequence(),
    137      * guaranteed to run on the handler thread.
    138      * @see #startEmergencyCallFromAirplaneModeSequence
    139      */
    140     private void startSequenceInternal(Message msg) {
    141         if (DBG) log("startSequenceInternal(): msg = " + msg);
    142 
    143         // First of all, clean up any state (including mPartialWakeLock!)
    144         // left over from a prior emergency call sequence.
    145         // This ensures that we'll behave sanely if another
    146         // startEmergencyCallFromAirplaneModeSequence() comes in while
    147         // we're already in the middle of the sequence.
    148         cleanup();
    149 
    150         mNumber = (String) msg.obj;
    151         if (DBG) log("- startSequenceInternal: Got mNumber: '" + mNumber + "'");
    152 
    153         mNumRetriesSoFar = 0;
    154 
    155         // Wake lock to make sure the processor doesn't go to sleep midway
    156         // through the emergency call sequence.
    157         PowerManager pm = (PowerManager) mApp.getSystemService(Context.POWER_SERVICE);
    158         mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    159         // Acquire with a timeout, just to be sure we won't hold the wake
    160         // lock forever even if a logic bug (in this class) causes us to
    161         // somehow never call cleanup().
    162         if (DBG) log("- startSequenceInternal: acquiring wake lock");
    163         mPartialWakeLock.acquire(WAKE_LOCK_TIMEOUT);
    164 
    165         // No need to check the current service state here, since the only
    166         // reason the CallController would call this method in the first
    167         // place is if the radio is powered-off.
    168         //
    169         // So just go ahead and turn the radio on.
    170 
    171         powerOnRadio();  // We'll get an onServiceStateChanged() callback
    172                          // when the radio successfully comes up.
    173 
    174         // Next step: when the SERVICE_STATE_CHANGED event comes in,
    175         // we'll retry the call; see placeEmergencyCall();
    176         // But also, just in case, start a timer to make sure we'll retry
    177         // the call even if the SERVICE_STATE_CHANGED event never comes in
    178         // for some reason.
    179         startRetryTimer();
    180 
    181         // (Our caller is responsible for calling mApp.displayCallScreen().)
    182     }
    183 
    184     /**
    185      * Handles the SERVICE_STATE_CHANGED event.
    186      *
    187      * (Normally this event tells us that the radio has finally come
    188      * up.  In that case, it's now safe to actually place the
    189      * emergency call.)
    190      */
    191     private void onServiceStateChanged(Message msg) {
    192         ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
    193         if (DBG) log("onServiceStateChanged()...  new state = " + state);
    194 
    195         // Possible service states:
    196         // - STATE_IN_SERVICE        // Normal operation
    197         // - STATE_OUT_OF_SERVICE    // Still searching for an operator to register to,
    198         //                           // or no radio signal
    199         // - STATE_EMERGENCY_ONLY    // Phone is locked; only emergency numbers are allowed
    200         // - STATE_POWER_OFF         // Radio is explicitly powered off (airplane mode)
    201 
    202         // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY,
    203         // it's finally OK to place the emergency call.
    204         boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE)
    205                 || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY);
    206 
    207         if (okToCall) {
    208             // Woo hoo!  It's OK to actually place the call.
    209             if (DBG) log("onServiceStateChanged: ok to call!");
    210 
    211             // Deregister for the service state change events.
    212             unregisterForServiceStateChanged();
    213 
    214             placeEmergencyCall();
    215         } else {
    216             // The service state changed, but we're still not ready to call yet.
    217             // (This probably was the transition from STATE_POWER_OFF to
    218             // STATE_OUT_OF_SERVICE, which happens immediately after powering-on
    219             // the radio.)
    220             //
    221             // So just keep waiting; we'll probably get to either
    222             // STATE_IN_SERVICE or STATE_EMERGENCY_ONLY very shortly.
    223             // (Or even if that doesn't happen, we'll at least do another retry
    224             // when the RETRY_TIMEOUT event fires.)
    225             if (DBG) log("onServiceStateChanged: not ready to call yet, keep waiting...");
    226         }
    227     }
    228 
    229     /**
    230      * Handles a DISCONNECT event from the telephony layer.
    231      *
    232      * Even after we successfully place an emergency call (after powering
    233      * on the radio), it's still possible for the call to fail with the
    234      * disconnect cause OUT_OF_SERVICE.  If so, schedule a retry.
    235      */
    236     private void onDisconnect(Message msg) {
    237         Connection conn = (Connection) ((AsyncResult) msg.obj).result;
    238         int cause = conn.getDisconnectCause();
    239         if (DBG) log("onDisconnect: connection '" + conn
    240                      + "', addr '" + conn.getAddress()
    241                      + "', cause = " + DisconnectCause.toString(cause));
    242 
    243         if (cause == DisconnectCause.OUT_OF_SERVICE) {
    244             // Wait a bit more and try again (or just bail out totally if
    245             // we've had too many failures.)
    246             if (DBG) log("- onDisconnect: OUT_OF_SERVICE, need to retry...");
    247             scheduleRetryOrBailOut();
    248         } else {
    249             // Any other disconnect cause means we're done.
    250             // Either the emergency call succeeded *and* ended normally,
    251             // or else there was some error that we can't retry.  In either
    252             // case, just clean up our internal state.)
    253 
    254             if (DBG) log("==> Disconnect event; clean up...");
    255             cleanup();
    256 
    257             // Nothing else to do here.  If the InCallScreen was visible,
    258             // it would have received this disconnect event too (so it'll
    259             // show the "Call ended" state and finish itself without any
    260             // help from us.)
    261         }
    262     }
    263 
    264     /**
    265      * Handles the retry timer expiring.
    266      */
    267     private void onRetryTimeout() {
    268         PhoneConstants.State phoneState = mCM.getState();
    269         int serviceState = mCM.getDefaultPhone().getServiceState().getState();
    270         if (DBG) log("onRetryTimeout():  phone state " + phoneState
    271                      + ", service state " + serviceState
    272                      + ", mNumRetriesSoFar = " + mNumRetriesSoFar);
    273 
    274         // - If we're actually in a call, we've succeeded.
    275         //
    276         // - Otherwise, if the radio is now on, that means we successfully got
    277         //   out of airplane mode but somehow didn't get the service state
    278         //   change event.  In that case, try to place the call.
    279         //
    280         // - If the radio is still powered off, try powering it on again.
    281 
    282         if (phoneState == PhoneConstants.State.OFFHOOK) {
    283             if (DBG) log("- onRetryTimeout: Call is active!  Cleaning up...");
    284             cleanup();
    285             return;
    286         }
    287 
    288         if (serviceState != ServiceState.STATE_POWER_OFF) {
    289             // Woo hoo -- we successfully got out of airplane mode.
    290 
    291             // Deregister for the service state change events; we don't need
    292             // these any more now that the radio is powered-on.
    293             unregisterForServiceStateChanged();
    294 
    295             placeEmergencyCall();  // If the call fails, placeEmergencyCall()
    296                                    // will schedule a retry.
    297         } else {
    298             // Uh oh; we've waited the full TIME_BETWEEN_RETRIES and the
    299             // radio is still not powered-on.  Try again...
    300 
    301             if (DBG) log("- Trying (again) to turn on the radio...");
    302             powerOnRadio();  // Again, we'll (hopefully) get an onServiceStateChanged()
    303                              // callback when the radio successfully comes up.
    304 
    305             // ...and also set a fresh retry timer (or just bail out
    306             // totally if we've had too many failures.)
    307             scheduleRetryOrBailOut();
    308         }
    309     }
    310 
    311     /**
    312      * Attempt to power on the radio (i.e. take the device out
    313      * of airplane mode.)
    314      *
    315      * Additionally, start listening for service state changes;
    316      * we'll eventually get an onServiceStateChanged() callback
    317      * when the radio successfully comes up.
    318      */
    319     private void powerOnRadio() {
    320         if (DBG) log("- powerOnRadio()...");
    321 
    322         // We're about to turn on the radio, so arrange to be notified
    323         // when the sequence is complete.
    324         registerForServiceStateChanged();
    325 
    326         // If airplane mode is on, we turn it off the same way that the
    327         // Settings activity turns it off.
    328         if (Settings.Global.getInt(mApp.getContentResolver(),
    329                                    Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
    330             if (DBG) log("==> Turning off airplane mode...");
    331 
    332             // Change the system setting
    333             Settings.Global.putInt(mApp.getContentResolver(),
    334                                    Settings.Global.AIRPLANE_MODE_ON, 0);
    335 
    336             // Post the intent
    337             Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    338             intent.putExtra("state", false);
    339             mApp.sendBroadcastAsUser(intent, UserHandle.ALL);
    340         } else {
    341             // Otherwise, for some strange reason the radio is off
    342             // (even though the Settings database doesn't think we're
    343             // in airplane mode.)  In this case just turn the radio
    344             // back on.
    345             if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on...");
    346             mCM.getDefaultPhone().setRadioPower(true);
    347         }
    348     }
    349 
    350     /**
    351      * Actually initiate the outgoing emergency call.
    352      * (We do this once the radio has successfully been powered-up.)
    353      *
    354      * If the call succeeds, we're done.
    355      * If the call fails, schedule a retry of the whole sequence.
    356      */
    357     private void placeEmergencyCall() {
    358         if (DBG) log("placeEmergencyCall()...");
    359 
    360         // Place an outgoing call to mNumber.
    361         // Note we call PhoneUtils.placeCall() directly; we don't want any
    362         // of the behavior from CallController.placeCallInternal() here.
    363         // (Specifically, we don't want to start the "emergency call from
    364         // airplane mode" sequence from the beginning again!)
    365 
    366         registerForDisconnect();  // Get notified when this call disconnects
    367 
    368         if (DBG) log("- placing call to '" + mNumber + "'...");
    369         int callStatus = PhoneUtils.placeCall(mApp,
    370                                               mCM.getDefaultPhone(),
    371                                               mNumber,
    372                                               null,  // contactUri
    373                                               true); // isEmergencyCall
    374         if (DBG) log("- PhoneUtils.placeCall() returned status = " + callStatus);
    375 
    376         boolean success;
    377         // Note PhoneUtils.placeCall() returns one of the CALL_STATUS_*
    378         // constants, not a CallStatusCode enum value.
    379         switch (callStatus) {
    380             case PhoneUtils.CALL_STATUS_DIALED:
    381                 success = true;
    382                 break;
    383 
    384             case PhoneUtils.CALL_STATUS_DIALED_MMI:
    385             case PhoneUtils.CALL_STATUS_FAILED:
    386             default:
    387                 // Anything else is a failure, and we'll need to retry.
    388                 Log.w(TAG, "placeEmergencyCall(): placeCall() failed: callStatus = " + callStatus);
    389                 success = false;
    390                 break;
    391         }
    392 
    393         if (success) {
    394             if (DBG) log("==> Success from PhoneUtils.placeCall()!");
    395             // Ok, the emergency call is (hopefully) under way.
    396 
    397             // We're not done yet, though, so don't call cleanup() here.
    398             // (It's still possible that this call will fail, and disconnect
    399             // with cause==OUT_OF_SERVICE.  If so, that will trigger a retry
    400             // from the onDisconnect() method.)
    401         } else {
    402             if (DBG) log("==> Failure.");
    403             // Wait a bit more and try again (or just bail out totally if
    404             // we've had too many failures.)
    405             scheduleRetryOrBailOut();
    406         }
    407     }
    408 
    409     /**
    410      * Schedules a retry in response to some failure (either the radio
    411      * failing to power on, or a failure when trying to place the call.)
    412      * Or, if we've hit the retry limit, bail out of this whole sequence
    413      * and display a failure message to the user.
    414      */
    415     private void scheduleRetryOrBailOut() {
    416         mNumRetriesSoFar++;
    417         if (DBG) log("scheduleRetryOrBailOut()...  mNumRetriesSoFar is now " + mNumRetriesSoFar);
    418 
    419         if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
    420             Log.w(TAG, "scheduleRetryOrBailOut: hit MAX_NUM_RETRIES; giving up...");
    421             cleanup();
    422         } else {
    423             if (DBG) log("- Scheduling another retry...");
    424             startRetryTimer();
    425         }
    426     }
    427 
    428     /**
    429      * Clean up when done with the whole sequence: either after
    430      * successfully placing *and* ending the emergency call, or after
    431      * bailing out because of too many failures.
    432      *
    433      * The exact cleanup steps are:
    434      * - Take down any progress UI (and also ask the in-call UI to refresh itself,
    435      *   if it's still visible)
    436      * - Double-check that we're not still registered for any telephony events
    437      * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
    438      * - Make sure we're not still holding any wake locks
    439      *
    440      * Basically this method guarantees that there will be no more
    441      * activity from the EmergencyCallHelper until the CallController
    442      * kicks off the whole sequence again with another call to
    443      * startEmergencyCallFromAirplaneModeSequence().
    444      *
    445      * Note we don't call this method simply after a successful call to
    446      * placeCall(), since it's still possible the call will disconnect
    447      * very quickly with an OUT_OF_SERVICE error.
    448      */
    449     private void cleanup() {
    450         if (DBG) log("cleanup()...");
    451 
    452         unregisterForServiceStateChanged();
    453         unregisterForDisconnect();
    454         cancelRetryTimer();
    455 
    456         // Release / clean up the wake lock
    457         if (mPartialWakeLock != null) {
    458             if (mPartialWakeLock.isHeld()) {
    459                 if (DBG) log("- releasing wake lock");
    460                 mPartialWakeLock.release();
    461             }
    462             mPartialWakeLock = null;
    463         }
    464     }
    465 
    466     private void startRetryTimer() {
    467         removeMessages(RETRY_TIMEOUT);
    468         sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES);
    469     }
    470 
    471     private void cancelRetryTimer() {
    472         removeMessages(RETRY_TIMEOUT);
    473     }
    474 
    475     private void registerForServiceStateChanged() {
    476         // Unregister first, just to make sure we never register ourselves
    477         // twice.  (We need this because Phone.registerForServiceStateChanged()
    478         // does not prevent multiple registration of the same handler.)
    479         Phone phone = mCM.getDefaultPhone();
    480         phone.unregisterForServiceStateChanged(this);  // Safe even if not currently registered
    481         phone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
    482     }
    483 
    484     private void unregisterForServiceStateChanged() {
    485         // This method is safe to call even if we haven't set mPhone yet.
    486         Phone phone = mCM.getDefaultPhone();
    487         if (phone != null) {
    488             phone.unregisterForServiceStateChanged(this);  // Safe even if unnecessary
    489         }
    490         removeMessages(SERVICE_STATE_CHANGED);  // Clean up any pending messages too
    491     }
    492 
    493     private void registerForDisconnect() {
    494         // Note: no need to unregister first, since
    495         // CallManager.registerForDisconnect() automatically prevents
    496         // multiple registration of the same handler.
    497         mCM.registerForDisconnect(this, DISCONNECT, null);
    498     }
    499 
    500     private void unregisterForDisconnect() {
    501         mCM.unregisterForDisconnect(this);  // Safe even if not currently registered
    502         removeMessages(DISCONNECT);  // Clean up any pending messages too
    503     }
    504 
    505 
    506     //
    507     // Debugging
    508     //
    509 
    510     private static void log(String msg) {
    511         Log.d(TAG, msg);
    512     }
    513 }
    514