| /* |
| * 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.LEFT); |
| } |
| |
| 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); |
| } |
| } |