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