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 {
onComplete(boolean isRadioReady)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 
EmergencyCallHelper(Context context)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      */
startTurnOnRadioSequence(Phone phone, Callback callback)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      */
startSequenceInternal(Phone phone, Callback callback)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      */
onServiceStateChanged(ServiceState state)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 
isOkToCall(int serviceState, PhoneConstants.State phoneState)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      */
onRetryTimeout()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      */
powerOnRadio()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      */
cleanup()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 
startRetryTimer()302     private void startRetryTimer() {
303         cancelRetryTimer();
304         mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
305     }
306 
cancelRetryTimer()307     private void cancelRetryTimer() {
308         mHandler.removeMessages(MSG_RETRY_TIMEOUT);
309     }
310 
registerForServiceStateChanged()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 
unregisterForServiceStateChanged()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 
onComplete(boolean isRadioReady)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