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 17 package com.android.services.telephony; 18 19 import android.content.Context; 20 21 import android.content.Intent; 22 import android.os.AsyncResult; 23 import android.os.Handler; 24 import android.os.Message; 25 import android.os.UserHandle; 26 import android.provider.Settings; 27 import android.telephony.ServiceState; 28 29 import com.android.internal.os.SomeArgs; 30 import com.android.internal.telephony.Phone; 31 import 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 */ 39 public class EmergencyCallHelper { 40 41 /** 42 * Receives the result of the EmergencyCallHelper's attempt to turn on the radio. 43 */ 44 interface Callback { onComplete(boolean isRadioReady)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 EmergencyCallHelper(Context context)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 */ startTurnOnRadioSequence(Phone phone, Callback callback)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 */ startSequenceInternal(Phone phone, Callback callback)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 */ onServiceStateChanged(ServiceState state)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 isOkToCall(int serviceState, PhoneConstants.State phoneState)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 */ onRetryTimeout()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 */ powerOnRadio()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 */ cleanup()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 startRetryTimer()302 private void startRetryTimer() { 303 cancelRetryTimer(); 304 mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS); 305 } 306 cancelRetryTimer()307 private void cancelRetryTimer() { 308 mHandler.removeMessages(MSG_RETRY_TIMEOUT); 309 } 310 registerForServiceStateChanged()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 unregisterForServiceStateChanged()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 onComplete(boolean isRadioReady)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