• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 
17 package com.android.services.telephony;
18 
19 import android.os.AsyncResult;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.telephony.ServiceState;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.internal.os.SomeArgs;
27 import com.android.internal.telephony.CommandsInterface;
28 import com.android.internal.telephony.Phone;
29 import com.android.internal.telephony.PhoneConstants;
30 import com.android.internal.telephony.SubscriptionController;
31 
32 /**
33  * Helper class that listens to a Phone's radio state and sends a callback when the radio state of
34  * that Phone is either "in service" or "emergency calls only."
35  */
36 public class EmergencyCallStateListener {
37 
38     /**
39      * Receives the result of the EmergencyCallStateListener's attempt to turn on the radio.
40      */
41     interface Callback {
onComplete(EmergencyCallStateListener listener, boolean isRadioReady)42         void onComplete(EmergencyCallStateListener listener, boolean isRadioReady);
43     }
44 
45     // Number of times to retry the call, and time between retry attempts.
46     private static int MAX_NUM_RETRIES = 5;
47     private static long TIME_BETWEEN_RETRIES_MILLIS = 5000;  // msec
48 
49     // Handler message codes; see handleMessage()
50     @VisibleForTesting
51     public static final int MSG_START_SEQUENCE = 1;
52     @VisibleForTesting
53     public static final int MSG_SERVICE_STATE_CHANGED = 2;
54     @VisibleForTesting
55     public static final int MSG_RETRY_TIMEOUT = 3;
56 
57     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
58         @Override
59         public void handleMessage(Message msg) {
60             switch (msg.what) {
61                 case MSG_START_SEQUENCE:
62                     SomeArgs args = (SomeArgs) msg.obj;
63                     try {
64                         Phone phone = (Phone) args.arg1;
65                         EmergencyCallStateListener.Callback callback =
66                                 (EmergencyCallStateListener.Callback) args.arg2;
67                         startSequenceInternal(phone, callback);
68                     } finally {
69                         args.recycle();
70                     }
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     /**
91      * Starts the "wait for radio" sequence. This is the (single) external API of the
92      * EmergencyCallStateListener class.
93      *
94      * This method kicks off the following sequence:
95      * - Listen for the service state change event telling us the radio has come up.
96      * - Retry if we've gone {@link #TIME_BETWEEN_RETRIES_MILLIS} without any response from the
97      *   radio.
98      * - Finally, clean up any leftover state.
99      *
100      * This method is safe to call from any thread, since it simply posts a message to the
101      * EmergencyCallStateListener's handler (thus ensuring that the rest of the sequence is entirely
102      * serialized, and runs only on the handler thread.)
103      */
waitForRadioOn(Phone phone, Callback callback)104     public void waitForRadioOn(Phone phone, Callback callback) {
105         Log.d(this, "waitForRadioOn: Phone " + phone.getPhoneId());
106 
107         if (mPhone != null) {
108             // If there already is an ongoing request, ignore the new one!
109             return;
110         }
111 
112         SomeArgs args = SomeArgs.obtain();
113         args.arg1 = phone;
114         args.arg2 = callback;
115         mHandler.obtainMessage(MSG_START_SEQUENCE, args).sendToTarget();
116     }
117 
118     /**
119      * Actual implementation of waitForRadioOn(), guaranteed to run on the handler thread.
120      *
121      * @see #waitForRadioOn
122      */
startSequenceInternal(Phone phone, Callback callback)123     private void startSequenceInternal(Phone phone, Callback callback) {
124         Log.d(this, "startSequenceInternal: Phone " + phone.getPhoneId());
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         registerForServiceStateChanged();
135         // Next step: when the SERVICE_STATE_CHANGED event comes in, we'll retry the call; see
136         // onServiceStateChanged(). But also, just in case, start a timer to make sure we'll retry
137         // the call even if the SERVICE_STATE_CHANGED event never comes in for some reason.
138         startRetryTimer();
139     }
140 
141     /**
142      * Handles the SERVICE_STATE_CHANGED event. Normally this event tells us that the radio has
143      * finally come up. In that case, it's now safe to actually place the emergency call.
144      */
onServiceStateChanged(ServiceState state)145     private void onServiceStateChanged(ServiceState state) {
146         Log.d(this, "onServiceStateChanged(), new state = %s, Phone = %s", state,
147                 mPhone.getPhoneId());
148 
149         // Possible service states:
150         // - STATE_IN_SERVICE        // Normal operation
151         // - STATE_OUT_OF_SERVICE    // Still searching for an operator to register to,
152         //                           // or no radio signal
153         // - STATE_EMERGENCY_ONLY    // Phone is locked; only emergency numbers are allowed
154         // - STATE_POWER_OFF         // Radio is explicitly powered off (airplane mode)
155 
156         if (isOkToCall(state.getState())) {
157             // Woo hoo!  It's OK to actually place the call.
158             Log.d(this, "onServiceStateChanged: ok to call!");
159 
160             onComplete(true);
161             cleanup();
162         } else {
163             // The service state changed, but we're still not ready to call yet.
164             Log.d(this, "onServiceStateChanged: not ready to call yet, keep waiting.");
165         }
166     }
167 
168     /**
169      * We currently only look to make sure that the radio is on before dialing. We should be able to
170      * make emergency calls at any time after the radio has been powered on and isn't in the
171      * UNAVAILABLE state, even if it is reporting the OUT_OF_SERVICE state.
172      */
isOkToCall(int serviceState)173     private boolean isOkToCall(int serviceState) {
174         return (mPhone.getState() == PhoneConstants.State.OFFHOOK) ||
175                 mPhone.getServiceStateTracker().isRadioOn();
176     }
177 
178     /**
179      * Handles the retry timer expiring.
180      */
onRetryTimeout()181     private void onRetryTimeout() {
182         int serviceState = mPhone.getServiceState().getState();
183         Log.d(this, "onRetryTimeout():  phone state = %s, service state = %d, retries = %d.",
184                 mPhone.getState(), serviceState, mNumRetriesSoFar);
185 
186         // - If we're actually in a call, we've succeeded.
187         // - Otherwise, if the radio is now on, that means we successfully got out of airplane mode
188         //   but somehow didn't get the service state change event.  In that case, try to place the
189         //   call.
190         // - If the radio is still powered off, try powering it on again.
191 
192         if (isOkToCall(serviceState)) {
193             Log.d(this, "onRetryTimeout: Radio is on. Cleaning up.");
194 
195             // Woo hoo -- we successfully got out of airplane mode.
196             onComplete(true);
197             cleanup();
198         } else {
199             // Uh oh; we've waited the full TIME_BETWEEN_RETRIES_MILLIS and the radio is still not
200             // powered-on.  Try again.
201 
202             mNumRetriesSoFar++;
203             Log.d(this, "mNumRetriesSoFar is now " + mNumRetriesSoFar);
204 
205             if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
206                 Log.w(this, "Hit MAX_NUM_RETRIES; giving up.");
207                 cleanup();
208             } else {
209                 Log.d(this, "Trying (again) to turn on the radio.");
210                 mPhone.setRadioPower(true);
211                 startRetryTimer();
212             }
213         }
214     }
215 
216     /**
217      * Clean up when done with the whole sequence: either after successfully turning on the radio,
218      * or after bailing out because of too many failures.
219      *
220      * The exact cleanup steps are:
221      * - Notify callback if we still hadn't sent it a response.
222      * - Double-check that we're not still registered for any telephony events
223      * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
224      *
225      * Basically this method guarantees that there will be no more activity from the
226      * EmergencyCallStateListener until someone kicks off the whole sequence again with another call
227      * to {@link #waitForRadioOn}
228      *
229      * TODO: Do the work for the comment below:
230      * Note we don't call this method simply after a successful call to placeCall(), since it's
231      * still possible the call will disconnect very quickly with an OUT_OF_SERVICE error.
232      */
cleanup()233     private void cleanup() {
234         Log.d(this, "cleanup()");
235 
236         // This will send a failure call back if callback has yet to be invoked.  If the callback
237         // was already invoked, it's a no-op.
238         onComplete(false);
239 
240         unregisterForServiceStateChanged();
241         cancelRetryTimer();
242 
243         // Used for unregisterForServiceStateChanged() so we null it out here instead.
244         mPhone = null;
245         mNumRetriesSoFar = 0;
246     }
247 
startRetryTimer()248     private void startRetryTimer() {
249         cancelRetryTimer();
250         mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
251     }
252 
cancelRetryTimer()253     private void cancelRetryTimer() {
254         mHandler.removeMessages(MSG_RETRY_TIMEOUT);
255     }
256 
registerForServiceStateChanged()257     private void registerForServiceStateChanged() {
258         // Unregister first, just to make sure we never register ourselves twice.  (We need this
259         // because Phone.registerForServiceStateChanged() does not prevent multiple registration of
260         // the same handler.)
261         unregisterForServiceStateChanged();
262         mPhone.registerForServiceStateChanged(mHandler, MSG_SERVICE_STATE_CHANGED, null);
263     }
264 
unregisterForServiceStateChanged()265     private void unregisterForServiceStateChanged() {
266         // This method is safe to call even if we haven't set mPhone yet.
267         if (mPhone != null) {
268             mPhone.unregisterForServiceStateChanged(mHandler);  // Safe even if unnecessary
269         }
270         mHandler.removeMessages(MSG_SERVICE_STATE_CHANGED);  // Clean up any pending messages too
271     }
272 
onComplete(boolean isRadioReady)273     private void onComplete(boolean isRadioReady) {
274         if (mCallback != null) {
275             Callback tempCallback = mCallback;
276             mCallback = null;
277             tempCallback.onComplete(this, isRadioReady);
278         }
279     }
280 
281     @VisibleForTesting
getHandler()282     public Handler getHandler() {
283         return mHandler;
284     }
285 
286     @VisibleForTesting
setMaxNumRetries(int retries)287     public void setMaxNumRetries(int retries) {
288         MAX_NUM_RETRIES = retries;
289     }
290 
291     @VisibleForTesting
setTimeBetweenRetriesMillis(long timeMs)292     public void setTimeBetweenRetriesMillis(long timeMs) {
293         TIME_BETWEEN_RETRIES_MILLIS = timeMs;
294     }
295 
296     @Override
equals(Object o)297     public boolean equals(Object o) {
298         if (this == o) return true;
299         if (o == null || !getClass().equals(o.getClass())) return false;
300 
301         EmergencyCallStateListener that = (EmergencyCallStateListener) o;
302 
303         if (mNumRetriesSoFar != that.mNumRetriesSoFar) {
304             return false;
305         }
306         if (mCallback != null ? !mCallback.equals(that.mCallback) : that.mCallback != null) {
307             return false;
308         }
309         return mPhone != null ? mPhone.equals(that.mPhone) : that.mPhone == null;
310 
311     }
312 }
313