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
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;
23
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.DisconnectCause;
33import 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;
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
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.
138     * @see #startEmergencyCallFromAirplaneModeSequence
139     */
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        // 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
181        // (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
214            placeEmergencyCall();
215        } 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;
238        int cause = conn.getDisconnectCause();
239        if (DBG) log("onDisconnect: connection '" + conn
240                     + "', addr '" + conn.getAddress()
241                     + "', cause = " + DisconnectCause.toString(cause));
242
243        if (cause == DisconnectCause.OUT_OF_SERVICE) {
244            // 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();
269        int serviceState = mCM.getDefaultPhone().getServiceState().getState();
270        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
295            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        }
309    }
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...");
346            mCM.getDefaultPhone().setRadioPower(true);
347        }
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,
370                                              mCM.getDefaultPhone(),
371                                              mNumber,
372                                              null,  // contactUri
373                                              true); // isEmergencyCall
374        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();
422        } else {
423            if (DBG) log("- Scheduling another retry...");
424            startRetryTimer();
425        }
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
452        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        }
464    }
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.)
479        Phone phone = mCM.getDefaultPhone();
480        phone.unregisterForServiceStateChanged(this);  // Safe even if not currently registered
481        phone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
482    }
483
484    private void unregisterForServiceStateChanged() {
485        // This method is safe to call even if we haven't set mPhone yet.
486        Phone phone = mCM.getDefaultPhone();
487        if (phone != null) {
488            phone.unregisterForServiceStateChanged(this);  // Safe even if unnecessary
489        }
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}
514