Blanket copy of PhoneApp to services/Telephony.
First phase of splitting out InCallUI from PhoneApp.
Change-Id: I237341c4ff00e96c677caa4580b251ef3432931b
diff --git a/src/com/android/phone/InCallScreen.java b/src/com/android/phone/InCallScreen.java
new file mode 100644
index 0000000..d5db689
--- /dev/null
+++ b/src/com/android/phone/InCallScreen.java
@@ -0,0 +1,4612 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothHeadsetPhone;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.media.AudioManager;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.text.method.DialerKeyListener;
+import android.util.EventLog;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.phone.Constants.CallStatusCode;
+import com.android.phone.InCallUiState.InCallScreenMode;
+import com.android.phone.OtaUtils.CdmaOtaScreenState;
+
+import java.util.List;
+
+
+/**
+ * Phone app "in call" screen.
+ */
+public class InCallScreen extends Activity
+ implements View.OnClickListener {
+ private static final String LOG_TAG = "InCallScreen";
+
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ /**
+ * Intent extra used to specify whether the DTMF dialpad should be
+ * initially visible when bringing up the InCallScreen. (If this
+ * extra is present, the dialpad will be initially shown if the extra
+ * has the boolean value true, and initially hidden otherwise.)
+ */
+ // TODO: Should be EXTRA_SHOW_DIALPAD for consistency.
+ static final String SHOW_DIALPAD_EXTRA = "com.android.phone.ShowDialpad";
+
+ /**
+ * Intent extra to specify the package name of the gateway
+ * provider. Used to get the name displayed in the in-call screen
+ * during the call setup. The value is a string.
+ */
+ // TODO: This extra is currently set by the gateway application as
+ // a temporary measure. Ultimately, the framework will securely
+ // set it.
+ /* package */ static final String EXTRA_GATEWAY_PROVIDER_PACKAGE =
+ "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE";
+
+ /**
+ * Intent extra to specify the URI of the provider to place the
+ * call. The value is a string. It holds the gateway address
+ * (phone gateway URL should start with the 'tel:' scheme) that
+ * will actually be contacted to call the number passed in the
+ * intent URL or in the EXTRA_PHONE_NUMBER extra.
+ */
+ // TODO: Should the value be a Uri (Parcelable)? Need to make sure
+ // MMI code '#' don't get confused as URI fragments.
+ /* package */ static final String EXTRA_GATEWAY_URI =
+ "com.android.phone.extra.GATEWAY_URI";
+
+ // Amount of time (in msec) that we display the "Call ended" state.
+ // The "short" value is for calls ended by the local user, and the
+ // "long" value is for calls ended by the remote caller.
+ private static final int CALL_ENDED_SHORT_DELAY = 200; // msec
+ private static final int CALL_ENDED_LONG_DELAY = 2000; // msec
+ private static final int CALL_ENDED_EXTRA_LONG_DELAY = 5000; // msec
+
+ // Amount of time that we display the PAUSE alert Dialog showing the
+ // post dial string yet to be send out to the n/w
+ private static final int PAUSE_PROMPT_DIALOG_TIMEOUT = 2000; //msec
+
+ // Amount of time that we display the provider info if applicable.
+ private static final int PROVIDER_INFO_TIMEOUT = 5000; // msec
+
+ // These are values for the settings of the auto retry mode:
+ // 0 = disabled
+ // 1 = enabled
+ // TODO (Moto):These constants don't really belong here,
+ // they should be moved to Settings where the value is being looked up in the first place
+ static final int AUTO_RETRY_OFF = 0;
+ static final int AUTO_RETRY_ON = 1;
+
+ // Message codes; see mHandler below.
+ // Note message codes < 100 are reserved for the PhoneApp.
+ private static final int PHONE_STATE_CHANGED = 101;
+ private static final int PHONE_DISCONNECT = 102;
+ private static final int EVENT_HEADSET_PLUG_STATE_CHANGED = 103;
+ private static final int POST_ON_DIAL_CHARS = 104;
+ private static final int WILD_PROMPT_CHAR_ENTERED = 105;
+ private static final int ADD_VOICEMAIL_NUMBER = 106;
+ private static final int DONT_ADD_VOICEMAIL_NUMBER = 107;
+ private static final int DELAYED_CLEANUP_AFTER_DISCONNECT = 108;
+ private static final int SUPP_SERVICE_FAILED = 110;
+ private static final int REQUEST_UPDATE_BLUETOOTH_INDICATION = 114;
+ private static final int PHONE_CDMA_CALL_WAITING = 115;
+ private static final int REQUEST_CLOSE_SPC_ERROR_NOTICE = 118;
+ private static final int REQUEST_CLOSE_OTA_FAILURE_NOTICE = 119;
+ private static final int EVENT_PAUSE_DIALOG_COMPLETE = 120;
+ private static final int EVENT_HIDE_PROVIDER_INFO = 121; // Time to remove the info.
+ private static final int REQUEST_UPDATE_SCREEN = 122;
+ private static final int PHONE_INCOMING_RING = 123;
+ private static final int PHONE_NEW_RINGING_CONNECTION = 124;
+
+ // When InCallScreenMode is UNDEFINED set the default action
+ // to ACTION_UNDEFINED so if we are resumed the activity will
+ // know its undefined. In particular checkIsOtaCall will return
+ // false.
+ public static final String ACTION_UNDEFINED = "com.android.phone.InCallScreen.UNDEFINED";
+
+ /** Status codes returned from syncWithPhoneState(). */
+ private enum SyncWithPhoneStateStatus {
+ /**
+ * Successfully updated our internal state based on the telephony state.
+ */
+ SUCCESS,
+
+ /**
+ * There was no phone state to sync with (i.e. the phone was
+ * completely idle). In most cases this means that the
+ * in-call UI shouldn't be visible in the first place, unless
+ * we need to remain in the foreground while displaying an
+ * error message.
+ */
+ PHONE_NOT_IN_USE
+ }
+
+ private boolean mRegisteredForPhoneStates;
+
+ private PhoneGlobals mApp;
+ private CallManager mCM;
+
+ // TODO: need to clean up all remaining uses of mPhone.
+ // (There may be more than one Phone instance on the device, so it's wrong
+ // to just keep a single mPhone field. Instead, any time we need a Phone
+ // reference we should get it dynamically from the CallManager, probably
+ // based on the current foreground Call.)
+ private Phone mPhone;
+
+ private BluetoothHeadset mBluetoothHeadset;
+ private BluetoothAdapter mBluetoothAdapter;
+ private boolean mBluetoothConnectionPending;
+ private long mBluetoothConnectionRequestTime;
+
+ /** Main in-call UI elements. */
+ private CallCard mCallCard;
+
+ // UI controls:
+ private InCallControlState mInCallControlState;
+ private InCallTouchUi mInCallTouchUi;
+ private RespondViaSmsManager mRespondViaSmsManager; // see internalRespondViaSms()
+ private ManageConferenceUtils mManageConferenceUtils;
+
+ // DTMF Dialer controller and its view:
+ private DTMFTwelveKeyDialer mDialer;
+
+ private EditText mWildPromptText;
+
+ // Various dialogs we bring up (see dismissAllDialogs()).
+ // TODO: convert these all to use the "managed dialogs" framework.
+ //
+ // The MMI started dialog can actually be one of 2 items:
+ // 1. An alert dialog if the MMI code is a normal MMI
+ // 2. A progress dialog if the user requested a USSD
+ private Dialog mMmiStartedDialog;
+ private AlertDialog mMissingVoicemailDialog;
+ private AlertDialog mGenericErrorDialog;
+ private AlertDialog mSuppServiceFailureDialog;
+ private AlertDialog mWaitPromptDialog;
+ private AlertDialog mWildPromptDialog;
+ private AlertDialog mCallLostDialog;
+ private AlertDialog mPausePromptDialog;
+ private AlertDialog mExitingECMDialog;
+ // NOTE: if you add a new dialog here, be sure to add it to dismissAllDialogs() also.
+
+ // ProgressDialog created by showProgressIndication()
+ private ProgressDialog mProgressDialog;
+
+ // TODO: If the Activity class ever provides an easy way to get the
+ // current "activity lifecycle" state, we can remove these flags.
+ private boolean mIsDestroyed = false;
+ private boolean mIsForegroundActivity = false;
+ private boolean mIsForegroundActivityForProximity = false;
+ private PowerManager mPowerManager;
+
+ // For use with Pause/Wait dialogs
+ private String mPostDialStrAfterPause;
+ private boolean mPauseInProgress = false;
+
+ // Info about the most-recently-disconnected Connection, which is used
+ // to determine what should happen when exiting the InCallScreen after a
+ // call. (This info is set by onDisconnect(), and used by
+ // delayedCleanupAfterDisconnect().)
+ private Connection.DisconnectCause mLastDisconnectCause;
+
+ /** In-call audio routing options; see switchInCallAudio(). */
+ public enum InCallAudioMode {
+ SPEAKER, // Speakerphone
+ BLUETOOTH, // Bluetooth headset (if available)
+ EARPIECE, // Handset earpiece (or wired headset, if connected)
+ }
+
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (mIsDestroyed) {
+ if (DBG) log("Handler: ignoring message " + msg + "; we're destroyed!");
+ return;
+ }
+ if (!mIsForegroundActivity) {
+ if (DBG) log("Handler: handling message " + msg + " while not in foreground");
+ // Continue anyway; some of the messages below *want* to
+ // be handled even if we're not the foreground activity
+ // (like DELAYED_CLEANUP_AFTER_DISCONNECT), and they all
+ // should at least be safe to handle if we're not in the
+ // foreground...
+ }
+
+ switch (msg.what) {
+ case SUPP_SERVICE_FAILED:
+ onSuppServiceFailed((AsyncResult) msg.obj);
+ break;
+
+ case PHONE_STATE_CHANGED:
+ onPhoneStateChanged((AsyncResult) msg.obj);
+ break;
+
+ case PHONE_DISCONNECT:
+ onDisconnect((AsyncResult) msg.obj);
+ break;
+
+ case EVENT_HEADSET_PLUG_STATE_CHANGED:
+ // Update the in-call UI, since some UI elements (such
+ // as the "Speaker" button) may change state depending on
+ // whether a headset is plugged in.
+ // TODO: A full updateScreen() is overkill here, since
+ // the value of PhoneApp.isHeadsetPlugged() only affects a
+ // single onscreen UI element. (But even a full updateScreen()
+ // is still pretty cheap, so let's keep this simple
+ // for now.)
+ updateScreen();
+
+ // Also, force the "audio mode" popup to refresh itself if
+ // it's visible, since one of its items is either "Wired
+ // headset" or "Handset earpiece" depending on whether the
+ // headset is plugged in or not.
+ mInCallTouchUi.refreshAudioModePopup(); // safe even if the popup's not active
+
+ break;
+
+ // TODO: sort out MMI code (probably we should remove this method entirely).
+ // See also MMI handling code in onResume()
+ // case PhoneApp.MMI_INITIATE:
+ // onMMIInitiate((AsyncResult) msg.obj);
+ // break;
+
+ case PhoneGlobals.MMI_CANCEL:
+ onMMICancel();
+ break;
+
+ // handle the mmi complete message.
+ // since the message display class has been replaced with
+ // a system dialog in PhoneUtils.displayMMIComplete(), we
+ // should finish the activity here to close the window.
+ case PhoneGlobals.MMI_COMPLETE:
+ onMMIComplete((MmiCode) ((AsyncResult) msg.obj).result);
+ break;
+
+ case POST_ON_DIAL_CHARS:
+ handlePostOnDialChars((AsyncResult) msg.obj, (char) msg.arg1);
+ break;
+
+ case ADD_VOICEMAIL_NUMBER:
+ addVoiceMailNumberPanel();
+ break;
+
+ case DONT_ADD_VOICEMAIL_NUMBER:
+ dontAddVoiceMailNumber();
+ break;
+
+ case DELAYED_CLEANUP_AFTER_DISCONNECT:
+ delayedCleanupAfterDisconnect();
+ break;
+
+ case REQUEST_UPDATE_BLUETOOTH_INDICATION:
+ if (VDBG) log("REQUEST_UPDATE_BLUETOOTH_INDICATION...");
+ // The bluetooth headset state changed, so some UI
+ // elements may need to update. (There's no need to
+ // look up the current state here, since any UI
+ // elements that care about the bluetooth state get it
+ // directly from PhoneApp.showBluetoothIndication().)
+ updateScreen();
+ break;
+
+ case PHONE_CDMA_CALL_WAITING:
+ if (DBG) log("Received PHONE_CDMA_CALL_WAITING event ...");
+ Connection cn = mCM.getFirstActiveRingingCall().getLatestConnection();
+
+ // Only proceed if we get a valid connection object
+ if (cn != null) {
+ // Finally update screen with Call waiting info and request
+ // screen to wake up
+ updateScreen();
+ mApp.updateWakeState();
+ }
+ break;
+
+ case REQUEST_CLOSE_SPC_ERROR_NOTICE:
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.onOtaCloseSpcNotice();
+ }
+ break;
+
+ case REQUEST_CLOSE_OTA_FAILURE_NOTICE:
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.onOtaCloseFailureNotice();
+ }
+ break;
+
+ case EVENT_PAUSE_DIALOG_COMPLETE:
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss(); // safe even if already dismissed
+ mPausePromptDialog = null;
+ }
+ break;
+
+ case EVENT_HIDE_PROVIDER_INFO:
+ mApp.inCallUiState.providerInfoVisible = false;
+ if (mCallCard != null) {
+ mCallCard.updateState(mCM);
+ }
+ break;
+ case REQUEST_UPDATE_SCREEN:
+ updateScreen();
+ break;
+
+ case PHONE_INCOMING_RING:
+ onIncomingRing();
+ break;
+
+ case PHONE_NEW_RINGING_CONNECTION:
+ onNewRingingConnection();
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
+ break;
+ }
+ }
+ };
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
+ // Listen for ACTION_HEADSET_PLUG broadcasts so that we
+ // can update the onscreen UI when the headset state changes.
+ // if (DBG) log("mReceiver: ACTION_HEADSET_PLUG");
+ // if (DBG) log("==> intent: " + intent);
+ // if (DBG) log(" state: " + intent.getIntExtra("state", 0));
+ // if (DBG) log(" name: " + intent.getStringExtra("name"));
+ // send the event and add the state as an argument.
+ Message message = Message.obtain(mHandler, EVENT_HEADSET_PLUG_STATE_CHANGED,
+ intent.getIntExtra("state", 0), 0);
+ mHandler.sendMessage(message);
+ }
+ }
+ };
+
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ Log.i(LOG_TAG, "onCreate()... this = " + this);
+ Profiler.callScreenOnCreate();
+ super.onCreate(icicle);
+
+ // Make sure this is a voice-capable device.
+ if (!PhoneGlobals.sVoiceCapable) {
+ // There should be no way to ever reach the InCallScreen on a
+ // non-voice-capable device, since this activity is not exported by
+ // our manifest, and we explicitly disable any other external APIs
+ // like the CALL intent and ITelephony.showCallScreen().
+ // So the fact that we got here indicates a phone app bug.
+ Log.wtf(LOG_TAG, "onCreate() reached on non-voice-capable device");
+ finish();
+ return;
+ }
+
+ mApp = PhoneGlobals.getInstance();
+ mApp.setInCallScreenInstance(this);
+
+ // set this flag so this activity will stay in front of the keyguard
+ int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ if (mApp.getPhoneState() == PhoneConstants.State.OFFHOOK) {
+ // While we are in call, the in-call screen should dismiss the keyguard.
+ // This allows the user to press Home to go directly home without going through
+ // an insecure lock screen.
+ // But we do not want to do this if there is no active call so we do not
+ // bypass the keyguard if the call is not answered or declined.
+ flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
+ }
+
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.flags |= flags;
+ if (!mApp.proximitySensorModeEnabled()) {
+ // If we don't have a proximity sensor, then the in-call screen explicitly
+ // controls user activity. This is to prevent spurious touches from waking
+ // the display.
+ lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
+ }
+ getWindow().setAttributes(lp);
+
+ setPhone(mApp.phone); // Sets mPhone
+
+ mCM = mApp.mCM;
+ log("- onCreate: phone state = " + mCM.getState());
+
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mBluetoothAdapter != null) {
+ mBluetoothAdapter.getProfileProxy(getApplicationContext(), mBluetoothProfileServiceListener,
+ BluetoothProfile.HEADSET);
+ }
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ // Inflate everything in incall_screen.xml and add it to the screen.
+ setContentView(R.layout.incall_screen);
+
+ // If in landscape, then one of the ViewStubs (instead of <include>) is used for the
+ // incall_touch_ui, because CDMA and GSM button layouts are noticeably different.
+ final ViewStub touchUiStub = (ViewStub) findViewById(
+ mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA
+ ? R.id.inCallTouchUiCdmaStub : R.id.inCallTouchUiStub);
+ if (touchUiStub != null) touchUiStub.inflate();
+
+ initInCallScreen();
+
+ registerForPhoneStates();
+
+ // No need to change wake state here; that happens in onResume() when we
+ // are actually displayed.
+
+ // Handle the Intent we were launched with, but only if this is the
+ // the very first time we're being launched (ie. NOT if we're being
+ // re-initialized after previously being shut down.)
+ // Once we're up and running, any future Intents we need
+ // to handle will come in via the onNewIntent() method.
+ if (icicle == null) {
+ if (DBG) log("onCreate(): this is our very first launch, checking intent...");
+ internalResolveIntent(getIntent());
+ }
+
+ Profiler.callScreenCreated();
+ if (DBG) log("onCreate(): exit");
+ }
+
+ private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+ new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ mBluetoothHeadset = null;
+ }
+ };
+
+ /**
+ * Sets the Phone object used internally by the InCallScreen.
+ *
+ * In normal operation this is called from onCreate(), and the
+ * passed-in Phone object comes from the PhoneApp.
+ * For testing, test classes can use this method to
+ * inject a test Phone instance.
+ */
+ /* package */ void setPhone(Phone phone) {
+ mPhone = phone;
+ }
+
+ @Override
+ protected void onResume() {
+ if (DBG) log("onResume()...");
+ super.onResume();
+
+ mIsForegroundActivity = true;
+ mIsForegroundActivityForProximity = true;
+
+ // The flag shouldn't be turned on when there are actual phone calls.
+ if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall()) {
+ mApp.inCallUiState.showAlreadyDisconnectedState = false;
+ }
+
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ if (VDBG) inCallUiState.dumpState();
+
+ updateExpandedViewState();
+
+ // ...and update the in-call notification too, since the status bar
+ // icon needs to be hidden while we're the foreground activity:
+ mApp.notificationMgr.updateInCallNotification();
+
+ // Listen for broadcast intents that might affect the onscreen UI.
+ registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
+
+ // Keep a "dialer session" active when we're in the foreground.
+ // (This is needed to play DTMF tones.)
+ mDialer.startDialerSession();
+
+ // Restore various other state from the InCallUiState object:
+
+ // Update the onscreen dialpad state to match the InCallUiState.
+ if (inCallUiState.showDialpad) {
+ openDialpadInternal(false); // no "opening" animation
+ } else {
+ closeDialpadInternal(false); // no "closing" animation
+ }
+
+ // Reset the dialpad context
+ // TODO: Dialpad digits should be set here as well (once they are saved)
+ mDialer.setDialpadContext(inCallUiState.dialpadContextText);
+
+ // If there's a "Respond via SMS" popup still around since the
+ // last time we were the foreground activity, make sure it's not
+ // still active!
+ // (The popup should *never* be visible initially when we first
+ // come to the foreground; it only ever comes up in response to
+ // the user selecting the "SMS" option from the incoming call
+ // widget.)
+ mRespondViaSmsManager.dismissPopup(); // safe even if already dismissed
+
+ // Display an error / diagnostic indication if necessary.
+ //
+ // When the InCallScreen comes to the foreground, we normally we
+ // display the in-call UI in whatever state is appropriate based on
+ // the state of the telephony framework (e.g. an outgoing call in
+ // DIALING state, an incoming call, etc.)
+ //
+ // But if the InCallUiState has a "pending call status code" set,
+ // that means we need to display some kind of status or error
+ // indication to the user instead of the regular in-call UI. (The
+ // most common example of this is when there's some kind of
+ // failure while initiating an outgoing call; see
+ // CallController.placeCall().)
+ //
+ boolean handledStartupError = false;
+ if (inCallUiState.hasPendingCallStatusCode()) {
+ if (DBG) log("- onResume: need to show status indication!");
+ showStatusIndication(inCallUiState.getPendingCallStatusCode());
+
+ // Set handledStartupError to ensure that we won't bail out below.
+ // (We need to stay here in the InCallScreen so that the user
+ // is able to see the error dialog!)
+ handledStartupError = true;
+ }
+
+ // Set the volume control handler while we are in the foreground.
+ final boolean bluetoothConnected = isBluetoothAudioConnected();
+
+ if (bluetoothConnected) {
+ setVolumeControlStream(AudioManager.STREAM_BLUETOOTH_SCO);
+ } else {
+ setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
+ }
+
+ takeKeyEvents(true);
+
+ // If an OTASP call is in progress, use the special OTASP-specific UI.
+ boolean inOtaCall = false;
+ if (TelephonyCapabilities.supportsOtasp(mPhone)) {
+ inOtaCall = checkOtaspStateOnResume();
+ }
+ if (!inOtaCall) {
+ // Always start off in NORMAL mode
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ }
+
+ // Before checking the state of the CallManager, clean up any
+ // connections in the DISCONNECTED state.
+ // (The DISCONNECTED state is used only to drive the "call ended"
+ // UI; it's totally useless when *entering* the InCallScreen.)
+ mCM.clearDisconnected();
+
+ // Update the onscreen UI to reflect the current telephony state.
+ SyncWithPhoneStateStatus status = syncWithPhoneState();
+
+ // Note there's no need to call updateScreen() here;
+ // syncWithPhoneState() already did that if necessary.
+
+ if (status != SyncWithPhoneStateStatus.SUCCESS) {
+ if (DBG) log("- onResume: syncWithPhoneState failed! status = " + status);
+ // Couldn't update the UI, presumably because the phone is totally
+ // idle.
+
+ // Even though the phone is idle, though, we do still need to
+ // stay here on the InCallScreen if we're displaying an
+ // error dialog (see "showStatusIndication()" above).
+
+ if (handledStartupError) {
+ // Stay here for now. We'll eventually leave the
+ // InCallScreen when the user presses the dialog's OK
+ // button (see bailOutAfterErrorDialog()), or when the
+ // progress indicator goes away.
+ Log.i(LOG_TAG, " ==> syncWithPhoneState failed, but staying here anyway.");
+ } else {
+ // The phone is idle, and we did NOT handle a
+ // startup error during this pass thru onResume.
+ //
+ // This basically means that we're being resumed because of
+ // some action *other* than a new intent. (For example,
+ // the user pressing POWER to wake up the device, causing
+ // the InCallScreen to come back to the foreground.)
+ //
+ // In this scenario we do NOT want to stay here on the
+ // InCallScreen: we're not showing any useful info to the
+ // user (like a dialog), and the in-call UI itself is
+ // useless if there's no active call. So bail out.
+
+ Log.i(LOG_TAG, " ==> syncWithPhoneState failed; bailing out!");
+ dismissAllDialogs();
+
+ // Force the InCallScreen to truly finish(), rather than just
+ // moving it to the back of the activity stack (which is what
+ // our finish() method usually does.)
+ // This is necessary to avoid an obscure scenario where the
+ // InCallScreen can get stuck in an inconsistent state, somehow
+ // causing a *subsequent* outgoing call to fail (bug 4172599).
+ endInCallScreenSession(true /* force a real finish() call */);
+ return;
+ }
+ } else if (TelephonyCapabilities.supportsOtasp(mPhone)) {
+ if (inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL ||
+ inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) {
+ if (mCallCard != null) mCallCard.setVisibility(View.GONE);
+ updateScreen();
+ return;
+ }
+ }
+
+ // InCallScreen is now active.
+ EventLog.writeEvent(EventLogTags.PHONE_UI_ENTER);
+
+ // Update the poke lock and wake lock when we move to the foreground.
+ // This will be no-op when prox sensor is effective.
+ mApp.updateWakeState();
+
+ // Restore the mute state if the last mute state change was NOT
+ // done by the user.
+ if (mApp.getRestoreMuteOnInCallResume()) {
+ // Mute state is based on the foreground call
+ PhoneUtils.restoreMuteState();
+ mApp.setRestoreMuteOnInCallResume(false);
+ }
+
+ Profiler.profileViewCreate(getWindow(), InCallScreen.class.getName());
+
+ // If there's a pending MMI code, we'll show a dialog here.
+ //
+ // Note: previously we had shown the dialog when MMI_INITIATE event's coming
+ // from telephony layer, while right now we don't because the event comes
+ // too early (before in-call screen is prepared).
+ // Now we instead check pending MMI code and show the dialog here.
+ //
+ // This *may* cause some problem, e.g. when the user really quickly starts
+ // MMI sequence and calls an actual phone number before the MMI request
+ // being completed, which is rather rare.
+ //
+ // TODO: streamline this logic and have a UX in a better manner.
+ // Right now syncWithPhoneState() above will return SUCCESS based on
+ // mPhone.getPendingMmiCodes().isEmpty(), while we check it again here.
+ // Also we show pre-populated in-call UI under the dialog, which looks
+ // not great. (issue 5210375, 5545506)
+ // After cleaning them, remove commented-out MMI handling code elsewhere.
+ if (!mPhone.getPendingMmiCodes().isEmpty()) {
+ if (mMmiStartedDialog == null) {
+ MmiCode mmiCode = mPhone.getPendingMmiCodes().get(0);
+ Message message = Message.obtain(mHandler, PhoneGlobals.MMI_CANCEL);
+ mMmiStartedDialog = PhoneUtils.displayMMIInitiate(this, mmiCode,
+ message, mMmiStartedDialog);
+ // mInCallScreen needs to receive MMI_COMPLETE/MMI_CANCEL event from telephony,
+ // which will dismiss the entire screen.
+ }
+ }
+
+ // This means the screen is shown even though there's no connection, which only happens
+ // when the phone call has hung up while the screen is turned off at that moment.
+ // We want to show "disconnected" state with photos with appropriate elapsed time for
+ // the finished phone call.
+ if (mApp.inCallUiState.showAlreadyDisconnectedState) {
+ // if (DBG) {
+ log("onResume(): detected \"show already disconnected state\" situation."
+ + " set up DELAYED_CLEANUP_AFTER_DISCONNECT message with "
+ + CALL_ENDED_LONG_DELAY + " msec delay.");
+ //}
+ mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT);
+ mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT,
+ CALL_ENDED_LONG_DELAY);
+ }
+
+ if (VDBG) log("onResume() done.");
+ }
+
+ // onPause is guaranteed to be called when the InCallScreen goes
+ // in the background.
+ @Override
+ protected void onPause() {
+ if (DBG) log("onPause()...");
+ super.onPause();
+
+ if (mPowerManager.isScreenOn()) {
+ // Set to false when the screen went background *not* by screen turned off. Probably
+ // the user bailed out of the in-call screen (by pressing BACK, HOME, etc.)
+ mIsForegroundActivityForProximity = false;
+ }
+ mIsForegroundActivity = false;
+
+ // Force a clear of the provider info frame. Since the
+ // frame is removed using a timed message, it is
+ // possible we missed it if the prev call was interrupted.
+ mApp.inCallUiState.providerInfoVisible = false;
+
+ // "show-already-disconnected-state" should be effective just during the first wake-up.
+ // We should never allow it to stay true after that.
+ mApp.inCallUiState.showAlreadyDisconnectedState = false;
+
+ // A safety measure to disable proximity sensor in case call failed
+ // and the telephony state did not change.
+ mApp.setBeginningCall(false);
+
+ // Make sure the "Manage conference" chronometer is stopped when
+ // we move away from the foreground.
+ mManageConferenceUtils.stopConferenceTime();
+
+ // as a catch-all, make sure that any dtmf tones are stopped
+ // when the UI is no longer in the foreground.
+ mDialer.onDialerKeyUp(null);
+
+ // Release any "dialer session" resources, now that we're no
+ // longer in the foreground.
+ mDialer.stopDialerSession();
+
+ // If the device is put to sleep as the phone call is ending,
+ // we may see cases where the DELAYED_CLEANUP_AFTER_DISCONNECT
+ // event gets handled AFTER the device goes to sleep and wakes
+ // up again.
+
+ // This is because it is possible for a sleep command
+ // (executed with the End Call key) to come during the 2
+ // seconds that the "Call Ended" screen is up. Sleep then
+ // pauses the device (including the cleanup event) and
+ // resumes the event when it wakes up.
+
+ // To fix this, we introduce a bit of code that pushes the UI
+ // to the background if we pause and see a request to
+ // DELAYED_CLEANUP_AFTER_DISCONNECT.
+
+ // Note: We can try to finish directly, by:
+ // 1. Removing the DELAYED_CLEANUP_AFTER_DISCONNECT messages
+ // 2. Calling delayedCleanupAfterDisconnect directly
+
+ // However, doing so can cause problems between the phone
+ // app and the keyguard - the keyguard is trying to sleep at
+ // the same time that the phone state is changing. This can
+ // end up causing the sleep request to be ignored.
+ if (mHandler.hasMessages(DELAYED_CLEANUP_AFTER_DISCONNECT)
+ && mCM.getState() != PhoneConstants.State.RINGING) {
+ if (DBG) log("DELAYED_CLEANUP_AFTER_DISCONNECT detected, moving UI to background.");
+ endInCallScreenSession();
+ }
+
+ EventLog.writeEvent(EventLogTags.PHONE_UI_EXIT);
+
+ // Dismiss any dialogs we may have brought up, just to be 100%
+ // sure they won't still be around when we get back here.
+ dismissAllDialogs();
+
+ updateExpandedViewState();
+
+ // ...and the in-call notification too:
+ mApp.notificationMgr.updateInCallNotification();
+ // ...and *always* reset the system bar back to its normal state
+ // when leaving the in-call UI.
+ // (While we're the foreground activity, we disable navigation in
+ // some call states; see InCallTouchUi.updateState().)
+ mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true);
+
+ // Unregister for broadcast intents. (These affect the visible UI
+ // of the InCallScreen, so we only care about them while we're in the
+ // foreground.)
+ unregisterReceiver(mReceiver);
+
+ // Make sure we revert the poke lock and wake lock when we move to
+ // the background.
+ mApp.updateWakeState();
+
+ // clear the dismiss keyguard flag so we are back to the default state
+ // when we next resume
+ updateKeyguardPolicy(false);
+
+ // See also PhoneApp#updatePhoneState(), which takes care of all the other release() calls.
+ if (mApp.getUpdateLock().isHeld() && mApp.getPhoneState() == PhoneConstants.State.IDLE) {
+ if (DBG) {
+ log("Release UpdateLock on onPause() because there's no active phone call.");
+ }
+ mApp.getUpdateLock().release();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (DBG) log("onStop()...");
+ super.onStop();
+
+ stopTimer();
+
+ PhoneConstants.State state = mCM.getState();
+ if (DBG) log("onStop: state = " + state);
+
+ if (state == PhoneConstants.State.IDLE) {
+ if (mRespondViaSmsManager.isShowingPopup()) {
+ // This means that the user has been opening the "Respond via SMS" dialog even
+ // after the incoming call hanging up, and the screen finally went background.
+ // In that case we just close the dialog and exit the whole in-call screen.
+ mRespondViaSmsManager.dismissPopup();
+ }
+
+ // when OTA Activation, OTA Success/Failure dialog or OTA SPC
+ // failure dialog is running, do not destroy inCallScreen. Because call
+ // is already ended and dialog will not get redrawn on slider event.
+ if ((mApp.cdmaOtaProvisionData != null) && (mApp.cdmaOtaScreenState != null)
+ && ((mApp.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION)
+ && (mApp.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG)
+ && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) {
+ // we don't want the call screen to remain in the activity history
+ // if there are not active or ringing calls.
+ if (DBG) log("- onStop: calling finish() to clear activity history...");
+ moveTaskToBack(true);
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.cleanOtaScreen(true);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(LOG_TAG, "onDestroy()... this = " + this);
+ super.onDestroy();
+
+ // Set the magic flag that tells us NOT to handle any handler
+ // messages that come in asynchronously after we get destroyed.
+ mIsDestroyed = true;
+
+ mApp.setInCallScreenInstance(null);
+
+ // Clear out the InCallScreen references in various helper objects
+ // (to let them know we've been destroyed).
+ if (mCallCard != null) {
+ mCallCard.setInCallScreenInstance(null);
+ }
+ if (mInCallTouchUi != null) {
+ mInCallTouchUi.setInCallScreenInstance(null);
+ }
+ mRespondViaSmsManager.setInCallScreenInstance(null);
+
+ mDialer.clearInCallScreenReference();
+ mDialer = null;
+
+ unregisterForPhoneStates();
+ // No need to change wake state here; that happens in onPause() when we
+ // are moving out of the foreground.
+
+ if (mBluetoothHeadset != null) {
+ mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
+ mBluetoothHeadset = null;
+ }
+
+ // Dismiss all dialogs, to be absolutely sure we won't leak any of
+ // them while changing orientation.
+ dismissAllDialogs();
+
+ // If there's an OtaUtils instance around, clear out its
+ // references to our internal widgets.
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.clearUiWidgets();
+ }
+ }
+
+ /**
+ * Dismisses the in-call screen.
+ *
+ * We never *really* finish() the InCallScreen, since we don't want to
+ * get destroyed and then have to be re-created from scratch for the
+ * next call. Instead, we just move ourselves to the back of the
+ * activity stack.
+ *
+ * This also means that we'll no longer be reachable via the BACK
+ * button (since moveTaskToBack() puts us behind the Home app, but the
+ * home app doesn't allow the BACK key to move you any farther down in
+ * the history stack.)
+ *
+ * (Since the Phone app itself is never killed, this basically means
+ * that we'll keep a single InCallScreen instance around for the
+ * entire uptime of the device. This noticeably improves the UI
+ * responsiveness for incoming calls.)
+ */
+ @Override
+ public void finish() {
+ if (DBG) log("finish()...");
+ moveTaskToBack(true);
+ }
+
+ /**
+ * End the current in call screen session.
+ *
+ * This must be called when an InCallScreen session has
+ * complete so that the next invocation via an onResume will
+ * not be in an old state.
+ */
+ public void endInCallScreenSession() {
+ if (DBG) log("endInCallScreenSession()... phone state = " + mCM.getState());
+ endInCallScreenSession(false);
+ }
+
+ /**
+ * Internal version of endInCallScreenSession().
+ *
+ * @param forceFinish If true, force the InCallScreen to
+ * truly finish() rather than just calling moveTaskToBack().
+ * @see finish()
+ */
+ private void endInCallScreenSession(boolean forceFinish) {
+ if (DBG) {
+ log("endInCallScreenSession(" + forceFinish + ")... phone state = " + mCM.getState());
+ }
+ if (forceFinish) {
+ Log.i(LOG_TAG, "endInCallScreenSession(): FORCING a call to super.finish()!");
+ super.finish(); // Call super.finish() rather than our own finish() method,
+ // which actually just calls moveTaskToBack().
+ } else {
+ moveTaskToBack(true);
+ }
+ setInCallScreenMode(InCallScreenMode.UNDEFINED);
+
+ // Call update screen so that the in-call screen goes back to a normal state.
+ // This avoids bugs where a previous state will filcker the next time phone is
+ // opened.
+ updateScreen();
+
+ if (mCallCard != null) {
+ mCallCard.clear();
+ }
+ }
+
+ /**
+ * True when this Activity is in foreground (between onResume() and onPause()).
+ */
+ /* package */ boolean isForegroundActivity() {
+ return mIsForegroundActivity;
+ }
+
+ /**
+ * Returns true when the Activity is in foreground (between onResume() and onPause()),
+ * or, is in background due to user's bailing out of the screen, not by screen turning off.
+ *
+ * @see #isForegroundActivity()
+ */
+ /* package */ boolean isForegroundActivityForProximity() {
+ return mIsForegroundActivityForProximity;
+ }
+
+ /* package */ void updateKeyguardPolicy(boolean dismissKeyguard) {
+ if (dismissKeyguard) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+ }
+
+ private void registerForPhoneStates() {
+ if (!mRegisteredForPhoneStates) {
+ mCM.registerForPreciseCallStateChanged(mHandler, PHONE_STATE_CHANGED, null);
+ mCM.registerForDisconnect(mHandler, PHONE_DISCONNECT, null);
+ // TODO: sort out MMI code (probably we should remove this method entirely).
+ // See also MMI handling code in onResume()
+ // mCM.registerForMmiInitiate(mHandler, PhoneApp.MMI_INITIATE, null);
+
+ // register for the MMI complete message. Upon completion,
+ // PhoneUtils will bring up a system dialog instead of the
+ // message display class in PhoneUtils.displayMMIComplete().
+ // We'll listen for that message too, so that we can finish
+ // the activity at the same time.
+ mCM.registerForMmiComplete(mHandler, PhoneGlobals.MMI_COMPLETE, null);
+ mCM.registerForCallWaiting(mHandler, PHONE_CDMA_CALL_WAITING, null);
+ mCM.registerForPostDialCharacter(mHandler, POST_ON_DIAL_CHARS, null);
+ mCM.registerForSuppServiceFailed(mHandler, SUPP_SERVICE_FAILED, null);
+ mCM.registerForIncomingRing(mHandler, PHONE_INCOMING_RING, null);
+ mCM.registerForNewRingingConnection(mHandler, PHONE_NEW_RINGING_CONNECTION, null);
+ mRegisteredForPhoneStates = true;
+ }
+ }
+
+ private void unregisterForPhoneStates() {
+ mCM.unregisterForPreciseCallStateChanged(mHandler);
+ mCM.unregisterForDisconnect(mHandler);
+ mCM.unregisterForMmiInitiate(mHandler);
+ mCM.unregisterForMmiComplete(mHandler);
+ mCM.unregisterForCallWaiting(mHandler);
+ mCM.unregisterForPostDialCharacter(mHandler);
+ mCM.unregisterForSuppServiceFailed(mHandler);
+ mCM.unregisterForIncomingRing(mHandler);
+ mCM.unregisterForNewRingingConnection(mHandler);
+ mRegisteredForPhoneStates = false;
+ }
+
+ /* package */ void updateAfterRadioTechnologyChange() {
+ if (DBG) Log.d(LOG_TAG, "updateAfterRadioTechnologyChange()...");
+
+ // Reset the call screen since the calls cannot be transferred
+ // across radio technologies.
+ resetInCallScreenMode();
+
+ // Unregister for all events from the old obsolete phone
+ unregisterForPhoneStates();
+
+ // (Re)register for all events relevant to the new active phone
+ registerForPhoneStates();
+
+ // And finally, refresh the onscreen UI. (Note that it's safe
+ // to call requestUpdateScreen() even if the radio change ended up
+ // causing us to exit the InCallScreen.)
+ requestUpdateScreen();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ log("onNewIntent: intent = " + intent + ", phone state = " + mCM.getState());
+
+ // We're being re-launched with a new Intent. Since it's possible for a
+ // single InCallScreen instance to persist indefinitely (even if we
+ // finish() ourselves), this sequence can potentially happen any time
+ // the InCallScreen needs to be displayed.
+
+ // Stash away the new intent so that we can get it in the future
+ // by calling getIntent(). (Otherwise getIntent() will return the
+ // original Intent from when we first got created!)
+ setIntent(intent);
+
+ // Activities are always paused before receiving a new intent, so
+ // we can count on our onResume() method being called next.
+
+ // Just like in onCreate(), handle the intent.
+ internalResolveIntent(intent);
+ }
+
+ private void internalResolveIntent(Intent intent) {
+ if (intent == null || intent.getAction() == null) {
+ return;
+ }
+ String action = intent.getAction();
+ if (DBG) log("internalResolveIntent: action=" + action);
+
+ // In gingerbread and earlier releases, the InCallScreen used to
+ // directly handle certain intent actions that could initiate phone
+ // calls, namely ACTION_CALL and ACTION_CALL_EMERGENCY, and also
+ // OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING.
+ //
+ // But it doesn't make sense to tie those actions to the InCallScreen
+ // (or especially to the *activity lifecycle* of the InCallScreen).
+ // Instead, the InCallScreen should only be concerned with running the
+ // onscreen UI while in a call. So we've now offloaded the call-control
+ // functionality to a new module called CallController, and OTASP calls
+ // are now launched from the OtaUtils startInteractiveOtasp() or
+ // startNonInteractiveOtasp() methods.
+ //
+ // So now, the InCallScreen is only ever launched using the ACTION_MAIN
+ // action, and (upon launch) performs no functionality other than
+ // displaying the UI in a state that matches the current telephony
+ // state.
+
+ if (action.equals(intent.ACTION_MAIN)) {
+ // This action is the normal way to bring up the in-call UI.
+ //
+ // Most of the interesting work of updating the onscreen UI (to
+ // match the current telephony state) happens in the
+ // syncWithPhoneState() => updateScreen() sequence that happens in
+ // onResume().
+ //
+ // But we do check here for one extra that can come along with the
+ // ACTION_MAIN intent:
+
+ if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) {
+ // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF
+ // dialpad should be initially visible. If the extra isn't
+ // present at all, we just leave the dialpad in its previous state.
+
+ boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false);
+ if (VDBG) log("- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad);
+
+ // If SHOW_DIALPAD_EXTRA is specified, that overrides whatever
+ // the previous state of inCallUiState.showDialpad was.
+ mApp.inCallUiState.showDialpad = showDialpad;
+
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ // There's only one line in use, AND it's on hold, at which we're sure the user
+ // wants to use the dialpad toward the exact line, so un-hold the holding line.
+ if (showDialpad && !hasActiveCall && hasHoldingCall) {
+ PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall());
+ }
+ }
+ // ...and in onResume() we'll update the onscreen dialpad state to
+ // match the InCallUiState.
+
+ return;
+ }
+
+ if (action.equals(OtaUtils.ACTION_DISPLAY_ACTIVATION_SCREEN)) {
+ // Bring up the in-call UI in the OTASP-specific "activate" state;
+ // see OtaUtils.startInteractiveOtasp(). Note that at this point
+ // the OTASP call has not been started yet; we won't actually make
+ // the call until the user presses the "Activate" button.
+
+ if (!TelephonyCapabilities.supportsOtasp(mPhone)) {
+ throw new IllegalStateException(
+ "Received ACTION_DISPLAY_ACTIVATION_SCREEN intent on non-OTASP-capable device: "
+ + intent);
+ }
+
+ setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+ if ((mApp.cdmaOtaProvisionData != null)
+ && (!mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed)) {
+ mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed = true;
+ mApp.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION;
+ }
+ return;
+ }
+
+ // Various intent actions that should no longer come here directly:
+ if (action.equals(OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING)) {
+ // This intent is now handled by the InCallScreenShowActivation
+ // activity, which translates it into a call to
+ // OtaUtils.startInteractiveOtasp().
+ throw new IllegalStateException(
+ "Unexpected ACTION_PERFORM_CDMA_PROVISIONING received by InCallScreen: "
+ + intent);
+ } else if (action.equals(Intent.ACTION_CALL)
+ || action.equals(Intent.ACTION_CALL_EMERGENCY)) {
+ // ACTION_CALL* intents go to the OutgoingCallBroadcaster, which now
+ // translates them into CallController.placeCall() calls rather than
+ // launching the InCallScreen directly.
+ throw new IllegalStateException("Unexpected CALL action received by InCallScreen: "
+ + intent);
+ } else if (action.equals(ACTION_UNDEFINED)) {
+ // This action is only used for internal bookkeeping; we should
+ // never actually get launched with it.
+ Log.wtf(LOG_TAG, "internalResolveIntent: got launched with ACTION_UNDEFINED");
+ return;
+ } else {
+ Log.wtf(LOG_TAG, "internalResolveIntent: unexpected intent action: " + action);
+ // But continue the best we can (basically treating this case
+ // like ACTION_MAIN...)
+ return;
+ }
+ }
+
+ private void stopTimer() {
+ if (mCallCard != null) mCallCard.stopTimer();
+ }
+
+ private void initInCallScreen() {
+ if (VDBG) log("initInCallScreen()...");
+
+ // Have the WindowManager filter out touch events that are "too fat".
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
+
+ // Initialize the CallCard.
+ mCallCard = (CallCard) findViewById(R.id.callCard);
+ if (VDBG) log(" - mCallCard = " + mCallCard);
+ mCallCard.setInCallScreenInstance(this);
+
+ // Initialize the onscreen UI elements.
+ initInCallTouchUi();
+
+ // Helper class to keep track of enabledness/state of UI controls
+ mInCallControlState = new InCallControlState(this, mCM);
+
+ // Helper class to run the "Manage conference" UI
+ mManageConferenceUtils = new ManageConferenceUtils(this, mCM);
+
+ // The DTMF Dialpad.
+ ViewStub stub = (ViewStub) findViewById(R.id.dtmf_twelve_key_dialer_stub);
+ mDialer = new DTMFTwelveKeyDialer(this, stub);
+ mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ }
+
+ /**
+ * Returns true if the phone is "in use", meaning that at least one line
+ * is active (ie. off hook or ringing or dialing). Conversely, a return
+ * value of false means there's currently no phone activity at all.
+ */
+ private boolean phoneIsInUse() {
+ return mCM.getState() != PhoneConstants.State.IDLE;
+ }
+
+ private boolean handleDialerKeyDown(int keyCode, KeyEvent event) {
+ if (VDBG) log("handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "...");
+
+ // As soon as the user starts typing valid dialable keys on the
+ // keyboard (presumably to type DTMF tones) we start passing the
+ // key events to the DTMFDialer's onDialerKeyDown. We do so
+ // only if the okToDialDTMFTones() conditions pass.
+ if (okToDialDTMFTones()) {
+ return mDialer.onDialerKeyDown(event);
+
+ // TODO: If the dialpad isn't currently visible, maybe
+ // consider automatically bringing it up right now?
+ // (Just to make sure the user sees the digits widget...)
+ // But this probably isn't too critical since it's awkward to
+ // use the hard keyboard while in-call in the first place,
+ // especially now that the in-call UI is portrait-only...
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (DBG) log("onBackPressed()...");
+
+ // To consume this BACK press, the code here should just do
+ // something and return. Otherwise, call super.onBackPressed() to
+ // get the default implementation (which simply finishes the
+ // current activity.)
+
+ if (mCM.hasActiveRingingCall()) {
+ // The Back key, just like the Home key, is always disabled
+ // while an incoming call is ringing. (The user *must* either
+ // answer or reject the call before leaving the incoming-call
+ // screen.)
+ if (DBG) log("BACK key while ringing: ignored");
+
+ // And consume this event; *don't* call super.onBackPressed().
+ return;
+ }
+
+ // BACK is also used to exit out of any "special modes" of the
+ // in-call UI:
+
+ if (mDialer.isOpened()) {
+ closeDialpadInternal(true); // do the "closing" animation
+ return;
+ }
+
+ if (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) {
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ requestUpdateScreen();
+ return;
+ }
+
+ // Nothing special to do. Fall back to the default behavior.
+ super.onBackPressed();
+ }
+
+ /**
+ * Handles the green CALL key while in-call.
+ * @return true if we consumed the event.
+ */
+ private boolean handleCallKey() {
+ // The green CALL button means either "Answer", "Unhold", or
+ // "Swap calls", or can be a no-op, depending on the current state
+ // of the Phone.
+
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // The green CALL button means either "Answer", "Swap calls/On Hold", or
+ // "Add to 3WC", depending on the current state of the Phone.
+
+ CdmaPhoneCallState.PhoneCallState currCallState =
+ mApp.cdmaPhoneCallState.getCurrentCallState();
+ if (hasRingingCall) {
+ //Scenario 1: Accepting the First Incoming and Call Waiting call
+ if (DBG) log("answerCall: First Incoming and Call Waiting scenario");
+ internalAnswerCall(); // Automatically holds the current active call,
+ // if there is one
+ } else if ((currCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && (hasActiveCall)) {
+ //Scenario 2: Merging 3Way calls
+ if (DBG) log("answerCall: Merge 3-way call scenario");
+ // Merge calls
+ PhoneUtils.mergeCalls(mCM);
+ } else if (currCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ //Scenario 3: Switching between two Call waiting calls or drop the latest
+ // connection if in a 3Way merge scenario
+ if (DBG) log("answerCall: Switch btwn 2 calls scenario");
+ internalSwapCalls();
+ }
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ if (hasRingingCall) {
+ // If an incoming call is ringing, the CALL button is actually
+ // handled by the PhoneWindowManager. (We do this to make
+ // sure that we'll respond to the key even if the InCallScreen
+ // hasn't come to the foreground yet.)
+ //
+ // We'd only ever get here in the extremely rare case that the
+ // incoming call started ringing *after*
+ // PhoneWindowManager.interceptKeyTq() but before the event
+ // got here, or else if the PhoneWindowManager had some
+ // problem connecting to the ITelephony service.
+ Log.w(LOG_TAG, "handleCallKey: incoming call is ringing!"
+ + " (PhoneWindowManager should have handled this key.)");
+ // But go ahead and handle the key as normal, since the
+ // PhoneWindowManager presumably did NOT handle it:
+
+ // There's an incoming ringing call: CALL means "Answer".
+ internalAnswerCall();
+ } else if (hasActiveCall && hasHoldingCall) {
+ // Two lines are in use: CALL means "Swap calls".
+ if (DBG) log("handleCallKey: both lines in use ==> swap calls.");
+ internalSwapCalls();
+ } else if (hasHoldingCall) {
+ // There's only one line in use, AND it's on hold.
+ // In this case CALL is a shortcut for "unhold".
+ if (DBG) log("handleCallKey: call on hold ==> unhold.");
+ PhoneUtils.switchHoldingAndActive(
+ mCM.getFirstActiveBgCall()); // Really means "unhold" in this state
+ } else {
+ // The most common case: there's only one line in use, and
+ // it's an active call (i.e. it's not on hold.)
+ // In this case CALL is a no-op.
+ // (This used to be a shortcut for "add call", but that was a
+ // bad idea because "Add call" is so infrequently-used, and
+ // because the user experience is pretty confusing if you
+ // inadvertently trigger it.)
+ if (VDBG) log("handleCallKey: call in foregound ==> ignoring.");
+ // But note we still consume this key event; see below.
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ // We *always* consume the CALL key, since the system-wide default
+ // action ("go to the in-call screen") is useless here.
+ return true;
+ }
+
+ boolean isKeyEventAcceptableDTMF (KeyEvent event) {
+ return (mDialer != null && mDialer.isKeyEventAcceptable(event));
+ }
+
+ /**
+ * Overriden to track relevant focus changes.
+ *
+ * If a key is down and some time later the focus changes, we may
+ * NOT recieve the keyup event; logically the keyup event has not
+ * occured in this window. This issue is fixed by treating a focus
+ * changed event as an interruption to the keydown, making sure
+ * that any code that needs to be run in onKeyUp is ALSO run here.
+ */
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ // the dtmf tones should no longer be played
+ if (VDBG) log("onWindowFocusChanged(" + hasFocus + ")...");
+ if (!hasFocus && mDialer != null) {
+ if (VDBG) log("- onWindowFocusChanged: faking onDialerKeyUp()...");
+ mDialer.onDialerKeyUp(null);
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // if (DBG) log("onKeyUp(keycode " + keyCode + ")...");
+
+ // push input to the dialer.
+ if ((mDialer != null) && (mDialer.onDialerKeyUp(event))){
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_CALL) {
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // if (DBG) log("onKeyDown(keycode " + keyCode + ")...");
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL:
+ boolean handled = handleCallKey();
+ if (!handled) {
+ Log.w(LOG_TAG, "InCallScreen should always handle KEYCODE_CALL in onKeyDown");
+ }
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+
+ // Note there's no KeyEvent.KEYCODE_ENDCALL case here.
+ // The standard system-wide handling of the ENDCALL key
+ // (see PhoneWindowManager's handling of KEYCODE_ENDCALL)
+ // already implements exactly what the UI spec wants,
+ // namely (1) "hang up" if there's a current active call,
+ // or (2) "don't answer" if there's a current ringing call.
+
+ case KeyEvent.KEYCODE_CAMERA:
+ // Disable the CAMERA button while in-call since it's too
+ // easy to press accidentally.
+ return true;
+
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ if (mCM.getState() == PhoneConstants.State.RINGING) {
+ // If an incoming call is ringing, the VOLUME buttons are
+ // actually handled by the PhoneWindowManager. (We do
+ // this to make sure that we'll respond to them even if
+ // the InCallScreen hasn't come to the foreground yet.)
+ //
+ // We'd only ever get here in the extremely rare case that the
+ // incoming call started ringing *after*
+ // PhoneWindowManager.interceptKeyTq() but before the event
+ // got here, or else if the PhoneWindowManager had some
+ // problem connecting to the ITelephony service.
+ Log.w(LOG_TAG, "VOLUME key: incoming call is ringing!"
+ + " (PhoneWindowManager should have handled this key.)");
+ // But go ahead and handle the key as normal, since the
+ // PhoneWindowManager presumably did NOT handle it:
+ internalSilenceRinger();
+
+ // As long as an incoming call is ringing, we always
+ // consume the VOLUME keys.
+ return true;
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MUTE:
+ onMuteClick();
+ return true;
+
+ // Various testing/debugging features, enabled ONLY when VDBG == true.
+ case KeyEvent.KEYCODE_SLASH:
+ if (VDBG) {
+ log("----------- InCallScreen View dump --------------");
+ // Dump starting from the top-level view of the entire activity:
+ Window w = this.getWindow();
+ View decorView = w.getDecorView();
+ decorView.debug();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_EQUALS:
+ if (VDBG) {
+ log("----------- InCallScreen call state dump --------------");
+ PhoneUtils.dumpCallState(mPhone);
+ PhoneUtils.dumpCallManager();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_GRAVE:
+ if (VDBG) {
+ // Placeholder for other misc temp testing
+ log("------------ Temp testing -----------------");
+ return true;
+ }
+ break;
+ }
+
+ if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * Handle a failure notification for a supplementary service
+ * (i.e. conference, switch, separate, transfer, etc.).
+ */
+ void onSuppServiceFailed(AsyncResult r) {
+ Phone.SuppService service = (Phone.SuppService) r.result;
+ if (DBG) log("onSuppServiceFailed: " + service);
+
+ int errorMessageResId;
+ switch (service) {
+ case SWITCH:
+ // Attempt to switch foreground and background/incoming calls failed
+ // ("Failed to switch calls")
+ errorMessageResId = R.string.incall_error_supp_service_switch;
+ break;
+
+ case SEPARATE:
+ // Attempt to separate a call from a conference call
+ // failed ("Failed to separate out call")
+ errorMessageResId = R.string.incall_error_supp_service_separate;
+ break;
+
+ case TRANSFER:
+ // Attempt to connect foreground and background calls to
+ // each other (and hanging up user's line) failed ("Call
+ // transfer failed")
+ errorMessageResId = R.string.incall_error_supp_service_transfer;
+ break;
+
+ case CONFERENCE:
+ // Attempt to add a call to conference call failed
+ // ("Conference call failed")
+ errorMessageResId = R.string.incall_error_supp_service_conference;
+ break;
+
+ case REJECT:
+ // Attempt to reject an incoming call failed
+ // ("Call rejection failed")
+ errorMessageResId = R.string.incall_error_supp_service_reject;
+ break;
+
+ case HANGUP:
+ // Attempt to release a call failed ("Failed to release call(s)")
+ errorMessageResId = R.string.incall_error_supp_service_hangup;
+ break;
+
+ case UNKNOWN:
+ default:
+ // Attempt to use a service we don't recognize or support
+ // ("Unsupported service" or "Selected service failed")
+ errorMessageResId = R.string.incall_error_supp_service_unknown;
+ break;
+ }
+
+ // mSuppServiceFailureDialog is a generic dialog used for any
+ // supp service failure, and there's only ever have one
+ // instance at a time. So just in case a previous dialog is
+ // still around, dismiss it.
+ if (mSuppServiceFailureDialog != null) {
+ if (DBG) log("- DISMISSING mSuppServiceFailureDialog.");
+ mSuppServiceFailureDialog.dismiss(); // It's safe to dismiss() a dialog
+ // that's already dismissed.
+ mSuppServiceFailureDialog = null;
+ }
+
+ mSuppServiceFailureDialog = new AlertDialog.Builder(this)
+ .setMessage(errorMessageResId)
+ .setPositiveButton(R.string.ok, null)
+ .create();
+ mSuppServiceFailureDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mSuppServiceFailureDialog.show();
+ }
+
+ /**
+ * Something has changed in the phone's state. Update the UI.
+ */
+ private void onPhoneStateChanged(AsyncResult r) {
+ PhoneConstants.State state = mCM.getState();
+ if (DBG) log("onPhoneStateChanged: current state = " + state);
+
+ // There's nothing to do here if we're not the foreground activity.
+ // (When we *do* eventually come to the foreground, we'll do a
+ // full update then.)
+ if (!mIsForegroundActivity) {
+ if (DBG) log("onPhoneStateChanged: Activity not in foreground! Bailing out...");
+ return;
+ }
+
+ updateExpandedViewState();
+
+ // Update the onscreen UI.
+ // We use requestUpdateScreen() here (which posts a handler message)
+ // instead of calling updateScreen() directly, which allows us to avoid
+ // unnecessary work if multiple onPhoneStateChanged() events come in all
+ // at the same time.
+
+ requestUpdateScreen();
+
+ // Make sure we update the poke lock and wake lock when certain
+ // phone state changes occur.
+ mApp.updateWakeState();
+ }
+
+ /**
+ * Updates the UI after a phone connection is disconnected, as follows:
+ *
+ * - If this was a missed or rejected incoming call, and no other
+ * calls are active, dismiss the in-call UI immediately. (The
+ * CallNotifier will still create a "missed call" notification if
+ * necessary.)
+ *
+ * - With any other disconnect cause, if the phone is now totally
+ * idle, display the "Call ended" state for a couple of seconds.
+ *
+ * - Or, if the phone is still in use, stay on the in-call screen
+ * (and update the UI to reflect the current state of the Phone.)
+ *
+ * @param r r.result contains the connection that just ended
+ */
+ private void onDisconnect(AsyncResult r) {
+ Connection c = (Connection) r.result;
+ Connection.DisconnectCause cause = c.getDisconnectCause();
+ if (DBG) log("onDisconnect: connection '" + c + "', cause = " + cause
+ + ", showing screen: " + mApp.isShowingCallScreen());
+
+ boolean currentlyIdle = !phoneIsInUse();
+ int autoretrySetting = AUTO_RETRY_OFF;
+ boolean phoneIsCdma = (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA);
+ if (phoneIsCdma) {
+ // Get the Auto-retry setting only if Phone State is IDLE,
+ // else let it stay as AUTO_RETRY_OFF
+ if (currentlyIdle) {
+ autoretrySetting = android.provider.Settings.Global.getInt(mPhone.getContext().
+ getContentResolver(), android.provider.Settings.Global.CALL_AUTO_RETRY, 0);
+ }
+ }
+
+ // for OTA Call, only if in OTA NORMAL mode, handle OTA END scenario
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ && ((mApp.cdmaOtaProvisionData != null)
+ && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) {
+ setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ updateScreen();
+ return;
+ } else if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ || ((mApp.cdmaOtaProvisionData != null)
+ && mApp.cdmaOtaProvisionData.inOtaSpcState)) {
+ if (DBG) log("onDisconnect: OTA Call end already handled");
+ return;
+ }
+
+ // Any time a call disconnects, clear out the "history" of DTMF
+ // digits you typed (to make sure it doesn't persist from one call
+ // to the next.)
+ mDialer.clearDigits();
+
+ // Under certain call disconnected states, we want to alert the user
+ // with a dialog instead of going through the normal disconnect
+ // routine.
+ if (cause == Connection.DisconnectCause.CALL_BARRED) {
+ showGenericErrorDialog(R.string.callFailed_cb_enabled, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.FDN_BLOCKED) {
+ showGenericErrorDialog(R.string.callFailed_fdn_only, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.CS_RESTRICTED) {
+ showGenericErrorDialog(R.string.callFailed_dsac_restricted, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.CS_RESTRICTED_EMERGENCY) {
+ showGenericErrorDialog(R.string.callFailed_dsac_restricted_emergency, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.CS_RESTRICTED_NORMAL) {
+ showGenericErrorDialog(R.string.callFailed_dsac_restricted_normal, false);
+ return;
+ }
+
+ if (phoneIsCdma) {
+ Call.State callState = mApp.notifier.getPreviousCdmaCallState();
+ if ((callState == Call.State.ACTIVE)
+ && (cause != Connection.DisconnectCause.INCOMING_MISSED)
+ && (cause != Connection.DisconnectCause.NORMAL)
+ && (cause != Connection.DisconnectCause.LOCAL)
+ && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) {
+ showCallLostDialog();
+ } else if ((callState == Call.State.DIALING || callState == Call.State.ALERTING)
+ && (cause != Connection.DisconnectCause.INCOMING_MISSED)
+ && (cause != Connection.DisconnectCause.NORMAL)
+ && (cause != Connection.DisconnectCause.LOCAL)
+ && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) {
+
+ if (mApp.inCallUiState.needToShowCallLostDialog) {
+ // Show the dialog now since the call that just failed was a retry.
+ showCallLostDialog();
+ mApp.inCallUiState.needToShowCallLostDialog = false;
+ } else {
+ if (autoretrySetting == AUTO_RETRY_OFF) {
+ // Show the dialog for failed call if Auto Retry is OFF in Settings.
+ showCallLostDialog();
+ mApp.inCallUiState.needToShowCallLostDialog = false;
+ } else {
+ // Set the needToShowCallLostDialog flag now, so we'll know to show
+ // the dialog if *this* call fails.
+ mApp.inCallUiState.needToShowCallLostDialog = true;
+ }
+ }
+ }
+ }
+
+ // Explicitly clean up up any DISCONNECTED connections
+ // in a conference call.
+ // [Background: Even after a connection gets disconnected, its
+ // Connection object still stays around for a few seconds, in the
+ // DISCONNECTED state. With regular calls, this state drives the
+ // "call ended" UI. But when a single person disconnects from a
+ // conference call there's no "call ended" state at all; in that
+ // case we blow away any DISCONNECTED connections right now to make sure
+ // the UI updates instantly to reflect the current state.]
+ final Call call = c.getCall();
+ if (call != null) {
+ // We only care about situation of a single caller
+ // disconnecting from a conference call. In that case, the
+ // call will have more than one Connection (including the one
+ // that just disconnected, which will be in the DISCONNECTED
+ // state) *and* at least one ACTIVE connection. (If the Call
+ // has *no* ACTIVE connections, that means that the entire
+ // conference call just ended, so we *do* want to show the
+ // "Call ended" state.)
+ List<Connection> connections = call.getConnections();
+ if (connections != null && connections.size() > 1) {
+ for (Connection conn : connections) {
+ if (conn.getState() == Call.State.ACTIVE) {
+ // This call still has at least one ACTIVE connection!
+ // So blow away any DISCONNECTED connections
+ // (including, presumably, the one that just
+ // disconnected from this conference call.)
+
+ // We also force the wake state to refresh, just in
+ // case the disconnected connections are removed
+ // before the phone state change.
+ if (VDBG) log("- Still-active conf call; clearing DISCONNECTED...");
+ mApp.updateWakeState();
+ mCM.clearDisconnected(); // This happens synchronously.
+ break;
+ }
+ }
+ }
+ }
+
+ // Note: see CallNotifier.onDisconnect() for some other behavior
+ // that might be triggered by a disconnect event, like playing the
+ // busy/congestion tone.
+
+ // Stash away some info about the call that just disconnected.
+ // (This might affect what happens after we exit the InCallScreen; see
+ // delayedCleanupAfterDisconnect().)
+ // TODO: rather than stashing this away now and then reading it in
+ // delayedCleanupAfterDisconnect(), it would be cleaner to just pass
+ // this as an argument to delayedCleanupAfterDisconnect() (if we call
+ // it directly) or else pass it as a Message argument when we post the
+ // DELAYED_CLEANUP_AFTER_DISCONNECT message.
+ mLastDisconnectCause = cause;
+
+ // We bail out immediately (and *don't* display the "call ended"
+ // state at all) if this was an incoming call.
+ boolean bailOutImmediately =
+ ((cause == Connection.DisconnectCause.INCOMING_MISSED)
+ || (cause == Connection.DisconnectCause.INCOMING_REJECTED))
+ && currentlyIdle;
+
+ boolean showingQuickResponseDialog =
+ mRespondViaSmsManager != null && mRespondViaSmsManager.isShowingPopup();
+
+ // Note: we also do some special handling for the case when a call
+ // disconnects with cause==OUT_OF_SERVICE while making an
+ // emergency call from airplane mode. That's handled by
+ // EmergencyCallHelper.onDisconnect().
+
+ if (bailOutImmediately && showingQuickResponseDialog) {
+ if (DBG) log("- onDisconnect: Respond-via-SMS dialog is still being displayed...");
+
+ // Do *not* exit the in-call UI yet!
+ // If the call was an incoming call that was missed *and* the user is using
+ // quick response screen, we keep showing the screen for a moment, assuming the
+ // user wants to reply the call anyway.
+ //
+ // For this case, we will exit the screen when:
+ // - the message is sent (RespondViaSmsManager)
+ // - the message is canceled (RespondViaSmsManager), or
+ // - when the whole in-call UI becomes background (onPause())
+ } else if (bailOutImmediately) {
+ if (DBG) log("- onDisconnect: bailOutImmediately...");
+
+ // Exit the in-call UI!
+ // (This is basically the same "delayed cleanup" we do below,
+ // just with zero delay. Since the Phone is currently idle,
+ // this call is guaranteed to immediately finish this activity.)
+ delayedCleanupAfterDisconnect();
+ } else {
+ if (DBG) log("- onDisconnect: delayed bailout...");
+ // Stay on the in-call screen for now. (Either the phone is
+ // still in use, or the phone is idle but we want to display
+ // the "call ended" state for a couple of seconds.)
+
+ // Switch to the special "Call ended" state when the phone is idle
+ // but there's still a call in the DISCONNECTED state:
+ if (currentlyIdle
+ && (mCM.hasDisconnectedFgCall() || mCM.hasDisconnectedBgCall())) {
+ if (DBG) log("- onDisconnect: switching to 'Call ended' state...");
+ setInCallScreenMode(InCallScreenMode.CALL_ENDED);
+ }
+
+ // Force a UI update in case we need to display anything
+ // special based on this connection's DisconnectCause
+ // (see CallCard.getCallFailedString()).
+ updateScreen();
+
+ // Some other misc cleanup that we do if the call that just
+ // disconnected was the foreground call.
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ if (!hasActiveCall) {
+ if (DBG) log("- onDisconnect: cleaning up after FG call disconnect...");
+
+ // Dismiss any dialogs which are only meaningful for an
+ // active call *and* which become moot if the call ends.
+ if (mWaitPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWaitPromptDialog.");
+ mWaitPromptDialog.dismiss(); // safe even if already dismissed
+ mWaitPromptDialog = null;
+ }
+ if (mWildPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWildPromptDialog.");
+ mWildPromptDialog.dismiss(); // safe even if already dismissed
+ mWildPromptDialog = null;
+ }
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss(); // safe even if already dismissed
+ mPausePromptDialog = null;
+ }
+ }
+
+ // Updating the screen wake state is done in onPhoneStateChanged().
+
+
+ // CDMA: We only clean up if the Phone state is IDLE as we might receive an
+ // onDisconnect for a Call Collision case (rare but possible).
+ // For Call collision cases i.e. when the user makes an out going call
+ // and at the same time receives an Incoming Call, the Incoming Call is given
+ // higher preference. At this time framework sends a disconnect for the Out going
+ // call connection hence we should *not* bring down the InCallScreen as the Phone
+ // State would be RINGING
+ if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (!currentlyIdle) {
+ // Clean up any connections in the DISCONNECTED state.
+ // This is necessary cause in CallCollision the foreground call might have
+ // connections in DISCONNECTED state which needs to be cleared.
+ mCM.clearDisconnected();
+
+ // The phone is still in use. Stay here in this activity.
+ // But we don't need to keep the screen on.
+ if (DBG) log("onDisconnect: Call Collision case - staying on InCallScreen.");
+ if (DBG) PhoneUtils.dumpCallState(mPhone);
+ return;
+ }
+ }
+
+ // This is onDisconnect() request from the last phone call; no available call anymore.
+ //
+ // When the in-call UI is in background *because* the screen is turned off (unlike the
+ // other case where the other activity is being shown), we wake up the screen and
+ // show "DISCONNECTED" state once, with appropriate elapsed time. After showing that
+ // we *must* bail out of the screen again, showing screen lock if needed.
+ //
+ // See also comments for isForegroundActivityForProximity()
+ //
+ // TODO: Consider moving this to CallNotifier. This code assumes the InCallScreen
+ // never gets destroyed. For this exact case, it works (since InCallScreen won't be
+ // destroyed), while technically this isn't right; Activity may be destroyed when
+ // in background.
+ if (currentlyIdle && !isForegroundActivity() && isForegroundActivityForProximity()) {
+ log("Force waking up the screen to let users see \"disconnected\" state");
+ if (call != null) {
+ mCallCard.updateElapsedTimeWidget(call);
+ }
+ // This variable will be kept true until the next InCallScreen#onPause(), which
+ // forcibly turns it off regardless of the situation (for avoiding unnecessary
+ // confusion around this special case).
+ mApp.inCallUiState.showAlreadyDisconnectedState = true;
+
+ // Finally request wake-up..
+ mApp.wakeUpScreen();
+
+ // InCallScreen#onResume() will set DELAYED_CLEANUP_AFTER_DISCONNECT message,
+ // so skip the following section.
+ return;
+ }
+
+ // Finally, arrange for delayedCleanupAfterDisconnect() to get
+ // called after a short interval (during which we display the
+ // "call ended" state.) At that point, if the
+ // Phone is idle, we'll finish out of this activity.
+ final int callEndedDisplayDelay;
+ switch (cause) {
+ // When the local user hanged up the ongoing call, it is ok to dismiss the screen
+ // soon. In other cases, we show the "hung up" screen longer.
+ //
+ // - For expected reasons we will use CALL_ENDED_LONG_DELAY.
+ // -- when the peer hanged up the call
+ // -- when the local user rejects the incoming call during the other ongoing call
+ // (TODO: there may be other cases which should be in this category)
+ //
+ // - For other unexpected reasons, we will use CALL_ENDED_EXTRA_LONG_DELAY,
+ // assuming the local user wants to confirm the disconnect reason.
+ case LOCAL:
+ callEndedDisplayDelay = CALL_ENDED_SHORT_DELAY;
+ break;
+ case NORMAL:
+ case INCOMING_REJECTED:
+ callEndedDisplayDelay = CALL_ENDED_LONG_DELAY;
+ break;
+ default:
+ callEndedDisplayDelay = CALL_ENDED_EXTRA_LONG_DELAY;
+ break;
+ }
+ mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT);
+ mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT,
+ callEndedDisplayDelay);
+ }
+
+ // Remove 3way timer (only meaningful for CDMA)
+ // TODO: this call needs to happen in the CallController, not here.
+ // (It should probably be triggered by the CallNotifier's onDisconnect method.)
+ // mHandler.removeMessages(THREEWAY_CALLERINFO_DISPLAY_DONE);
+ }
+
+ /**
+ * Brings up the "MMI Started" dialog.
+ */
+ /* TODO: sort out MMI code (probably we should remove this method entirely). See also
+ MMI handling code in onResume()
+ private void onMMIInitiate(AsyncResult r) {
+ if (VDBG) log("onMMIInitiate()... AsyncResult r = " + r);
+
+ // Watch out: don't do this if we're not the foreground activity,
+ // mainly since in the Dialog.show() might fail if we don't have a
+ // valid window token any more...
+ // (Note that this exact sequence can happen if you try to start
+ // an MMI code while the radio is off or out of service.)
+ if (!mIsForegroundActivity) {
+ if (VDBG) log("Activity not in foreground! Bailing out...");
+ return;
+ }
+
+ // Also, if any other dialog is up right now (presumably the
+ // generic error dialog displaying the "Starting MMI..." message)
+ // take it down before bringing up the real "MMI Started" dialog
+ // in its place.
+ dismissAllDialogs();
+
+ MmiCode mmiCode = (MmiCode) r.result;
+ if (VDBG) log(" - MmiCode: " + mmiCode);
+
+ Message message = Message.obtain(mHandler, PhoneApp.MMI_CANCEL);
+ mMmiStartedDialog = PhoneUtils.displayMMIInitiate(this, mmiCode,
+ message, mMmiStartedDialog);
+ }*/
+
+ /**
+ * Handles an MMI_CANCEL event, which is triggered by the button
+ * (labeled either "OK" or "Cancel") on the "MMI Started" dialog.
+ * @see PhoneUtils#cancelMmiCode(Phone)
+ */
+ private void onMMICancel() {
+ if (VDBG) log("onMMICancel()...");
+
+ // First of all, cancel the outstanding MMI code (if possible.)
+ PhoneUtils.cancelMmiCode(mPhone);
+
+ // Regardless of whether the current MMI code was cancelable, the
+ // PhoneApp will get an MMI_COMPLETE event very soon, which will
+ // take us to the MMI Complete dialog (see
+ // PhoneUtils.displayMMIComplete().)
+ //
+ // But until that event comes in, we *don't* want to stay here on
+ // the in-call screen, since we'll be visible in a
+ // partially-constructed state as soon as the "MMI Started" dialog
+ // gets dismissed. So let's forcibly bail out right now.
+ if (DBG) log("onMMICancel: finishing InCallScreen...");
+ dismissAllDialogs();
+ endInCallScreenSession();
+ }
+
+ /**
+ * Handles an MMI_COMPLETE event, which is triggered by telephony,
+ * implying MMI
+ */
+ private void onMMIComplete(MmiCode mmiCode) {
+ // Check the code to see if the request is ready to
+ // finish, this includes any MMI state that is not
+ // PENDING.
+
+ // if phone is a CDMA phone display feature code completed message
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ PhoneUtils.displayMMIComplete(mPhone, mApp, mmiCode, null, null);
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ if (mmiCode.getState() != MmiCode.State.PENDING) {
+ if (DBG) log("Got MMI_COMPLETE, finishing InCallScreen...");
+ dismissAllDialogs();
+ endInCallScreenSession();
+ }
+ }
+ }
+
+ /**
+ * Handles the POST_ON_DIAL_CHARS message from the Phone
+ * (see our call to mPhone.setOnPostDialCharacter() above.)
+ *
+ * TODO: NEED TO TEST THIS SEQUENCE now that we no longer handle
+ * "dialable" key events here in the InCallScreen: we do directly to the
+ * Dialer UI instead. Similarly, we may now need to go directly to the
+ * Dialer to handle POST_ON_DIAL_CHARS too.
+ */
+ private void handlePostOnDialChars(AsyncResult r, char ch) {
+ Connection c = (Connection) r.result;
+
+ if (c != null) {
+ Connection.PostDialState state =
+ (Connection.PostDialState) r.userObj;
+
+ if (VDBG) log("handlePostOnDialChar: state = " +
+ state + ", ch = " + ch);
+
+ switch (state) {
+ case STARTED:
+ mDialer.stopLocalToneIfNeeded();
+ if (mPauseInProgress) {
+ /**
+ * Note that on some devices, this will never happen,
+ * because we will not ever enter the PAUSE state.
+ */
+ showPausePromptDialog(c, mPostDialStrAfterPause);
+ }
+ mPauseInProgress = false;
+ mDialer.startLocalToneIfNeeded(ch);
+
+ // TODO: is this needed, now that you can't actually
+ // type DTMF chars or dial directly from here?
+ // If so, we'd need to yank you out of the in-call screen
+ // here too (and take you to the 12-key dialer in "in-call" mode.)
+ // displayPostDialedChar(ch);
+ break;
+
+ case WAIT:
+ // wait shows a prompt.
+ if (DBG) log("handlePostOnDialChars: show WAIT prompt...");
+ mDialer.stopLocalToneIfNeeded();
+ String postDialStr = c.getRemainingPostDialString();
+ showWaitPromptDialog(c, postDialStr);
+ break;
+
+ case WILD:
+ if (DBG) log("handlePostOnDialChars: show WILD prompt");
+ mDialer.stopLocalToneIfNeeded();
+ showWildPromptDialog(c);
+ break;
+
+ case COMPLETE:
+ mDialer.stopLocalToneIfNeeded();
+ break;
+
+ case PAUSE:
+ // pauses for a brief period of time then continue dialing.
+ mDialer.stopLocalToneIfNeeded();
+ mPostDialStrAfterPause = c.getRemainingPostDialString();
+ mPauseInProgress = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Pop up an alert dialog with OK and Cancel buttons to allow user to
+ * Accept or Reject the WAIT inserted as part of the Dial string.
+ */
+ private void showWaitPromptDialog(final Connection c, String postDialStr) {
+ if (DBG) log("showWaitPromptDialogChoice: '" + postDialStr + "'...");
+
+ Resources r = getResources();
+ StringBuilder buf = new StringBuilder();
+ buf.append(r.getText(R.string.wait_prompt_str));
+ buf.append(postDialStr);
+
+ // if (DBG) log("- mWaitPromptDialog = " + mWaitPromptDialog);
+ if (mWaitPromptDialog != null) {
+ if (DBG) log("- DISMISSING mWaitPromptDialog.");
+ mWaitPromptDialog.dismiss(); // safe even if already dismissed
+ mWaitPromptDialog = null;
+ }
+
+ mWaitPromptDialog = new AlertDialog.Builder(this)
+ .setMessage(buf.toString())
+ .setPositiveButton(R.string.pause_prompt_yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (DBG) log("handle WAIT_PROMPT_CONFIRMED, proceed...");
+ c.proceedAfterWaitChar();
+ }
+ })
+ .setNegativeButton(R.string.pause_prompt_no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (DBG) log("handle POST_DIAL_CANCELED!");
+ c.cancelPostDial();
+ }
+ })
+ .create();
+ mWaitPromptDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ mWaitPromptDialog.show();
+ }
+
+ /**
+ * Pop up an alert dialog which waits for 2 seconds for each P (Pause) Character entered
+ * as part of the Dial String.
+ */
+ private void showPausePromptDialog(final Connection c, String postDialStrAfterPause) {
+ Resources r = getResources();
+ StringBuilder buf = new StringBuilder();
+ buf.append(r.getText(R.string.pause_prompt_str));
+ buf.append(postDialStrAfterPause);
+
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss(); // safe even if already dismissed
+ mPausePromptDialog = null;
+ }
+
+ mPausePromptDialog = new AlertDialog.Builder(this)
+ .setMessage(buf.toString())
+ .create();
+ mPausePromptDialog.show();
+ // 2 second timer
+ Message msg = Message.obtain(mHandler, EVENT_PAUSE_DIALOG_COMPLETE);
+ mHandler.sendMessageDelayed(msg, PAUSE_PROMPT_DIALOG_TIMEOUT);
+ }
+
+ private View createWildPromptView() {
+ LinearLayout result = new LinearLayout(this);
+ result.setOrientation(LinearLayout.VERTICAL);
+ result.setPadding(5, 5, 5, 5);
+
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ TextView promptMsg = new TextView(this);
+ promptMsg.setTextSize(14);
+ promptMsg.setTypeface(Typeface.DEFAULT_BOLD);
+ promptMsg.setText(getResources().getText(R.string.wild_prompt_str));
+
+ result.addView(promptMsg, lp);
+
+ mWildPromptText = new EditText(this);
+ mWildPromptText.setKeyListener(DialerKeyListener.getInstance());
+ mWildPromptText.setMovementMethod(null);
+ mWildPromptText.setTextSize(14);
+ mWildPromptText.setMaxLines(1);
+ mWildPromptText.setHorizontallyScrolling(true);
+ mWildPromptText.setBackgroundResource(android.R.drawable.editbox_background);
+
+ LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp2.setMargins(0, 3, 0, 0);
+
+ result.addView(mWildPromptText, lp2);
+
+ return result;
+ }
+
+ private void showWildPromptDialog(final Connection c) {
+ View v = createWildPromptView();
+
+ if (mWildPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWildPromptDialog.");
+ mWildPromptDialog.dismiss(); // safe even if already dismissed
+ mWildPromptDialog = null;
+ }
+
+ mWildPromptDialog = new AlertDialog.Builder(this)
+ .setView(v)
+ .setPositiveButton(
+ R.string.send_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (VDBG) log("handle WILD_PROMPT_CHAR_ENTERED, proceed...");
+ String replacement = null;
+ if (mWildPromptText != null) {
+ replacement = mWildPromptText.getText().toString();
+ mWildPromptText = null;
+ }
+ c.proceedAfterWildChar(replacement);
+ mApp.pokeUserActivity();
+ }
+ })
+ .setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (VDBG) log("handle POST_DIAL_CANCELED!");
+ c.cancelPostDial();
+ mApp.pokeUserActivity();
+ }
+ })
+ .create();
+ mWildPromptDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mWildPromptDialog.show();
+
+ mWildPromptText.requestFocus();
+ }
+
+ /**
+ * Updates the state of the in-call UI based on the current state of
+ * the Phone. This call has no effect if we're not currently the
+ * foreground activity.
+ *
+ * This method is only allowed to be called from the UI thread (since it
+ * manipulates our View hierarchy). If you need to update the screen from
+ * some other thread, or if you just want to "post a request" for the screen
+ * to be updated (rather than doing it synchronously), call
+ * requestUpdateScreen() instead.
+ *
+ * Right now this method will update UI visibility immediately, with no animation.
+ * TODO: have animate flag here and use it anywhere possible.
+ */
+ private void updateScreen() {
+ if (DBG) log("updateScreen()...");
+ final InCallScreenMode inCallScreenMode = mApp.inCallUiState.inCallScreenMode;
+ if (VDBG) {
+ PhoneConstants.State state = mCM.getState();
+ log(" - phone state = " + state);
+ log(" - inCallScreenMode = " + inCallScreenMode);
+ }
+
+ // Don't update anything if we're not in the foreground (there's
+ // no point updating our UI widgets since we're not visible!)
+ // Also note this check also ensures we won't update while we're
+ // in the middle of pausing, which could cause a visible glitch in
+ // the "activity ending" transition.
+ if (!mIsForegroundActivity) {
+ if (DBG) log("- updateScreen: not the foreground Activity! Bailing out...");
+ return;
+ }
+
+ if (inCallScreenMode == InCallScreenMode.OTA_NORMAL) {
+ if (DBG) log("- updateScreen: OTA call state NORMAL (NOT updating in-call UI)...");
+ mCallCard.setVisibility(View.GONE);
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.otaShowProperScreen();
+ } else {
+ Log.w(LOG_TAG, "OtaUtils object is null, not showing any screen for that.");
+ }
+ return; // Return without updating in-call UI.
+ } else if (inCallScreenMode == InCallScreenMode.OTA_ENDED) {
+ if (DBG) log("- updateScreen: OTA call ended state (NOT updating in-call UI)...");
+ mCallCard.setVisibility(View.GONE);
+ // Wake up the screen when we get notification, good or bad.
+ mApp.wakeUpScreen();
+ if (mApp.cdmaOtaScreenState.otaScreenState
+ == CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION) {
+ if (DBG) log("- updateScreen: OTA_STATUS_ACTIVATION");
+ if (mApp.otaUtils != null) {
+ if (DBG) log("- updateScreen: mApp.otaUtils is not null, "
+ + "call otaShowActivationScreen");
+ mApp.otaUtils.otaShowActivateScreen();
+ }
+ } else {
+ if (DBG) log("- updateScreen: OTA Call end state for Dialogs");
+ if (mApp.otaUtils != null) {
+ if (DBG) log("- updateScreen: Show OTA Success Failure dialog");
+ mApp.otaUtils.otaShowSuccessFailure();
+ }
+ }
+ return; // Return without updating in-call UI.
+ } else if (inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) {
+ if (DBG) log("- updateScreen: manage conference mode (NOT updating in-call UI)...");
+ mCallCard.setVisibility(View.GONE);
+ updateManageConferencePanelIfNecessary();
+ return; // Return without updating in-call UI.
+ } else if (inCallScreenMode == InCallScreenMode.CALL_ENDED) {
+ if (DBG) log("- updateScreen: call ended state...");
+ // Continue with the rest of updateScreen() as usual, since we do
+ // need to update the background (to the special "call ended" color)
+ // and the CallCard (to show the "Call ended" label.)
+ }
+
+ if (DBG) log("- updateScreen: updating the in-call UI...");
+ // Note we update the InCallTouchUi widget before the CallCard,
+ // since the CallCard adjusts its size based on how much vertical
+ // space the InCallTouchUi widget needs.
+ updateInCallTouchUi();
+ mCallCard.updateState(mCM);
+
+ // If an incoming call is ringing, make sure the dialpad is
+ // closed. (We do this to make sure we're not covering up the
+ // "incoming call" UI.)
+ if (mCM.getState() == PhoneConstants.State.RINGING) {
+ if (mDialer.isOpened()) {
+ Log.i(LOG_TAG, "During RINGING state we force hiding dialpad.");
+ closeDialpadInternal(false); // don't do the "closing" animation
+ }
+
+ // At this point, we are guranteed that the dialer is closed.
+ // This means that it is safe to clear out the "history" of DTMF digits
+ // you may have typed into the previous call (so you don't see the
+ // previous call's digits if you answer this call and then bring up the
+ // dialpad.)
+ //
+ // TODO: it would be more precise to do this when you *answer* the
+ // incoming call, rather than as soon as it starts ringing, but
+ // the InCallScreen doesn't keep enough state right now to notice
+ // that specific transition in onPhoneStateChanged().
+ // TODO: This clears out the dialpad context as well so when a second
+ // call comes in while a voicemail call is happening, the voicemail
+ // dialpad will no longer have the "Voice Mail" context. It's a small
+ // case so not terribly bad, but we need to maintain a better
+ // call-to-callstate mapping before we can fix this.
+ mDialer.clearDigits();
+ }
+
+
+ // Now that we're sure DTMF dialpad is in an appropriate state, reflect
+ // the dialpad state into CallCard
+ updateCallCardVisibilityPerDialerState(false);
+
+ updateProgressIndication();
+
+ // Forcibly take down all dialog if an incoming call is ringing.
+ if (mCM.hasActiveRingingCall()) {
+ dismissAllDialogs();
+ } else {
+ // Wait prompt dialog is not currently up. But it *should* be
+ // up if the FG call has a connection in the WAIT state and
+ // the phone isn't ringing.
+ String postDialStr = null;
+ List<Connection> fgConnections = mCM.getFgCallConnections();
+ int phoneType = mCM.getFgPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ Connection fgLatestConnection = mCM.getFgCallLatestConnection();
+ if (mApp.cdmaPhoneCallState.getCurrentCallState() ==
+ CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ for (Connection cn : fgConnections) {
+ if ((cn != null) && (cn.getPostDialState() ==
+ Connection.PostDialState.WAIT)) {
+ cn.cancelPostDial();
+ }
+ }
+ } else if ((fgLatestConnection != null)
+ && (fgLatestConnection.getPostDialState() == Connection.PostDialState.WAIT)) {
+ if(DBG) log("show the Wait dialog for CDMA");
+ postDialStr = fgLatestConnection.getRemainingPostDialString();
+ showWaitPromptDialog(fgLatestConnection, postDialStr);
+ }
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ for (Connection cn : fgConnections) {
+ if ((cn != null) && (cn.getPostDialState() == Connection.PostDialState.WAIT)) {
+ postDialStr = cn.getRemainingPostDialString();
+ showWaitPromptDialog(cn, postDialStr);
+ }
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+ }
+
+ /**
+ * (Re)synchronizes the onscreen UI with the current state of the
+ * telephony framework.
+ *
+ * @return SyncWithPhoneStateStatus.SUCCESS if we successfully updated the UI, or
+ * SyncWithPhoneStateStatus.PHONE_NOT_IN_USE if there was no phone state to sync
+ * with (ie. the phone was completely idle). In the latter case, we
+ * shouldn't even be in the in-call UI in the first place, and it's
+ * the caller's responsibility to bail out of this activity by
+ * calling endInCallScreenSession if appropriate.
+ *
+ * This method directly calls updateScreen() in the normal "phone is
+ * in use" case, so there's no need for the caller to do so.
+ */
+ private SyncWithPhoneStateStatus syncWithPhoneState() {
+ boolean updateSuccessful = false;
+ if (DBG) log("syncWithPhoneState()...");
+ if (DBG) PhoneUtils.dumpCallState(mPhone);
+ if (VDBG) dumpBluetoothState();
+
+ // Make sure the Phone is "in use". (If not, we shouldn't be on
+ // this screen in the first place.)
+
+ // An active or just-ended OTA call counts as "in use".
+ if (TelephonyCapabilities.supportsOtasp(mCM.getFgPhone())
+ && ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED))) {
+ // Even when OTA Call ends, need to show OTA End UI,
+ // so return Success to allow UI update.
+ return SyncWithPhoneStateStatus.SUCCESS;
+ }
+
+ // If an MMI code is running that also counts as "in use".
+ //
+ // TODO: We currently only call getPendingMmiCodes() for GSM
+ // phones. (The code's been that way all along.) But CDMAPhone
+ // does in fact implement getPendingMmiCodes(), so should we
+ // check that here regardless of the phone type?
+ boolean hasPendingMmiCodes =
+ (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)
+ && !mPhone.getPendingMmiCodes().isEmpty();
+
+ // Finally, it's also OK to stay here on the InCallScreen if we
+ // need to display a progress indicator while something's
+ // happening in the background.
+ boolean showProgressIndication = mApp.inCallUiState.isProgressIndicationActive();
+
+ boolean showScreenEvenAfterDisconnect = mApp.inCallUiState.showAlreadyDisconnectedState;
+
+ if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall()
+ || hasPendingMmiCodes || showProgressIndication || showScreenEvenAfterDisconnect) {
+ if (VDBG) log("syncWithPhoneState: it's ok to be here; update the screen...");
+ updateScreen();
+ return SyncWithPhoneStateStatus.SUCCESS;
+ }
+
+ Log.i(LOG_TAG, "syncWithPhoneState: phone is idle (shouldn't be here)");
+ return SyncWithPhoneStateStatus.PHONE_NOT_IN_USE;
+ }
+
+
+
+ private void handleMissingVoiceMailNumber() {
+ if (DBG) log("handleMissingVoiceMailNumber");
+
+ final Message msg = Message.obtain(mHandler);
+ msg.what = DONT_ADD_VOICEMAIL_NUMBER;
+
+ final Message msg2 = Message.obtain(mHandler);
+ msg2.what = ADD_VOICEMAIL_NUMBER;
+
+ mMissingVoicemailDialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_vm_number)
+ .setMessage(R.string.no_vm_number_msg)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (VDBG) log("Missing voicemail AlertDialog: POSITIVE click...");
+ msg.sendToTarget(); // see dontAddVoiceMailNumber()
+ mApp.pokeUserActivity();
+ }})
+ .setNegativeButton(R.string.add_vm_number_str,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (VDBG) log("Missing voicemail AlertDialog: NEGATIVE click...");
+ msg2.sendToTarget(); // see addVoiceMailNumber()
+ mApp.pokeUserActivity();
+ }})
+ .setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ if (VDBG) log("Missing voicemail AlertDialog: CANCEL handler...");
+ msg.sendToTarget(); // see dontAddVoiceMailNumber()
+ mApp.pokeUserActivity();
+ }})
+ .create();
+
+ // When the dialog is up, completely hide the in-call UI
+ // underneath (which is in a partially-constructed state).
+ mMissingVoicemailDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ mMissingVoicemailDialog.show();
+ }
+
+ private void addVoiceMailNumberPanel() {
+ if (mMissingVoicemailDialog != null) {
+ mMissingVoicemailDialog.dismiss();
+ mMissingVoicemailDialog = null;
+ }
+ if (DBG) log("addVoiceMailNumberPanel: finishing InCallScreen...");
+ endInCallScreenSession();
+
+ if (DBG) log("show vm setting");
+
+ // navigate to the Voicemail setting in the Call Settings activity.
+ Intent intent = new Intent(CallFeaturesSetting.ACTION_ADD_VOICEMAIL);
+ intent.setClass(this, CallFeaturesSetting.class);
+ startActivity(intent);
+ }
+
+ private void dontAddVoiceMailNumber() {
+ if (mMissingVoicemailDialog != null) {
+ mMissingVoicemailDialog.dismiss();
+ mMissingVoicemailDialog = null;
+ }
+ if (DBG) log("dontAddVoiceMailNumber: finishing InCallScreen...");
+ endInCallScreenSession();
+ }
+
+ /**
+ * Do some delayed cleanup after a Phone call gets disconnected.
+ *
+ * This method gets called a couple of seconds after any DISCONNECT
+ * event from the Phone; it's triggered by the
+ * DELAYED_CLEANUP_AFTER_DISCONNECT message we send in onDisconnect().
+ *
+ * If the Phone is totally idle right now, that means we've already
+ * shown the "call ended" state for a couple of seconds, and it's now
+ * time to endInCallScreenSession this activity.
+ *
+ * If the Phone is *not* idle right now, that probably means that one
+ * call ended but the other line is still in use. In that case, do
+ * nothing, and instead stay here on the InCallScreen.
+ */
+ private void delayedCleanupAfterDisconnect() {
+ if (VDBG) log("delayedCleanupAfterDisconnect()... Phone state = " + mCM.getState());
+
+ // Clean up any connections in the DISCONNECTED state.
+ //
+ // [Background: Even after a connection gets disconnected, its
+ // Connection object still stays around, in the special
+ // DISCONNECTED state. This is necessary because we we need the
+ // caller-id information from that Connection to properly draw the
+ // "Call ended" state of the CallCard.
+ // But at this point we truly don't need that connection any
+ // more, so tell the Phone that it's now OK to to clean up any
+ // connections still in that state.]
+ mCM.clearDisconnected();
+
+ // There are two cases where we should *not* exit the InCallScreen:
+ // (1) Phone is still in use
+ // or
+ // (2) There's an active progress indication (i.e. the "Retrying..."
+ // progress dialog) that we need to continue to display.
+
+ boolean stayHere = phoneIsInUse() || mApp.inCallUiState.isProgressIndicationActive();
+
+ if (stayHere) {
+ if (DBG) log("- delayedCleanupAfterDisconnect: staying on the InCallScreen...");
+ } else {
+ // Phone is idle! We should exit the in-call UI now.
+ if (DBG) log("- delayedCleanupAfterDisconnect: phone is idle...");
+
+ // And (finally!) exit from the in-call screen
+ // (but not if we're already in the process of pausing...)
+ if (mIsForegroundActivity) {
+ if (DBG) log("- delayedCleanupAfterDisconnect: finishing InCallScreen...");
+
+ // In some cases we finish the call by taking the user to the
+ // Call Log. Otherwise, we simply call endInCallScreenSession,
+ // which will take us back to wherever we came from.
+ //
+ // UI note: In eclair and earlier, we went to the Call Log
+ // after outgoing calls initiated on the device, but never for
+ // incoming calls. Now we do it for incoming calls too, as
+ // long as the call was answered by the user. (We always go
+ // back where you came from after a rejected or missed incoming
+ // call.)
+ //
+ // And in any case, *never* go to the call log if we're in
+ // emergency mode (i.e. if the screen is locked and a lock
+ // pattern or PIN/password is set), or if we somehow got here
+ // on a non-voice-capable device.
+
+ if (VDBG) log("- Post-call behavior:");
+ if (VDBG) log(" - mLastDisconnectCause = " + mLastDisconnectCause);
+ if (VDBG) log(" - isPhoneStateRestricted() = " + isPhoneStateRestricted());
+
+ // DisconnectCause values in the most common scenarios:
+ // - INCOMING_MISSED: incoming ringing call times out, or the
+ // other end hangs up while still ringing
+ // - INCOMING_REJECTED: user rejects the call while ringing
+ // - LOCAL: user hung up while a call was active (after
+ // answering an incoming call, or after making an
+ // outgoing call)
+ // - NORMAL: the other end hung up (after answering an incoming
+ // call, or after making an outgoing call)
+
+ if ((mLastDisconnectCause != Connection.DisconnectCause.INCOMING_MISSED)
+ && (mLastDisconnectCause != Connection.DisconnectCause.INCOMING_REJECTED)
+ && !isPhoneStateRestricted()
+ && PhoneGlobals.sVoiceCapable) {
+ final Intent intent = mApp.createPhoneEndIntentUsingCallOrigin();
+ ActivityOptions opts = ActivityOptions.makeCustomAnimation(this,
+ R.anim.activity_close_enter, R.anim.activity_close_exit);
+ if (VDBG) {
+ log("- Show Call Log (or Dialtacts) after disconnect. Current intent: "
+ + intent);
+ }
+ try {
+ startActivity(intent, opts.toBundle());
+ } catch (ActivityNotFoundException e) {
+ // Don't crash if there's somehow no "Call log" at
+ // all on this device.
+ // (This should never happen, though, since we already
+ // checked PhoneApp.sVoiceCapable above, and any
+ // voice-capable device surely *should* have a call
+ // log activity....)
+ Log.w(LOG_TAG, "delayedCleanupAfterDisconnect: "
+ + "transition to call log failed; intent = " + intent);
+ // ...so just return back where we came from....
+ }
+ // Even if we did go to the call log, note that we still
+ // call endInCallScreenSession (below) to make sure we don't
+ // stay in the activity history.
+ }
+
+ }
+ endInCallScreenSession();
+
+ // Reset the call origin when the session ends and this in-call UI is being finished.
+ mApp.setLatestActiveCallOrigin(null);
+ }
+ }
+
+
+ /**
+ * View.OnClickListener implementation.
+ *
+ * This method handles clicks from UI elements that use the
+ * InCallScreen itself as their OnClickListener.
+ *
+ * Note: Currently this method is used only for a few special buttons:
+ * - the mButtonManageConferenceDone "Back to call" button
+ * - the "dim" effect for the secondary call photo in CallCard as the second "swap" button
+ * - other OTASP-specific buttons managed by OtaUtils.java.
+ *
+ * *Most* in-call controls are handled by the handleOnscreenButtonClick() method, via the
+ * InCallTouchUi widget.
+ */
+ @Override
+ public void onClick(View view) {
+ int id = view.getId();
+ if (VDBG) log("onClick(View " + view + ", id " + id + ")...");
+
+ switch (id) {
+ case R.id.manage_done: // mButtonManageConferenceDone
+ if (VDBG) log("onClick: mButtonManageConferenceDone...");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ requestUpdateScreen();
+ break;
+
+ case R.id.dim_effect_for_secondary_photo:
+ if (mInCallControlState.canSwap) {
+ internalSwapCalls();
+ }
+ break;
+
+ default:
+ // Presumably one of the OTASP-specific buttons managed by
+ // OtaUtils.java.
+ // (TODO: It would be cleaner for the OtaUtils instance itself to
+ // be the OnClickListener for its own buttons.)
+
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL
+ || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ && mApp.otaUtils != null) {
+ mApp.otaUtils.onClickHandler(id);
+ } else {
+ // Uh oh: we *should* only receive clicks here from the
+ // buttons managed by OtaUtils.java, but if we're not in one
+ // of the special OTASP modes, those buttons shouldn't have
+ // been visible in the first place.
+ Log.w(LOG_TAG,
+ "onClick: unexpected click from ID " + id + " (View = " + view + ")");
+ }
+ break;
+ }
+
+ EventLog.writeEvent(EventLogTags.PHONE_UI_BUTTON_CLICK,
+ (view instanceof TextView) ? ((TextView) view).getText() : "");
+
+ // Clicking any onscreen UI element counts as explicit "user activity".
+ mApp.pokeUserActivity();
+ }
+
+ private void onHoldClick() {
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+ log("onHoldClick: hasActiveCall = " + hasActiveCall
+ + ", hasHoldingCall = " + hasHoldingCall);
+ boolean newHoldState;
+ boolean holdButtonEnabled;
+ if (hasActiveCall && !hasHoldingCall) {
+ // There's only one line in use, and that line is active.
+ PhoneUtils.switchHoldingAndActive(
+ mCM.getFirstActiveBgCall()); // Really means "hold" in this state
+ newHoldState = true;
+ holdButtonEnabled = true;
+ } else if (!hasActiveCall && hasHoldingCall) {
+ // There's only one line in use, and that line is on hold.
+ PhoneUtils.switchHoldingAndActive(
+ mCM.getFirstActiveBgCall()); // Really means "unhold" in this state
+ newHoldState = false;
+ holdButtonEnabled = true;
+ } else {
+ // Either zero or 2 lines are in use; "hold/unhold" is meaningless.
+ newHoldState = false;
+ holdButtonEnabled = false;
+ }
+ // No need to forcibly update the onscreen UI; just wait for the
+ // onPhoneStateChanged() callback. (This seems to be responsive
+ // enough.)
+
+ // Also, any time we hold or unhold, force the DTMF dialpad to close.
+ closeDialpadInternal(true); // do the "closing" animation
+ }
+
+ /**
+ * Toggles in-call audio between speaker and the built-in earpiece (or
+ * wired headset.)
+ */
+ public void toggleSpeaker() {
+ // TODO: Turning on the speaker seems to enable the mic
+ // whether or not the "mute" feature is active!
+ // Not sure if this is an feature of the telephony API
+ // that I need to handle specially, or just a bug.
+ boolean newSpeakerState = !PhoneUtils.isSpeakerOn(this);
+ log("toggleSpeaker(): newSpeakerState = " + newSpeakerState);
+
+ if (newSpeakerState && isBluetoothAvailable() && isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ }
+ PhoneUtils.turnOnSpeaker(this, newSpeakerState, true);
+
+ // And update the InCallTouchUi widget (since the "audio mode"
+ // button might need to change its appearance based on the new
+ // audio state.)
+ updateInCallTouchUi();
+ }
+
+ /*
+ * onMuteClick is called only when there is a foreground call
+ */
+ private void onMuteClick() {
+ boolean newMuteState = !PhoneUtils.getMute();
+ log("onMuteClick(): newMuteState = " + newMuteState);
+ PhoneUtils.setMute(newMuteState);
+ }
+
+ /**
+ * Toggles whether or not to route in-call audio to the bluetooth
+ * headset, or do nothing (but log a warning) if no bluetooth device
+ * is actually connected.
+ *
+ * TODO: this method is currently unused, but the "audio mode" UI
+ * design is still in flux so let's keep it around for now.
+ * (But if we ultimately end up *not* providing any way for the UI to
+ * simply "toggle bluetooth", we can get rid of this method.)
+ */
+ public void toggleBluetooth() {
+ if (VDBG) log("toggleBluetooth()...");
+
+ if (isBluetoothAvailable()) {
+ // Toggle the bluetooth audio connection state:
+ if (isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ } else {
+ // Manually turn the speaker phone off, instead of allowing the
+ // Bluetooth audio routing to handle it, since there's other
+ // important state-updating that needs to happen in the
+ // PhoneUtils.turnOnSpeaker() method.
+ // (Similarly, whenever the user turns *on* the speaker, we
+ // manually disconnect the active bluetooth headset;
+ // see toggleSpeaker() and/or switchInCallAudio().)
+ if (PhoneUtils.isSpeakerOn(this)) {
+ PhoneUtils.turnOnSpeaker(this, false, true);
+ }
+
+ connectBluetoothAudio();
+ }
+ } else {
+ // Bluetooth isn't available; the onscreen UI shouldn't have
+ // allowed this request in the first place!
+ Log.w(LOG_TAG, "toggleBluetooth(): bluetooth is unavailable");
+ }
+
+ // And update the InCallTouchUi widget (since the "audio mode"
+ // button might need to change its appearance based on the new
+ // audio state.)
+ updateInCallTouchUi();
+ }
+
+ /**
+ * Switches the current routing of in-call audio between speaker,
+ * bluetooth, and the built-in earpiece (or wired headset.)
+ *
+ * This method is used on devices that provide a single 3-way switch
+ * for audio routing. For devices that provide separate toggles for
+ * Speaker and Bluetooth, see toggleBluetooth() and toggleSpeaker().
+ *
+ * TODO: UI design is still in flux. If we end up totally
+ * eliminating the concept of Speaker and Bluetooth toggle buttons,
+ * we can get rid of toggleBluetooth() and toggleSpeaker().
+ */
+ public void switchInCallAudio(InCallAudioMode newMode) {
+ log("switchInCallAudio: new mode = " + newMode);
+ switch (newMode) {
+ case SPEAKER:
+ if (!PhoneUtils.isSpeakerOn(this)) {
+ // Switch away from Bluetooth, if it was active.
+ if (isBluetoothAvailable() && isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ }
+ PhoneUtils.turnOnSpeaker(this, true, true);
+ }
+ break;
+
+ case BLUETOOTH:
+ // If already connected to BT, there's nothing to do here.
+ if (isBluetoothAvailable() && !isBluetoothAudioConnected()) {
+ // Manually turn the speaker phone off, instead of allowing the
+ // Bluetooth audio routing to handle it, since there's other
+ // important state-updating that needs to happen in the
+ // PhoneUtils.turnOnSpeaker() method.
+ // (Similarly, whenever the user turns *on* the speaker, we
+ // manually disconnect the active bluetooth headset;
+ // see toggleSpeaker() and/or switchInCallAudio().)
+ if (PhoneUtils.isSpeakerOn(this)) {
+ PhoneUtils.turnOnSpeaker(this, false, true);
+ }
+ connectBluetoothAudio();
+ }
+ break;
+
+ case EARPIECE:
+ // Switch to either the handset earpiece, or the wired headset (if connected.)
+ // (Do this by simply making sure both speaker and bluetooth are off.)
+ if (isBluetoothAvailable() && isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ }
+ if (PhoneUtils.isSpeakerOn(this)) {
+ PhoneUtils.turnOnSpeaker(this, false, true);
+ }
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "switchInCallAudio: unexpected mode " + newMode);
+ break;
+ }
+
+ // And finally, update the InCallTouchUi widget (since the "audio
+ // mode" button might need to change its appearance based on the
+ // new audio state.)
+ updateInCallTouchUi();
+ }
+
+ /**
+ * Handle a click on the "Open/Close dialpad" button.
+ *
+ * @see DTMFTwelveKeyDialer#openDialer(boolean)
+ * @see DTMFTwelveKeyDialer#closeDialer(boolean)
+ */
+ private void onOpenCloseDialpad() {
+ if (VDBG) log("onOpenCloseDialpad()...");
+ if (mDialer.isOpened()) {
+ closeDialpadInternal(true); // do the "closing" animation
+ } else {
+ openDialpadInternal(true); // do the "opening" animation
+ }
+ mApp.updateProximitySensorMode(mCM.getState());
+ }
+
+ /** Internal wrapper around {@link DTMFTwelveKeyDialer#openDialer(boolean)} */
+ private void openDialpadInternal(boolean animate) {
+ mDialer.openDialer(animate);
+ // And update the InCallUiState (so that we'll restore the dialpad
+ // to the correct state if we get paused/resumed).
+ mApp.inCallUiState.showDialpad = true;
+ }
+
+ // Internal wrapper around DTMFTwelveKeyDialer.closeDialer()
+ private void closeDialpadInternal(boolean animate) {
+ mDialer.closeDialer(animate);
+ // And update the InCallUiState (so that we'll restore the dialpad
+ // to the correct state if we get paused/resumed).
+ mApp.inCallUiState.showDialpad = false;
+ }
+
+ /**
+ * Handles button clicks from the InCallTouchUi widget.
+ */
+ /* package */ void handleOnscreenButtonClick(int id) {
+ if (DBG) log("handleOnscreenButtonClick(id " + id + ")...");
+
+ switch (id) {
+ // Actions while an incoming call is ringing:
+ case R.id.incomingCallAnswer:
+ internalAnswerCall();
+ break;
+ case R.id.incomingCallReject:
+ hangupRingingCall();
+ break;
+ case R.id.incomingCallRespondViaSms:
+ internalRespondViaSms();
+ break;
+
+ // The other regular (single-tap) buttons used while in-call:
+ case R.id.holdButton:
+ onHoldClick();
+ break;
+ case R.id.swapButton:
+ internalSwapCalls();
+ break;
+ case R.id.endButton:
+ internalHangup();
+ break;
+ case R.id.dialpadButton:
+ onOpenCloseDialpad();
+ break;
+ case R.id.muteButton:
+ onMuteClick();
+ break;
+ case R.id.addButton:
+ PhoneUtils.startNewCall(mCM); // Fires off an ACTION_DIAL intent
+ break;
+ case R.id.mergeButton:
+ case R.id.cdmaMergeButton:
+ PhoneUtils.mergeCalls(mCM);
+ break;
+ case R.id.manageConferenceButton:
+ // Show the Manage Conference panel.
+ setInCallScreenMode(InCallScreenMode.MANAGE_CONFERENCE);
+ requestUpdateScreen();
+ break;
+
+ default:
+ Log.w(LOG_TAG, "handleOnscreenButtonClick: unexpected ID " + id);
+ break;
+ }
+
+ // Clicking any onscreen UI element counts as explicit "user activity".
+ mApp.pokeUserActivity();
+
+ // Just in case the user clicked a "stateful" UI element (like one
+ // of the toggle buttons), we force the in-call buttons to update,
+ // to make sure the user sees the *new* current state.
+ //
+ // Note that some in-call buttons will *not* immediately change the
+ // state of the UI, namely those that send a request to the telephony
+ // layer (like "Hold" or "End call".) For those buttons, the
+ // updateInCallTouchUi() call here won't have any visible effect.
+ // Instead, the UI will be updated eventually when the next
+ // onPhoneStateChanged() event comes in and triggers an updateScreen()
+ // call.
+ //
+ // TODO: updateInCallTouchUi() is overkill here; it would be
+ // more efficient to update *only* the affected button(s).
+ // (But this isn't a big deal since updateInCallTouchUi() is pretty
+ // cheap anyway...)
+ updateInCallTouchUi();
+ }
+
+ /**
+ * Display a status or error indication to the user according to the
+ * specified InCallUiState.CallStatusCode value.
+ */
+ private void showStatusIndication(CallStatusCode status) {
+ switch (status) {
+ case SUCCESS:
+ // The InCallScreen does not need to display any kind of error indication,
+ // so we shouldn't have gotten here in the first place.
+ Log.wtf(LOG_TAG, "showStatusIndication: nothing to display");
+ break;
+
+ case POWER_OFF:
+ // Radio is explictly powered off, presumably because the
+ // device is in airplane mode.
+ //
+ // TODO: For now this UI is ultra-simple: we simply display
+ // a message telling the user to turn off airplane mode.
+ // But it might be nicer for the dialog to offer the option
+ // to turn the radio on right there (and automatically retry
+ // the call once network registration is complete.)
+ showGenericErrorDialog(R.string.incall_error_power_off,
+ true /* isStartupError */);
+ break;
+
+ case EMERGENCY_ONLY:
+ // Only emergency numbers are allowed, but we tried to dial
+ // a non-emergency number.
+ // (This state is currently unused; see comments above.)
+ showGenericErrorDialog(R.string.incall_error_emergency_only,
+ true /* isStartupError */);
+ break;
+
+ case OUT_OF_SERVICE:
+ // No network connection.
+ showGenericErrorDialog(R.string.incall_error_out_of_service,
+ true /* isStartupError */);
+ break;
+
+ case NO_PHONE_NUMBER_SUPPLIED:
+ // The supplied Intent didn't contain a valid phone number.
+ // (This is rare and should only ever happen with broken
+ // 3rd-party apps.) For now just show a generic error.
+ showGenericErrorDialog(R.string.incall_error_no_phone_number_supplied,
+ true /* isStartupError */);
+ break;
+
+ case DIALED_MMI:
+ // Our initial phone number was actually an MMI sequence.
+ // There's no real "error" here, but we do bring up the
+ // a Toast (as requested of the New UI paradigm).
+ //
+ // In-call MMIs do not trigger the normal MMI Initiate
+ // Notifications, so we should notify the user here.
+ // Otherwise, the code in PhoneUtils.java should handle
+ // user notifications in the form of Toasts or Dialogs.
+ if (mCM.getState() == PhoneConstants.State.OFFHOOK) {
+ Toast.makeText(mApp, R.string.incall_status_dialed_mmi, Toast.LENGTH_SHORT)
+ .show();
+ }
+ break;
+
+ case CALL_FAILED:
+ // We couldn't successfully place the call; there was some
+ // failure in the telephony layer.
+ // TODO: Need UI spec for this failure case; for now just
+ // show a generic error.
+ showGenericErrorDialog(R.string.incall_error_call_failed,
+ true /* isStartupError */);
+ break;
+
+ case VOICEMAIL_NUMBER_MISSING:
+ // We tried to call a voicemail: URI but the device has no
+ // voicemail number configured.
+ handleMissingVoiceMailNumber();
+ break;
+
+ case CDMA_CALL_LOST:
+ // This status indicates that InCallScreen should display the
+ // CDMA-specific "call lost" dialog. (If an outgoing call fails,
+ // and the CDMA "auto-retry" feature is enabled, *and* the retried
+ // call fails too, we display this specific dialog.)
+ //
+ // TODO: currently unused; see InCallUiState.needToShowCallLostDialog
+ break;
+
+ case EXITED_ECM:
+ // This status indicates that InCallScreen needs to display a
+ // warning that we're exiting ECM (emergency callback mode).
+ showExitingECMDialog();
+ break;
+
+ default:
+ throw new IllegalStateException(
+ "showStatusIndication: unexpected status code: " + status);
+ }
+
+ // TODO: still need to make sure that pressing OK or BACK from
+ // *any* of the dialogs we launch here ends up calling
+ // inCallUiState.clearPendingCallStatusCode()
+ // *and*
+ // make sure the Dialog handles both OK *and* cancel by calling
+ // endInCallScreenSession. (See showGenericErrorDialog() for an
+ // example.)
+ //
+ // (showGenericErrorDialog() currently does this correctly,
+ // but handleMissingVoiceMailNumber() probably needs to be fixed too.)
+ //
+ // Also need to make sure that bailing out of any of these dialogs by
+ // pressing Home clears out the pending status code too. (If you do
+ // that, neither the dialog's clickListener *or* cancelListener seems
+ // to run...)
+ }
+
+ /**
+ * Utility function to bring up a generic "error" dialog, and then bail
+ * out of the in-call UI when the user hits OK (or the BACK button.)
+ */
+ private void showGenericErrorDialog(int resid, boolean isStartupError) {
+ CharSequence msg = getResources().getText(resid);
+ if (DBG) log("showGenericErrorDialog('" + msg + "')...");
+
+ // create the clicklistener and cancel listener as needed.
+ DialogInterface.OnClickListener clickListener;
+ OnCancelListener cancelListener;
+ if (isStartupError) {
+ clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ bailOutAfterErrorDialog();
+ }};
+ cancelListener = new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ bailOutAfterErrorDialog();
+ }};
+ } else {
+ clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ delayedCleanupAfterDisconnect();
+ }};
+ cancelListener = new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ delayedCleanupAfterDisconnect();
+ }};
+ }
+
+ // TODO: Consider adding a setTitle() call here (with some generic
+ // "failure" title?)
+ mGenericErrorDialog = new AlertDialog.Builder(this)
+ .setMessage(msg)
+ .setPositiveButton(R.string.ok, clickListener)
+ .setOnCancelListener(cancelListener)
+ .create();
+
+ // When the dialog is up, completely hide the in-call UI
+ // underneath (which is in a partially-constructed state).
+ mGenericErrorDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ mGenericErrorDialog.show();
+ }
+
+ private void showCallLostDialog() {
+ if (DBG) log("showCallLostDialog()...");
+
+ // Don't need to show the dialog if InCallScreen isn't in the forgeround
+ if (!mIsForegroundActivity) {
+ if (DBG) log("showCallLostDialog: not the foreground Activity! Bailing out...");
+ return;
+ }
+
+ // Don't need to show the dialog again, if there is one already.
+ if (mCallLostDialog != null) {
+ if (DBG) log("showCallLostDialog: There is a mCallLostDialog already.");
+ return;
+ }
+
+ mCallLostDialog = new AlertDialog.Builder(this)
+ .setMessage(R.string.call_lost)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .create();
+ mCallLostDialog.show();
+ }
+
+ /**
+ * Displays the "Exiting ECM" warning dialog.
+ *
+ * Background: If the phone is currently in ECM (Emergency callback
+ * mode) and we dial a non-emergency number, that automatically
+ * *cancels* ECM. (That behavior comes from CdmaCallTracker.dial().)
+ * When that happens, we need to warn the user that they're no longer
+ * in ECM (bug 4207607.)
+ *
+ * So bring up a dialog explaining what's happening. There's nothing
+ * for the user to do, by the way; we're simply providing an
+ * indication that they're exiting ECM. We *could* use a Toast for
+ * this, but toasts are pretty easy to miss, so instead use a dialog
+ * with a single "OK" button.
+ *
+ * TODO: it's ugly that the code here has to make assumptions about
+ * the behavior of the telephony layer (namely that dialing a
+ * non-emergency number while in ECM causes us to exit ECM.)
+ *
+ * Instead, this warning dialog should really be triggered by our
+ * handler for the
+ * TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED intent in
+ * PhoneApp.java. But that won't work until that intent also
+ * includes a *reason* why we're exiting ECM, since we need to
+ * display this dialog when exiting ECM because of an outgoing call,
+ * but NOT if we're exiting ECM because the user manually turned it
+ * off via the EmergencyCallbackModeExitDialog.
+ *
+ * Or, it might be simpler to just have outgoing non-emergency calls
+ * *not* cancel ECM. That way the UI wouldn't have to do anything
+ * special here.
+ */
+ private void showExitingECMDialog() {
+ Log.i(LOG_TAG, "showExitingECMDialog()...");
+
+ if (mExitingECMDialog != null) {
+ if (DBG) log("- DISMISSING mExitingECMDialog.");
+ mExitingECMDialog.dismiss(); // safe even if already dismissed
+ mExitingECMDialog = null;
+ }
+
+ // When the user dismisses the "Exiting ECM" dialog, we clear out
+ // the pending call status code field (since we're done with this
+ // dialog), but do *not* bail out of the InCallScreen.
+
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ inCallUiState.clearPendingCallStatusCode();
+ }};
+ OnCancelListener cancelListener = new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ inCallUiState.clearPendingCallStatusCode();
+ }};
+
+ // Ultra-simple AlertDialog with only an OK button:
+ mExitingECMDialog = new AlertDialog.Builder(this)
+ .setMessage(R.string.progress_dialog_exiting_ecm)
+ .setPositiveButton(R.string.ok, clickListener)
+ .setOnCancelListener(cancelListener)
+ .create();
+ mExitingECMDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mExitingECMDialog.show();
+ }
+
+ private void bailOutAfterErrorDialog() {
+ if (mGenericErrorDialog != null) {
+ if (DBG) log("bailOutAfterErrorDialog: DISMISSING mGenericErrorDialog.");
+ mGenericErrorDialog.dismiss();
+ mGenericErrorDialog = null;
+ }
+ if (DBG) log("bailOutAfterErrorDialog(): end InCallScreen session...");
+
+ // Now that the user has dismissed the error dialog (presumably by
+ // either hitting the OK button or pressing Back, we can now reset
+ // the pending call status code field.
+ //
+ // (Note that the pending call status is NOT cleared simply
+ // by the InCallScreen being paused or finished, since the resulting
+ // dialog is supposed to persist across orientation changes or if the
+ // screen turns off.)
+ //
+ // See the "Error / diagnostic indications" section of
+ // InCallUiState.java for more detailed info about the
+ // pending call status code field.
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ inCallUiState.clearPendingCallStatusCode();
+
+ // Force the InCallScreen to truly finish(), rather than just
+ // moving it to the back of the activity stack (which is what
+ // our finish() method usually does.)
+ // This is necessary to avoid an obscure scenario where the
+ // InCallScreen can get stuck in an inconsistent state, somehow
+ // causing a *subsequent* outgoing call to fail (bug 4172599).
+ endInCallScreenSession(true /* force a real finish() call */);
+ }
+
+ /**
+ * Dismisses (and nulls out) all persistent Dialogs managed
+ * by the InCallScreen. Useful if (a) we're about to bring up
+ * a dialog and want to pre-empt any currently visible dialogs,
+ * or (b) as a cleanup step when the Activity is going away.
+ */
+ private void dismissAllDialogs() {
+ if (DBG) log("dismissAllDialogs()...");
+
+ // Note it's safe to dismiss() a dialog that's already dismissed.
+ // (Even if the AlertDialog object(s) below are still around, it's
+ // possible that the actual dialog(s) may have already been
+ // dismissed by the user.)
+
+ if (mMissingVoicemailDialog != null) {
+ if (VDBG) log("- DISMISSING mMissingVoicemailDialog.");
+ mMissingVoicemailDialog.dismiss();
+ mMissingVoicemailDialog = null;
+ }
+ if (mMmiStartedDialog != null) {
+ if (VDBG) log("- DISMISSING mMmiStartedDialog.");
+ mMmiStartedDialog.dismiss();
+ mMmiStartedDialog = null;
+ }
+ if (mGenericErrorDialog != null) {
+ if (VDBG) log("- DISMISSING mGenericErrorDialog.");
+ mGenericErrorDialog.dismiss();
+ mGenericErrorDialog = null;
+ }
+ if (mSuppServiceFailureDialog != null) {
+ if (VDBG) log("- DISMISSING mSuppServiceFailureDialog.");
+ mSuppServiceFailureDialog.dismiss();
+ mSuppServiceFailureDialog = null;
+ }
+ if (mWaitPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWaitPromptDialog.");
+ mWaitPromptDialog.dismiss();
+ mWaitPromptDialog = null;
+ }
+ if (mWildPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWildPromptDialog.");
+ mWildPromptDialog.dismiss();
+ mWildPromptDialog = null;
+ }
+ if (mCallLostDialog != null) {
+ if (VDBG) log("- DISMISSING mCallLostDialog.");
+ mCallLostDialog.dismiss();
+ mCallLostDialog = null;
+ }
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL
+ || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ && mApp.otaUtils != null) {
+ mApp.otaUtils.dismissAllOtaDialogs();
+ }
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss();
+ mPausePromptDialog = null;
+ }
+ if (mExitingECMDialog != null) {
+ if (DBG) log("- DISMISSING mExitingECMDialog.");
+ mExitingECMDialog.dismiss();
+ mExitingECMDialog = null;
+ }
+ }
+
+ /**
+ * Updates the state of the onscreen "progress indication" used in
+ * some (relatively rare) scenarios where we need to wait for
+ * something to happen before enabling the in-call UI.
+ *
+ * If necessary, this method will cause a ProgressDialog (i.e. a
+ * spinning wait cursor) to be drawn *on top of* whatever the current
+ * state of the in-call UI is.
+ *
+ * @see InCallUiState.ProgressIndicationType
+ */
+ private void updateProgressIndication() {
+ // If an incoming call is ringing, that takes priority over any
+ // possible value of inCallUiState.progressIndication.
+ if (mCM.hasActiveRingingCall()) {
+ dismissProgressIndication();
+ return;
+ }
+
+ // Otherwise, put up a progress indication if indicated by the
+ // inCallUiState.progressIndication field.
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ switch (inCallUiState.getProgressIndication()) {
+ case NONE:
+ // No progress indication necessary, so make sure it's dismissed.
+ dismissProgressIndication();
+ break;
+
+ case TURNING_ON_RADIO:
+ showProgressIndication(
+ R.string.emergency_enable_radio_dialog_title,
+ R.string.emergency_enable_radio_dialog_message);
+ break;
+
+ case RETRYING:
+ showProgressIndication(
+ R.string.emergency_enable_radio_dialog_title,
+ R.string.emergency_enable_radio_dialog_retry);
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "updateProgressIndication: unexpected value: "
+ + inCallUiState.getProgressIndication());
+ dismissProgressIndication();
+ break;
+ }
+ }
+
+ /**
+ * Show an onscreen "progress indication" with the specified title and message.
+ */
+ private void showProgressIndication(int titleResId, int messageResId) {
+ if (DBG) log("showProgressIndication(message " + messageResId + ")...");
+
+ // TODO: make this be a no-op if the progress indication is
+ // already visible with the exact same title and message.
+
+ dismissProgressIndication(); // Clean up any prior progress indication
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setTitle(getText(titleResId));
+ mProgressDialog.setMessage(getText(messageResId));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ mProgressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mProgressDialog.show();
+ }
+
+ /**
+ * Dismiss the onscreen "progress indication" (if present).
+ */
+ private void dismissProgressIndication() {
+ if (DBG) log("dismissProgressIndication()...");
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss(); // safe even if already dismissed
+ mProgressDialog = null;
+ }
+ }
+
+
+ //
+ // Helper functions for answering incoming calls.
+ //
+
+ /**
+ * Answer a ringing call. This method does nothing if there's no
+ * ringing or waiting call.
+ */
+ private void internalAnswerCall() {
+ if (DBG) log("internalAnswerCall()...");
+ // if (DBG) PhoneUtils.dumpCallState(mPhone);
+
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+
+ if (hasRingingCall) {
+ Phone phone = mCM.getRingingPhone();
+ Call ringing = mCM.getFirstActiveRingingCall();
+ int phoneType = phone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (DBG) log("internalAnswerCall: answering (CDMA)...");
+ if (mCM.hasActiveFgCall()
+ && mCM.getFgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) {
+ // The incoming call is CDMA call and the ongoing
+ // call is a SIP call. The CDMA network does not
+ // support holding an active call, so there's no
+ // way to swap between a CDMA call and a SIP call.
+ // So for now, we just don't allow a CDMA call and
+ // a SIP call to be active at the same time.We'll
+ // "answer incoming, end ongoing" in this case.
+ if (DBG) log("internalAnswerCall: answer "
+ + "CDMA incoming and end SIP ongoing");
+ PhoneUtils.answerAndEndActive(mCM, ringing);
+ } else {
+ PhoneUtils.answerCall(ringing);
+ }
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
+ if (DBG) log("internalAnswerCall: answering (SIP)...");
+ if (mCM.hasActiveFgCall()
+ && mCM.getFgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ // Similar to the PHONE_TYPE_CDMA handling.
+ // The incoming call is SIP call and the ongoing
+ // call is a CDMA call. The CDMA network does not
+ // support holding an active call, so there's no
+ // way to swap between a CDMA call and a SIP call.
+ // So for now, we just don't allow a CDMA call and
+ // a SIP call to be active at the same time.We'll
+ // "answer incoming, end ongoing" in this case.
+ if (DBG) log("internalAnswerCall: answer "
+ + "SIP incoming and end CDMA ongoing");
+ PhoneUtils.answerAndEndActive(mCM, ringing);
+ } else {
+ PhoneUtils.answerCall(ringing);
+ }
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ if (DBG) log("internalAnswerCall: answering (GSM)...");
+ // GSM: this is usually just a wrapper around
+ // PhoneUtils.answerCall(), *but* we also need to do
+ // something special for the "both lines in use" case.
+
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ if (hasActiveCall && hasHoldingCall) {
+ if (DBG) log("internalAnswerCall: answering (both lines in use!)...");
+ // The relatively rare case where both lines are
+ // already in use. We "answer incoming, end ongoing"
+ // in this case, according to the current UI spec.
+ PhoneUtils.answerAndEndActive(mCM, ringing);
+
+ // Alternatively, we could use
+ // PhoneUtils.answerAndEndHolding(mPhone);
+ // here to end the on-hold call instead.
+ } else {
+ if (DBG) log("internalAnswerCall: answering...");
+ PhoneUtils.answerCall(ringing); // Automatically holds the current active call,
+ // if there is one
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ // Call origin is valid only with outgoing calls. Disable it on incoming calls.
+ mApp.setLatestActiveCallOrigin(null);
+ }
+ }
+
+ /**
+ * Hang up the ringing call (aka "Don't answer").
+ */
+ /* package */ void hangupRingingCall() {
+ if (DBG) log("hangupRingingCall()...");
+ if (VDBG) PhoneUtils.dumpCallManager();
+ // In the rare case when multiple calls are ringing, the UI policy
+ // it to always act on the first ringing call.
+ PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall());
+ }
+
+ /**
+ * Silence the ringer (if an incoming call is ringing.)
+ */
+ private void internalSilenceRinger() {
+ if (DBG) log("internalSilenceRinger()...");
+ final CallNotifier notifier = mApp.notifier;
+ if (notifier.isRinging()) {
+ // ringer is actually playing, so silence it.
+ notifier.silenceRinger();
+ }
+ }
+
+ /**
+ * Respond via SMS to the ringing call.
+ * @see RespondViaSmsManager
+ */
+ private void internalRespondViaSms() {
+ log("internalRespondViaSms()...");
+ if (VDBG) PhoneUtils.dumpCallManager();
+
+ // In the rare case when multiple calls are ringing, the UI policy
+ // it to always act on the first ringing call.
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+
+ mRespondViaSmsManager.showRespondViaSmsPopup(ringingCall);
+
+ // Silence the ringer, since it would be distracting while you're trying
+ // to pick a response. (Note that we'll restart the ringer if you bail
+ // out of the popup, though; see RespondViaSmsCancelListener.)
+ internalSilenceRinger();
+ }
+
+ /**
+ * Hang up the current active call.
+ */
+ private void internalHangup() {
+ PhoneConstants.State state = mCM.getState();
+ log("internalHangup()... phone state = " + state);
+
+ // Regardless of the phone state, issue a hangup request.
+ // (If the phone is already idle, this call will presumably have no
+ // effect (but also see the note below.))
+ PhoneUtils.hangup(mCM);
+
+ // If the user just hung up the only active call, we'll eventually exit
+ // the in-call UI after the following sequence:
+ // - When the hangup() succeeds, we'll get a DISCONNECT event from
+ // the telephony layer (see onDisconnect()).
+ // - We immediately switch to the "Call ended" state (see the "delayed
+ // bailout" code path in onDisconnect()) and also post a delayed
+ // DELAYED_CLEANUP_AFTER_DISCONNECT message.
+ // - When the DELAYED_CLEANUP_AFTER_DISCONNECT message comes in (see
+ // delayedCleanupAfterDisconnect()) we do some final cleanup, and exit
+ // this activity unless the phone is still in use (i.e. if there's
+ // another call, or something else going on like an active MMI
+ // sequence.)
+
+ if (state == PhoneConstants.State.IDLE) {
+ // The user asked us to hang up, but the phone was (already) idle!
+ Log.w(LOG_TAG, "internalHangup(): phone is already IDLE!");
+
+ // This is rare, but can happen in a few cases:
+ // (a) If the user quickly double-taps the "End" button. In this case
+ // we'll see that 2nd press event during the brief "Call ended"
+ // state (where the phone is IDLE), or possibly even before the
+ // radio has been able to respond to the initial hangup request.
+ // (b) More rarely, this can happen if the user presses "End" at the
+ // exact moment that the call ends on its own (like because of the
+ // other person hanging up.)
+ // (c) Finally, this could also happen if we somehow get stuck here on
+ // the InCallScreen with the phone truly idle, perhaps due to a
+ // bug where we somehow *didn't* exit when the phone became idle
+ // in the first place.
+
+ // TODO: as a "safety valve" for case (c), consider immediately
+ // bailing out of the in-call UI right here. (The user can always
+ // bail out by pressing Home, of course, but they'll probably try
+ // pressing End first.)
+ //
+ // Log.i(LOG_TAG, "internalHangup(): phone is already IDLE! Bailing out...");
+ // endInCallScreenSession();
+ }
+ }
+
+ /**
+ * InCallScreen-specific wrapper around PhoneUtils.switchHoldingAndActive().
+ */
+ private void internalSwapCalls() {
+ if (DBG) log("internalSwapCalls()...");
+
+ // Any time we swap calls, force the DTMF dialpad to close.
+ // (We want the regular in-call UI to be visible right now, so the
+ // user can clearly see which call is now in the foreground.)
+ closeDialpadInternal(true); // do the "closing" animation
+
+ // Also, clear out the "history" of DTMF digits you typed, to make
+ // sure you don't see digits from call #1 while call #2 is active.
+ // (Yes, this does mean that swapping calls twice will cause you
+ // to lose any previous digits from the current call; see the TODO
+ // comment on DTMFTwelvKeyDialer.clearDigits() for more info.)
+ mDialer.clearDigits();
+
+ // Swap the fg and bg calls.
+ // In the future we may provides some way for user to choose among
+ // multiple background calls, for now, always act on the first background calll.
+ PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall());
+
+ // If we have a valid BluetoothPhoneService then since CDMA network or
+ // Telephony FW does not send us information on which caller got swapped
+ // we need to update the second call active state in BluetoothPhoneService internally
+ if (mCM.getBgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ IBluetoothHeadsetPhone btPhone = mApp.getBluetoothPhoneService();
+ if (btPhone != null) {
+ try {
+ btPhone.cdmaSwapSecondCallState();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Sets the current high-level "mode" of the in-call UI.
+ *
+ * NOTE: if newMode is CALL_ENDED, the caller is responsible for
+ * posting a delayed DELAYED_CLEANUP_AFTER_DISCONNECT message, to make
+ * sure the "call ended" state goes away after a couple of seconds.
+ *
+ * Note this method does NOT refresh of the onscreen UI; the caller is
+ * responsible for calling updateScreen() or requestUpdateScreen() if
+ * necessary.
+ */
+ private void setInCallScreenMode(InCallScreenMode newMode) {
+ if (DBG) log("setInCallScreenMode: " + newMode);
+ mApp.inCallUiState.inCallScreenMode = newMode;
+
+ switch (newMode) {
+ case MANAGE_CONFERENCE:
+ if (!PhoneUtils.isConferenceCall(mCM.getActiveFgCall())) {
+ Log.w(LOG_TAG, "MANAGE_CONFERENCE: no active conference call!");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ return;
+ }
+ List<Connection> connections = mCM.getFgCallConnections();
+ // There almost certainly will be > 1 connection,
+ // since isConferenceCall() just returned true.
+ if ((connections == null) || (connections.size() <= 1)) {
+ Log.w(LOG_TAG,
+ "MANAGE_CONFERENCE: Bogus TRUE from isConferenceCall(); connections = "
+ + connections);
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ return;
+ }
+
+ // TODO: Don't do this here. The call to
+ // initManageConferencePanel() should instead happen
+ // automagically in ManageConferenceUtils the very first
+ // time you call updateManageConferencePanel() or
+ // setPanelVisible(true).
+ mManageConferenceUtils.initManageConferencePanel(); // if necessary
+
+ mManageConferenceUtils.updateManageConferencePanel(connections);
+
+ // The "Manage conference" UI takes up the full main frame,
+ // replacing the CallCard PopupWindow.
+ mManageConferenceUtils.setPanelVisible(true);
+
+ // Start the chronometer.
+ // TODO: Similarly, we shouldn't expose startConferenceTime()
+ // and stopConferenceTime(); the ManageConferenceUtils
+ // class ought to manage the conferenceTime widget itself
+ // based on setPanelVisible() calls.
+
+ // Note: there is active Fg call since we are in conference call
+ long callDuration =
+ mCM.getActiveFgCall().getEarliestConnection().getDurationMillis();
+ mManageConferenceUtils.startConferenceTime(
+ SystemClock.elapsedRealtime() - callDuration);
+
+ // No need to close the dialer here, since the Manage
+ // Conference UI will just cover it up anyway.
+
+ break;
+
+ case CALL_ENDED:
+ case NORMAL:
+ mManageConferenceUtils.setPanelVisible(false);
+ mManageConferenceUtils.stopConferenceTime();
+ break;
+
+ case OTA_NORMAL:
+ mApp.otaUtils.setCdmaOtaInCallScreenUiState(
+ OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL);
+ break;
+
+ case OTA_ENDED:
+ mApp.otaUtils.setCdmaOtaInCallScreenUiState(
+ OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED);
+ break;
+
+ case UNDEFINED:
+ // Set our Activities intent to ACTION_UNDEFINED so
+ // that if we get resumed after we've completed a call
+ // the next call will not cause checkIsOtaCall to
+ // return true.
+ //
+ // TODO(OTASP): update these comments
+ //
+ // With the framework as of October 2009 the sequence below
+ // causes the framework to call onResume, onPause, onNewIntent,
+ // onResume. If we don't call setIntent below then when the
+ // first onResume calls checkIsOtaCall via checkOtaspStateOnResume it will
+ // return true and the Activity will be confused.
+ //
+ // 1) Power up Phone A
+ // 2) Place *22899 call and activate Phone A
+ // 3) Press the power key on Phone A to turn off the display
+ // 4) Call Phone A from Phone B answering Phone A
+ // 5) The screen will be blank (Should be normal InCallScreen)
+ // 6) Hang up the Phone B
+ // 7) Phone A displays the activation screen.
+ //
+ // Step 3 is the critical step to cause the onResume, onPause
+ // onNewIntent, onResume sequence. If step 3 is skipped the
+ // sequence will be onNewIntent, onResume and all will be well.
+ setIntent(new Intent(ACTION_UNDEFINED));
+
+ // Cleanup Ota Screen if necessary and set the panel
+ // to VISIBLE.
+ if (mCM.getState() != PhoneConstants.State.OFFHOOK) {
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.cleanOtaScreen(true);
+ }
+ } else {
+ log("WARNING: Setting mode to UNDEFINED but phone is OFFHOOK,"
+ + " skip cleanOtaScreen.");
+ }
+ break;
+ }
+ }
+
+ /**
+ * @return true if the "Manage conference" UI is currently visible.
+ */
+ /* package */ boolean isManageConferenceMode() {
+ return (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE);
+ }
+
+ /**
+ * Checks if the "Manage conference" UI needs to be updated.
+ * If the state of the current conference call has changed
+ * since our previous call to updateManageConferencePanel()),
+ * do a fresh update. Also, if the current call is no longer a
+ * conference call at all, bail out of the "Manage conference" UI and
+ * return to InCallScreenMode.NORMAL mode.
+ */
+ private void updateManageConferencePanelIfNecessary() {
+ if (VDBG) log("updateManageConferencePanelIfNecessary: " + mCM.getActiveFgCall() + "...");
+
+ List<Connection> connections = mCM.getFgCallConnections();
+ if (connections == null) {
+ if (VDBG) log("==> no connections on foreground call!");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ SyncWithPhoneStateStatus status = syncWithPhoneState();
+ if (status != SyncWithPhoneStateStatus.SUCCESS) {
+ Log.w(LOG_TAG, "- syncWithPhoneState failed! status = " + status);
+ // We shouldn't even be in the in-call UI in the first
+ // place, so bail out:
+ if (DBG) log("updateManageConferencePanelIfNecessary: endInCallScreenSession... 1");
+ endInCallScreenSession();
+ return;
+ }
+ return;
+ }
+
+ int numConnections = connections.size();
+ if (numConnections <= 1) {
+ if (VDBG) log("==> foreground call no longer a conference!");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ SyncWithPhoneStateStatus status = syncWithPhoneState();
+ if (status != SyncWithPhoneStateStatus.SUCCESS) {
+ Log.w(LOG_TAG, "- syncWithPhoneState failed! status = " + status);
+ // We shouldn't even be in the in-call UI in the first
+ // place, so bail out:
+ if (DBG) log("updateManageConferencePanelIfNecessary: endInCallScreenSession... 2");
+ endInCallScreenSession();
+ return;
+ }
+ return;
+ }
+
+ // TODO: the test to see if numConnections has changed can go in
+ // updateManageConferencePanel(), rather than here.
+ if (numConnections != mManageConferenceUtils.getNumCallersInConference()) {
+ if (VDBG) log("==> Conference size has changed; need to rebuild UI!");
+ mManageConferenceUtils.updateManageConferencePanel(connections);
+ }
+ }
+
+ /**
+ * Updates {@link #mCallCard}'s visibility state per DTMF dialpad visibility. They
+ * cannot be shown simultaneously and thus we should reflect DTMF dialpad visibility into
+ * another.
+ *
+ * Note: During OTA calls or users' managing conference calls, we should *not* call this method
+ * but manually manage both visibility.
+ *
+ * @see #updateScreen()
+ */
+ private void updateCallCardVisibilityPerDialerState(boolean animate) {
+ // We need to hide the CallCard while the dialpad is visible.
+ if (isDialerOpened()) {
+ if (VDBG) {
+ log("- updateCallCardVisibilityPerDialerState(animate="
+ + animate + "): dialpad open, hide mCallCard...");
+ }
+ if (animate) {
+ AnimationUtils.Fade.hide(mCallCard, View.GONE);
+ } else {
+ mCallCard.setVisibility(View.GONE);
+ }
+ } else {
+ // Dialpad is dismissed; bring back the CallCard if it's supposed to be visible.
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED)) {
+ if (VDBG) {
+ log("- updateCallCardVisibilityPerDialerState(animate="
+ + animate + "): dialpad dismissed, show mCallCard...");
+ }
+ if (animate) {
+ AnimationUtils.Fade.show(mCallCard);
+ } else {
+ mCallCard.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+ }
+
+ /**
+ * @see DTMFTwelveKeyDialer#isOpened()
+ */
+ /* package */ boolean isDialerOpened() {
+ return (mDialer != null && mDialer.isOpened());
+ }
+
+ /**
+ * Called any time the DTMF dialpad is opened.
+ * @see DTMFTwelveKeyDialer#openDialer(boolean)
+ */
+ /* package */ void onDialerOpen(boolean animate) {
+ if (DBG) log("onDialerOpen()...");
+
+ // Update the in-call touch UI.
+ updateInCallTouchUi();
+
+ // Update CallCard UI, which depends on the dialpad.
+ updateCallCardVisibilityPerDialerState(animate);
+
+ // This counts as explicit "user activity".
+ mApp.pokeUserActivity();
+
+ //If on OTA Call, hide OTA Screen
+ // TODO: This may not be necessary, now that the dialpad is
+ // always visible in OTA mode.
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL
+ || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ && mApp.otaUtils != null) {
+ mApp.otaUtils.hideOtaScreen();
+ }
+ }
+
+ /**
+ * Called any time the DTMF dialpad is closed.
+ * @see DTMFTwelveKeyDialer#closeDialer(boolean)
+ */
+ /* package */ void onDialerClose(boolean animate) {
+ if (DBG) log("onDialerClose()...");
+
+ // OTA-specific cleanup upon closing the dialpad.
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ || ((mApp.cdmaOtaScreenState != null)
+ && (mApp.cdmaOtaScreenState.otaScreenState ==
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) {
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.otaShowProperScreen();
+ }
+ }
+
+ // Update the in-call touch UI.
+ updateInCallTouchUi();
+
+ // Update CallCard UI, which depends on the dialpad.
+ updateCallCardVisibilityPerDialerState(animate);
+
+ // This counts as explicit "user activity".
+ mApp.pokeUserActivity();
+ }
+
+ /**
+ * Determines when we can dial DTMF tones.
+ */
+ /* package */ boolean okToDialDTMFTones() {
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+ final Call.State fgCallState = mCM.getActiveFgCallState();
+
+ // We're allowed to send DTMF tones when there's an ACTIVE
+ // foreground call, and not when an incoming call is ringing
+ // (since DTMF tones are useless in that state), or if the
+ // Manage Conference UI is visible (since the tab interferes
+ // with the "Back to call" button.)
+
+ // We can also dial while in ALERTING state because there are
+ // some connections that never update to an ACTIVE state (no
+ // indication from the network).
+ boolean canDial =
+ (fgCallState == Call.State.ACTIVE || fgCallState == Call.State.ALERTING)
+ && !hasRingingCall
+ && (mApp.inCallUiState.inCallScreenMode != InCallScreenMode.MANAGE_CONFERENCE);
+
+ if (VDBG) log ("[okToDialDTMFTones] foreground state: " + fgCallState +
+ ", ringing state: " + hasRingingCall +
+ ", call screen mode: " + mApp.inCallUiState.inCallScreenMode +
+ ", result: " + canDial);
+
+ return canDial;
+ }
+
+ /**
+ * @return true if the in-call DTMF dialpad should be available to the
+ * user, given the current state of the phone and the in-call UI.
+ * (This is used to control the enabledness of the "Show
+ * dialpad" onscreen button; see InCallControlState.dialpadEnabled.)
+ */
+ /* package */ boolean okToShowDialpad() {
+ // Very similar to okToDialDTMFTones(), but allow DIALING here.
+ final Call.State fgCallState = mCM.getActiveFgCallState();
+ return okToDialDTMFTones() || (fgCallState == Call.State.DIALING);
+ }
+
+ /**
+ * Initializes the in-call touch UI on devices that need it.
+ */
+ private void initInCallTouchUi() {
+ if (DBG) log("initInCallTouchUi()...");
+ // TODO: we currently use the InCallTouchUi widget in at least
+ // some states on ALL platforms. But if some devices ultimately
+ // end up not using *any* onscreen touch UI, we should make sure
+ // to not even inflate the InCallTouchUi widget on those devices.
+ mInCallTouchUi = (InCallTouchUi) findViewById(R.id.inCallTouchUi);
+ mInCallTouchUi.setInCallScreenInstance(this);
+
+ // RespondViaSmsManager implements the "Respond via SMS"
+ // feature that's triggered from the incoming call widget.
+ mRespondViaSmsManager = new RespondViaSmsManager();
+ mRespondViaSmsManager.setInCallScreenInstance(this);
+ }
+
+ /**
+ * Updates the state of the in-call touch UI.
+ */
+ private void updateInCallTouchUi() {
+ if (mInCallTouchUi != null) {
+ mInCallTouchUi.updateState(mCM);
+ }
+ }
+
+ /**
+ * @return the InCallTouchUi widget
+ */
+ /* package */ InCallTouchUi getInCallTouchUi() {
+ return mInCallTouchUi;
+ }
+
+ /**
+ * Posts a handler message telling the InCallScreen to refresh the
+ * onscreen in-call UI.
+ *
+ * This is just a wrapper around updateScreen(), for use by the
+ * rest of the phone app or from a thread other than the UI thread.
+ *
+ * updateScreen() is a no-op if the InCallScreen is not the foreground
+ * activity, so it's safe to call this whether or not the InCallScreen
+ * is currently visible.
+ */
+ /* package */ void requestUpdateScreen() {
+ if (DBG) log("requestUpdateScreen()...");
+ mHandler.removeMessages(REQUEST_UPDATE_SCREEN);
+ mHandler.sendEmptyMessage(REQUEST_UPDATE_SCREEN);
+ }
+
+ /**
+ * @return true if we're in restricted / emergency dialing only mode.
+ */
+ public boolean isPhoneStateRestricted() {
+ // TODO: This needs to work IN TANDEM with the KeyGuardViewMediator Code.
+ // Right now, it looks like the mInputRestricted flag is INTERNAL to the
+ // KeyGuardViewMediator and SPECIFICALLY set to be FALSE while the emergency
+ // phone call is being made, to allow for input into the InCallScreen.
+ // Having the InCallScreen judge the state of the device from this flag
+ // becomes meaningless since it is always false for us. The mediator should
+ // have an additional API to let this app know that it should be restricted.
+ int serviceState = mCM.getServiceState();
+ return ((serviceState == ServiceState.STATE_EMERGENCY_ONLY) ||
+ (serviceState == ServiceState.STATE_OUT_OF_SERVICE) ||
+ (mApp.getKeyguardManager().inKeyguardRestrictedInputMode()));
+ }
+
+
+ //
+ // Bluetooth helper methods.
+ //
+ // - BluetoothAdapter is the Bluetooth system service. If
+ // getDefaultAdapter() returns null
+ // then the device is not BT capable. Use BluetoothDevice.isEnabled()
+ // to see if BT is enabled on the device.
+ //
+ // - BluetoothHeadset is the API for the control connection to a
+ // Bluetooth Headset. This lets you completely connect/disconnect a
+ // headset (which we don't do from the Phone UI!) but also lets you
+ // get the address of the currently active headset and see whether
+ // it's currently connected.
+
+ /**
+ * @return true if the Bluetooth on/off switch in the UI should be
+ * available to the user (i.e. if the device is BT-capable
+ * and a headset is connected.)
+ */
+ /* package */ boolean isBluetoothAvailable() {
+ if (VDBG) log("isBluetoothAvailable()...");
+
+ // There's no need to ask the Bluetooth system service if BT is enabled:
+ //
+ // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ // if ((adapter == null) || !adapter.isEnabled()) {
+ // if (DBG) log(" ==> FALSE (BT not enabled)");
+ // return false;
+ // }
+ // if (DBG) log(" - BT enabled! device name " + adapter.getName()
+ // + ", address " + adapter.getAddress());
+ //
+ // ...since we already have a BluetoothHeadset instance. We can just
+ // call isConnected() on that, and assume it'll be false if BT isn't
+ // enabled at all.
+
+ // Check if there's a connected headset, using the BluetoothHeadset API.
+ boolean isConnected = false;
+ if (mBluetoothHeadset != null) {
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.size() > 0) {
+ BluetoothDevice device = deviceList.get(0);
+ isConnected = true;
+
+ if (VDBG) log(" - headset state = " +
+ mBluetoothHeadset.getConnectionState(device));
+ if (VDBG) log(" - headset address: " + device);
+ if (VDBG) log(" - isConnected: " + isConnected);
+ }
+ }
+
+ if (VDBG) log(" ==> " + isConnected);
+ return isConnected;
+ }
+
+ /**
+ * @return true if a BT Headset is available, and its audio is currently connected.
+ */
+ /* package */ boolean isBluetoothAudioConnected() {
+ if (mBluetoothHeadset == null) {
+ if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
+ return false;
+ }
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.isEmpty()) {
+ return false;
+ }
+ BluetoothDevice device = deviceList.get(0);
+ boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
+ if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
+ return isAudioOn;
+ }
+
+ /**
+ * Helper method used to control the onscreen "Bluetooth" indication;
+ * see InCallControlState.bluetoothIndicatorOn.
+ *
+ * @return true if a BT device is available and its audio is currently connected,
+ * <b>or</b> if we issued a BluetoothHeadset.connectAudio()
+ * call within the last 5 seconds (which presumably means
+ * that the BT audio connection is currently being set
+ * up, and will be connected soon.)
+ */
+ /* package */ boolean isBluetoothAudioConnectedOrPending() {
+ if (isBluetoothAudioConnected()) {
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
+ return true;
+ }
+
+ // If we issued a connectAudio() call "recently enough", even
+ // if BT isn't actually connected yet, let's still pretend BT is
+ // on. This makes the onscreen indication more responsive.
+ if (mBluetoothConnectionPending) {
+ long timeSinceRequest =
+ SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
+ if (timeSinceRequest < 5000 /* 5 seconds */) {
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+ + timeSinceRequest + " msec ago)");
+ return true;
+ } else {
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
+ + timeSinceRequest + " msec ago)");
+ mBluetoothConnectionPending = false;
+ return false;
+ }
+ }
+
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE");
+ return false;
+ }
+
+ /**
+ * Posts a message to our handler saying to update the onscreen UI
+ * based on a bluetooth headset state change.
+ */
+ /* package */ void requestUpdateBluetoothIndication() {
+ if (VDBG) log("requestUpdateBluetoothIndication()...");
+ // No need to look at the current state here; any UI elements that
+ // care about the bluetooth state (i.e. the CallCard) get
+ // the necessary state directly from PhoneApp.showBluetoothIndication().
+ mHandler.removeMessages(REQUEST_UPDATE_BLUETOOTH_INDICATION);
+ mHandler.sendEmptyMessage(REQUEST_UPDATE_BLUETOOTH_INDICATION);
+ }
+
+ private void dumpBluetoothState() {
+ log("============== dumpBluetoothState() =============");
+ log("= isBluetoothAvailable: " + isBluetoothAvailable());
+ log("= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
+ log("= isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
+ log("= PhoneApp.showBluetoothIndication: "
+ + mApp.showBluetoothIndication());
+ log("=");
+ if (mBluetoothAdapter != null) {
+ if (mBluetoothHeadset != null) {
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.size() > 0) {
+ BluetoothDevice device = deviceList.get(0);
+ log("= BluetoothHeadset.getCurrentDevice: " + device);
+ log("= BluetoothHeadset.State: "
+ + mBluetoothHeadset.getConnectionState(device));
+ log("= BluetoothHeadset audio connected: " +
+ mBluetoothHeadset.isAudioConnected(device));
+ }
+ } else {
+ log("= mBluetoothHeadset is null");
+ }
+ } else {
+ log("= mBluetoothAdapter is null; device is not BT capable");
+ }
+ }
+
+ /* package */ void connectBluetoothAudio() {
+ if (VDBG) log("connectBluetoothAudio()...");
+ if (mBluetoothHeadset != null) {
+ // TODO(BT) check return
+ mBluetoothHeadset.connectAudio();
+ }
+
+ // Watch out: The bluetooth connection doesn't happen instantly;
+ // the connectAudio() call returns instantly but does its real
+ // work in another thread. The mBluetoothConnectionPending flag
+ // is just a little trickery to ensure that the onscreen UI updates
+ // instantly. (See isBluetoothAudioConnectedOrPending() above.)
+ mBluetoothConnectionPending = true;
+ mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
+ }
+
+ /* package */ void disconnectBluetoothAudio() {
+ if (VDBG) log("disconnectBluetoothAudio()...");
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.disconnectAudio();
+ }
+ mBluetoothConnectionPending = false;
+ }
+
+ /**
+ * Posts a handler message telling the InCallScreen to close
+ * the OTA failure notice after the specified delay.
+ * @see OtaUtils.otaShowProgramFailureNotice
+ */
+ /* package */ void requestCloseOtaFailureNotice(long timeout) {
+ if (DBG) log("requestCloseOtaFailureNotice() with timeout: " + timeout);
+ mHandler.sendEmptyMessageDelayed(REQUEST_CLOSE_OTA_FAILURE_NOTICE, timeout);
+
+ // TODO: we probably ought to call removeMessages() for this
+ // message code in either onPause or onResume, just to be 100%
+ // sure that the message we just posted has no way to affect a
+ // *different* call if the user quickly backs out and restarts.
+ // (This is also true for requestCloseSpcErrorNotice() below, and
+ // probably anywhere else we use mHandler.sendEmptyMessageDelayed().)
+ }
+
+ /**
+ * Posts a handler message telling the InCallScreen to close
+ * the SPC error notice after the specified delay.
+ * @see OtaUtils.otaShowSpcErrorNotice
+ */
+ /* package */ void requestCloseSpcErrorNotice(long timeout) {
+ if (DBG) log("requestCloseSpcErrorNotice() with timeout: " + timeout);
+ mHandler.sendEmptyMessageDelayed(REQUEST_CLOSE_SPC_ERROR_NOTICE, timeout);
+ }
+
+ public boolean isOtaCallInActiveState() {
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || ((mApp.cdmaOtaScreenState != null)
+ && (mApp.cdmaOtaScreenState.otaScreenState ==
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Handle OTA Call End scenario when display becomes dark during OTA Call
+ * and InCallScreen is in pause mode. CallNotifier will listen for call
+ * end indication and call this api to handle OTA Call end scenario
+ */
+ public void handleOtaCallEnd() {
+ if (DBG) log("handleOtaCallEnd entering");
+ if (((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || ((mApp.cdmaOtaScreenState != null)
+ && (mApp.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED)))
+ && ((mApp.cdmaOtaProvisionData != null)
+ && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) {
+ if (DBG) log("handleOtaCallEnd - Set OTA Call End stater");
+ setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ updateScreen();
+ }
+ }
+
+ public boolean isOtaCallInEndState() {
+ return (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED);
+ }
+
+
+ /**
+ * Upon resuming the in-call UI, check to see if an OTASP call is in
+ * progress, and if so enable the special OTASP-specific UI.
+ *
+ * TODO: have a simple single flag in InCallUiState for this rather than
+ * needing to know about all those mApp.cdma*State objects.
+ *
+ * @return true if any OTASP-related UI is active
+ */
+ private boolean checkOtaspStateOnResume() {
+ // If there's no OtaUtils instance, that means we haven't even tried
+ // to start an OTASP call (yet), so there's definitely nothing to do here.
+ if (mApp.otaUtils == null) {
+ if (DBG) log("checkOtaspStateOnResume: no OtaUtils instance; nothing to do.");
+ return false;
+ }
+
+ if ((mApp.cdmaOtaScreenState == null) || (mApp.cdmaOtaProvisionData == null)) {
+ // Uh oh -- something wrong with our internal OTASP state.
+ // (Since this is an OTASP-capable device, these objects
+ // *should* have already been created by PhoneApp.onCreate().)
+ throw new IllegalStateException("checkOtaspStateOnResume: "
+ + "app.cdmaOta* objects(s) not initialized");
+ }
+
+ // The PhoneApp.cdmaOtaInCallScreenUiState instance is the
+ // authoritative source saying whether or not the in-call UI should
+ // show its OTASP-related UI.
+
+ OtaUtils.CdmaOtaInCallScreenUiState.State cdmaOtaInCallScreenState =
+ mApp.otaUtils.getCdmaOtaInCallScreenUiState();
+ // These states are:
+ // - UNDEFINED: no OTASP-related UI is visible
+ // - NORMAL: OTASP call in progress, so show in-progress OTASP UI
+ // - ENDED: OTASP call just ended, so show success/failure indication
+
+ boolean otaspUiActive =
+ (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL)
+ || (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED);
+
+ if (otaspUiActive) {
+ // Make sure the OtaUtils instance knows about the InCallScreen's
+ // OTASP-related UI widgets.
+ //
+ // (This call has no effect if the UI widgets have already been set up.
+ // It only really matters the very first time that the InCallScreen instance
+ // is onResume()d after starting an OTASP call.)
+ mApp.otaUtils.updateUiWidgets(this, mInCallTouchUi, mCallCard);
+
+ // Also update the InCallScreenMode based on the cdmaOtaInCallScreenState.
+
+ if (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL) {
+ if (DBG) log("checkOtaspStateOnResume - in OTA Normal mode");
+ setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+ } else if (cdmaOtaInCallScreenState ==
+ OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED) {
+ if (DBG) log("checkOtaspStateOnResume - in OTA END mode");
+ setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ }
+
+ // TODO(OTASP): we might also need to go into OTA_ENDED mode
+ // in one extra case:
+ //
+ // else if (mApp.cdmaOtaScreenState.otaScreenState ==
+ // CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG) {
+ // if (DBG) log("checkOtaspStateOnResume - set OTA END Mode");
+ // setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ // }
+
+ } else {
+ // OTASP is not active; reset to regular in-call UI.
+
+ if (DBG) log("checkOtaspStateOnResume - Set OTA NORMAL Mode");
+ setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.cleanOtaScreen(false);
+ }
+ }
+
+ // TODO(OTASP):
+ // The original check from checkIsOtaCall() when handling ACTION_MAIN was this:
+ //
+ // [ . . . ]
+ // else if (action.equals(intent.ACTION_MAIN)) {
+ // if (DBG) log("checkIsOtaCall action ACTION_MAIN");
+ // boolean isRingingCall = mCM.hasActiveRingingCall();
+ // if (isRingingCall) {
+ // if (DBG) log("checkIsOtaCall isRingingCall: " + isRingingCall);
+ // return false;
+ // } else if ((mApp.cdmaOtaInCallScreenUiState.state
+ // == CdmaOtaInCallScreenUiState.State.NORMAL)
+ // || (mApp.cdmaOtaInCallScreenUiState.state
+ // == CdmaOtaInCallScreenUiState.State.ENDED)) {
+ // if (DBG) log("action ACTION_MAIN, OTA call already in progress");
+ // isOtaCall = true;
+ // } else {
+ // if (mApp.cdmaOtaScreenState.otaScreenState !=
+ // CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED) {
+ // if (DBG) log("checkIsOtaCall action ACTION_MAIN, "
+ // + "OTA call in progress with UNDEFINED");
+ // isOtaCall = true;
+ // }
+ // }
+ // }
+ //
+ // Also, in internalResolveIntent() we used to do this:
+ //
+ // if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ // || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)) {
+ // // If in OTA Call, update the OTA UI
+ // updateScreen();
+ // return;
+ // }
+ //
+ // We still need more cleanup to simplify the mApp.cdma*State objects.
+
+ return otaspUiActive;
+ }
+
+ /**
+ * Updates and returns the InCallControlState instance.
+ */
+ public InCallControlState getUpdatedInCallControlState() {
+ if (VDBG) log("getUpdatedInCallControlState()...");
+ mInCallControlState.update();
+ return mInCallControlState;
+ }
+
+ public void resetInCallScreenMode() {
+ if (DBG) log("resetInCallScreenMode: setting mode to UNDEFINED...");
+ setInCallScreenMode(InCallScreenMode.UNDEFINED);
+ }
+
+ /**
+ * Updates the onscreen hint displayed while the user is dragging one
+ * of the handles of the RotarySelector widget used for incoming
+ * calls.
+ *
+ * @param hintTextResId resource ID of the hint text to display,
+ * or 0 if no hint should be visible.
+ * @param hintColorResId resource ID for the color of the hint text
+ */
+ /* package */ void updateIncomingCallWidgetHint(int hintTextResId, int hintColorResId) {
+ if (VDBG) log("updateIncomingCallWidgetHint(" + hintTextResId + ")...");
+ if (mCallCard != null) {
+ mCallCard.setIncomingCallWidgetHint(hintTextResId, hintColorResId);
+ mCallCard.updateState(mCM);
+ // TODO: if hintTextResId == 0, consider NOT clearing the onscreen
+ // hint right away, but instead post a delayed handler message to
+ // keep it onscreen for an extra second or two. (This might make
+ // the hint more helpful if the user quickly taps one of the
+ // handles without dragging at all...)
+ // (Or, maybe this should happen completely within the RotarySelector
+ // widget, since the widget itself probably wants to keep the colored
+ // arrow visible for some extra time also...)
+ }
+ }
+
+
+ /**
+ * Used when we need to update buttons outside InCallTouchUi's updateInCallControls() along
+ * with that method being called. CallCard may call this too because it doesn't have
+ * enough information to update buttons inside itself (more specifically, the class cannot
+ * obtain mInCallControllState without some side effect. See also
+ * {@link #getUpdatedInCallControlState()}. We probably don't want a method like
+ * getRawCallControlState() which returns raw intance with no side effect just for this
+ * corner case scenario)
+ *
+ * TODO: need better design for buttons outside InCallTouchUi.
+ */
+ /* package */ void updateButtonStateOutsideInCallTouchUi() {
+ if (mCallCard != null) {
+ mCallCard.setSecondaryCallClickable(mInCallControlState.canSwap);
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.dispatchPopulateAccessibilityEvent(event);
+ mCallCard.dispatchPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ /**
+ * Manually handle configuration changes.
+ *
+ * Originally android:configChanges was set to "orientation|keyboardHidden|uiMode"
+ * in order "to make sure the system doesn't destroy and re-create us due to the
+ * above config changes". However it is currently set to "keyboardHidden" since
+ * the system needs to handle rotation when inserted into a compatible cardock.
+ * Even without explicitly handling orientation and uiMode, the app still runs
+ * and does not drop the call when rotated.
+ *
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (DBG) log("onConfigurationChanged: newConfig = " + newConfig);
+
+ // Note: At the time this function is called, our Resources object
+ // will have already been updated to return resource values matching
+ // the new configuration.
+
+ // Watch out: we *can* still get destroyed and recreated if a
+ // configuration change occurs that is *not* listed in the
+ // android:configChanges attribute. TODO: Any others we need to list?
+
+ super.onConfigurationChanged(newConfig);
+
+ // Nothing else to do here, since (currently) the InCallScreen looks
+ // exactly the same regardless of configuration.
+ // (Specifically, we'll never be in landscape mode because we set
+ // android:screenOrientation="portrait" in our manifest, and we don't
+ // change our UI at all based on newConfig.keyboardHidden or
+ // newConfig.uiMode.)
+
+ // TODO: we do eventually want to handle at least some config changes, such as:
+ boolean isKeyboardOpen = (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO);
+ if (DBG) log(" - isKeyboardOpen = " + isKeyboardOpen);
+ boolean isLandscape = (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
+ if (DBG) log(" - isLandscape = " + isLandscape);
+ if (DBG) log(" - uiMode = " + newConfig.uiMode);
+ // See bug 2089513.
+ }
+
+ /**
+ * Handles an incoming RING event from the telephony layer.
+ */
+ private void onIncomingRing() {
+ if (DBG) log("onIncomingRing()...");
+ // IFF we're visible, forward this event to the InCallTouchUi
+ // instance (which uses this event to drive the animation of the
+ // incoming-call UI.)
+ if (mIsForegroundActivity && (mInCallTouchUi != null)) {
+ mInCallTouchUi.onIncomingRing();
+ }
+ }
+
+ /**
+ * Handles a "new ringing connection" event from the telephony layer.
+ *
+ * This event comes in right at the start of the incoming-call sequence,
+ * exactly once per incoming call.
+ *
+ * Watch out: this won't be called if InCallScreen isn't ready yet,
+ * which typically happens for the first incoming phone call (even before
+ * the possible first outgoing call).
+ */
+ private void onNewRingingConnection() {
+ if (DBG) log("onNewRingingConnection()...");
+
+ // We use this event to reset any incoming-call-related UI elements
+ // that might have been left in an inconsistent state after a prior
+ // incoming call.
+ // (Note we do this whether or not we're the foreground activity,
+ // since this event comes in *before* we actually get launched to
+ // display the incoming-call UI.)
+
+ // If there's a "Respond via SMS" popup still around since the
+ // last time we were the foreground activity, make sure it's not
+ // still active(!) since that would interfere with *this* incoming
+ // call.
+ // (Note that we also do this same check in onResume(). But we
+ // need it here too, to make sure the popup gets reset in the case
+ // where a call-waiting call comes in while the InCallScreen is
+ // already in the foreground.)
+ mRespondViaSmsManager.dismissPopup(); // safe even if already dismissed
+ }
+
+ /**
+ * Enables or disables the status bar "window shade" based on the current situation.
+ */
+ private void updateExpandedViewState() {
+ if (mIsForegroundActivity) {
+ if (mApp.proximitySensorModeEnabled()) {
+ // We should not enable notification's expanded view on RINGING state.
+ mApp.notificationMgr.statusBarHelper.enableExpandedView(
+ mCM.getState() != PhoneConstants.State.RINGING);
+ } else {
+ // If proximity sensor is unavailable on the device, disable it to avoid false
+ // touches toward notifications.
+ mApp.notificationMgr.statusBarHelper.enableExpandedView(false);
+ }
+ } else {
+ mApp.notificationMgr.statusBarHelper.enableExpandedView(true);
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ /**
+ * Requests to remove provider info frame after having
+ * {@link #PROVIDER_INFO_TIMEOUT}) msec delay.
+ */
+ /* package */ void requestRemoveProviderInfoWithDelay() {
+ // Remove any zombie messages and then send a message to
+ // self to remove the provider info after some time.
+ mHandler.removeMessages(EVENT_HIDE_PROVIDER_INFO);
+ Message msg = Message.obtain(mHandler, EVENT_HIDE_PROVIDER_INFO);
+ mHandler.sendMessageDelayed(msg, PROVIDER_INFO_TIMEOUT);
+ if (DBG) {
+ log("Requested to remove provider info after " + PROVIDER_INFO_TIMEOUT + " msec.");
+ }
+ }
+
+ /**
+ * Indicates whether or not the QuickResponseDialog is currently showing in the call screen
+ */
+ public boolean isQuickResponseDialogShowing() {
+ return mRespondViaSmsManager != null && mRespondViaSmsManager.isShowingPopup();
+ }
+}