blob: 7f5b0d2cc4eb05a2583c4634e9a344ad42a75be9 [file] [log] [blame]
Santos Cordon7d4ddf62013-07-10 11:58:08 -07001/*
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
17package com.android.phone;
18
19import com.android.internal.telephony.CallManager;
20import com.android.internal.telephony.Connection;
21import com.android.internal.telephony.Phone;
22import com.android.internal.telephony.PhoneConstants;
23import com.android.phone.Constants.CallStatusCode;
24import com.android.phone.InCallUiState.ProgressIndicationType;
25
26import android.content.Context;
27import android.content.Intent;
28import android.os.AsyncResult;
29import android.os.Handler;
30import android.os.Message;
31import android.os.PowerManager;
32import android.os.UserHandle;
33import android.provider.Settings;
34import android.telephony.ServiceState;
35import 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 */
50public 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}