1/*
2 * Copyright (C) 2014 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.services.telephony;
18
19import android.content.Context;
20
21import android.content.Intent;
22import android.os.AsyncResult;
23import android.os.Handler;
24import android.os.Message;
25import android.os.UserHandle;
26import android.provider.Settings;
27import android.telephony.ServiceState;
28
29import com.android.internal.os.SomeArgs;
30import com.android.internal.telephony.Phone;
31import com.android.internal.telephony.PhoneConstants;
32
33/**
34 * Helper class that implements special behavior related to emergency calls. Specifically, this
35 * class handles the case of the user trying to dial an emergency number while the radio is off
36 * (i.e. the device is in airplane mode), by forcibly turning the radio back on, waiting for it to
37 * come up, and then retrying the emergency call.
38 */
39public class EmergencyCallHelper {
40
41    /**
42     * Receives the result of the EmergencyCallHelper's attempt to turn on the radio.
43     */
44    interface Callback {
45        void onComplete(boolean isRadioReady);
46    }
47
48    // Number of times to retry the call, and time between retry attempts.
49    public static final int MAX_NUM_RETRIES = 5;
50    public static final long TIME_BETWEEN_RETRIES_MILLIS = 5000;  // msec
51
52    // Handler message codes; see handleMessage()
53    private static final int MSG_START_SEQUENCE = 1;
54    private static final int MSG_SERVICE_STATE_CHANGED = 2;
55    private static final int MSG_RETRY_TIMEOUT = 3;
56
57    private final Context mContext;
58
59    private final Handler mHandler = new Handler() {
60        @Override
61        public void handleMessage(Message msg) {
62            switch (msg.what) {
63                case MSG_START_SEQUENCE:
64                    SomeArgs args = (SomeArgs) msg.obj;
65                    Phone phone = (Phone) args.arg1;
66                    EmergencyCallHelper.Callback callback =
67                            (EmergencyCallHelper.Callback) args.arg2;
68                    args.recycle();
69
70                    startSequenceInternal(phone, callback);
71                    break;
72                case MSG_SERVICE_STATE_CHANGED:
73                    onServiceStateChanged((ServiceState) ((AsyncResult) msg.obj).result);
74                    break;
75                case MSG_RETRY_TIMEOUT:
76                    onRetryTimeout();
77                    break;
78                default:
79                    Log.wtf(this, "handleMessage: unexpected message: %d.", msg.what);
80                    break;
81            }
82        }
83    };
84
85
86    private Callback mCallback;  // The callback to notify upon completion.
87    private Phone mPhone;  // The phone that will attempt to place the call.
88    private int mNumRetriesSoFar;
89
90    public EmergencyCallHelper(Context context) {
91        Log.d(this, "EmergencyCallHelper constructor.");
92        mContext = context;
93    }
94
95    /**
96     * Starts the "turn on radio" sequence. This is the (single) external API of the
97     * EmergencyCallHelper class.
98     *
99     * This method kicks off the following sequence:
100     * - Power on the radio.
101     * - Listen for the service state change event telling us the radio has come up.
102     * - Retry if we've gone {@link #TIME_BETWEEN_RETRIES_MILLIS} without any response from the
103     *   radio.
104     * - Finally, clean up any leftover state.
105     *
106     * This method is safe to call from any thread, since it simply posts a message to the
107     * EmergencyCallHelper's handler (thus ensuring that the rest of the sequence is entirely
108     * serialized, and runs only on the handler thread.)
109     */
110    public void startTurnOnRadioSequence(Phone phone, Callback callback) {
111        Log.d(this, "startTurnOnRadioSequence");
112
113        SomeArgs args = SomeArgs.obtain();
114        args.arg1 = phone;
115        args.arg2 = callback;
116        mHandler.obtainMessage(MSG_START_SEQUENCE, args).sendToTarget();
117    }
118
119    /**
120     * Actual implementation of startTurnOnRadioSequence(), guaranteed to run on the handler thread.
121     * @see #startTurnOnRadioSequence
122     */
123    private void startSequenceInternal(Phone phone, Callback callback) {
124        Log.d(this, "startSequenceInternal()");
125
126        // First of all, clean up any state left over from a prior emergency call sequence. This
127        // ensures that we'll behave sanely if another startTurnOnRadioSequence() comes in while
128        // we're already in the middle of the sequence.
129        cleanup();
130
131        mPhone = phone;
132        mCallback = callback;
133
134
135        // No need to check the current service state here, since the only reason to invoke this
136        // method in the first place is if the radio is powered-off. So just go ahead and turn the
137        // radio on.
138
139        powerOnRadio();  // We'll get an onServiceStateChanged() callback
140                         // when the radio successfully comes up.
141
142        // Next step: when the SERVICE_STATE_CHANGED event comes in, we'll retry the call; see
143        // onServiceStateChanged(). But also, just in case, start a timer to make sure we'll retry
144        // the call even if the SERVICE_STATE_CHANGED event never comes in for some reason.
145        startRetryTimer();
146    }
147
148    /**
149     * Handles the SERVICE_STATE_CHANGED event. Normally this event tells us that the radio has
150     * finally come up. In that case, it's now safe to actually place the emergency call.
151     */
152    private void onServiceStateChanged(ServiceState state) {
153        Log.d(this, "onServiceStateChanged(), new state = %s.", state);
154
155        // Possible service states:
156        // - STATE_IN_SERVICE        // Normal operation
157        // - STATE_OUT_OF_SERVICE    // Still searching for an operator to register to,
158        //                           // or no radio signal
159        // - STATE_EMERGENCY_ONLY    // Phone is locked; only emergency numbers are allowed
160        // - STATE_POWER_OFF         // Radio is explicitly powered off (airplane mode)
161
162        if (isOkToCall(state.getState(), mPhone.getState())) {
163            // Woo hoo!  It's OK to actually place the call.
164            Log.d(this, "onServiceStateChanged: ok to call!");
165
166            onComplete(true);
167            cleanup();
168        } else {
169            // The service state changed, but we're still not ready to call yet. (This probably was
170            // the transition from STATE_POWER_OFF to STATE_OUT_OF_SERVICE, which happens
171            // immediately after powering-on the radio.)
172            //
173            // So just keep waiting; we'll probably get to either STATE_IN_SERVICE or
174            // STATE_EMERGENCY_ONLY very shortly. (Or even if that doesn't happen, we'll at least do
175            // another retry when the RETRY_TIMEOUT event fires.)
176            Log.d(this, "onServiceStateChanged: not ready to call yet, keep waiting.");
177        }
178    }
179
180    private boolean isOkToCall(int serviceState, PhoneConstants.State phoneState) {
181        // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY, it's finally OK to place
182        // the emergency call.
183        return ((phoneState == PhoneConstants.State.OFFHOOK)
184                || (serviceState == ServiceState.STATE_IN_SERVICE)
185                || (serviceState == ServiceState.STATE_EMERGENCY_ONLY)) ||
186
187                // Allow STATE_OUT_OF_SERVICE if we are at the max number of retries.
188                (mNumRetriesSoFar == MAX_NUM_RETRIES &&
189                 serviceState == ServiceState.STATE_OUT_OF_SERVICE);
190    }
191
192    /**
193     * Handles the retry timer expiring.
194     */
195    private void onRetryTimeout() {
196        PhoneConstants.State phoneState = mPhone.getState();
197        int serviceState = mPhone.getServiceState().getState();
198        Log.d(this, "onRetryTimeout():  phone state = %s, service state = %d, retries = %d.",
199               phoneState, serviceState, mNumRetriesSoFar);
200
201        // - If we're actually in a call, we've succeeded.
202        // - Otherwise, if the radio is now on, that means we successfully got out of airplane mode
203        //   but somehow didn't get the service state change event.  In that case, try to place the
204        //   call.
205        // - If the radio is still powered off, try powering it on again.
206
207        if (isOkToCall(serviceState, phoneState)) {
208            Log.d(this, "onRetryTimeout: Radio is on. Cleaning up.");
209
210            // Woo hoo -- we successfully got out of airplane mode.
211            onComplete(true);
212            cleanup();
213        } else {
214            // Uh oh; we've waited the full TIME_BETWEEN_RETRIES_MILLIS and the radio is still not
215            // powered-on.  Try again.
216
217            mNumRetriesSoFar++;
218            Log.d(this, "mNumRetriesSoFar is now " + mNumRetriesSoFar);
219
220            if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
221                Log.w(this, "Hit MAX_NUM_RETRIES; giving up.");
222                cleanup();
223            } else {
224                Log.d(this, "Trying (again) to turn on the radio.");
225                powerOnRadio();  // Again, we'll (hopefully) get an onServiceStateChanged() callback
226                                 // when the radio successfully comes up.
227                startRetryTimer();
228            }
229        }
230    }
231
232    /**
233     * Attempt to power on the radio (i.e. take the device out of airplane mode.)
234     * Additionally, start listening for service state changes; we'll eventually get an
235     * onServiceStateChanged() callback when the radio successfully comes up.
236     */
237    private void powerOnRadio() {
238        Log.d(this, "powerOnRadio().");
239
240        // We're about to turn on the radio, so arrange to be notified when the sequence is
241        // complete.
242        registerForServiceStateChanged();
243
244        // If airplane mode is on, we turn it off the same way that the Settings activity turns it
245        // off.
246        if (Settings.Global.getInt(mContext.getContentResolver(),
247                                   Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
248            Log.d(this, "==> Turning off airplane mode.");
249
250            // Change the system setting
251            Settings.Global.putInt(mContext.getContentResolver(),
252                                   Settings.Global.AIRPLANE_MODE_ON, 0);
253
254            // Post the broadcast intend for change in airplane mode
255            // TODO: We really should not be in charge of sending this broadcast.
256            //     If changing the setting is sufficent to trigger all of the rest of the logic,
257            //     then that should also trigger the broadcast intent.
258            Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
259            intent.putExtra("state", false);
260            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
261        } else {
262            // Otherwise, for some strange reason the radio is off (even though the Settings
263            // database doesn't think we're in airplane mode.)  In this case just turn the radio
264            // back on.
265            Log.d(this, "==> (Apparently) not in airplane mode; manually powering radio on.");
266            mPhone.setRadioPower(true);
267        }
268    }
269
270    /**
271     * Clean up when done with the whole sequence: either after successfully turning on the radio,
272     * or after bailing out because of too many failures.
273     *
274     * The exact cleanup steps are:
275     * - Notify callback if we still hadn't sent it a response.
276     * - Double-check that we're not still registered for any telephony events
277     * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
278     *
279     * Basically this method guarantees that there will be no more activity from the
280     * EmergencyCallHelper until someone kicks off the whole sequence again with another call to
281     * {@link #startTurnOnRadioSequence}
282     *
283     * TODO: Do the work for the comment below:
284     * Note we don't call this method simply after a successful call to placeCall(), since it's
285     * still possible the call will disconnect very quickly with an OUT_OF_SERVICE error.
286     */
287    private void cleanup() {
288        Log.d(this, "cleanup()");
289
290        // This will send a failure call back if callback has yet to be invoked.  If the callback
291        // was already invoked, it's a no-op.
292        onComplete(false);
293
294        unregisterForServiceStateChanged();
295        cancelRetryTimer();
296
297        // Used for unregisterForServiceStateChanged() so we null it out here instead.
298        mPhone = null;
299        mNumRetriesSoFar = 0;
300    }
301
302    private void startRetryTimer() {
303        cancelRetryTimer();
304        mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
305    }
306
307    private void cancelRetryTimer() {
308        mHandler.removeMessages(MSG_RETRY_TIMEOUT);
309    }
310
311    private void registerForServiceStateChanged() {
312        // Unregister first, just to make sure we never register ourselves twice.  (We need this
313        // because Phone.registerForServiceStateChanged() does not prevent multiple registration of
314        // the same handler.)
315        unregisterForServiceStateChanged();
316        mPhone.registerForServiceStateChanged(mHandler, MSG_SERVICE_STATE_CHANGED, null);
317    }
318
319    private void unregisterForServiceStateChanged() {
320        // This method is safe to call even if we haven't set mPhone yet.
321        if (mPhone != null) {
322            mPhone.unregisterForServiceStateChanged(mHandler);  // Safe even if unnecessary
323        }
324        mHandler.removeMessages(MSG_SERVICE_STATE_CHANGED);  // Clean up any pending messages too
325    }
326
327    private void onComplete(boolean isRadioReady) {
328        if (mCallback != null) {
329            Callback tempCallback = mCallback;
330            mCallback = null;
331            tempCallback.onComplete(isRadioReady);
332        }
333    }
334}
335