summaryrefslogtreecommitdiff
path: root/src/com/android/phone/InCallTouchUi.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/phone/InCallTouchUi.java')
-rw-r--r--src/com/android/phone/InCallTouchUi.java1362
1 files changed, 1362 insertions, 0 deletions
diff --git a/src/com/android/phone/InCallTouchUi.java b/src/com/android/phone/InCallTouchUi.java
new file mode 100644
index 00000000..9ce0458c
--- /dev/null
+++ b/src/com/android/phone/InCallTouchUi.java
@@ -0,0 +1,1362 @@
+/*
+ * 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;
+
+ // "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);
+
+ // 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:
+ // 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: {
+ 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);
+ }
+
+ if (DBG) {
+ log("At the end of updateInCallControls().");
+ dumpBottomButtonState();
+ }
+ }
+
+ /**
+ * 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);
+ }
+}