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/InCallTouchUi.java b/src/com/android/phone/InCallTouchUi.java
new file mode 100644
index 0000000..a68d066
--- /dev/null
+++ b/src/com/android/phone/InCallTouchUi.java
@@ -0,0 +1,1382 @@
+/*
+ * Copyright (C) 2009 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewStub;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.CompoundButton;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+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.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.widget.multiwaveview.GlowPadView;
+import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener;
+import com.android.phone.InCallUiState.InCallScreenMode;
+
+/**
+ * In-call onscreen touch UI elements, used on some platforms.
+ *
+ * This widget is a fullscreen overlay, drawn on top of the
+ * non-touch-sensitive parts of the in-call UI (i.e. the call card).
+ */
+public class InCallTouchUi extends FrameLayout
+        implements View.OnClickListener, View.OnLongClickListener, OnTriggerListener,
+        PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
+    private static final String LOG_TAG = "InCallTouchUi";
+    private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+    // Incoming call widget targets
+    private static final int ANSWER_CALL_ID = 0;  // drag right
+    private static final int SEND_SMS_ID = 1;  // drag up
+    private static final int DECLINE_CALL_ID = 2;  // drag left
+
+    /**
+     * Reference to the InCallScreen activity that owns us.  This may be
+     * null if we haven't been initialized yet *or* after the InCallScreen
+     * activity has been destroyed.
+     */
+    private InCallScreen mInCallScreen;
+
+    // Phone app instance
+    private PhoneGlobals mApp;
+
+    // UI containers / elements
+    private GlowPadView mIncomingCallWidget;  // UI used for an incoming call
+    private boolean mIncomingCallWidgetIsFadingOut;
+    private boolean mIncomingCallWidgetShouldBeReset = true;
+
+    /** UI elements while on a regular call (bottom buttons, DTMF dialpad) */
+    private View mInCallControls;
+    private boolean mShowInCallControlsDuringHidingAnimation;
+
+    //
+    private ImageButton mAddButton;
+    private ImageButton mMergeButton;
+    private ImageButton mEndButton;
+    private CompoundButton mDialpadButton;
+    private CompoundButton mMuteButton;
+    private CompoundButton mAudioButton;
+    private CompoundButton mHoldButton;
+    private ImageButton mSwapButton;
+    private View mHoldSwapSpacer;
+    private View mVideoSpacer;
+    private ImageButton mVideoButton;
+
+    // "Extra button row"
+    private ViewStub mExtraButtonRow;
+    private ViewGroup mCdmaMergeButton;
+    private ViewGroup mManageConferenceButton;
+    private ImageButton mManageConferenceButtonImage;
+
+    // "Audio mode" PopupMenu
+    private PopupMenu mAudioModePopup;
+    private boolean mAudioModePopupVisible = false;
+
+    // Time of the most recent "answer" or "reject" action (see updateState())
+    private long mLastIncomingCallActionTime;  // in SystemClock.uptimeMillis() time base
+
+    // Parameters for the GlowPadView "ping" animation; see triggerPing().
+    private static final boolean ENABLE_PING_ON_RING_EVENTS = false;
+    private static final boolean ENABLE_PING_AUTO_REPEAT = true;
+    private static final long PING_AUTO_REPEAT_DELAY_MSEC = 1200;
+
+    private static final int INCOMING_CALL_WIDGET_PING = 101;
+    private Handler mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                // If the InCallScreen activity isn't around any more,
+                // there's no point doing anything here.
+                if (mInCallScreen == null) return;
+
+                switch (msg.what) {
+                    case INCOMING_CALL_WIDGET_PING:
+                        if (DBG) log("INCOMING_CALL_WIDGET_PING...");
+                        triggerPing();
+                        break;
+                    default:
+                        Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
+                        break;
+                }
+            }
+        };
+
+    public InCallTouchUi(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        if (DBG) log("InCallTouchUi constructor...");
+        if (DBG) log("- this = " + this);
+        if (DBG) log("- context " + context + ", attrs " + attrs);
+        mApp = PhoneGlobals.getInstance();
+    }
+
+    void setInCallScreenInstance(InCallScreen inCallScreen) {
+        mInCallScreen = inCallScreen;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        if (DBG) log("InCallTouchUi onFinishInflate(this = " + this + ")...");
+
+        // Look up the various UI elements.
+
+        // "Drag-to-answer" widget for incoming calls.
+        mIncomingCallWidget = (GlowPadView) findViewById(R.id.incomingCallWidget);
+        mIncomingCallWidget.setOnTriggerListener(this);
+
+        // Container for the UI elements shown while on a regular call.
+        mInCallControls = findViewById(R.id.inCallControls);
+
+        // Regular (single-tap) buttons, where we listen for click events:
+        // Main cluster of buttons:
+        mAddButton = (ImageButton) mInCallControls.findViewById(R.id.addButton);
+        mAddButton.setOnClickListener(this);
+        mAddButton.setOnLongClickListener(this);
+        mMergeButton = (ImageButton) mInCallControls.findViewById(R.id.mergeButton);
+        mMergeButton.setOnClickListener(this);
+        mMergeButton.setOnLongClickListener(this);
+        mEndButton = (ImageButton) mInCallControls.findViewById(R.id.endButton);
+        mEndButton.setOnClickListener(this);
+        mDialpadButton = (CompoundButton) mInCallControls.findViewById(R.id.dialpadButton);
+        mDialpadButton.setOnClickListener(this);
+        mDialpadButton.setOnLongClickListener(this);
+        mMuteButton = (CompoundButton) mInCallControls.findViewById(R.id.muteButton);
+        mMuteButton.setOnClickListener(this);
+        mMuteButton.setOnLongClickListener(this);
+        mAudioButton = (CompoundButton) mInCallControls.findViewById(R.id.audioButton);
+        mAudioButton.setOnClickListener(this);
+        mAudioButton.setOnLongClickListener(this);
+        mHoldButton = (CompoundButton) mInCallControls.findViewById(R.id.holdButton);
+        mHoldButton.setOnClickListener(this);
+        mHoldButton.setOnLongClickListener(this);
+        mSwapButton = (ImageButton) mInCallControls.findViewById(R.id.swapButton);
+        mSwapButton.setOnClickListener(this);
+        mSwapButton.setOnLongClickListener(this);
+        mHoldSwapSpacer = mInCallControls.findViewById(R.id.holdSwapSpacer);
+        mVideoButton = (ImageButton) mInCallControls.findViewById(R.id.videoCallButton);
+        mVideoButton.setOnClickListener(this);
+        mVideoButton.setOnLongClickListener(this);
+        mVideoSpacer = mInCallControls.findViewById(R.id.videoCallSpacer);
+
+        // TODO: Back when these buttons had text labels, we changed
+        // the label of mSwapButton for CDMA as follows:
+        //
+        //      if (PhoneApp.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+        //          // In CDMA we use a generalized text - "Manage call", as behavior on selecting
+        //          // this option depends entirely on what the current call state is.
+        //          mSwapButtonLabel.setText(R.string.onscreenManageCallsText);
+        //      } else {
+        //          mSwapButtonLabel.setText(R.string.onscreenSwapCallsText);
+        //      }
+        //
+        // If this is still needed, consider having a special icon for this
+        // button in CDMA.
+
+        // Buttons shown on the "extra button row", only visible in certain (rare) states.
+        mExtraButtonRow = (ViewStub) mInCallControls.findViewById(R.id.extraButtonRow);
+
+        // If in PORTRAIT, add a custom OnTouchListener to shrink the "hit target".
+        if (!PhoneUtils.isLandscape(this.getContext())) {
+            mEndButton.setOnTouchListener(new SmallerHitTargetTouchListener());
+        }
+
+    }
+
+    /**
+     * Updates the visibility and/or state of our UI elements, based on
+     * the current state of the phone.
+     *
+     * TODO: This function should be relying on a state defined by InCallScreen,
+     * and not generic call states. The incoming call screen handles more states
+     * than Call.State or PhoneConstant.State know about.
+     */
+    /* package */ void updateState(CallManager cm) {
+        if (mInCallScreen == null) {
+            log("- updateState: mInCallScreen has been destroyed; bailing out...");
+            return;
+        }
+
+        PhoneConstants.State state = cm.getState();  // IDLE, RINGING, or OFFHOOK
+        if (DBG) log("updateState: current state = " + state);
+
+        boolean showIncomingCallControls = false;
+        boolean showInCallControls = false;
+
+        final Call ringingCall = cm.getFirstActiveRingingCall();
+        final Call.State fgCallState = cm.getActiveFgCallState();
+
+        // If the FG call is dialing/alerting, we should display for that call
+        // and ignore the ringing call. This case happens when the telephony
+        // layer rejects the ringing call while the FG call is dialing/alerting,
+        // but the incoming call *does* briefly exist in the DISCONNECTING or
+        // DISCONNECTED state.
+        if ((ringingCall.getState() != Call.State.IDLE) && !fgCallState.isDialing()) {
+            // A phone call is ringing *or* call waiting.
+
+            // Watch out: even if the phone state is RINGING, it's
+            // possible for the ringing call to be in the DISCONNECTING
+            // state.  (This typically happens immediately after the user
+            // rejects an incoming call, and in that case we *don't* show
+            // the incoming call controls.)
+            if (ringingCall.getState().isAlive()) {
+                if (DBG) log("- updateState: RINGING!  Showing incoming call controls...");
+                showIncomingCallControls = true;
+            }
+
+            // Ugly hack to cover up slow response from the radio:
+            // if we get an updateState() call immediately after answering/rejecting a call
+            // (via onTrigger()), *don't* show the incoming call
+            // UI even if the phone is still in the RINGING state.
+            // This covers up a slow response from the radio for some actions.
+            // To detect that situation, we are using "500 msec" heuristics.
+            //
+            // Watch out: we should *not* rely on this behavior when "instant text response" action
+            // has been chosen. See also onTrigger() for why.
+            long now = SystemClock.uptimeMillis();
+            if (now < mLastIncomingCallActionTime + 500) {
+                log("updateState: Too soon after last action; not drawing!");
+                showIncomingCallControls = false;
+            }
+
+            // b/6765896
+            // If the glowview triggers two hits of the respond-via-sms gadget in
+            // quick succession, it can cause the incoming call widget to show and hide
+            // twice in a row.  However, the second hide doesn't get triggered because
+            // we are already attemping to hide.  This causes an additional glowview to
+            // stay up above all other screens.
+            // In reality, we shouldn't even be showing incoming-call UI while we are
+            // showing the respond-via-sms popup, so we check for that here.
+            //
+            // TODO: In the future, this entire state machine
+            // should be reworked.  Respond-via-sms was stapled onto the current
+            // design (and so were other states) and should be made a first-class
+            // citizen in a new state machine.
+            if (mInCallScreen.isQuickResponseDialogShowing()) {
+                log("updateState: quickResponse visible. Cancel showing incoming call controls.");
+                showIncomingCallControls = false;
+            }
+        } else {
+            // Ok, show the regular in-call touch UI (with some exceptions):
+            if (okToShowInCallControls()) {
+                showInCallControls = true;
+            } else {
+                if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
+            }
+        }
+
+        // In usual cases we don't allow showing both incoming call controls and in-call controls.
+        //
+        // There's one exception: if this call is during fading-out animation for the incoming
+        // call controls, we need to show both for smoother transition.
+        if (showIncomingCallControls && showInCallControls) {
+            throw new IllegalStateException(
+                "'Incoming' and 'in-call' touch controls visible at the same time!");
+        }
+        if (mShowInCallControlsDuringHidingAnimation) {
+            if (DBG) {
+                log("- updateState: FORCE showing in-call controls during incoming call widget"
+                        + " being hidden with animation");
+            }
+            showInCallControls = true;
+        }
+
+        // Update visibility and state of the incoming call controls or
+        // the normal in-call controls.
+
+        if (showInCallControls) {
+            if (DBG) log("- updateState: showing in-call controls...");
+            updateInCallControls(cm);
+            mInCallControls.setVisibility(View.VISIBLE);
+        } else {
+            if (DBG) log("- updateState: HIDING in-call controls...");
+            mInCallControls.setVisibility(View.GONE);
+        }
+
+        if (showIncomingCallControls) {
+            if (DBG) log("- updateState: showing incoming call widget...");
+            showIncomingCallWidget(ringingCall);
+
+            // On devices with a system bar (soft buttons at the bottom of
+            // the screen), disable navigation while the incoming-call UI
+            // is up.
+            // This prevents false touches (e.g. on the "Recents" button)
+            // from interfering with the incoming call UI, like if you
+            // accidentally touch the system bar while pulling the phone
+            // out of your pocket.
+            mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(false);
+        } else {
+            if (DBG) log("- updateState: HIDING incoming call widget...");
+            hideIncomingCallWidget();
+
+            // The system bar is allowed to work normally in regular
+            // in-call states.
+            mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true);
+        }
+
+        // Dismiss the "Audio mode" PopupMenu if necessary.
+        //
+        // The "Audio mode" popup is only relevant in call states that support
+        // in-call audio, namely when the phone is OFFHOOK (not RINGING), *and*
+        // the foreground call is either ALERTING (where you can hear the other
+        // end ringing) or ACTIVE (when the call is actually connected.)  In any
+        // state *other* than these, the popup should not be visible.
+
+        if ((state == PhoneConstants.State.OFFHOOK)
+            && (fgCallState == Call.State.ALERTING || fgCallState == Call.State.ACTIVE)) {
+            // The audio mode popup is allowed to be visible in this state.
+            // So if it's up, leave it alone.
+        } else {
+            // The Audio mode popup isn't relevant in this state, so make sure
+            // it's not visible.
+            dismissAudioModePopup();  // safe even if not active
+        }
+    }
+
+    private boolean okToShowInCallControls() {
+        // Note that this method is concerned only with the internal state
+        // of the InCallScreen.  (The InCallTouchUi widget has separate
+        // logic to make sure it's OK to display the touch UI given the
+        // current telephony state, and that it's allowed on the current
+        // device in the first place.)
+
+        // The touch UI is available in the following InCallScreenModes:
+        // - NORMAL (obviously)
+        // - CALL_ENDED (which is intended to look mostly the same as
+        //               a normal in-call state, even though the in-call
+        //               buttons are mostly disabled)
+        // and is hidden in any of the other modes, like MANAGE_CONFERENCE
+        // or one of the OTA modes (which use totally different UIs.)
+
+        return ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL)
+                || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED));
+    }
+
+    @Override
+    public void onClick(View view) {
+        int id = view.getId();
+        if (DBG) log("onClick(View " + view + ", id " + id + ")...");
+
+        switch (id) {
+            case R.id.addButton:
+            case R.id.mergeButton:
+            case R.id.endButton:
+            case R.id.dialpadButton:
+            case R.id.muteButton:
+            case R.id.holdButton:
+            case R.id.swapButton:
+            case R.id.cdmaMergeButton:
+            case R.id.manageConferenceButton:
+            case R.id.videoCallButton:
+                // Clicks on the regular onscreen buttons get forwarded
+                // straight to the InCallScreen.
+                mInCallScreen.handleOnscreenButtonClick(id);
+                break;
+
+            case R.id.audioButton:
+                handleAudioButtonClick();
+                break;
+
+            default:
+                Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
+                break;
+        }
+    }
+
+    @Override
+    public boolean onLongClick(View view) {
+        final int id = view.getId();
+        if (DBG) log("onLongClick(View " + view + ", id " + id + ")...");
+
+        switch (id) {
+            case R.id.addButton:
+            case R.id.mergeButton:
+            case R.id.dialpadButton:
+            case R.id.muteButton:
+            case R.id.holdButton:
+            case R.id.swapButton:
+            case R.id.audioButton:
+            case R.id.videoCallButton: {
+                final CharSequence description = view.getContentDescription();
+                if (!TextUtils.isEmpty(description)) {
+                    // Show description as ActionBar's menu buttons do.
+                    // See also ActionMenuItemView#onLongClick() for the original implementation.
+                    final Toast cheatSheet =
+                            Toast.makeText(view.getContext(), description, Toast.LENGTH_SHORT);
+                    cheatSheet.setGravity(
+                            Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, view.getHeight());
+                    cheatSheet.show();
+                }
+                return true;
+            }
+            default:
+                Log.w(LOG_TAG, "onLongClick() with unexpected View " + view + ". Ignoring it.");
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * Updates the enabledness and "checked" state of the buttons on the
+     * "inCallControls" panel, based on the current telephony state.
+     */
+    private void updateInCallControls(CallManager cm) {
+        int phoneType = cm.getActiveFgCall().getPhone().getPhoneType();
+
+        // Note we do NOT need to worry here about cases where the entire
+        // in-call touch UI is disabled, like during an OTA call or if the
+        // dtmf dialpad is up.  (That's handled by updateState(), which
+        // calls okToShowInCallControls().)
+        //
+        // If we get here, it *is* OK to show the in-call touch UI, so we
+        // now need to update the enabledness and/or "checked" state of
+        // each individual button.
+        //
+
+        // The InCallControlState object tells us the enabledness and/or
+        // state of the various onscreen buttons:
+        InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
+
+        if (DBG) {
+            log("updateInCallControls()...");
+            inCallControlState.dumpState();
+        }
+
+        // "Add" / "Merge":
+        // These two buttons occupy the same space onscreen, so at any
+        // given point exactly one of them must be VISIBLE and the other
+        // must be GONE.
+        if (inCallControlState.canAddCall) {
+            mAddButton.setVisibility(View.VISIBLE);
+            mAddButton.setEnabled(true);
+            mMergeButton.setVisibility(View.GONE);
+        } else if (inCallControlState.canMerge) {
+            if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+                // In CDMA "Add" option is always given to the user and the
+                // "Merge" option is provided as a button on the top left corner of the screen,
+                // we always set the mMergeButton to GONE
+                mMergeButton.setVisibility(View.GONE);
+            } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+                    || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+                mMergeButton.setVisibility(View.VISIBLE);
+                mMergeButton.setEnabled(true);
+                mAddButton.setVisibility(View.GONE);
+            } else {
+                throw new IllegalStateException("Unexpected phone type: " + phoneType);
+            }
+        } else {
+            // Neither "Add" nor "Merge" is available.  (This happens in
+            // some transient states, like while dialing an outgoing call,
+            // and in other rare cases like if you have both lines in use
+            // *and* there are already 5 people on the conference call.)
+            // Since the common case here is "while dialing", we show the
+            // "Add" button in a disabled state so that there won't be any
+            // jarring change in the UI when the call finally connects.
+            mAddButton.setVisibility(View.VISIBLE);
+            mAddButton.setEnabled(false);
+            mMergeButton.setVisibility(View.GONE);
+        }
+        if (inCallControlState.canAddCall && inCallControlState.canMerge) {
+            if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+                    || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+                // Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
+                // should both be available right now.  This *should* never
+                // happen with GSM, but if it's possible on any
+                // future devices we may need to re-layout Add and Merge so
+                // they can both be visible at the same time...
+                Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
+                        " but can't show both!");
+            } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+                // In CDMA "Add" option is always given to the user and the hence
+                // in this case both "Add" and "Merge" options would be available to user
+                if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
+            } else {
+                throw new IllegalStateException("Unexpected phone type: " + phoneType);
+            }
+        }
+
+        // "End call"
+        mEndButton.setEnabled(inCallControlState.canEndCall);
+
+        // "Dialpad": Enabled only when it's OK to use the dialpad in the
+        // first place.
+        mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
+        mDialpadButton.setChecked(inCallControlState.dialpadVisible);
+
+        // "Mute"
+        mMuteButton.setEnabled(inCallControlState.canMute);
+        mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
+
+        // "Audio"
+        updateAudioButton(inCallControlState);
+
+        // "Hold" / "Swap":
+        // These two buttons occupy the same space onscreen, so at any
+        // given point exactly one of them must be VISIBLE and the other
+        // must be GONE.
+        if (inCallControlState.canHold) {
+            mHoldButton.setVisibility(View.VISIBLE);
+            mHoldButton.setEnabled(true);
+            mHoldButton.setChecked(inCallControlState.onHold);
+            mSwapButton.setVisibility(View.GONE);
+            mHoldSwapSpacer.setVisibility(View.VISIBLE);
+        } else if (inCallControlState.canSwap) {
+            mSwapButton.setVisibility(View.VISIBLE);
+            mSwapButton.setEnabled(true);
+            mHoldButton.setVisibility(View.GONE);
+            mHoldSwapSpacer.setVisibility(View.VISIBLE);
+        } else {
+            // Neither "Hold" nor "Swap" is available.  This can happen for two
+            // reasons:
+            //   (1) this is a transient state on a device that *can*
+            //       normally hold or swap, or
+            //   (2) this device just doesn't have the concept of hold/swap.
+            //
+            // In case (1), show the "Hold" button in a disabled state.  In case
+            // (2), remove the button entirely.  (This means that the button row
+            // will only have 4 buttons on some devices.)
+
+            if (inCallControlState.supportsHold) {
+                mHoldButton.setVisibility(View.VISIBLE);
+                mHoldButton.setEnabled(false);
+                mHoldButton.setChecked(false);
+                mSwapButton.setVisibility(View.GONE);
+                mHoldSwapSpacer.setVisibility(View.VISIBLE);
+            } else {
+                mHoldButton.setVisibility(View.GONE);
+                mSwapButton.setVisibility(View.GONE);
+                mHoldSwapSpacer.setVisibility(View.GONE);
+            }
+        }
+        mInCallScreen.updateButtonStateOutsideInCallTouchUi();
+        if (inCallControlState.canSwap && inCallControlState.canHold) {
+            // Uh oh, the InCallControlState thinks that Swap *and* Hold
+            // should both be available.  This *should* never happen with
+            // either GSM or CDMA, but if it's possible on any future
+            // devices we may need to re-layout Hold and Swap so they can
+            // both be visible at the same time...
+            Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
+        }
+
+        if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+            if (inCallControlState.canSwap && inCallControlState.canMerge) {
+                // Uh oh, the InCallControlState thinks that Swap *and* Merge
+                // should both be available.  This *should* never happen with
+                // CDMA, but if it's possible on any future
+                // devices we may need to re-layout Merge and Swap so they can
+                // both be visible at the same time...
+                Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
+                        "enabled, but can't show both!");
+            }
+        }
+
+        // Finally, update the "extra button row": It's displayed above the
+        // "End" button, but only if necessary.  Also, it's never displayed
+        // while the dialpad is visible (since it would overlap.)
+        //
+        // The row contains two buttons:
+        //
+        // - "Manage conference" (used only on GSM devices)
+        // - "Merge" button (used only on CDMA devices)
+        //
+        // Note that mExtraButtonRow is ViewStub, which will be inflated for the first time when
+        // any of its buttons becomes visible.
+        final boolean showCdmaMerge =
+                (phoneType == PhoneConstants.PHONE_TYPE_CDMA) && inCallControlState.canMerge;
+        final boolean showExtraButtonRow =
+                showCdmaMerge || inCallControlState.manageConferenceVisible;
+        if (showExtraButtonRow && !inCallControlState.dialpadVisible) {
+            // This will require the ViewStub inflate itself.
+            mExtraButtonRow.setVisibility(View.VISIBLE);
+
+            // Need to set up mCdmaMergeButton and mManageConferenceButton if this is the first
+            // time they're visible.
+            if (mCdmaMergeButton == null) {
+                setupExtraButtons();
+            }
+            mCdmaMergeButton.setVisibility(showCdmaMerge ? View.VISIBLE : View.GONE);
+            if (inCallControlState.manageConferenceVisible) {
+                mManageConferenceButton.setVisibility(View.VISIBLE);
+                mManageConferenceButtonImage.setEnabled(inCallControlState.manageConferenceEnabled);
+            } else {
+                mManageConferenceButton.setVisibility(View.GONE);
+            }
+        } else {
+            mExtraButtonRow.setVisibility(View.GONE);
+        }
+
+        setupVideoCallButton();
+
+        if (DBG) {
+            log("At the end of updateInCallControls().");
+            dumpBottomButtonState();
+        }
+    }
+
+    /**
+     * Set up the video call button.  Checks the system for any video call providers before
+     * displaying the video chat button.
+     */
+    private void setupVideoCallButton() {
+        // TODO: Check system to see if there are video chat providers and if not, disable the
+        // button.
+    }
+
+
+    /**
+     * Set up the buttons that are part of the "extra button row"
+     */
+    private void setupExtraButtons() {
+        // The two "buttons" here (mCdmaMergeButton and mManageConferenceButton)
+        // are actually layouts containing an icon and a text label side-by-side.
+        mCdmaMergeButton = (ViewGroup) mInCallControls.findViewById(R.id.cdmaMergeButton);
+        if (mCdmaMergeButton == null) {
+            Log.wtf(LOG_TAG, "CDMA Merge button is null even after ViewStub being inflated.");
+            return;
+        }
+        mCdmaMergeButton.setOnClickListener(this);
+
+        mManageConferenceButton =
+                (ViewGroup) mInCallControls.findViewById(R.id.manageConferenceButton);
+        mManageConferenceButton.setOnClickListener(this);
+        mManageConferenceButtonImage =
+                (ImageButton) mInCallControls.findViewById(R.id.manageConferenceButtonImage);
+    }
+
+    private void dumpBottomButtonState() {
+        log(" - dialpad: " + getButtonState(mDialpadButton));
+        log(" - speaker: " + getButtonState(mAudioButton));
+        log(" - mute: " + getButtonState(mMuteButton));
+        log(" - hold: " + getButtonState(mHoldButton));
+        log(" - swap: " + getButtonState(mSwapButton));
+        log(" - add: " + getButtonState(mAddButton));
+        log(" - merge: " + getButtonState(mMergeButton));
+        log(" - cdmaMerge: " + getButtonState(mCdmaMergeButton));
+        log(" - swap: " + getButtonState(mSwapButton));
+        log(" - manageConferenceButton: " + getButtonState(mManageConferenceButton));
+    }
+
+    private static String getButtonState(View view) {
+        if (view == null) {
+            return "(null)";
+        }
+        StringBuilder builder = new StringBuilder();
+        builder.append("visibility: " + (view.getVisibility() == View.VISIBLE ? "VISIBLE"
+                : view.getVisibility() == View.INVISIBLE ? "INVISIBLE" : "GONE"));
+        if (view instanceof ImageButton) {
+            builder.append(", enabled: " + ((ImageButton) view).isEnabled());
+        } else if (view instanceof CompoundButton) {
+            builder.append(", enabled: " + ((CompoundButton) view).isEnabled());
+            builder.append(", checked: " + ((CompoundButton) view).isChecked());
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Updates the onscreen "Audio mode" button based on the current state.
+     *
+     * - If bluetooth is available, this button's function is to bring up the
+     *   "Audio mode" popup (which provides a 3-way choice between earpiece /
+     *   speaker / bluetooth).  So it should look like a regular action button,
+     *   but should also have the small "more_indicator" triangle that indicates
+     *   that a menu will pop up.
+     *
+     * - If speaker (but not bluetooth) is available, this button should look like
+     *   a regular toggle button (and indicate the current speaker state.)
+     *
+     * - If even speaker isn't available, disable the button entirely.
+     */
+    private void updateAudioButton(InCallControlState inCallControlState) {
+        if (DBG) log("updateAudioButton()...");
+
+        // The various layers of artwork for this button come from
+        // btn_compound_audio.xml.  Keep track of which layers we want to be
+        // visible:
+        //
+        // - This selector shows the blue bar below the button icon when
+        //   this button is a toggle *and* it's currently "checked".
+        boolean showToggleStateIndication = false;
+        //
+        // - This is visible if the popup menu is enabled:
+        boolean showMoreIndicator = false;
+        //
+        // - Foreground icons for the button.  Exactly one of these is enabled:
+        boolean showSpeakerOnIcon = false;
+        boolean showSpeakerOffIcon = false;
+        boolean showHandsetIcon = false;
+        boolean showBluetoothIcon = false;
+
+        if (inCallControlState.bluetoothEnabled) {
+            if (DBG) log("- updateAudioButton: 'popup menu action button' mode...");
+
+            mAudioButton.setEnabled(true);
+
+            // The audio button is NOT a toggle in this state.  (And its
+            // setChecked() state is irrelevant since we completely hide the
+            // btn_compound_background layer anyway.)
+
+            // Update desired layers:
+            showMoreIndicator = true;
+            if (inCallControlState.bluetoothIndicatorOn) {
+                showBluetoothIcon = true;
+            } else if (inCallControlState.speakerOn) {
+                showSpeakerOnIcon = true;
+            } else {
+                showHandsetIcon = true;
+                // TODO: if a wired headset is plugged in, that takes precedence
+                // over the handset earpiece.  If so, maybe we should show some
+                // sort of "wired headset" icon here instead of the "handset
+                // earpiece" icon.  (Still need an asset for that, though.)
+            }
+        } else if (inCallControlState.speakerEnabled) {
+            if (DBG) log("- updateAudioButton: 'speaker toggle' mode...");
+
+            mAudioButton.setEnabled(true);
+
+            // The audio button *is* a toggle in this state, and indicates the
+            // current state of the speakerphone.
+            mAudioButton.setChecked(inCallControlState.speakerOn);
+
+            // Update desired layers:
+            showToggleStateIndication = true;
+
+            showSpeakerOnIcon = inCallControlState.speakerOn;
+            showSpeakerOffIcon = !inCallControlState.speakerOn;
+        } else {
+            if (DBG) log("- updateAudioButton: disabled...");
+
+            // The audio button is a toggle in this state, but that's mostly
+            // irrelevant since it's always disabled and unchecked.
+            mAudioButton.setEnabled(false);
+            mAudioButton.setChecked(false);
+
+            // Update desired layers:
+            showToggleStateIndication = true;
+            showSpeakerOffIcon = true;
+        }
+
+        // Finally, update the drawable layers (see btn_compound_audio.xml).
+
+        // Constants used below with Drawable.setAlpha():
+        final int HIDDEN = 0;
+        final int VISIBLE = 255;
+
+        LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
+        if (DBG) log("- 'layers' drawable: " + layers);
+
+        layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
+                .setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.moreIndicatorItem)
+                .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.bluetoothItem)
+                .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.handsetItem)
+                .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
+                .setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
+                .setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
+    }
+
+    /**
+     * Handles a click on the "Audio mode" button.
+     * - If bluetooth is available, bring up the "Audio mode" popup
+     *   (which provides a 3-way choice between earpiece / speaker / bluetooth).
+     * - If bluetooth is *not* available, just toggle between earpiece and
+     *   speaker, with no popup at all.
+     */
+    private void handleAudioButtonClick() {
+        InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
+        if (inCallControlState.bluetoothEnabled) {
+            if (DBG) log("- handleAudioButtonClick: 'popup menu' mode...");
+            showAudioModePopup();
+        } else {
+            if (DBG) log("- handleAudioButtonClick: 'speaker toggle' mode...");
+            mInCallScreen.toggleSpeaker();
+        }
+    }
+
+    /**
+     * Brings up the "Audio mode" popup.
+     */
+    private void showAudioModePopup() {
+        if (DBG) log("showAudioModePopup()...");
+
+        mAudioModePopup = new PopupMenu(mInCallScreen /* context */,
+                                        mAudioButton /* anchorView */);
+        mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
+                                                  mAudioModePopup.getMenu());
+        mAudioModePopup.setOnMenuItemClickListener(this);
+        mAudioModePopup.setOnDismissListener(this);
+
+        // Update the enabled/disabledness of menu items based on the
+        // current call state.
+        InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
+
+        Menu menu = mAudioModePopup.getMenu();
+
+        // TODO: Still need to have the "currently active" audio mode come
+        // up pre-selected (or focused?) with a blue highlight.  Still
+        // need exact visual design, and possibly framework support for this.
+        // See comments below for the exact logic.
+
+        MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
+        speakerItem.setEnabled(inCallControlState.speakerEnabled);
+        // TODO: Show speakerItem as initially "selected" if
+        // inCallControlState.speakerOn is true.
+
+        // We display *either* "earpiece" or "wired headset", never both,
+        // depending on whether a wired headset is physically plugged in.
+        MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
+        MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
+        final boolean usingHeadset = mApp.isHeadsetPlugged();
+        earpieceItem.setVisible(!usingHeadset);
+        earpieceItem.setEnabled(!usingHeadset);
+        wiredHeadsetItem.setVisible(usingHeadset);
+        wiredHeadsetItem.setEnabled(usingHeadset);
+        // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
+        // as initially "selected" if inCallControlState.speakerOn and
+        // inCallControlState.bluetoothIndicatorOn are both false.
+
+        MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
+        bluetoothItem.setEnabled(inCallControlState.bluetoothEnabled);
+        // TODO: Show bluetoothItem as initially "selected" if
+        // inCallControlState.bluetoothIndicatorOn is true.
+
+        mAudioModePopup.show();
+
+        // Unfortunately we need to manually keep track of the popup menu's
+        // visiblity, since PopupMenu doesn't have an isShowing() method like
+        // Dialogs do.
+        mAudioModePopupVisible = true;
+    }
+
+    /**
+     * Dismisses the "Audio mode" popup if it's visible.
+     *
+     * This is safe to call even if the popup is already dismissed, or even if
+     * you never called showAudioModePopup() in the first place.
+     */
+    public void dismissAudioModePopup() {
+        if (mAudioModePopup != null) {
+            mAudioModePopup.dismiss();  // safe even if already dismissed
+            mAudioModePopup = null;
+            mAudioModePopupVisible = false;
+        }
+    }
+
+    /**
+     * Refreshes the "Audio mode" popup if it's visible.  This is useful
+     * (for example) when a wired headset is plugged or unplugged,
+     * since we need to switch back and forth between the "earpiece"
+     * and "wired headset" items.
+     *
+     * This is safe to call even if the popup is already dismissed, or even if
+     * you never called showAudioModePopup() in the first place.
+     */
+    public void refreshAudioModePopup() {
+        if (mAudioModePopup != null && mAudioModePopupVisible) {
+            // Dismiss the previous one
+            mAudioModePopup.dismiss();  // safe even if already dismissed
+            // And bring up a fresh PopupMenu
+            showAudioModePopup();
+        }
+    }
+
+    // PopupMenu.OnMenuItemClickListener implementation; see showAudioModePopup()
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        if (DBG) log("- onMenuItemClick: " + item);
+        if (DBG) log("  id: " + item.getItemId());
+        if (DBG) log("  title: '" + item.getTitle() + "'");
+
+        if (mInCallScreen == null) {
+            Log.w(LOG_TAG, "onMenuItemClick(" + item + "), but null mInCallScreen!");
+            return true;
+        }
+
+        switch (item.getItemId()) {
+            case R.id.audio_mode_speaker:
+                mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.SPEAKER);
+                break;
+            case R.id.audio_mode_earpiece:
+            case R.id.audio_mode_wired_headset:
+                // InCallAudioMode.EARPIECE means either the handset earpiece,
+                // or the wired headset (if connected.)
+                mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.EARPIECE);
+                break;
+            case R.id.audio_mode_bluetooth:
+                mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.BLUETOOTH);
+                break;
+            default:
+                Log.wtf(LOG_TAG,
+                        "onMenuItemClick:  unexpected View ID " + item.getItemId()
+                        + " (MenuItem = '" + item + "')");
+                break;
+        }
+        return true;
+    }
+
+    // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
+    // This gets called when the PopupMenu gets dismissed for *any* reason, like
+    // the user tapping outside its bounds, or pressing Back, or selecting one
+    // of the menu items.
+    @Override
+    public void onDismiss(PopupMenu menu) {
+        if (DBG) log("- onDismiss: " + menu);
+        mAudioModePopupVisible = false;
+    }
+
+    /**
+     * @return the amount of vertical space (in pixels) that needs to be
+     * reserved for the button cluster at the bottom of the screen.
+     * (The CallCard uses this measurement to determine how big
+     * the main "contact photo" area can be.)
+     *
+     * NOTE that this returns the "canonical height" of the main in-call
+     * button cluster, which may not match the amount of vertical space
+     * actually used.  Specifically:
+     *
+     *   - If an incoming call is ringing, the button cluster isn't
+     *     visible at all.  (And the GlowPadView widget is actually
+     *     much taller than the button cluster.)
+     *
+     *   - If the InCallTouchUi widget's "extra button row" is visible
+     *     (in some rare phone states) the button cluster will actually
+     *     be slightly taller than the "canonical height".
+     *
+     * In either of these cases, we allow the bottom edge of the contact
+     * photo to be covered up by whatever UI is actually onscreen.
+     */
+    public int getTouchUiHeight() {
+        // Add up the vertical space consumed by the various rows of buttons.
+        int height = 0;
+
+        // - The main row of buttons:
+        height += (int) getResources().getDimension(R.dimen.in_call_button_height);
+
+        // - The End button:
+        height += (int) getResources().getDimension(R.dimen.in_call_end_button_height);
+
+        // - Note we *don't* consider the InCallTouchUi widget's "extra
+        //   button row" here.
+
+        //- And an extra bit of margin:
+        height += (int) getResources().getDimension(R.dimen.in_call_touch_ui_upper_margin);
+
+        return height;
+    }
+
+
+    //
+    // GlowPadView.OnTriggerListener implementation
+    //
+
+    @Override
+    public void onGrabbed(View v, int handle) {
+
+    }
+
+    @Override
+    public void onReleased(View v, int handle) {
+
+    }
+
+    /**
+     * Handles "Answer" and "Reject" actions for an incoming call.
+     * We get this callback from the incoming call widget
+     * when the user triggers an action.
+     */
+    @Override
+    public void onTrigger(View view, int whichHandle) {
+        if (DBG) log("onTrigger(whichHandle = " + whichHandle + ")...");
+
+        if (mInCallScreen == null) {
+            Log.wtf(LOG_TAG, "onTrigger(" + whichHandle
+                    + ") from incoming-call widget, but null mInCallScreen!");
+            return;
+        }
+
+        // The InCallScreen actually implements all of these actions.
+        // Each possible action from the incoming call widget corresponds
+        // to an R.id value; we pass those to the InCallScreen's "button
+        // click" handler (even though the UI elements aren't actually
+        // buttons; see InCallScreen.handleOnscreenButtonClick().)
+
+        mShowInCallControlsDuringHidingAnimation = false;
+        switch (whichHandle) {
+            case ANSWER_CALL_ID:
+                if (DBG) log("ANSWER_CALL_ID: answer!");
+                mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer);
+                mShowInCallControlsDuringHidingAnimation = true;
+
+                // ...and also prevent it from reappearing right away.
+                // (This covers up a slow response from the radio for some
+                // actions; see updateState().)
+                mLastIncomingCallActionTime = SystemClock.uptimeMillis();
+                break;
+
+            case SEND_SMS_ID:
+                if (DBG) log("SEND_SMS_ID!");
+                mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms);
+
+                // Watch out: mLastIncomingCallActionTime should not be updated for this case.
+                //
+                // The variable is originally for avoiding a problem caused by delayed phone state
+                // update; RINGING state may remain just after answering/declining an incoming
+                // call, so we need to wait a bit (500ms) until we get the effective phone state.
+                // For this case, we shouldn't rely on that hack.
+                //
+                // When the user selects this case, there are two possibilities, neither of which
+                // should rely on the hack.
+                //
+                // 1. The first possibility is that, the device eventually sends one of canned
+                //    responses per the user's "send" request, and reject the call after sending it.
+                //    At that moment the code introducing the canned responses should handle the
+                //    case separately.
+                //
+                // 2. The second possibility is that, the device will show incoming call widget
+                //    again per the user's "cancel" request, where the incoming call will still
+                //    remain. At that moment the incoming call will keep its RINGING state.
+                //    The remaining phone state should never be ignored by the hack for
+                //    answering/declining calls because the RINGING state is legitimate. If we
+                //    use the hack for answer/decline cases, the user loses the incoming call
+                //    widget, until further screen update occurs afterward, which often results in
+                //    missed calls.
+                break;
+
+            case DECLINE_CALL_ID:
+                if (DBG) log("DECLINE_CALL_ID: reject!");
+                mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject);
+
+                // Same as "answer" case.
+                mLastIncomingCallActionTime = SystemClock.uptimeMillis();
+                break;
+
+            default:
+                Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
+                break;
+        }
+
+        // On any action by the user, hide the widget.
+        //
+        // If requested above (i.e. if mShowInCallControlsDuringHidingAnimation is set to true),
+        // in-call controls will start being shown too.
+        //
+        // TODO: The decision to hide this should be made by the controller
+        // (InCallScreen), and not this view.
+        hideIncomingCallWidget();
+
+        // Regardless of what action the user did, be sure to clear out
+        // the hint text we were displaying while the user was dragging.
+        mInCallScreen.updateIncomingCallWidgetHint(0, 0);
+    }
+
+    public void onFinishFinalAnimation() {
+        // Not used
+    }
+
+    /**
+     * Apply an animation to hide the incoming call widget.
+     */
+    private void hideIncomingCallWidget() {
+        if (DBG) log("hideIncomingCallWidget()...");
+        if (mIncomingCallWidget.getVisibility() != View.VISIBLE
+                || mIncomingCallWidgetIsFadingOut) {
+            if (DBG) log("Skipping hideIncomingCallWidget action");
+            // Widget is already hidden or in the process of being hidden
+            return;
+        }
+
+        // Hide the incoming call screen with a transition
+        mIncomingCallWidgetIsFadingOut = true;
+        ViewPropertyAnimator animator = mIncomingCallWidget.animate();
+        animator.cancel();
+        animator.setDuration(AnimationUtils.ANIMATION_DURATION);
+        animator.setListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                if (mShowInCallControlsDuringHidingAnimation) {
+                    if (DBG) log("IncomingCallWidget's hiding animation started");
+                    updateInCallControls(mApp.mCM);
+                    mInCallControls.setVisibility(View.VISIBLE);
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (DBG) log("IncomingCallWidget's hiding animation ended");
+                mIncomingCallWidget.setAlpha(1);
+                mIncomingCallWidget.setVisibility(View.GONE);
+                mIncomingCallWidget.animate().setListener(null);
+                mShowInCallControlsDuringHidingAnimation = false;
+                mIncomingCallWidgetIsFadingOut = false;
+                mIncomingCallWidgetShouldBeReset = true;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mIncomingCallWidget.animate().setListener(null);
+                mShowInCallControlsDuringHidingAnimation = false;
+                mIncomingCallWidgetIsFadingOut = false;
+                mIncomingCallWidgetShouldBeReset = true;
+
+                // Note: the code which reset this animation should be responsible for
+                // alpha and visibility.
+            }
+        });
+        animator.alpha(0f);
+    }
+
+    /**
+     * Shows the incoming call widget and cancels any animation that may be fading it out.
+     */
+    private void showIncomingCallWidget(Call ringingCall) {
+        if (DBG) log("showIncomingCallWidget()...");
+
+        // TODO: wouldn't be ok to suppress this whole request if the widget is already VISIBLE
+        //       and we don't need to reset it?
+        // log("showIncomingCallWidget(). widget visibility: " + mIncomingCallWidget.getVisibility());
+
+        ViewPropertyAnimator animator = mIncomingCallWidget.animate();
+        if (animator != null) {
+            animator.cancel();
+            // If animation is cancelled before it's running,
+            // onAnimationCancel will not be called and mIncomingCallWidgetIsFadingOut
+            // will be alway true. hideIncomingCallWidget() will not be excuted in this case.
+            mIncomingCallWidgetIsFadingOut = false;
+        }
+        mIncomingCallWidget.setAlpha(1.0f);
+
+        // Update the GlowPadView widget's targets based on the state of
+        // the ringing call.  (Specifically, we need to disable the
+        // "respond via SMS" option for certain types of calls, like SIP
+        // addresses or numbers with blocked caller-id.)
+        final boolean allowRespondViaSms =
+                RespondViaSmsManager.allowRespondViaSmsForCall(mInCallScreen, ringingCall);
+        final int targetResourceId = allowRespondViaSms
+                ? R.array.incoming_call_widget_3way_targets
+                : R.array.incoming_call_widget_2way_targets;
+        // The widget should be updated only when appropriate; if the previous choice can be reused
+        // for this incoming call, we'll just keep using it. Otherwise we'll see UI glitch
+        // everytime when this method is called during a single incoming call.
+        if (targetResourceId != mIncomingCallWidget.getTargetResourceId()) {
+            if (allowRespondViaSms) {
+                // The GlowPadView widget is allowed to have all 3 choices:
+                // Answer, Decline, and Respond via SMS.
+                mIncomingCallWidget.setTargetResources(targetResourceId);
+                mIncomingCallWidget.setTargetDescriptionsResourceId(
+                        R.array.incoming_call_widget_3way_target_descriptions);
+                mIncomingCallWidget.setDirectionDescriptionsResourceId(
+                        R.array.incoming_call_widget_3way_direction_descriptions);
+            } else {
+                // You only get two choices: Answer or Decline.
+                mIncomingCallWidget.setTargetResources(targetResourceId);
+                mIncomingCallWidget.setTargetDescriptionsResourceId(
+                        R.array.incoming_call_widget_2way_target_descriptions);
+                mIncomingCallWidget.setDirectionDescriptionsResourceId(
+                        R.array.incoming_call_widget_2way_direction_descriptions);
+            }
+
+            // This will be used right after this block.
+            mIncomingCallWidgetShouldBeReset = true;
+        }
+        if (mIncomingCallWidgetShouldBeReset) {
+            // Watch out: be sure to call reset() and setVisibility() *after*
+            // updating the target resources, since otherwise the GlowPadView
+            // widget will make the targets visible initially (even before you
+            // touch the widget.)
+            mIncomingCallWidget.reset(false);
+            mIncomingCallWidgetShouldBeReset = false;
+        }
+
+        // On an incoming call, if the layout is landscape, then align the "incoming call" text
+        // to the left, because the incomingCallWidget (black background with glowing ring)
+        // is aligned to the right and would cover the "incoming call" text.
+        // Note that callStateLabel is within CallCard, outside of the context of InCallTouchUi
+        if (PhoneUtils.isLandscape(this.getContext())) {
+            TextView callStateLabel = (TextView) mIncomingCallWidget
+                    .getRootView().findViewById(R.id.callStateLabel);
+            if (callStateLabel != null) callStateLabel.setGravity(Gravity.START);
+        }
+
+        mIncomingCallWidget.setVisibility(View.VISIBLE);
+
+        // Finally, manually trigger a "ping" animation.
+        //
+        // Normally, the ping animation is triggered by RING events from
+        // the telephony layer (see onIncomingRing().)  But that *doesn't*
+        // happen for the very first RING event of an incoming call, since
+        // the incoming-call UI hasn't been set up yet at that point!
+        //
+        // So trigger an explicit ping() here, to force the animation to
+        // run when the widget first appears.
+        //
+        mHandler.removeMessages(INCOMING_CALL_WIDGET_PING);
+        mHandler.sendEmptyMessageDelayed(
+                INCOMING_CALL_WIDGET_PING,
+                // Visual polish: add a small delay here, to make the
+                // GlowPadView widget visible for a brief moment
+                // *before* starting the ping animation.
+                // This value doesn't need to be very precise.
+                250 /* msec */);
+    }
+
+    /**
+     * Handles state changes of the incoming-call widget.
+     *
+     * In previous releases (where we used a SlidingTab widget) we would
+     * display an onscreen hint depending on which "handle" the user was
+     * dragging.  But we now use a GlowPadView widget, which has only
+     * one handle, so for now we don't display a hint at all (see the TODO
+     * comment below.)
+     */
+    @Override
+    public void onGrabbedStateChange(View v, int grabbedState) {
+        if (mInCallScreen != null) {
+            // Look up the hint based on which handle is currently grabbed.
+            // (Note we don't simply pass grabbedState thru to the InCallScreen,
+            // since *this* class is the only place that knows that the left
+            // handle means "Answer" and the right handle means "Decline".)
+            int hintTextResId, hintColorResId;
+            switch (grabbedState) {
+                case GlowPadView.OnTriggerListener.NO_HANDLE:
+                case GlowPadView.OnTriggerListener.CENTER_HANDLE:
+                    hintTextResId = 0;
+                    hintColorResId = 0;
+                    break;
+                default:
+                    Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
+                          + grabbedState);
+                    hintTextResId = 0;
+                    hintColorResId = 0;
+                    break;
+            }
+
+            // Tell the InCallScreen to update the CallCard and force the
+            // screen to redraw.
+            mInCallScreen.updateIncomingCallWidgetHint(hintTextResId, hintColorResId);
+        }
+    }
+
+    /**
+     * Handles an incoming RING event from the telephony layer.
+     */
+    public void onIncomingRing() {
+        if (ENABLE_PING_ON_RING_EVENTS) {
+            // Each RING from the telephony layer triggers a "ping" animation
+            // of the GlowPadView widget.  (The intent here is to make the
+            // pinging appear to be synchronized with the ringtone, although
+            // that only works for non-looping ringtones.)
+            triggerPing();
+        }
+    }
+
+    /**
+     * Runs a single "ping" animation of the GlowPadView widget,
+     * or do nothing if the GlowPadView widget is no longer visible.
+     *
+     * Also, if ENABLE_PING_AUTO_REPEAT is true, schedule the next ping as
+     * well (but again, only if the GlowPadView widget is still visible.)
+     */
+    public void triggerPing() {
+        if (DBG) log("triggerPing: mIncomingCallWidget = " + mIncomingCallWidget);
+
+        if (!mInCallScreen.isForegroundActivity()) {
+            // InCallScreen has been dismissed; no need to run a ping *or*
+            // schedule another one.
+            log("- triggerPing: InCallScreen no longer in foreground; ignoring...");
+            return;
+        }
+
+        if (mIncomingCallWidget == null) {
+            // This shouldn't happen; the GlowPadView widget should
+            // always be present in our layout file.
+            Log.w(LOG_TAG, "- triggerPing: null mIncomingCallWidget!");
+            return;
+        }
+
+        if (DBG) log("- triggerPing: mIncomingCallWidget visibility = "
+                     + mIncomingCallWidget.getVisibility());
+
+        if (mIncomingCallWidget.getVisibility() != View.VISIBLE) {
+            if (DBG) log("- triggerPing: mIncomingCallWidget no longer visible; ignoring...");
+            return;
+        }
+
+        // Ok, run a ping (and schedule the next one too, if desired...)
+
+        mIncomingCallWidget.ping();
+
+        if (ENABLE_PING_AUTO_REPEAT) {
+            // Schedule the next ping.  (ENABLE_PING_AUTO_REPEAT mode
+            // allows the ping animation to repeat much faster than in
+            // the ENABLE_PING_ON_RING_EVENTS case, since telephony RING
+            // events come fairly slowly (about 3 seconds apart.))
+
+            // No need to check here if the call is still ringing, by
+            // the way, since we hide mIncomingCallWidget as soon as the
+            // ringing stops, or if the user answers.  (And at that
+            // point, any future triggerPing() call will be a no-op.)
+
+            // TODO: Rather than having a separate timer here, maybe try
+            // having these pings synchronized with the vibrator (see
+            // VibratorThread in Ringer.java; we'd just need to get
+            // events routed from there to here, probably via the
+            // PhoneApp instance.)  (But watch out: make sure pings
+            // still work even if the Vibrate setting is turned off!)
+
+            mHandler.sendEmptyMessageDelayed(INCOMING_CALL_WIDGET_PING,
+                                             PING_AUTO_REPEAT_DELAY_MSEC);
+        }
+    }
+
+    // Debugging / testing code
+
+    private void log(String msg) {
+        Log.d(LOG_TAG, msg);
+    }
+}