Home | History | Annotate | Download | only in telephony
      1 /*
      2  * Copyright (C) 2014 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.services.telephony;
     18 
     19 import android.content.Context;
     20 
     21 import android.content.Intent;
     22 import android.os.AsyncResult;
     23 import android.os.Handler;
     24 import android.os.Message;
     25 import android.os.UserHandle;
     26 import android.provider.Settings;
     27 import android.telephony.ServiceState;
     28 
     29 import com.android.internal.os.SomeArgs;
     30 import com.android.internal.telephony.Phone;
     31 import com.android.internal.telephony.PhoneConstants;
     32 
     33 /**
     34  * Helper class that implements special behavior related to emergency calls. Specifically, this
     35  * class handles the case of the user trying to dial an emergency number while the radio is off
     36  * (i.e. the device is in airplane mode), by forcibly turning the radio back on, waiting for it to
     37  * come up, and then retrying the emergency call.
     38  */
     39 public class EmergencyCallHelper {
     40 
     41     /**
     42      * Receives the result of the EmergencyCallHelper's attempt to turn on the radio.
     43      */
     44     interface Callback {
     45         void onComplete(boolean isRadioReady);
     46     }
     47 
     48     // Number of times to retry the call, and time between retry attempts.
     49     public static final int MAX_NUM_RETRIES = 5;
     50     public static final long TIME_BETWEEN_RETRIES_MILLIS = 5000;  // msec
     51 
     52     // Handler message codes; see handleMessage()
     53     private static final int MSG_START_SEQUENCE = 1;
     54     private static final int MSG_SERVICE_STATE_CHANGED = 2;
     55     private static final int MSG_RETRY_TIMEOUT = 3;
     56 
     57     private final Context mContext;
     58 
     59     private final Handler mHandler = new Handler() {
     60         @Override
     61         public void handleMessage(Message msg) {
     62             switch (msg.what) {
     63                 case MSG_START_SEQUENCE:
     64                     SomeArgs args = (SomeArgs) msg.obj;
     65                     Phone phone = (Phone) args.arg1;
     66                     EmergencyCallHelper.Callback callback =
     67                             (EmergencyCallHelper.Callback) args.arg2;
     68                     args.recycle();
     69 
     70                     startSequenceInternal(phone, callback);
     71                     break;
     72                 case MSG_SERVICE_STATE_CHANGED:
     73                     onServiceStateChanged((ServiceState) ((AsyncResult) msg.obj).result);
     74                     break;
     75                 case MSG_RETRY_TIMEOUT:
     76                     onRetryTimeout();
     77                     break;
     78                 default:
     79                     Log.wtf(this, "handleMessage: unexpected message: %d.", msg.what);
     80                     break;
     81             }
     82         }
     83     };
     84 
     85 
     86     private Callback mCallback;  // The callback to notify upon completion.
     87     private Phone mPhone;  // The phone that will attempt to place the call.
     88     private int mNumRetriesSoFar;
     89 
     90     public EmergencyCallHelper(Context context) {
     91         Log.d(this, "EmergencyCallHelper constructor.");
     92         mContext = context;
     93     }
     94 
     95     /**
     96      * Starts the "turn on radio" sequence. This is the (single) external API of the
     97      * EmergencyCallHelper class.
     98      *
     99      * This method kicks off the following sequence:
    100      * - Power on the radio.
    101      * - Listen for the service state change event telling us the radio has come up.
    102      * - Retry if we've gone {@link #TIME_BETWEEN_RETRIES_MILLIS} without any response from the
    103      *   radio.
    104      * - Finally, clean up any leftover state.
    105      *
    106      * This method is safe to call from any thread, since it simply posts a message to the
    107      * EmergencyCallHelper's handler (thus ensuring that the rest of the sequence is entirely
    108      * serialized, and runs only on the handler thread.)
    109      */
    110     public void startTurnOnRadioSequence(Phone phone, Callback callback) {
    111         Log.d(this, "startTurnOnRadioSequence");
    112 
    113         SomeArgs args = SomeArgs.obtain();
    114         args.arg1 = phone;
    115         args.arg2 = callback;
    116         mHandler.obtainMessage(MSG_START_SEQUENCE, args).sendToTarget();
    117     }
    118 
    119     /**
    120      * Actual implementation of startTurnOnRadioSequence(), guaranteed to run on the handler thread.
    121      * @see #startTurnOnRadioSequence
    122      */
    123     private void startSequenceInternal(Phone phone, Callback callback) {
    124         Log.d(this, "startSequenceInternal()");
    125 
    126         // First of all, clean up any state left over from a prior emergency call sequence. This
    127         // ensures that we'll behave sanely if another startTurnOnRadioSequence() comes in while
    128         // we're already in the middle of the sequence.
    129         cleanup();
    130 
    131         mPhone = phone;
    132         mCallback = callback;
    133 
    134 
    135         // No need to check the current service state here, since the only reason to invoke this
    136         // method in the first place is if the radio is powered-off. So just go ahead and turn the
    137         // radio on.
    138 
    139         powerOnRadio();  // We'll get an onServiceStateChanged() callback
    140                          // when the radio successfully comes up.
    141 
    142         // Next step: when the SERVICE_STATE_CHANGED event comes in, we'll retry the call; see
    143         // onServiceStateChanged(). But also, just in case, start a timer to make sure we'll retry
    144         // the call even if the SERVICE_STATE_CHANGED event never comes in for some reason.
    145         startRetryTimer();
    146     }
    147 
    148     /**
    149      * Handles the SERVICE_STATE_CHANGED event. Normally this event tells us that the radio has
    150      * finally come up. In that case, it's now safe to actually place the emergency call.
    151      */
    152     private void onServiceStateChanged(ServiceState state) {
    153         Log.d(this, "onServiceStateChanged(), new state = %s.", state);
    154 
    155         // Possible service states:
    156         // - STATE_IN_SERVICE        // Normal operation
    157         // - STATE_OUT_OF_SERVICE    // Still searching for an operator to register to,
    158         //                           // or no radio signal
    159         // - STATE_EMERGENCY_ONLY    // Phone is locked; only emergency numbers are allowed
    160         // - STATE_POWER_OFF         // Radio is explicitly powered off (airplane mode)
    161 
    162         if (isOkToCall(state.getState(), mPhone.getState())) {
    163             // Woo hoo!  It's OK to actually place the call.
    164             Log.d(this, "onServiceStateChanged: ok to call!");
    165 
    166             onComplete(true);
    167             cleanup();
    168         } else {
    169             // The service state changed, but we're still not ready to call yet. (This probably was
    170             // the transition from STATE_POWER_OFF to STATE_OUT_OF_SERVICE, which happens
    171             // immediately after powering-on the radio.)
    172             //
    173             // So just keep waiting; we'll probably get to either STATE_IN_SERVICE or
    174             // STATE_EMERGENCY_ONLY very shortly. (Or even if that doesn't happen, we'll at least do
    175             // another retry when the RETRY_TIMEOUT event fires.)
    176             Log.d(this, "onServiceStateChanged: not ready to call yet, keep waiting.");
    177         }
    178     }
    179 
    180     private boolean isOkToCall(int serviceState, PhoneConstants.State phoneState) {
    181         // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY, it's finally OK to place
    182         // the emergency call.
    183         return ((phoneState == PhoneConstants.State.OFFHOOK)
    184                 || (serviceState == ServiceState.STATE_IN_SERVICE)
    185                 || (serviceState == ServiceState.STATE_EMERGENCY_ONLY)) ||
    186 
    187                 // Allow STATE_OUT_OF_SERVICE if we are at the max number of retries.
    188                 (mNumRetriesSoFar == MAX_NUM_RETRIES &&
    189                  serviceState == ServiceState.STATE_OUT_OF_SERVICE);
    190     }
    191 
    192     /**
    193      * Handles the retry timer expiring.
    194      */
    195     private void onRetryTimeout() {
    196         PhoneConstants.State phoneState = mPhone.getState();
    197         int serviceState = mPhone.getServiceState().getState();
    198         Log.d(this, "onRetryTimeout():  phone state = %s, service state = %d, retries = %d.",
    199                phoneState, serviceState, mNumRetriesSoFar);
    200 
    201         // - If we're actually in a call, we've succeeded.
    202         // - Otherwise, if the radio is now on, that means we successfully got out of airplane mode
    203         //   but somehow didn't get the service state change event.  In that case, try to place the
    204         //   call.
    205         // - If the radio is still powered off, try powering it on again.
    206 
    207         if (isOkToCall(serviceState, phoneState)) {
    208             Log.d(this, "onRetryTimeout: Radio is on. Cleaning up.");
    209 
    210             // Woo hoo -- we successfully got out of airplane mode.
    211             onComplete(true);
    212             cleanup();
    213         } else {
    214             // Uh oh; we've waited the full TIME_BETWEEN_RETRIES_MILLIS and the radio is still not
    215             // powered-on.  Try again.
    216 
    217             mNumRetriesSoFar++;
    218             Log.d(this, "mNumRetriesSoFar is now " + mNumRetriesSoFar);
    219 
    220             if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
    221                 Log.w(this, "Hit MAX_NUM_RETRIES; giving up.");
    222                 cleanup();
    223             } else {
    224                 Log.d(this, "Trying (again) to turn on the radio.");
    225                 powerOnRadio();  // Again, we'll (hopefully) get an onServiceStateChanged() callback
    226                                  // when the radio successfully comes up.
    227                 startRetryTimer();
    228             }
    229         }
    230     }
    231 
    232     /**
    233      * Attempt to power on the radio (i.e. take the device out of airplane mode.)
    234      * Additionally, start listening for service state changes; we'll eventually get an
    235      * onServiceStateChanged() callback when the radio successfully comes up.
    236      */
    237     private void powerOnRadio() {
    238         Log.d(this, "powerOnRadio().");
    239 
    240         // We're about to turn on the radio, so arrange to be notified when the sequence is
    241         // complete.
    242         registerForServiceStateChanged();
    243 
    244         // If airplane mode is on, we turn it off the same way that the Settings activity turns it
    245         // off.
    246         if (Settings.Global.getInt(mContext.getContentResolver(),
    247                                    Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
    248             Log.d(this, "==> Turning off airplane mode.");
    249 
    250             // Change the system setting
    251             Settings.Global.putInt(mContext.getContentResolver(),
    252                                    Settings.Global.AIRPLANE_MODE_ON, 0);
    253 
    254             // Post the broadcast intend for change in airplane mode
    255             // TODO: We really should not be in charge of sending this broadcast.
    256             //     If changing the setting is sufficent to trigger all of the rest of the logic,
    257             //     then that should also trigger the broadcast intent.
    258             Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
    259             intent.putExtra("state", false);
    260             mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
    261         } else {
    262             // Otherwise, for some strange reason the radio is off (even though the Settings
    263             // database doesn't think we're in airplane mode.)  In this case just turn the radio
    264             // back on.
    265             Log.d(this, "==> (Apparently) not in airplane mode; manually powering radio on.");
    266             mPhone.setRadioPower(true);
    267         }
    268     }
    269 
    270     /**
    271      * Clean up when done with the whole sequence: either after successfully turning on the radio,
    272      * or after bailing out because of too many failures.
    273      *
    274      * The exact cleanup steps are:
    275      * - Notify callback if we still hadn't sent it a response.
    276      * - Double-check that we're not still registered for any telephony events
    277      * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
    278      *
    279      * Basically this method guarantees that there will be no more activity from the
    280      * EmergencyCallHelper until someone kicks off the whole sequence again with another call to
    281      * {@link #startTurnOnRadioSequence}
    282      *
    283      * TODO: Do the work for the comment below:
    284      * Note we don't call this method simply after a successful call to placeCall(), since it's
    285      * still possible the call will disconnect very quickly with an OUT_OF_SERVICE error.
    286      */
    287     private void cleanup() {
    288         Log.d(this, "cleanup()");
    289 
    290         // This will send a failure call back if callback has yet to be invoked.  If the callback
    291         // was already invoked, it's a no-op.
    292         onComplete(false);
    293 
    294         unregisterForServiceStateChanged();
    295         cancelRetryTimer();
    296 
    297         // Used for unregisterForServiceStateChanged() so we null it out here instead.
    298         mPhone = null;
    299         mNumRetriesSoFar = 0;
    300     }
    301 
    302     private void startRetryTimer() {
    303         cancelRetryTimer();
    304         mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
    305     }
    306 
    307     private void cancelRetryTimer() {
    308         mHandler.removeMessages(MSG_RETRY_TIMEOUT);
    309     }
    310 
    311     private void registerForServiceStateChanged() {
    312         // Unregister first, just to make sure we never register ourselves twice.  (We need this
    313         // because Phone.registerForServiceStateChanged() does not prevent multiple registration of
    314         // the same handler.)
    315         unregisterForServiceStateChanged();
    316         mPhone.registerForServiceStateChanged(mHandler, MSG_SERVICE_STATE_CHANGED, null);
    317     }
    318 
    319     private void unregisterForServiceStateChanged() {
    320         // This method is safe to call even if we haven't set mPhone yet.
    321         if (mPhone != null) {
    322             mPhone.unregisterForServiceStateChanged(mHandler);  // Safe even if unnecessary
    323         }
    324         mHandler.removeMessages(MSG_SERVICE_STATE_CHANGED);  // Clean up any pending messages too
    325     }
    326 
    327     private void onComplete(boolean isRadioReady) {
    328         if (mCallback != null) {
    329             Callback tempCallback = mCallback;
    330             mCallback = null;
    331             tempCallback.onComplete(isRadioReady);
    332         }
    333     }
    334 }
    335