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.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;
69    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.
137     * @see #startEmergencyCallFromAirplaneModeSequence
138     */
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
154        // 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
180        // (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
213            placeEmergencyCall();
214        } 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();
267        int serviceState = mCM.getDefaultPhone().getServiceState().getState();
268        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
293            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        }
307    }
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...");
344            mCM.getDefaultPhone().setRadioPower(true);
345        }
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,
368                                              mCM.getDefaultPhone(),
369                                              mNumber,
370                                              null,  // contactUri
371                                              true); // isEmergencyCall
372        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();
420        } else {
421            if (DBG) log("- Scheduling another retry...");
422            startRetryTimer();
423        }
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
450        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        }
462    }
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.)
477        Phone phone = mCM.getDefaultPhone();
478        phone.unregisterForServiceStateChanged(this);  // Safe even if not currently registered
479        phone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
480    }
481
482    private void unregisterForServiceStateChanged() {
483        // This method is safe to call even if we haven't set mPhone yet.
484        Phone phone = mCM.getDefaultPhone();
485        if (phone != null) {
486            phone.unregisterForServiceStateChanged(this);  // Safe even if unnecessary
487        }
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}
512