diff options
Diffstat (limited to 'src/com/android/phone/InCallTouchUi.java')
-rw-r--r-- | src/com/android/phone/InCallTouchUi.java | 1362 |
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); + } +} |