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