blob: 74ce0880ae4fdf0d5da193f838ab8daae0092445 [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;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070023
24import android.content.Context;
25import android.content.Intent;
26import android.os.AsyncResult;
27import android.os.Handler;
28import android.os.Message;
29import android.os.PowerManager;
30import android.os.UserHandle;
31import android.provider.Settings;
Anders Kristensen0b35f042014-02-27 14:31:07 -080032import android.telephony.DisconnectCause;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070033import android.telephony.ServiceState;
34import android.util.Log;
35
36
37/**
38 * Helper class for the {@link CallController} that implements special
39 * behavior related to emergency calls. Specifically, this class handles
40 * the case of the user trying to dial an emergency number while the radio
41 * is off (i.e. the device is in airplane mode), by forcibly turning the
42 * radio back on, waiting for it to come up, and then retrying the
43 * emergency call.
44 *
45 * This class is instantiated lazily (the first time the user attempts to
46 * make an emergency call from airplane mode) by the the
47 * {@link CallController} singleton.
48 */
49public class EmergencyCallHelper extends Handler {
50 private static final String TAG = "EmergencyCallHelper";
51 private static final boolean DBG = false;
52
53 // Number of times to retry the call, and time between retry attempts.
54 public static final int MAX_NUM_RETRIES = 6;
55 public static final long TIME_BETWEEN_RETRIES = 5000; // msec
56
57 // Timeout used with our wake lock (just as a safety valve to make
58 // sure we don't hold it forever).
59 public static final long WAKE_LOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in msec
60
61 // Handler message codes; see handleMessage()
62 private static final int START_SEQUENCE = 1;
63 private static final int SERVICE_STATE_CHANGED = 2;
64 private static final int DISCONNECT = 3;
65 private static final int RETRY_TIMEOUT = 4;
66
67 private CallController mCallController;
68 private PhoneGlobals mApp;
69 private CallManager mCM;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070070 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
76 public EmergencyCallHelper(CallController callController) {
77 if (DBG) log("EmergencyCallHelper constructor...");
78 mCallController = callController;
79 mApp = PhoneGlobals.getInstance();
80 mCM = mApp.mCM;
81 }
82
83 @Override
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 */
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.
Jake Hamby3a2daab2013-11-08 12:35:57 -0800138 * @see #startEmergencyCallFromAirplaneModeSequence
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700139 */
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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700155 // Wake lock to make sure the processor doesn't go to sleep midway
156 // through the emergency call sequence.
157 PowerManager pm = (PowerManager) mApp.getSystemService(Context.POWER_SERVICE);
158 mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
159 // Acquire with a timeout, just to be sure we won't hold the wake
160 // lock forever even if a logic bug (in this class) causes us to
161 // somehow never call cleanup().
162 if (DBG) log("- startSequenceInternal: acquiring wake lock");
163 mPartialWakeLock.acquire(WAKE_LOCK_TIMEOUT);
164
165 // No need to check the current service state here, since the only
166 // reason the CallController would call this method in the first
167 // place is if the radio is powered-off.
168 //
169 // So just go ahead and turn the radio on.
170
171 powerOnRadio(); // We'll get an onServiceStateChanged() callback
172 // when the radio successfully comes up.
173
174 // Next step: when the SERVICE_STATE_CHANGED event comes in,
175 // we'll retry the call; see placeEmergencyCall();
176 // But also, just in case, start a timer to make sure we'll retry
177 // the call even if the SERVICE_STATE_CHANGED event never comes in
178 // for some reason.
179 startRetryTimer();
180
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700181 // (Our caller is responsible for calling mApp.displayCallScreen().)
182 }
183
184 /**
185 * Handles the SERVICE_STATE_CHANGED event.
186 *
187 * (Normally this event tells us that the radio has finally come
188 * up. In that case, it's now safe to actually place the
189 * emergency call.)
190 */
191 private void onServiceStateChanged(Message msg) {
192 ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
193 if (DBG) log("onServiceStateChanged()... new state = " + state);
194
195 // Possible service states:
196 // - STATE_IN_SERVICE // Normal operation
197 // - STATE_OUT_OF_SERVICE // Still searching for an operator to register to,
198 // // or no radio signal
199 // - STATE_EMERGENCY_ONLY // Phone is locked; only emergency numbers are allowed
200 // - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode)
201
202 // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY,
203 // it's finally OK to place the emergency call.
204 boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE)
205 || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY);
206
207 if (okToCall) {
208 // Woo hoo! It's OK to actually place the call.
209 if (DBG) log("onServiceStateChanged: ok to call!");
210
211 // Deregister for the service state change events.
212 unregisterForServiceStateChanged();
213
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700214 placeEmergencyCall();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700215 } else {
216 // The service state changed, but we're still not ready to call yet.
217 // (This probably was the transition from STATE_POWER_OFF to
218 // STATE_OUT_OF_SERVICE, which happens immediately after powering-on
219 // the radio.)
220 //
221 // So just keep waiting; we'll probably get to either
222 // STATE_IN_SERVICE or STATE_EMERGENCY_ONLY very shortly.
223 // (Or even if that doesn't happen, we'll at least do another retry
224 // when the RETRY_TIMEOUT event fires.)
225 if (DBG) log("onServiceStateChanged: not ready to call yet, keep waiting...");
226 }
227 }
228
229 /**
230 * Handles a DISCONNECT event from the telephony layer.
231 *
232 * Even after we successfully place an emergency call (after powering
233 * on the radio), it's still possible for the call to fail with the
234 * disconnect cause OUT_OF_SERVICE. If so, schedule a retry.
235 */
236 private void onDisconnect(Message msg) {
237 Connection conn = (Connection) ((AsyncResult) msg.obj).result;
Anders Kristensen0b35f042014-02-27 14:31:07 -0800238 int cause = conn.getDisconnectCause();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700239 if (DBG) log("onDisconnect: connection '" + conn
Anders Kristensen0b35f042014-02-27 14:31:07 -0800240 + "', addr '" + conn.getAddress()
241 + "', cause = " + DisconnectCause.toString(cause));
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700242
Anders Kristensen0b35f042014-02-27 14:31:07 -0800243 if (cause == DisconnectCause.OUT_OF_SERVICE) {
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700244 // Wait a bit more and try again (or just bail out totally if
245 // we've had too many failures.)
246 if (DBG) log("- onDisconnect: OUT_OF_SERVICE, need to retry...");
247 scheduleRetryOrBailOut();
248 } else {
249 // Any other disconnect cause means we're done.
250 // Either the emergency call succeeded *and* ended normally,
251 // or else there was some error that we can't retry. In either
252 // case, just clean up our internal state.)
253
254 if (DBG) log("==> Disconnect event; clean up...");
255 cleanup();
256
257 // Nothing else to do here. If the InCallScreen was visible,
258 // it would have received this disconnect event too (so it'll
259 // show the "Call ended" state and finish itself without any
260 // help from us.)
261 }
262 }
263
264 /**
265 * Handles the retry timer expiring.
266 */
267 private void onRetryTimeout() {
268 PhoneConstants.State phoneState = mCM.getState();
Jake Hamby3a2daab2013-11-08 12:35:57 -0800269 int serviceState = mCM.getDefaultPhone().getServiceState().getState();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700270 if (DBG) log("onRetryTimeout(): phone state " + phoneState
271 + ", service state " + serviceState
272 + ", mNumRetriesSoFar = " + mNumRetriesSoFar);
273
274 // - If we're actually in a call, we've succeeded.
275 //
276 // - Otherwise, if the radio is now on, that means we successfully got
277 // out of airplane mode but somehow didn't get the service state
278 // change event. In that case, try to place the call.
279 //
280 // - If the radio is still powered off, try powering it on again.
281
282 if (phoneState == PhoneConstants.State.OFFHOOK) {
283 if (DBG) log("- onRetryTimeout: Call is active! Cleaning up...");
284 cleanup();
285 return;
286 }
287
288 if (serviceState != ServiceState.STATE_POWER_OFF) {
289 // Woo hoo -- we successfully got out of airplane mode.
290
291 // Deregister for the service state change events; we don't need
292 // these any more now that the radio is powered-on.
293 unregisterForServiceStateChanged();
294
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700295 placeEmergencyCall(); // If the call fails, placeEmergencyCall()
296 // will schedule a retry.
297 } else {
298 // Uh oh; we've waited the full TIME_BETWEEN_RETRIES and the
299 // radio is still not powered-on. Try again...
300
301 if (DBG) log("- Trying (again) to turn on the radio...");
302 powerOnRadio(); // Again, we'll (hopefully) get an onServiceStateChanged()
303 // callback when the radio successfully comes up.
304
305 // ...and also set a fresh retry timer (or just bail out
306 // totally if we've had too many failures.)
307 scheduleRetryOrBailOut();
308 }
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700309 }
310
311 /**
312 * Attempt to power on the radio (i.e. take the device out
313 * of airplane mode.)
314 *
315 * Additionally, start listening for service state changes;
316 * we'll eventually get an onServiceStateChanged() callback
317 * when the radio successfully comes up.
318 */
319 private void powerOnRadio() {
320 if (DBG) log("- powerOnRadio()...");
321
322 // We're about to turn on the radio, so arrange to be notified
323 // when the sequence is complete.
324 registerForServiceStateChanged();
325
326 // If airplane mode is on, we turn it off the same way that the
327 // Settings activity turns it off.
328 if (Settings.Global.getInt(mApp.getContentResolver(),
329 Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
330 if (DBG) log("==> Turning off airplane mode...");
331
332 // Change the system setting
333 Settings.Global.putInt(mApp.getContentResolver(),
334 Settings.Global.AIRPLANE_MODE_ON, 0);
335
336 // Post the intent
337 Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
338 intent.putExtra("state", false);
339 mApp.sendBroadcastAsUser(intent, UserHandle.ALL);
340 } else {
341 // Otherwise, for some strange reason the radio is off
342 // (even though the Settings database doesn't think we're
343 // in airplane mode.) In this case just turn the radio
344 // back on.
345 if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on...");
Jake Hamby3a2daab2013-11-08 12:35:57 -0800346 mCM.getDefaultPhone().setRadioPower(true);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700347 }
348 }
349
350 /**
351 * Actually initiate the outgoing emergency call.
352 * (We do this once the radio has successfully been powered-up.)
353 *
354 * If the call succeeds, we're done.
355 * If the call fails, schedule a retry of the whole sequence.
356 */
357 private void placeEmergencyCall() {
358 if (DBG) log("placeEmergencyCall()...");
359
360 // Place an outgoing call to mNumber.
361 // Note we call PhoneUtils.placeCall() directly; we don't want any
362 // of the behavior from CallController.placeCallInternal() here.
363 // (Specifically, we don't want to start the "emergency call from
364 // airplane mode" sequence from the beginning again!)
365
366 registerForDisconnect(); // Get notified when this call disconnects
367
368 if (DBG) log("- placing call to '" + mNumber + "'...");
369 int callStatus = PhoneUtils.placeCall(mApp,
Jake Hamby3a2daab2013-11-08 12:35:57 -0800370 mCM.getDefaultPhone(),
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700371 mNumber,
372 null, // contactUri
Santos Cordon69a69192013-08-22 14:25:42 -0700373 true); // isEmergencyCall
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700374 if (DBG) log("- PhoneUtils.placeCall() returned status = " + callStatus);
375
376 boolean success;
377 // Note PhoneUtils.placeCall() returns one of the CALL_STATUS_*
378 // constants, not a CallStatusCode enum value.
379 switch (callStatus) {
380 case PhoneUtils.CALL_STATUS_DIALED:
381 success = true;
382 break;
383
384 case PhoneUtils.CALL_STATUS_DIALED_MMI:
385 case PhoneUtils.CALL_STATUS_FAILED:
386 default:
387 // Anything else is a failure, and we'll need to retry.
388 Log.w(TAG, "placeEmergencyCall(): placeCall() failed: callStatus = " + callStatus);
389 success = false;
390 break;
391 }
392
393 if (success) {
394 if (DBG) log("==> Success from PhoneUtils.placeCall()!");
395 // Ok, the emergency call is (hopefully) under way.
396
397 // We're not done yet, though, so don't call cleanup() here.
398 // (It's still possible that this call will fail, and disconnect
399 // with cause==OUT_OF_SERVICE. If so, that will trigger a retry
400 // from the onDisconnect() method.)
401 } else {
402 if (DBG) log("==> Failure.");
403 // Wait a bit more and try again (or just bail out totally if
404 // we've had too many failures.)
405 scheduleRetryOrBailOut();
406 }
407 }
408
409 /**
410 * Schedules a retry in response to some failure (either the radio
411 * failing to power on, or a failure when trying to place the call.)
412 * Or, if we've hit the retry limit, bail out of this whole sequence
413 * and display a failure message to the user.
414 */
415 private void scheduleRetryOrBailOut() {
416 mNumRetriesSoFar++;
417 if (DBG) log("scheduleRetryOrBailOut()... mNumRetriesSoFar is now " + mNumRetriesSoFar);
418
419 if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
420 Log.w(TAG, "scheduleRetryOrBailOut: hit MAX_NUM_RETRIES; giving up...");
421 cleanup();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700422 } else {
423 if (DBG) log("- Scheduling another retry...");
424 startRetryTimer();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700425 }
426 }
427
428 /**
429 * Clean up when done with the whole sequence: either after
430 * successfully placing *and* ending the emergency call, or after
431 * bailing out because of too many failures.
432 *
433 * The exact cleanup steps are:
434 * - Take down any progress UI (and also ask the in-call UI to refresh itself,
435 * if it's still visible)
436 * - Double-check that we're not still registered for any telephony events
437 * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
438 * - Make sure we're not still holding any wake locks
439 *
440 * Basically this method guarantees that there will be no more
441 * activity from the EmergencyCallHelper until the CallController
442 * kicks off the whole sequence again with another call to
443 * startEmergencyCallFromAirplaneModeSequence().
444 *
445 * Note we don't call this method simply after a successful call to
446 * placeCall(), since it's still possible the call will disconnect
447 * very quickly with an OUT_OF_SERVICE error.
448 */
449 private void cleanup() {
450 if (DBG) log("cleanup()...");
451
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700452 unregisterForServiceStateChanged();
453 unregisterForDisconnect();
454 cancelRetryTimer();
455
456 // Release / clean up the wake lock
457 if (mPartialWakeLock != null) {
458 if (mPartialWakeLock.isHeld()) {
459 if (DBG) log("- releasing wake lock");
460 mPartialWakeLock.release();
461 }
462 mPartialWakeLock = null;
463 }
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700464 }
465
466 private void startRetryTimer() {
467 removeMessages(RETRY_TIMEOUT);
468 sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES);
469 }
470
471 private void cancelRetryTimer() {
472 removeMessages(RETRY_TIMEOUT);
473 }
474
475 private void registerForServiceStateChanged() {
476 // Unregister first, just to make sure we never register ourselves
477 // twice. (We need this because Phone.registerForServiceStateChanged()
478 // does not prevent multiple registration of the same handler.)
Jake Hamby3a2daab2013-11-08 12:35:57 -0800479 Phone phone = mCM.getDefaultPhone();
480 phone.unregisterForServiceStateChanged(this); // Safe even if not currently registered
481 phone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700482 }
483
484 private void unregisterForServiceStateChanged() {
485 // This method is safe to call even if we haven't set mPhone yet.
Jake Hamby3a2daab2013-11-08 12:35:57 -0800486 Phone phone = mCM.getDefaultPhone();
487 if (phone != null) {
488 phone.unregisterForServiceStateChanged(this); // Safe even if unnecessary
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700489 }
490 removeMessages(SERVICE_STATE_CHANGED); // Clean up any pending messages too
491 }
492
493 private void registerForDisconnect() {
494 // Note: no need to unregister first, since
495 // CallManager.registerForDisconnect() automatically prevents
496 // multiple registration of the same handler.
497 mCM.registerForDisconnect(this, DISCONNECT, null);
498 }
499
500 private void unregisterForDisconnect() {
501 mCM.unregisterForDisconnect(this); // Safe even if not currently registered
502 removeMessages(DISCONNECT); // Clean up any pending messages too
503 }
504
505
506 //
507 // Debugging
508 //
509
510 private static void log(String msg) {
511 Log.d(TAG, msg);
512 }
513}