/src/com/android/phone/EmergencyCallHelper.java

https://bitbucket.org/cyanogenmod/android_packages_apps_phone · Java · 543 lines · 230 code · 75 blank · 238 comment · 52 complexity · 7142b3718aaa9da5e24e2eca8e03d39e MD5 · raw file

  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. package com.android.phone;
  17. import com.android.internal.telephony.CallManager;
  18. import com.android.internal.telephony.Connection;
  19. import com.android.internal.telephony.Phone;
  20. import com.android.phone.Constants.CallStatusCode;
  21. import com.android.phone.InCallUiState.ProgressIndicationType;
  22. import android.content.Context;
  23. import android.content.Intent;
  24. import android.os.AsyncResult;
  25. import android.os.Handler;
  26. import android.os.Message;
  27. import android.os.PowerManager;
  28. import android.provider.Settings;
  29. import android.telephony.ServiceState;
  30. import android.util.Log;
  31. /**
  32. * Helper class for the {@link CallController} that implements special
  33. * behavior related to emergency calls. Specifically, this class handles
  34. * the case of the user trying to dial an emergency number while the radio
  35. * is off (i.e. the device is in airplane mode), by forcibly turning the
  36. * radio back on, waiting for it to come up, and then retrying the
  37. * emergency call.
  38. *
  39. * This class is instantiated lazily (the first time the user attempts to
  40. * make an emergency call from airplane mode) by the the
  41. * {@link CallController} singleton.
  42. */
  43. public class EmergencyCallHelper extends Handler {
  44. private static final String TAG = "EmergencyCallHelper";
  45. private static final boolean DBG = false;
  46. // Number of times to retry the call, and time between retry attempts.
  47. public static final int MAX_NUM_RETRIES = 6;
  48. public static final long TIME_BETWEEN_RETRIES = 5000; // msec
  49. // Timeout used with our wake lock (just as a safety valve to make
  50. // sure we don't hold it forever).
  51. public static final long WAKE_LOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in msec
  52. // Handler message codes; see handleMessage()
  53. private static final int START_SEQUENCE = 1;
  54. private static final int SERVICE_STATE_CHANGED = 2;
  55. private static final int DISCONNECT = 3;
  56. private static final int RETRY_TIMEOUT = 4;
  57. private CallController mCallController;
  58. private PhoneApp mApp;
  59. private CallManager mCM;
  60. private Phone mPhone;
  61. private String mNumber; // The emergency number we're trying to dial
  62. private int mNumRetriesSoFar;
  63. // Wake lock we hold while running the whole sequence
  64. private PowerManager.WakeLock mPartialWakeLock;
  65. public EmergencyCallHelper(CallController callController) {
  66. if (DBG) log("EmergencyCallHelper constructor...");
  67. mCallController = callController;
  68. mApp = PhoneApp.getInstance();
  69. mCM = mApp.mCM;
  70. }
  71. @Override
  72. public void handleMessage(Message msg) {
  73. switch (msg.what) {
  74. case START_SEQUENCE:
  75. startSequenceInternal(msg);
  76. break;
  77. case SERVICE_STATE_CHANGED:
  78. onServiceStateChanged(msg);
  79. break;
  80. case DISCONNECT:
  81. onDisconnect(msg);
  82. break;
  83. case RETRY_TIMEOUT:
  84. onRetryTimeout();
  85. break;
  86. default:
  87. Log.wtf(TAG, "handleMessage: unexpected message: " + msg);
  88. break;
  89. }
  90. }
  91. /**
  92. * Starts the "emergency call from airplane mode" sequence.
  93. *
  94. * This is the (single) external API of the EmergencyCallHelper class.
  95. * This method is called from the CallController placeCall() sequence
  96. * if the user dials a valid emergency number, but the radio is
  97. * powered-off (presumably due to airplane mode.)
  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. * - Then launch the emergency call
  103. * - Retry if the call fails with an OUT_OF_SERVICE error
  104. * - Retry if we've gone 5 seconds without any response from the radio
  105. * - Finally, clean up any leftover state (progress UI, wake locks, etc.)
  106. *
  107. * This method is safe to call from any thread, since it simply posts
  108. * a message to the EmergencyCallHelper's handler (thus ensuring that
  109. * the rest of the sequence is entirely serialized, and runs only on
  110. * the handler thread.)
  111. *
  112. * This method does *not* force the in-call UI to come up; our caller
  113. * is responsible for doing that (presumably by calling
  114. * PhoneApp.displayCallScreen().)
  115. */
  116. public void startEmergencyCallFromAirplaneModeSequence(String number) {
  117. if (DBG) log("startEmergencyCallFromAirplaneModeSequence('" + number + "')...");
  118. Message msg = obtainMessage(START_SEQUENCE, number);
  119. sendMessage(msg);
  120. }
  121. /**
  122. * Actual implementation of startEmergencyCallFromAirplaneModeSequence(),
  123. * guaranteed to run on the handler thread.
  124. * @see startEmergencyCallFromAirplaneModeSequence()
  125. */
  126. private void startSequenceInternal(Message msg) {
  127. if (DBG) log("startSequenceInternal(): msg = " + msg);
  128. // First of all, clean up any state (including mPartialWakeLock!)
  129. // left over from a prior emergency call sequence.
  130. // This ensures that we'll behave sanely if another
  131. // startEmergencyCallFromAirplaneModeSequence() comes in while
  132. // we're already in the middle of the sequence.
  133. cleanup();
  134. mNumber = (String) msg.obj;
  135. if (DBG) log("- startSequenceInternal: Got mNumber: '" + mNumber + "'");
  136. mNumRetriesSoFar = 0;
  137. // Reset mPhone to whatever the current default phone is right now.
  138. mPhone = mApp.mCM.getDefaultPhone();
  139. // Wake lock to make sure the processor doesn't go to sleep midway
  140. // through the emergency call sequence.
  141. PowerManager pm = (PowerManager) mApp.getSystemService(Context.POWER_SERVICE);
  142. mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
  143. // Acquire with a timeout, just to be sure we won't hold the wake
  144. // lock forever even if a logic bug (in this class) causes us to
  145. // somehow never call cleanup().
  146. if (DBG) log("- startSequenceInternal: acquiring wake lock");
  147. mPartialWakeLock.acquire(WAKE_LOCK_TIMEOUT);
  148. // No need to check the current service state here, since the only
  149. // reason the CallController would call this method in the first
  150. // place is if the radio is powered-off.
  151. //
  152. // So just go ahead and turn the radio on.
  153. powerOnRadio(); // We'll get an onServiceStateChanged() callback
  154. // when the radio successfully comes up.
  155. // Next step: when the SERVICE_STATE_CHANGED event comes in,
  156. // we'll retry the call; see placeEmergencyCall();
  157. // But also, just in case, start a timer to make sure we'll retry
  158. // the call even if the SERVICE_STATE_CHANGED event never comes in
  159. // for some reason.
  160. startRetryTimer();
  161. // And finally, let the in-call UI know that we need to
  162. // display the "Turning on radio..." progress indication.
  163. mApp.inCallUiState.setProgressIndication(ProgressIndicationType.TURNING_ON_RADIO);
  164. // (Our caller is responsible for calling mApp.displayCallScreen().)
  165. }
  166. /**
  167. * Handles the SERVICE_STATE_CHANGED event.
  168. *
  169. * (Normally this event tells us that the radio has finally come
  170. * up. In that case, it's now safe to actually place the
  171. * emergency call.)
  172. */
  173. private void onServiceStateChanged(Message msg) {
  174. ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
  175. if (DBG) log("onServiceStateChanged()... new state = " + state);
  176. // Possible service states:
  177. // - STATE_IN_SERVICE // Normal operation
  178. // - STATE_OUT_OF_SERVICE // Still searching for an operator to register to,
  179. // // or no radio signal
  180. // - STATE_EMERGENCY_ONLY // Phone is locked; only emergency numbers are allowed
  181. // - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode)
  182. // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY,
  183. // it's finally OK to place the emergency call.
  184. boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE)
  185. || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY);
  186. if (okToCall) {
  187. // Woo hoo! It's OK to actually place the call.
  188. if (DBG) log("onServiceStateChanged: ok to call!");
  189. // Deregister for the service state change events.
  190. unregisterForServiceStateChanged();
  191. // Take down the "Turning on radio..." indication.
  192. mApp.inCallUiState.clearProgressIndication();
  193. placeEmergencyCall();
  194. // The in-call UI is probably still up at this point,
  195. // but make sure of that:
  196. mApp.displayCallScreen();
  197. } else {
  198. // The service state changed, but we're still not ready to call yet.
  199. // (This probably was the transition from STATE_POWER_OFF to
  200. // STATE_OUT_OF_SERVICE, which happens immediately after powering-on
  201. // the radio.)
  202. //
  203. // So just keep waiting; we'll probably get to either
  204. // STATE_IN_SERVICE or STATE_EMERGENCY_ONLY very shortly.
  205. // (Or even if that doesn't happen, we'll at least do another retry
  206. // when the RETRY_TIMEOUT event fires.)
  207. if (DBG) log("onServiceStateChanged: not ready to call yet, keep waiting...");
  208. }
  209. }
  210. /**
  211. * Handles a DISCONNECT event from the telephony layer.
  212. *
  213. * Even after we successfully place an emergency call (after powering
  214. * on the radio), it's still possible for the call to fail with the
  215. * disconnect cause OUT_OF_SERVICE. If so, schedule a retry.
  216. */
  217. private void onDisconnect(Message msg) {
  218. Connection conn = (Connection) ((AsyncResult) msg.obj).result;
  219. Connection.DisconnectCause cause = conn.getDisconnectCause();
  220. if (DBG) log("onDisconnect: connection '" + conn
  221. + "', addr '" + conn.getAddress() + "', cause = " + cause);
  222. if (cause == Connection.DisconnectCause.OUT_OF_SERVICE) {
  223. // Wait a bit more and try again (or just bail out totally if
  224. // we've had too many failures.)
  225. if (DBG) log("- onDisconnect: OUT_OF_SERVICE, need to retry...");
  226. scheduleRetryOrBailOut();
  227. } else {
  228. // Any other disconnect cause means we're done.
  229. // Either the emergency call succeeded *and* ended normally,
  230. // or else there was some error that we can't retry. In either
  231. // case, just clean up our internal state.)
  232. if (DBG) log("==> Disconnect event; clean up...");
  233. cleanup();
  234. // Nothing else to do here. If the InCallScreen was visible,
  235. // it would have received this disconnect event too (so it'll
  236. // show the "Call ended" state and finish itself without any
  237. // help from us.)
  238. }
  239. }
  240. /**
  241. * Handles the retry timer expiring.
  242. */
  243. private void onRetryTimeout() {
  244. Phone.State phoneState = mCM.getState();
  245. int serviceState = mPhone.getServiceState().getState();
  246. if (DBG) log("onRetryTimeout(): phone state " + phoneState
  247. + ", service state " + serviceState
  248. + ", mNumRetriesSoFar = " + mNumRetriesSoFar);
  249. // - If we're actually in a call, we've succeeded.
  250. //
  251. // - Otherwise, if the radio is now on, that means we successfully got
  252. // out of airplane mode but somehow didn't get the service state
  253. // change event. In that case, try to place the call.
  254. //
  255. // - If the radio is still powered off, try powering it on again.
  256. if (phoneState == Phone.State.OFFHOOK) {
  257. if (DBG) log("- onRetryTimeout: Call is active! Cleaning up...");
  258. cleanup();
  259. return;
  260. }
  261. if (serviceState != ServiceState.STATE_POWER_OFF) {
  262. // Woo hoo -- we successfully got out of airplane mode.
  263. // Deregister for the service state change events; we don't need
  264. // these any more now that the radio is powered-on.
  265. unregisterForServiceStateChanged();
  266. // Take down the "Turning on radio..." indication.
  267. mApp.inCallUiState.clearProgressIndication();
  268. placeEmergencyCall(); // If the call fails, placeEmergencyCall()
  269. // will schedule a retry.
  270. } else {
  271. // Uh oh; we've waited the full TIME_BETWEEN_RETRIES and the
  272. // radio is still not powered-on. Try again...
  273. if (DBG) log("- Trying (again) to turn on the radio...");
  274. powerOnRadio(); // Again, we'll (hopefully) get an onServiceStateChanged()
  275. // callback when the radio successfully comes up.
  276. // ...and also set a fresh retry timer (or just bail out
  277. // totally if we've had too many failures.)
  278. scheduleRetryOrBailOut();
  279. }
  280. // Finally, the in-call UI is probably still up at this point,
  281. // but make sure of that:
  282. mApp.displayCallScreen();
  283. }
  284. /**
  285. * Attempt to power on the radio (i.e. take the device out
  286. * of airplane mode.)
  287. *
  288. * Additionally, start listening for service state changes;
  289. * we'll eventually get an onServiceStateChanged() callback
  290. * when the radio successfully comes up.
  291. */
  292. private void powerOnRadio() {
  293. if (DBG) log("- powerOnRadio()...");
  294. // We're about to turn on the radio, so arrange to be notified
  295. // when the sequence is complete.
  296. registerForServiceStateChanged();
  297. // If airplane mode is on, we turn it off the same way that the
  298. // Settings activity turns it off.
  299. if (Settings.System.getInt(mApp.getContentResolver(),
  300. Settings.System.AIRPLANE_MODE_ON, 0) > 0) {
  301. if (DBG) log("==> Turning off airplane mode...");
  302. // Change the system setting
  303. Settings.System.putInt(mApp.getContentResolver(),
  304. Settings.System.AIRPLANE_MODE_ON, 0);
  305. // Post the intent
  306. Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
  307. intent.putExtra("state", false);
  308. mApp.sendBroadcast(intent);
  309. } else {
  310. // Otherwise, for some strange reason the radio is off
  311. // (even though the Settings database doesn't think we're
  312. // in airplane mode.) In this case just turn the radio
  313. // back on.
  314. if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on...");
  315. mPhone.setRadioPower(true);
  316. }
  317. }
  318. /**
  319. * Actually initiate the outgoing emergency call.
  320. * (We do this once the radio has successfully been powered-up.)
  321. *
  322. * If the call succeeds, we're done.
  323. * If the call fails, schedule a retry of the whole sequence.
  324. */
  325. private void placeEmergencyCall() {
  326. if (DBG) log("placeEmergencyCall()...");
  327. // Place an outgoing call to mNumber.
  328. // Note we call PhoneUtils.placeCall() directly; we don't want any
  329. // of the behavior from CallController.placeCallInternal() here.
  330. // (Specifically, we don't want to start the "emergency call from
  331. // airplane mode" sequence from the beginning again!)
  332. registerForDisconnect(); // Get notified when this call disconnects
  333. if (DBG) log("- placing call to '" + mNumber + "'...");
  334. int callStatus = PhoneUtils.placeCall(mApp,
  335. mPhone,
  336. mNumber,
  337. null, // contactUri
  338. true, // isEmergencyCall
  339. null); // gatewayUri
  340. if (DBG) log("- PhoneUtils.placeCall() returned status = " + callStatus);
  341. boolean success;
  342. // Note PhoneUtils.placeCall() returns one of the CALL_STATUS_*
  343. // constants, not a CallStatusCode enum value.
  344. switch (callStatus) {
  345. case PhoneUtils.CALL_STATUS_DIALED:
  346. success = true;
  347. break;
  348. case PhoneUtils.CALL_STATUS_DIALED_MMI:
  349. case PhoneUtils.CALL_STATUS_FAILED:
  350. default:
  351. // Anything else is a failure, and we'll need to retry.
  352. Log.w(TAG, "placeEmergencyCall(): placeCall() failed: callStatus = " + callStatus);
  353. success = false;
  354. break;
  355. }
  356. if (success) {
  357. if (DBG) log("==> Success from PhoneUtils.placeCall()!");
  358. // Ok, the emergency call is (hopefully) under way.
  359. // We're not done yet, though, so don't call cleanup() here.
  360. // (It's still possible that this call will fail, and disconnect
  361. // with cause==OUT_OF_SERVICE. If so, that will trigger a retry
  362. // from the onDisconnect() method.)
  363. } else {
  364. if (DBG) log("==> Failure.");
  365. // Wait a bit more and try again (or just bail out totally if
  366. // we've had too many failures.)
  367. scheduleRetryOrBailOut();
  368. }
  369. }
  370. /**
  371. * Schedules a retry in response to some failure (either the radio
  372. * failing to power on, or a failure when trying to place the call.)
  373. * Or, if we've hit the retry limit, bail out of this whole sequence
  374. * and display a failure message to the user.
  375. */
  376. private void scheduleRetryOrBailOut() {
  377. mNumRetriesSoFar++;
  378. if (DBG) log("scheduleRetryOrBailOut()... mNumRetriesSoFar is now " + mNumRetriesSoFar);
  379. if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
  380. Log.w(TAG, "scheduleRetryOrBailOut: hit MAX_NUM_RETRIES; giving up...");
  381. cleanup();
  382. // ...and have the InCallScreen display a generic failure
  383. // message.
  384. mApp.inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED);
  385. } else {
  386. if (DBG) log("- Scheduling another retry...");
  387. startRetryTimer();
  388. mApp.inCallUiState.setProgressIndication(ProgressIndicationType.RETRYING);
  389. }
  390. }
  391. /**
  392. * Clean up when done with the whole sequence: either after
  393. * successfully placing *and* ending the emergency call, or after
  394. * bailing out because of too many failures.
  395. *
  396. * The exact cleanup steps are:
  397. * - Take down any progress UI (and also ask the in-call UI to refresh itself,
  398. * if it's still visible)
  399. * - Double-check that we're not still registered for any telephony events
  400. * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
  401. * - Make sure we're not still holding any wake locks
  402. *
  403. * Basically this method guarantees that there will be no more
  404. * activity from the EmergencyCallHelper until the CallController
  405. * kicks off the whole sequence again with another call to
  406. * startEmergencyCallFromAirplaneModeSequence().
  407. *
  408. * Note we don't call this method simply after a successful call to
  409. * placeCall(), since it's still possible the call will disconnect
  410. * very quickly with an OUT_OF_SERVICE error.
  411. */
  412. private void cleanup() {
  413. if (DBG) log("cleanup()...");
  414. // Take down the "Turning on radio..." indication.
  415. mApp.inCallUiState.clearProgressIndication();
  416. unregisterForServiceStateChanged();
  417. unregisterForDisconnect();
  418. cancelRetryTimer();
  419. // Release / clean up the wake lock
  420. if (mPartialWakeLock != null) {
  421. if (mPartialWakeLock.isHeld()) {
  422. if (DBG) log("- releasing wake lock");
  423. mPartialWakeLock.release();
  424. }
  425. mPartialWakeLock = null;
  426. }
  427. // And finally, ask the in-call UI to refresh itself (to clean up the
  428. // progress indication if necessary), if it's currently visible.
  429. mApp.updateInCallScreen();
  430. }
  431. private void startRetryTimer() {
  432. removeMessages(RETRY_TIMEOUT);
  433. sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES);
  434. }
  435. private void cancelRetryTimer() {
  436. removeMessages(RETRY_TIMEOUT);
  437. }
  438. private void registerForServiceStateChanged() {
  439. // Unregister first, just to make sure we never register ourselves
  440. // twice. (We need this because Phone.registerForServiceStateChanged()
  441. // does not prevent multiple registration of the same handler.)
  442. mPhone.unregisterForServiceStateChanged(this); // Safe even if not currently registered
  443. mPhone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
  444. }
  445. private void unregisterForServiceStateChanged() {
  446. // This method is safe to call even if we haven't set mPhone yet.
  447. if (mPhone != null) {
  448. mPhone.unregisterForServiceStateChanged(this); // Safe even if unnecessary
  449. }
  450. removeMessages(SERVICE_STATE_CHANGED); // Clean up any pending messages too
  451. }
  452. private void registerForDisconnect() {
  453. // Note: no need to unregister first, since
  454. // CallManager.registerForDisconnect() automatically prevents
  455. // multiple registration of the same handler.
  456. mCM.registerForDisconnect(this, DISCONNECT, null);
  457. }
  458. private void unregisterForDisconnect() {
  459. mCM.unregisterForDisconnect(this); // Safe even if not currently registered
  460. removeMessages(DISCONNECT); // Clean up any pending messages too
  461. }
  462. //
  463. // Debugging
  464. //
  465. private static void log(String msg) {
  466. Log.d(TAG, msg);
  467. }
  468. }