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