blob: 47f0e54330c19c0372956637cfa544ac04913869 [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;
32import android.telephony.ServiceState;
33import 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 */
48public 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;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070069 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
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
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 */
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.
Jake Hamby3a2daab2013-11-08 12:35:57 -0800137 * @see #startEmergencyCallFromAirplaneModeSequence
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700138 */
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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700154 // 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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700180 // (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 */
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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700213 placeEmergencyCall();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700214 } 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 */
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 */
265 private void onRetryTimeout() {
266 PhoneConstants.State phoneState = mCM.getState();
Jake Hamby3a2daab2013-11-08 12:35:57 -0800267 int serviceState = mCM.getDefaultPhone().getServiceState().getState();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700268 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
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700293 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 }
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700307 }
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 */
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...");
Jake Hamby3a2daab2013-11-08 12:35:57 -0800344 mCM.getDefaultPhone().setRadioPower(true);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700345 }
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 */
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,
Jake Hamby3a2daab2013-11-08 12:35:57 -0800368 mCM.getDefaultPhone(),
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700369 mNumber,
370 null, // contactUri
Santos Cordon69a69192013-08-22 14:25:42 -0700371 true); // isEmergencyCall
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700372 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 */
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();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700420 } else {
421 if (DBG) log("- Scheduling another retry...");
422 startRetryTimer();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700423 }
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 */
447 private void cleanup() {
448 if (DBG) log("cleanup()...");
449
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700450 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 }
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700462 }
463
464 private void startRetryTimer() {
465 removeMessages(RETRY_TIMEOUT);
466 sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES);
467 }
468
469 private void cancelRetryTimer() {
470 removeMessages(RETRY_TIMEOUT);
471 }
472
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.)
Jake Hamby3a2daab2013-11-08 12:35:57 -0800477 Phone phone = mCM.getDefaultPhone();
478 phone.unregisterForServiceStateChanged(this); // Safe even if not currently registered
479 phone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700480 }
481
482 private void unregisterForServiceStateChanged() {
483 // This method is safe to call even if we haven't set mPhone yet.
Jake Hamby3a2daab2013-11-08 12:35:57 -0800484 Phone phone = mCM.getDefaultPhone();
485 if (phone != null) {
486 phone.unregisterForServiceStateChanged(this); // Safe even if unnecessary
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700487 }
488 removeMessages(SERVICE_STATE_CHANGED); // Clean up any pending messages too
489 }
490
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
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
508 private static void log(String msg) {
509 Log.d(TAG, msg);
510 }
511}