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