Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.phone;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.content.Context;
     22 import android.graphics.drawable.LayerDrawable;
     23 import android.os.Handler;
     24 import android.os.Message;
     25 import android.os.SystemClock;
     26 import android.text.TextUtils;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.view.Gravity;
     30 import android.view.Menu;
     31 import android.view.MenuItem;
     32 import android.view.MotionEvent;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 import android.view.ViewPropertyAnimator;
     36 import android.view.ViewStub;
     37 import android.view.animation.AlphaAnimation;
     38 import android.view.animation.Animation;
     39 import android.view.animation.Animation.AnimationListener;
     40 import android.widget.CompoundButton;
     41 import android.widget.FrameLayout;
     42 import android.widget.ImageButton;
     43 import android.widget.PopupMenu;
     44 import android.widget.TextView;
     45 import android.widget.Toast;
     46 
     47 import com.android.internal.telephony.Call;
     48 import com.android.internal.telephony.CallManager;
     49 import com.android.internal.telephony.Phone;
     50 import com.android.internal.telephony.PhoneConstants;
     51 import com.android.internal.widget.multiwaveview.GlowPadView;
     52 import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener;
     53 import com.android.phone.InCallUiState.InCallScreenMode;
     54 
     55 /**
     56  * In-call onscreen touch UI elements, used on some platforms.
     57  *
     58  * This widget is a fullscreen overlay, drawn on top of the
     59  * non-touch-sensitive parts of the in-call UI (i.e. the call card).
     60  */
     61 public class InCallTouchUi extends FrameLayout
     62         implements View.OnClickListener, View.OnLongClickListener, OnTriggerListener,
     63         PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
     64     private static final String LOG_TAG = "InCallTouchUi";
     65     private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
     66 
     67     // Incoming call widget targets
     68     private static final int ANSWER_CALL_ID = 0;  // drag right
     69     private static final int SEND_SMS_ID = 1;  // drag up
     70     private static final int DECLINE_CALL_ID = 2;  // drag left
     71 
     72     /**
     73      * Reference to the InCallScreen activity that owns us.  This may be
     74      * null if we haven't been initialized yet *or* after the InCallScreen
     75      * activity has been destroyed.
     76      */
     77     private InCallScreen mInCallScreen;
     78 
     79     // Phone app instance
     80     private PhoneGlobals mApp;
     81 
     82     // UI containers / elements
     83     private GlowPadView mIncomingCallWidget;  // UI used for an incoming call
     84     private boolean mIncomingCallWidgetIsFadingOut;
     85     private boolean mIncomingCallWidgetShouldBeReset = true;
     86 
     87     /** UI elements while on a regular call (bottom buttons, DTMF dialpad) */
     88     private View mInCallControls;
     89     private boolean mShowInCallControlsDuringHidingAnimation;
     90 
     91     //
     92     private ImageButton mAddButton;
     93     private ImageButton mMergeButton;
     94     private ImageButton mEndButton;
     95     private CompoundButton mDialpadButton;
     96     private CompoundButton mMuteButton;
     97     private CompoundButton mAudioButton;
     98     private CompoundButton mHoldButton;
     99     private ImageButton mSwapButton;
    100     private View mHoldSwapSpacer;
    101 
    102     // "Extra button row"
    103     private ViewStub mExtraButtonRow;
    104     private ViewGroup mCdmaMergeButton;
    105     private ViewGroup mManageConferenceButton;
    106     private ImageButton mManageConferenceButtonImage;
    107 
    108     // "Audio mode" PopupMenu
    109     private PopupMenu mAudioModePopup;
    110     private boolean mAudioModePopupVisible = false;
    111 
    112     // Time of the most recent "answer" or "reject" action (see updateState())
    113     private long mLastIncomingCallActionTime;  // in SystemClock.uptimeMillis() time base
    114 
    115     // Parameters for the GlowPadView "ping" animation; see triggerPing().
    116     private static final boolean ENABLE_PING_ON_RING_EVENTS = false;
    117     private static final boolean ENABLE_PING_AUTO_REPEAT = true;
    118     private static final long PING_AUTO_REPEAT_DELAY_MSEC = 1200;
    119 
    120     private static final int INCOMING_CALL_WIDGET_PING = 101;
    121     private Handler mHandler = new Handler() {
    122             @Override
    123             public void handleMessage(Message msg) {
    124                 // If the InCallScreen activity isn't around any more,
    125                 // there's no point doing anything here.
    126                 if (mInCallScreen == null) return;
    127 
    128                 switch (msg.what) {
    129                     case INCOMING_CALL_WIDGET_PING:
    130                         if (DBG) log("INCOMING_CALL_WIDGET_PING...");
    131                         triggerPing();
    132                         break;
    133                     default:
    134                         Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
    135                         break;
    136                 }
    137             }
    138         };
    139 
    140     public InCallTouchUi(Context context, AttributeSet attrs) {
    141         super(context, attrs);
    142 
    143         if (DBG) log("InCallTouchUi constructor...");
    144         if (DBG) log("- this = " + this);
    145         if (DBG) log("- context " + context + ", attrs " + attrs);
    146         mApp = PhoneGlobals.getInstance();
    147     }
    148 
    149     void setInCallScreenInstance(InCallScreen inCallScreen) {
    150         mInCallScreen = inCallScreen;
    151     }
    152 
    153     @Override
    154     protected void onFinishInflate() {
    155         super.onFinishInflate();
    156         if (DBG) log("InCallTouchUi onFinishInflate(this = " + this + ")...");
    157 
    158         // Look up the various UI elements.
    159 
    160         // "Drag-to-answer" widget for incoming calls.
    161         mIncomingCallWidget = (GlowPadView) findViewById(R.id.incomingCallWidget);
    162         mIncomingCallWidget.setOnTriggerListener(this);
    163 
    164         // Container for the UI elements shown while on a regular call.
    165         mInCallControls = findViewById(R.id.inCallControls);
    166 
    167         // Regular (single-tap) buttons, where we listen for click events:
    168         // Main cluster of buttons:
    169         mAddButton = (ImageButton) mInCallControls.findViewById(R.id.addButton);
    170         mAddButton.setOnClickListener(this);
    171         mAddButton.setOnLongClickListener(this);
    172         mMergeButton = (ImageButton) mInCallControls.findViewById(R.id.mergeButton);
    173         mMergeButton.setOnClickListener(this);
    174         mMergeButton.setOnLongClickListener(this);
    175         mEndButton = (ImageButton) mInCallControls.findViewById(R.id.endButton);
    176         mEndButton.setOnClickListener(this);
    177         mDialpadButton = (CompoundButton) mInCallControls.findViewById(R.id.dialpadButton);
    178         mDialpadButton.setOnClickListener(this);
    179         mDialpadButton.setOnLongClickListener(this);
    180         mMuteButton = (CompoundButton) mInCallControls.findViewById(R.id.muteButton);
    181         mMuteButton.setOnClickListener(this);
    182         mMuteButton.setOnLongClickListener(this);
    183         mAudioButton = (CompoundButton) mInCallControls.findViewById(R.id.audioButton);
    184         mAudioButton.setOnClickListener(this);
    185         mAudioButton.setOnLongClickListener(this);
    186         mHoldButton = (CompoundButton) mInCallControls.findViewById(R.id.holdButton);
    187         mHoldButton.setOnClickListener(this);
    188         mHoldButton.setOnLongClickListener(this);
    189         mSwapButton = (ImageButton) mInCallControls.findViewById(R.id.swapButton);
    190         mSwapButton.setOnClickListener(this);
    191         mSwapButton.setOnLongClickListener(this);
    192         mHoldSwapSpacer = mInCallControls.findViewById(R.id.holdSwapSpacer);
    193 
    194         // TODO: Back when these buttons had text labels, we changed
    195         // the label of mSwapButton for CDMA as follows:
    196         //
    197         //      if (PhoneApp.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
    198         //          // In CDMA we use a generalized text - "Manage call", as behavior on selecting
    199         //          // this option depends entirely on what the current call state is.
    200         //          mSwapButtonLabel.setText(R.string.onscreenManageCallsText);
    201         //      } else {
    202         //          mSwapButtonLabel.setText(R.string.onscreenSwapCallsText);
    203         //      }
    204         //
    205         // If this is still needed, consider having a special icon for this
    206         // button in CDMA.
    207 
    208         // Buttons shown on the "extra button row", only visible in certain (rare) states.
    209         mExtraButtonRow = (ViewStub) mInCallControls.findViewById(R.id.extraButtonRow);
    210 
    211         // If in PORTRAIT, add a custom OnTouchListener to shrink the "hit target".
    212         if (!PhoneUtils.isLandscape(this.getContext())) {
    213             mEndButton.setOnTouchListener(new SmallerHitTargetTouchListener());
    214         }
    215 
    216     }
    217 
    218     /**
    219      * Updates the visibility and/or state of our UI elements, based on
    220      * the current state of the phone.
    221      *
    222      * TODO: This function should be relying on a state defined by InCallScreen,
    223      * and not generic call states. The incoming call screen handles more states
    224      * than Call.State or PhoneConstant.State know about.
    225      */
    226     /* package */ void updateState(CallManager cm) {
    227         if (mInCallScreen == null) {
    228             log("- updateState: mInCallScreen has been destroyed; bailing out...");
    229             return;
    230         }
    231 
    232         PhoneConstants.State state = cm.getState();  // IDLE, RINGING, or OFFHOOK
    233         if (DBG) log("updateState: current state = " + state);
    234 
    235         boolean showIncomingCallControls = false;
    236         boolean showInCallControls = false;
    237 
    238         final Call ringingCall = cm.getFirstActiveRingingCall();
    239         final Call.State fgCallState = cm.getActiveFgCallState();
    240 
    241         // If the FG call is dialing/alerting, we should display for that call
    242         // and ignore the ringing call. This case happens when the telephony
    243         // layer rejects the ringing call while the FG call is dialing/alerting,
    244         // but the incoming call *does* briefly exist in the DISCONNECTING or
    245         // DISCONNECTED state.
    246         if ((ringingCall.getState() != Call.State.IDLE) && !fgCallState.isDialing()) {
    247             // A phone call is ringing *or* call waiting.
    248 
    249             // Watch out: even if the phone state is RINGING, it's
    250             // possible for the ringing call to be in the DISCONNECTING
    251             // state.  (This typically happens immediately after the user
    252             // rejects an incoming call, and in that case we *don't* show
    253             // the incoming call controls.)
    254             if (ringingCall.getState().isAlive()) {
    255                 if (DBG) log("- updateState: RINGING!  Showing incoming call controls...");
    256                 showIncomingCallControls = true;
    257             }
    258 
    259             // Ugly hack to cover up slow response from the radio:
    260             // if we get an updateState() call immediately after answering/rejecting a call
    261             // (via onTrigger()), *don't* show the incoming call
    262             // UI even if the phone is still in the RINGING state.
    263             // This covers up a slow response from the radio for some actions.
    264             // To detect that situation, we are using "500 msec" heuristics.
    265             //
    266             // Watch out: we should *not* rely on this behavior when "instant text response" action
    267             // has been chosen. See also onTrigger() for why.
    268             long now = SystemClock.uptimeMillis();
    269             if (now < mLastIncomingCallActionTime + 500) {
    270                 log("updateState: Too soon after last action; not drawing!");
    271                 showIncomingCallControls = false;
    272             }
    273 
    274             // b/6765896
    275             // If the glowview triggers two hits of the respond-via-sms gadget in
    276             // quick succession, it can cause the incoming call widget to show and hide
    277             // twice in a row.  However, the second hide doesn't get triggered because
    278             // we are already attemping to hide.  This causes an additional glowview to
    279             // stay up above all other screens.
    280             // In reality, we shouldn't even be showing incoming-call UI while we are
    281             // showing the respond-via-sms popup, so we check for that here.
    282             //
    283             // TODO: In the future, this entire state machine
    284             // should be reworked.  Respond-via-sms was stapled onto the current
    285             // design (and so were other states) and should be made a first-class
    286             // citizen in a new state machine.
    287             if (mInCallScreen.isQuickResponseDialogShowing()) {
    288                 log("updateState: quickResponse visible. Cancel showing incoming call controls.");
    289                 showIncomingCallControls = false;
    290             }
    291         } else {
    292             // Ok, show the regular in-call touch UI (with some exceptions):
    293             if (okToShowInCallControls()) {
    294                 showInCallControls = true;
    295             } else {
    296                 if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
    297             }
    298         }
    299 
    300         // In usual cases we don't allow showing both incoming call controls and in-call controls.
    301         //
    302         // There's one exception: if this call is during fading-out animation for the incoming
    303         // call controls, we need to show both for smoother transition.
    304         if (showIncomingCallControls && showInCallControls) {
    305             throw new IllegalStateException(
    306                 "'Incoming' and 'in-call' touch controls visible at the same time!");
    307         }
    308         if (mShowInCallControlsDuringHidingAnimation) {
    309             if (DBG) {
    310                 log("- updateState: FORCE showing in-call controls during incoming call widget"
    311                         + " being hidden with animation");
    312             }
    313             showInCallControls = true;
    314         }
    315 
    316         // Update visibility and state of the incoming call controls or
    317         // the normal in-call controls.
    318 
    319         if (showInCallControls) {
    320             if (DBG) log("- updateState: showing in-call controls...");
    321             updateInCallControls(cm);
    322             mInCallControls.setVisibility(View.VISIBLE);
    323         } else {
    324             if (DBG) log("- updateState: HIDING in-call controls...");
    325             mInCallControls.setVisibility(View.GONE);
    326         }
    327 
    328         if (showIncomingCallControls) {
    329             if (DBG) log("- updateState: showing incoming call widget...");
    330             showIncomingCallWidget(ringingCall);
    331 
    332             // On devices with a system bar (soft buttons at the bottom of
    333             // the screen), disable navigation while the incoming-call UI
    334             // is up.
    335             // This prevents false touches (e.g. on the "Recents" button)
    336             // from interfering with the incoming call UI, like if you
    337             // accidentally touch the system bar while pulling the phone
    338             // out of your pocket.
    339             mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(false);
    340         } else {
    341             if (DBG) log("- updateState: HIDING incoming call widget...");
    342             hideIncomingCallWidget();
    343 
    344             // The system bar is allowed to work normally in regular
    345             // in-call states.
    346             mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true);
    347         }
    348 
    349         // Dismiss the "Audio mode" PopupMenu if necessary.
    350         //
    351         // The "Audio mode" popup is only relevant in call states that support
    352         // in-call audio, namely when the phone is OFFHOOK (not RINGING), *and*
    353         // the foreground call is either ALERTING (where you can hear the other
    354         // end ringing) or ACTIVE (when the call is actually connected.)  In any
    355         // state *other* than these, the popup should not be visible.
    356 
    357         if ((state == PhoneConstants.State.OFFHOOK)
    358             && (fgCallState == Call.State.ALERTING || fgCallState == Call.State.ACTIVE)) {
    359             // The audio mode popup is allowed to be visible in this state.
    360             // So if it's up, leave it alone.
    361         } else {
    362             // The Audio mode popup isn't relevant in this state, so make sure
    363             // it's not visible.
    364             dismissAudioModePopup();  // safe even if not active
    365         }
    366     }
    367 
    368     private boolean okToShowInCallControls() {
    369         // Note that this method is concerned only with the internal state
    370         // of the InCallScreen.  (The InCallTouchUi widget has separate
    371         // logic to make sure it's OK to display the touch UI given the
    372         // current telephony state, and that it's allowed on the current
    373         // device in the first place.)
    374 
    375         // The touch UI is available in the following InCallScreenModes:
    376         // - NORMAL (obviously)
    377         // - CALL_ENDED (which is intended to look mostly the same as
    378         //               a normal in-call state, even though the in-call
    379         //               buttons are mostly disabled)
    380         // and is hidden in any of the other modes, like MANAGE_CONFERENCE
    381         // or one of the OTA modes (which use totally different UIs.)
    382 
    383         return ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL)
    384                 || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED));
    385     }
    386 
    387     @Override
    388     public void onClick(View view) {
    389         int id = view.getId();
    390         if (DBG) log("onClick(View " + view + ", id " + id + ")...");
    391 
    392         switch (id) {
    393             case R.id.addButton:
    394             case R.id.mergeButton:
    395             case R.id.endButton:
    396             case R.id.dialpadButton:
    397             case R.id.muteButton:
    398             case R.id.holdButton:
    399             case R.id.swapButton:
    400             case R.id.cdmaMergeButton:
    401             case R.id.manageConferenceButton:
    402                 // Clicks on the regular onscreen buttons get forwarded
    403                 // straight to the InCallScreen.
    404                 mInCallScreen.handleOnscreenButtonClick(id);
    405                 break;
    406 
    407             case R.id.audioButton:
    408                 handleAudioButtonClick();
    409                 break;
    410 
    411             default:
    412                 Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
    413                 break;
    414         }
    415     }
    416 
    417     @Override
    418     public boolean onLongClick(View view) {
    419         final int id = view.getId();
    420         if (DBG) log("onLongClick(View " + view + ", id " + id + ")...");
    421 
    422         switch (id) {
    423             case R.id.addButton:
    424             case R.id.mergeButton:
    425             case R.id.dialpadButton:
    426             case R.id.muteButton:
    427             case R.id.holdButton:
    428             case R.id.swapButton:
    429             case R.id.audioButton: {
    430                 final CharSequence description = view.getContentDescription();
    431                 if (!TextUtils.isEmpty(description)) {
    432                     // Show description as ActionBar's menu buttons do.
    433                     // See also ActionMenuItemView#onLongClick() for the original implementation.
    434                     final Toast cheatSheet =
    435                             Toast.makeText(view.getContext(), description, Toast.LENGTH_SHORT);
    436                     cheatSheet.setGravity(
    437                             Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, view.getHeight());
    438                     cheatSheet.show();
    439                 }
    440                 return true;
    441             }
    442             default:
    443                 Log.w(LOG_TAG, "onLongClick() with unexpected View " + view + ". Ignoring it.");
    444                 break;
    445         }
    446         return false;
    447     }
    448 
    449     /**
    450      * Updates the enabledness and "checked" state of the buttons on the
    451      * "inCallControls" panel, based on the current telephony state.
    452      */
    453     private void updateInCallControls(CallManager cm) {
    454         int phoneType = cm.getActiveFgCall().getPhone().getPhoneType();
    455 
    456         // Note we do NOT need to worry here about cases where the entire
    457         // in-call touch UI is disabled, like during an OTA call or if the
    458         // dtmf dialpad is up.  (That's handled by updateState(), which
    459         // calls okToShowInCallControls().)
    460         //
    461         // If we get here, it *is* OK to show the in-call touch UI, so we
    462         // now need to update the enabledness and/or "checked" state of
    463         // each individual button.
    464         //
    465 
    466         // The InCallControlState object tells us the enabledness and/or
    467         // state of the various onscreen buttons:
    468         InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
    469 
    470         if (DBG) {
    471             log("updateInCallControls()...");
    472             inCallControlState.dumpState();
    473         }
    474 
    475         // "Add" / "Merge":
    476         // These two buttons occupy the same space onscreen, so at any
    477         // given point exactly one of them must be VISIBLE and the other
    478         // must be GONE.
    479         if (inCallControlState.canAddCall) {
    480             mAddButton.setVisibility(View.VISIBLE);
    481             mAddButton.setEnabled(true);
    482             mMergeButton.setVisibility(View.GONE);
    483         } else if (inCallControlState.canMerge) {
    484             if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
    485                 // In CDMA "Add" option is always given to the user and the
    486                 // "Merge" option is provided as a button on the top left corner of the screen,
    487                 // we always set the mMergeButton to GONE
    488                 mMergeButton.setVisibility(View.GONE);
    489             } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
    490                     || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
    491                 mMergeButton.setVisibility(View.VISIBLE);
    492                 mMergeButton.setEnabled(true);
    493                 mAddButton.setVisibility(View.GONE);
    494             } else {
    495                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
    496             }
    497         } else {
    498             // Neither "Add" nor "Merge" is available.  (This happens in
    499             // some transient states, like while dialing an outgoing call,
    500             // and in other rare cases like if you have both lines in use
    501             // *and* there are already 5 people on the conference call.)
    502             // Since the common case here is "while dialing", we show the
    503             // "Add" button in a disabled state so that there won't be any
    504             // jarring change in the UI when the call finally connects.
    505             mAddButton.setVisibility(View.VISIBLE);
    506             mAddButton.setEnabled(false);
    507             mMergeButton.setVisibility(View.GONE);
    508         }
    509         if (inCallControlState.canAddCall && inCallControlState.canMerge) {
    510             if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
    511                     || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
    512                 // Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
    513                 // should both be available right now.  This *should* never
    514                 // happen with GSM, but if it's possible on any
    515                 // future devices we may need to re-layout Add and Merge so
    516                 // they can both be visible at the same time...
    517                 Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
    518                         " but can't show both!");
    519             } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
    520                 // In CDMA "Add" option is always given to the user and the hence
    521                 // in this case both "Add" and "Merge" options would be available to user
    522                 if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
    523             } else {
    524                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
    525             }
    526         }
    527 
    528         // "End call"
    529         mEndButton.setEnabled(inCallControlState.canEndCall);
    530 
    531         // "Dialpad": Enabled only when it's OK to use the dialpad in the
    532         // first place.
    533         mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
    534         mDialpadButton.setChecked(inCallControlState.dialpadVisible);
    535 
    536         // "Mute"
    537         mMuteButton.setEnabled(inCallControlState.canMute);
    538         mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
    539 
    540         // "Audio"
    541         updateAudioButton(inCallControlState);
    542 
    543         // "Hold" / "Swap":
    544         // These two buttons occupy the same space onscreen, so at any
    545         // given point exactly one of them must be VISIBLE and the other
    546         // must be GONE.
    547         if (inCallControlState.canHold) {
    548             mHoldButton.setVisibility(View.VISIBLE);
    549             mHoldButton.setEnabled(true);
    550             mHoldButton.setChecked(inCallControlState.onHold);
    551             mSwapButton.setVisibility(View.GONE);
    552             mHoldSwapSpacer.setVisibility(View.VISIBLE);
    553         } else if (inCallControlState.canSwap) {
    554             mSwapButton.setVisibility(View.VISIBLE);
    555             mSwapButton.setEnabled(true);
    556             mHoldButton.setVisibility(View.GONE);
    557             mHoldSwapSpacer.setVisibility(View.VISIBLE);
    558         } else {
    559             // Neither "Hold" nor "Swap" is available.  This can happen for two
    560             // reasons:
    561             //   (1) this is a transient state on a device that *can*
    562             //       normally hold or swap, or
    563             //   (2) this device just doesn't have the concept of hold/swap.
    564             //
    565             // In case (1), show the "Hold" button in a disabled state.  In case
    566             // (2), remove the button entirely.  (This means that the button row
    567             // will only have 4 buttons on some devices.)
    568 
    569             if (inCallControlState.supportsHold) {
    570                 mHoldButton.setVisibility(View.VISIBLE);
    571                 mHoldButton.setEnabled(false);
    572                 mHoldButton.setChecked(false);
    573                 mSwapButton.setVisibility(View.GONE);
    574                 mHoldSwapSpacer.setVisibility(View.VISIBLE);
    575             } else {
    576                 mHoldButton.setVisibility(View.GONE);
    577                 mSwapButton.setVisibility(View.GONE);
    578                 mHoldSwapSpacer.setVisibility(View.GONE);
    579             }
    580         }
    581         mInCallScreen.updateButtonStateOutsideInCallTouchUi();
    582         if (inCallControlState.canSwap && inCallControlState.canHold) {
    583             // Uh oh, the InCallControlState thinks that Swap *and* Hold
    584             // should both be available.  This *should* never happen with
    585             // either GSM or CDMA, but if it's possible on any future
    586             // devices we may need to re-layout Hold and Swap so they can
    587             // both be visible at the same time...
    588             Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
    589         }
    590 
    591         if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
    592             if (inCallControlState.canSwap && inCallControlState.canMerge) {
    593                 // Uh oh, the InCallControlState thinks that Swap *and* Merge
    594                 // should both be available.  This *should* never happen with
    595                 // CDMA, but if it's possible on any future
    596                 // devices we may need to re-layout Merge and Swap so they can
    597                 // both be visible at the same time...
    598                 Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
    599                         "enabled, but can't show both!");
    600             }
    601         }
    602 
    603         // Finally, update the "extra button row": It's displayed above the
    604         // "End" button, but only if necessary.  Also, it's never displayed
    605         // while the dialpad is visible (since it would overlap.)
    606         //
    607         // The row contains two buttons:
    608         //
    609         // - "Manage conference" (used only on GSM devices)
    610         // - "Merge" button (used only on CDMA devices)
    611         //
    612         // Note that mExtraButtonRow is ViewStub, which will be inflated for the first time when
    613         // any of its buttons becomes visible.
    614         final boolean showCdmaMerge =
    615                 (phoneType == PhoneConstants.PHONE_TYPE_CDMA) && inCallControlState.canMerge;
    616         final boolean showExtraButtonRow =
    617                 showCdmaMerge || inCallControlState.manageConferenceVisible;
    618         if (showExtraButtonRow && !inCallControlState.dialpadVisible) {
    619             // This will require the ViewStub inflate itself.
    620             mExtraButtonRow.setVisibility(View.VISIBLE);
    621 
    622             // Need to set up mCdmaMergeButton and mManageConferenceButton if this is the first
    623             // time they're visible.
    624             if (mCdmaMergeButton == null) {
    625                 setupExtraButtons();
    626             }
    627             mCdmaMergeButton.setVisibility(showCdmaMerge ? View.VISIBLE : View.GONE);
    628             if (inCallControlState.manageConferenceVisible) {
    629                 mManageConferenceButton.setVisibility(View.VISIBLE);
    630                 mManageConferenceButtonImage.setEnabled(inCallControlState.manageConferenceEnabled);
    631             } else {
    632                 mManageConferenceButton.setVisibility(View.GONE);
    633             }
    634         } else {
    635             mExtraButtonRow.setVisibility(View.GONE);
    636         }
    637 
    638         if (DBG) {
    639             log("At the end of updateInCallControls().");
    640             dumpBottomButtonState();
    641         }
    642     }
    643 
    644     /**
    645      * Set up the buttons that are part of the "extra button row"
    646      */
    647     private void setupExtraButtons() {
    648         // The two "buttons" here (mCdmaMergeButton and mManageConferenceButton)
    649         // are actually layouts containing an icon and a text label side-by-side.
    650         mCdmaMergeButton = (ViewGroup) mInCallControls.findViewById(R.id.cdmaMergeButton);
    651         if (mCdmaMergeButton == null) {
    652             Log.wtf(LOG_TAG, "CDMA Merge button is null even after ViewStub being inflated.");
    653             return;
    654         }
    655         mCdmaMergeButton.setOnClickListener(this);
    656 
    657         mManageConferenceButton =
    658                 (ViewGroup) mInCallControls.findViewById(R.id.manageConferenceButton);
    659         mManageConferenceButton.setOnClickListener(this);
    660         mManageConferenceButtonImage =
    661                 (ImageButton) mInCallControls.findViewById(R.id.manageConferenceButtonImage);
    662     }
    663 
    664     private void dumpBottomButtonState() {
    665         log(" - dialpad: " + getButtonState(mDialpadButton));
    666         log(" - speaker: " + getButtonState(mAudioButton));
    667         log(" - mute: " + getButtonState(mMuteButton));
    668         log(" - hold: " + getButtonState(mHoldButton));
    669         log(" - swap: " + getButtonState(mSwapButton));
    670         log(" - add: " + getButtonState(mAddButton));
    671         log(" - merge: " + getButtonState(mMergeButton));
    672         log(" - cdmaMerge: " + getButtonState(mCdmaMergeButton));
    673         log(" - swap: " + getButtonState(mSwapButton));
    674         log(" - manageConferenceButton: " + getButtonState(mManageConferenceButton));
    675     }
    676 
    677     private static String getButtonState(View view) {
    678         if (view == null) {
    679             return "(null)";
    680         }
    681         StringBuilder builder = new StringBuilder();
    682         builder.append("visibility: " + (view.getVisibility() == View.VISIBLE ? "VISIBLE"
    683                 : view.getVisibility() == View.INVISIBLE ? "INVISIBLE" : "GONE"));
    684         if (view instanceof ImageButton) {
    685             builder.append(", enabled: " + ((ImageButton) view).isEnabled());
    686         } else if (view instanceof CompoundButton) {
    687             builder.append(", enabled: " + ((CompoundButton) view).isEnabled());
    688             builder.append(", checked: " + ((CompoundButton) view).isChecked());
    689         }
    690         return builder.toString();
    691     }
    692 
    693     /**
    694      * Updates the onscreen "Audio mode" button based on the current state.
    695      *
    696      * - If bluetooth is available, this button's function is to bring up the
    697      *   "Audio mode" popup (which provides a 3-way choice between earpiece /
    698      *   speaker / bluetooth).  So it should look like a regular action button,
    699      *   but should also have the small "more_indicator" triangle that indicates
    700      *   that a menu will pop up.
    701      *
    702      * - If speaker (but not bluetooth) is available, this button should look like
    703      *   a regular toggle button (and indicate the current speaker state.)
    704      *
    705      * - If even speaker isn't available, disable the button entirely.
    706      */
    707     private void updateAudioButton(InCallControlState inCallControlState) {
    708         if (DBG) log("updateAudioButton()...");
    709 
    710         // The various layers of artwork for this button come from
    711         // btn_compound_audio.xml.  Keep track of which layers we want to be
    712         // visible:
    713         //
    714         // - This selector shows the blue bar below the button icon when
    715         //   this button is a toggle *and* it's currently "checked".
    716         boolean showToggleStateIndication = false;
    717         //
    718         // - This is visible if the popup menu is enabled:
    719         boolean showMoreIndicator = false;
    720         //
    721         // - Foreground icons for the button.  Exactly one of these is enabled:
    722         boolean showSpeakerOnIcon = false;
    723         boolean showSpeakerOffIcon = false;
    724         boolean showHandsetIcon = false;
    725         boolean showBluetoothIcon = false;
    726 
    727         if (inCallControlState.bluetoothEnabled) {
    728             if (DBG) log("- updateAudioButton: 'popup menu action button' mode...");
    729 
    730             mAudioButton.setEnabled(true);
    731 
    732             // The audio button is NOT a toggle in this state.  (And its
    733             // setChecked() state is irrelevant since we completely hide the
    734             // btn_compound_background layer anyway.)
    735 
    736             // Update desired layers:
    737             showMoreIndicator = true;
    738             if (inCallControlState.bluetoothIndicatorOn) {
    739                 showBluetoothIcon = true;
    740             } else if (inCallControlState.speakerOn) {
    741                 showSpeakerOnIcon = true;
    742             } else {
    743                 showHandsetIcon = true;
    744                 // TODO: if a wired headset is plugged in, that takes precedence
    745                 // over the handset earpiece.  If so, maybe we should show some
    746                 // sort of "wired headset" icon here instead of the "handset
    747                 // earpiece" icon.  (Still need an asset for that, though.)
    748             }
    749         } else if (inCallControlState.speakerEnabled) {
    750             if (DBG) log("- updateAudioButton: 'speaker toggle' mode...");
    751 
    752             mAudioButton.setEnabled(true);
    753 
    754             // The audio button *is* a toggle in this state, and indicates the
    755             // current state of the speakerphone.
    756             mAudioButton.setChecked(inCallControlState.speakerOn);
    757 
    758             // Update desired layers:
    759             showToggleStateIndication = true;
    760 
    761             showSpeakerOnIcon = inCallControlState.speakerOn;
    762             showSpeakerOffIcon = !inCallControlState.speakerOn;
    763         } else {
    764             if (DBG) log("- updateAudioButton: disabled...");
    765 
    766             // The audio button is a toggle in this state, but that's mostly
    767             // irrelevant since it's always disabled and unchecked.
    768             mAudioButton.setEnabled(false);
    769             mAudioButton.setChecked(false);
    770 
    771             // Update desired layers:
    772             showToggleStateIndication = true;
    773             showSpeakerOffIcon = true;
    774         }
    775 
    776         // Finally, update the drawable layers (see btn_compound_audio.xml).
    777 
    778         // Constants used below with Drawable.setAlpha():
    779         final int HIDDEN = 0;
    780         final int VISIBLE = 255;
    781 
    782         LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
    783         if (DBG) log("- 'layers' drawable: " + layers);
    784 
    785         layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
    786                 .setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
    787 
    788         layers.findDrawableByLayerId(R.id.moreIndicatorItem)
    789                 .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
    790 
    791         layers.findDrawableByLayerId(R.id.bluetoothItem)
    792                 .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
    793 
    794         layers.findDrawableByLayerId(R.id.handsetItem)
    795                 .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
    796 
    797         layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
    798                 .setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
    799 
    800         layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
    801                 .setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
    802     }
    803 
    804     /**
    805      * Handles a click on the "Audio mode" button.
    806      * - If bluetooth is available, bring up the "Audio mode" popup
    807      *   (which provides a 3-way choice between earpiece / speaker / bluetooth).
    808      * - If bluetooth is *not* available, just toggle between earpiece and
    809      *   speaker, with no popup at all.
    810      */
    811     private void handleAudioButtonClick() {
    812         InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
    813         if (inCallControlState.bluetoothEnabled) {
    814             if (DBG) log("- handleAudioButtonClick: 'popup menu' mode...");
    815             showAudioModePopup();
    816         } else {
    817             if (DBG) log("- handleAudioButtonClick: 'speaker toggle' mode...");
    818             mInCallScreen.toggleSpeaker();
    819         }
    820     }
    821 
    822     /**
    823      * Brings up the "Audio mode" popup.
    824      */
    825     private void showAudioModePopup() {
    826         if (DBG) log("showAudioModePopup()...");
    827 
    828         mAudioModePopup = new PopupMenu(mInCallScreen /* context */,
    829                                         mAudioButton /* anchorView */);
    830         mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
    831                                                   mAudioModePopup.getMenu());
    832         mAudioModePopup.setOnMenuItemClickListener(this);
    833         mAudioModePopup.setOnDismissListener(this);
    834 
    835         // Update the enabled/disabledness of menu items based on the
    836         // current call state.
    837         InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
    838 
    839         Menu menu = mAudioModePopup.getMenu();
    840 
    841         // TODO: Still need to have the "currently active" audio mode come
    842         // up pre-selected (or focused?) with a blue highlight.  Still
    843         // need exact visual design, and possibly framework support for this.
    844         // See comments below for the exact logic.
    845 
    846         MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
    847         speakerItem.setEnabled(inCallControlState.speakerEnabled);
    848         // TODO: Show speakerItem as initially "selected" if
    849         // inCallControlState.speakerOn is true.
    850 
    851         // We display *either* "earpiece" or "wired headset", never both,
    852         // depending on whether a wired headset is physically plugged in.
    853         MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
    854         MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
    855         final boolean usingHeadset = mApp.isHeadsetPlugged();
    856         earpieceItem.setVisible(!usingHeadset);
    857         earpieceItem.setEnabled(!usingHeadset);
    858         wiredHeadsetItem.setVisible(usingHeadset);
    859         wiredHeadsetItem.setEnabled(usingHeadset);
    860         // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
    861         // as initially "selected" if inCallControlState.speakerOn and
    862         // inCallControlState.bluetoothIndicatorOn are both false.
    863 
    864         MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
    865         bluetoothItem.setEnabled(inCallControlState.bluetoothEnabled);
    866         // TODO: Show bluetoothItem as initially "selected" if
    867         // inCallControlState.bluetoothIndicatorOn is true.
    868 
    869         mAudioModePopup.show();
    870 
    871         // Unfortunately we need to manually keep track of the popup menu's
    872         // visiblity, since PopupMenu doesn't have an isShowing() method like
    873         // Dialogs do.
    874         mAudioModePopupVisible = true;
    875     }
    876 
    877     /**
    878      * Dismisses the "Audio mode" popup if it's visible.
    879      *
    880      * This is safe to call even if the popup is already dismissed, or even if
    881      * you never called showAudioModePopup() in the first place.
    882      */
    883     public void dismissAudioModePopup() {
    884         if (mAudioModePopup != null) {
    885             mAudioModePopup.dismiss();  // safe even if already dismissed
    886             mAudioModePopup = null;
    887             mAudioModePopupVisible = false;
    888         }
    889     }
    890 
    891     /**
    892      * Refreshes the "Audio mode" popup if it's visible.  This is useful
    893      * (for example) when a wired headset is plugged or unplugged,
    894      * since we need to switch back and forth between the "earpiece"
    895      * and "wired headset" items.
    896      *
    897      * This is safe to call even if the popup is already dismissed, or even if
    898      * you never called showAudioModePopup() in the first place.
    899      */
    900     public void refreshAudioModePopup() {
    901         if (mAudioModePopup != null && mAudioModePopupVisible) {
    902             // Dismiss the previous one
    903             mAudioModePopup.dismiss();  // safe even if already dismissed
    904             // And bring up a fresh PopupMenu
    905             showAudioModePopup();
    906         }
    907     }
    908 
    909     // PopupMenu.OnMenuItemClickListener implementation; see showAudioModePopup()
    910     @Override
    911     public boolean onMenuItemClick(MenuItem item) {
    912         if (DBG) log("- onMenuItemClick: " + item);
    913         if (DBG) log("  id: " + item.getItemId());
    914         if (DBG) log("  title: '" + item.getTitle() + "'");
    915 
    916         if (mInCallScreen == null) {
    917             Log.w(LOG_TAG, "onMenuItemClick(" + item + "), but null mInCallScreen!");
    918             return true;
    919         }
    920 
    921         switch (item.getItemId()) {
    922             case R.id.audio_mode_speaker:
    923                 mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.SPEAKER);
    924                 break;
    925             case R.id.audio_mode_earpiece:
    926             case R.id.audio_mode_wired_headset:
    927                 // InCallAudioMode.EARPIECE means either the handset earpiece,
    928                 // or the wired headset (if connected.)
    929                 mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.EARPIECE);
    930                 break;
    931             case R.id.audio_mode_bluetooth:
    932                 mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.BLUETOOTH);
    933                 break;
    934             default:
    935                 Log.wtf(LOG_TAG,
    936                         "onMenuItemClick:  unexpected View ID " + item.getItemId()
    937                         + " (MenuItem = '" + item + "')");
    938                 break;
    939         }
    940         return true;
    941     }
    942 
    943     // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
    944     // This gets called when the PopupMenu gets dismissed for *any* reason, like
    945     // the user tapping outside its bounds, or pressing Back, or selecting one
    946     // of the menu items.
    947     @Override
    948     public void onDismiss(PopupMenu menu) {
    949         if (DBG) log("- onDismiss: " + menu);
    950         mAudioModePopupVisible = false;
    951     }
    952 
    953     /**
    954      * @return the amount of vertical space (in pixels) that needs to be
    955      * reserved for the button cluster at the bottom of the screen.
    956      * (The CallCard uses this measurement to determine how big
    957      * the main "contact photo" area can be.)
    958      *
    959      * NOTE that this returns the "canonical height" of the main in-call
    960      * button cluster, which may not match the amount of vertical space
    961      * actually used.  Specifically:
    962      *
    963      *   - If an incoming call is ringing, the button cluster isn't
    964      *     visible at all.  (And the GlowPadView widget is actually
    965      *     much taller than the button cluster.)
    966      *
    967      *   - If the InCallTouchUi widget's "extra button row" is visible
    968      *     (in some rare phone states) the button cluster will actually
    969      *     be slightly taller than the "canonical height".
    970      *
    971      * In either of these cases, we allow the bottom edge of the contact
    972      * photo to be covered up by whatever UI is actually onscreen.
    973      */
    974     public int getTouchUiHeight() {
    975         // Add up the vertical space consumed by the various rows of buttons.
    976         int height = 0;
    977 
    978         // - The main row of buttons:
    979         height += (int) getResources().getDimension(R.dimen.in_call_button_height);
    980 
    981         // - The End button:
    982         height += (int) getResources().getDimension(R.dimen.in_call_end_button_height);
    983 
    984         // - Note we *don't* consider the InCallTouchUi widget's "extra
    985         //   button row" here.
    986 
    987         //- And an extra bit of margin:
    988         height += (int) getResources().getDimension(R.dimen.in_call_touch_ui_upper_margin);
    989 
    990         return height;
    991     }
    992 
    993 
    994     //
    995     // GlowPadView.OnTriggerListener implementation
    996     //
    997 
    998     @Override
    999     public void onGrabbed(View v, int handle) {
   1000 
   1001     }
   1002 
   1003     @Override
   1004     public void onReleased(View v, int handle) {
   1005 
   1006     }
   1007 
   1008     /**
   1009      * Handles "Answer" and "Reject" actions for an incoming call.
   1010      * We get this callback from the incoming call widget
   1011      * when the user triggers an action.
   1012      */
   1013     @Override
   1014     public void onTrigger(View view, int whichHandle) {
   1015         if (DBG) log("onTrigger(whichHandle = " + whichHandle + ")...");
   1016 
   1017         if (mInCallScreen == null) {
   1018             Log.wtf(LOG_TAG, "onTrigger(" + whichHandle
   1019                     + ") from incoming-call widget, but null mInCallScreen!");
   1020             return;
   1021         }
   1022 
   1023         // The InCallScreen actually implements all of these actions.
   1024         // Each possible action from the incoming call widget corresponds
   1025         // to an R.id value; we pass those to the InCallScreen's "button
   1026         // click" handler (even though the UI elements aren't actually
   1027         // buttons; see InCallScreen.handleOnscreenButtonClick().)
   1028 
   1029         mShowInCallControlsDuringHidingAnimation = false;
   1030         switch (whichHandle) {
   1031             case ANSWER_CALL_ID:
   1032                 if (DBG) log("ANSWER_CALL_ID: answer!");
   1033                 mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer);
   1034                 mShowInCallControlsDuringHidingAnimation = true;
   1035 
   1036                 // ...and also prevent it from reappearing right away.
   1037                 // (This covers up a slow response from the radio for some
   1038                 // actions; see updateState().)
   1039                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
   1040                 break;
   1041 
   1042             case SEND_SMS_ID:
   1043                 if (DBG) log("SEND_SMS_ID!");
   1044                 mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms);
   1045 
   1046                 // Watch out: mLastIncomingCallActionTime should not be updated for this case.
   1047                 //
   1048                 // The variable is originally for avoiding a problem caused by delayed phone state
   1049                 // update; RINGING state may remain just after answering/declining an incoming
   1050                 // call, so we need to wait a bit (500ms) until we get the effective phone state.
   1051                 // For this case, we shouldn't rely on that hack.
   1052                 //
   1053                 // When the user selects this case, there are two possibilities, neither of which
   1054                 // should rely on the hack.
   1055                 //
   1056                 // 1. The first possibility is that, the device eventually sends one of canned
   1057                 //    responses per the user's "send" request, and reject the call after sending it.
   1058                 //    At that moment the code introducing the canned responses should handle the
   1059                 //    case separately.
   1060                 //
   1061                 // 2. The second possibility is that, the device will show incoming call widget
   1062                 //    again per the user's "cancel" request, where the incoming call will still
   1063                 //    remain. At that moment the incoming call will keep its RINGING state.
   1064                 //    The remaining phone state should never be ignored by the hack for
   1065                 //    answering/declining calls because the RINGING state is legitimate. If we
   1066                 //    use the hack for answer/decline cases, the user loses the incoming call
   1067                 //    widget, until further screen update occurs afterward, which often results in
   1068                 //    missed calls.
   1069                 break;
   1070 
   1071             case DECLINE_CALL_ID:
   1072                 if (DBG) log("DECLINE_CALL_ID: reject!");
   1073                 mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject);
   1074 
   1075                 // Same as "answer" case.
   1076                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
   1077                 break;
   1078 
   1079             default:
   1080                 Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
   1081                 break;
   1082         }
   1083 
   1084         // On any action by the user, hide the widget.
   1085         //
   1086         // If requested above (i.e. if mShowInCallControlsDuringHidingAnimation is set to true),
   1087         // in-call controls will start being shown too.
   1088         //
   1089         // TODO: The decision to hide this should be made by the controller
   1090         // (InCallScreen), and not this view.
   1091         hideIncomingCallWidget();
   1092 
   1093         // Regardless of what action the user did, be sure to clear out
   1094         // the hint text we were displaying while the user was dragging.
   1095         mInCallScreen.updateIncomingCallWidgetHint(0, 0);
   1096     }
   1097 
   1098     public void onFinishFinalAnimation() {
   1099         // Not used
   1100     }
   1101 
   1102     /**
   1103      * Apply an animation to hide the incoming call widget.
   1104      */
   1105     private void hideIncomingCallWidget() {
   1106         if (DBG) log("hideIncomingCallWidget()...");
   1107         if (mIncomingCallWidget.getVisibility() != View.VISIBLE
   1108                 || mIncomingCallWidgetIsFadingOut) {
   1109             if (DBG) log("Skipping hideIncomingCallWidget action");
   1110             // Widget is already hidden or in the process of being hidden
   1111             return;
   1112         }
   1113 
   1114         // Hide the incoming call screen with a transition
   1115         mIncomingCallWidgetIsFadingOut = true;
   1116         ViewPropertyAnimator animator = mIncomingCallWidget.animate();
   1117         animator.cancel();
   1118         animator.setDuration(AnimationUtils.ANIMATION_DURATION);
   1119         animator.setListener(new AnimatorListenerAdapter() {
   1120             @Override
   1121             public void onAnimationStart(Animator animation) {
   1122                 if (mShowInCallControlsDuringHidingAnimation) {
   1123                     if (DBG) log("IncomingCallWidget's hiding animation started");
   1124                     updateInCallControls(mApp.mCM);
   1125                     mInCallControls.setVisibility(View.VISIBLE);
   1126                 }
   1127             }
   1128 
   1129             @Override
   1130             public void onAnimationEnd(Animator animation) {
   1131                 if (DBG) log("IncomingCallWidget's hiding animation ended");
   1132                 mIncomingCallWidget.setAlpha(1);
   1133                 mIncomingCallWidget.setVisibility(View.GONE);
   1134                 mIncomingCallWidget.animate().setListener(null);
   1135                 mShowInCallControlsDuringHidingAnimation = false;
   1136                 mIncomingCallWidgetIsFadingOut = false;
   1137                 mIncomingCallWidgetShouldBeReset = true;
   1138             }
   1139 
   1140             @Override
   1141             public void onAnimationCancel(Animator animation) {
   1142                 mIncomingCallWidget.animate().setListener(null);
   1143                 mShowInCallControlsDuringHidingAnimation = false;
   1144                 mIncomingCallWidgetIsFadingOut = false;
   1145                 mIncomingCallWidgetShouldBeReset = true;
   1146 
   1147                 // Note: the code which reset this animation should be responsible for
   1148                 // alpha and visibility.
   1149             }
   1150         });
   1151         animator.alpha(0f);
   1152     }
   1153 
   1154     /**
   1155      * Shows the incoming call widget and cancels any animation that may be fading it out.
   1156      */
   1157     private void showIncomingCallWidget(Call ringingCall) {
   1158         if (DBG) log("showIncomingCallWidget()...");
   1159 
   1160         // TODO: wouldn't be ok to suppress this whole request if the widget is already VISIBLE
   1161         //       and we don't need to reset it?
   1162         // log("showIncomingCallWidget(). widget visibility: " + mIncomingCallWidget.getVisibility());
   1163 
   1164         ViewPropertyAnimator animator = mIncomingCallWidget.animate();
   1165         if (animator != null) {
   1166             animator.cancel();
   1167             // If animation is cancelled before it's running,
   1168             // onAnimationCancel will not be called and mIncomingCallWidgetIsFadingOut
   1169             // will be alway true. hideIncomingCallWidget() will not be excuted in this case.
   1170             mIncomingCallWidgetIsFadingOut = false;
   1171         }
   1172         mIncomingCallWidget.setAlpha(1.0f);
   1173 
   1174         // Update the GlowPadView widget's targets based on the state of
   1175         // the ringing call.  (Specifically, we need to disable the
   1176         // "respond via SMS" option for certain types of calls, like SIP
   1177         // addresses or numbers with blocked caller-id.)
   1178         final boolean allowRespondViaSms =
   1179                 RespondViaSmsManager.allowRespondViaSmsForCall(mInCallScreen, ringingCall);
   1180         final int targetResourceId = allowRespondViaSms
   1181                 ? R.array.incoming_call_widget_3way_targets
   1182                 : R.array.incoming_call_widget_2way_targets;
   1183         // The widget should be updated only when appropriate; if the previous choice can be reused
   1184         // for this incoming call, we'll just keep using it. Otherwise we'll see UI glitch
   1185         // everytime when this method is called during a single incoming call.
   1186         if (targetResourceId != mIncomingCallWidget.getTargetResourceId()) {
   1187             if (allowRespondViaSms) {
   1188                 // The GlowPadView widget is allowed to have all 3 choices:
   1189                 // Answer, Decline, and Respond via SMS.
   1190                 mIncomingCallWidget.setTargetResources(targetResourceId);
   1191                 mIncomingCallWidget.setTargetDescriptionsResourceId(
   1192                         R.array.incoming_call_widget_3way_target_descriptions);
   1193                 mIncomingCallWidget.setDirectionDescriptionsResourceId(
   1194                         R.array.incoming_call_widget_3way_direction_descriptions);
   1195             } else {
   1196                 // You only get two choices: Answer or Decline.
   1197                 mIncomingCallWidget.setTargetResources(targetResourceId);
   1198                 mIncomingCallWidget.setTargetDescriptionsResourceId(
   1199                         R.array.incoming_call_widget_2way_target_descriptions);
   1200                 mIncomingCallWidget.setDirectionDescriptionsResourceId(
   1201                         R.array.incoming_call_widget_2way_direction_descriptions);
   1202             }
   1203 
   1204             // This will be used right after this block.
   1205             mIncomingCallWidgetShouldBeReset = true;
   1206         }
   1207         if (mIncomingCallWidgetShouldBeReset) {
   1208             // Watch out: be sure to call reset() and setVisibility() *after*
   1209             // updating the target resources, since otherwise the GlowPadView
   1210             // widget will make the targets visible initially (even before you
   1211             // touch the widget.)
   1212             mIncomingCallWidget.reset(false);
   1213             mIncomingCallWidgetShouldBeReset = false;
   1214         }
   1215 
   1216         // On an incoming call, if the layout is landscape, then align the "incoming call" text
   1217         // to the left, because the incomingCallWidget (black background with glowing ring)
   1218         // is aligned to the right and would cover the "incoming call" text.
   1219         // Note that callStateLabel is within CallCard, outside of the context of InCallTouchUi
   1220         if (PhoneUtils.isLandscape(this.getContext())) {
   1221             TextView callStateLabel = (TextView) mIncomingCallWidget
   1222                     .getRootView().findViewById(R.id.callStateLabel);
   1223             if (callStateLabel != null) callStateLabel.setGravity(Gravity.START);
   1224         }
   1225 
   1226         mIncomingCallWidget.setVisibility(View.VISIBLE);
   1227 
   1228         // Finally, manually trigger a "ping" animation.
   1229         //
   1230         // Normally, the ping animation is triggered by RING events from
   1231         // the telephony layer (see onIncomingRing().)  But that *doesn't*
   1232         // happen for the very first RING event of an incoming call, since
   1233         // the incoming-call UI hasn't been set up yet at that point!
   1234         //
   1235         // So trigger an explicit ping() here, to force the animation to
   1236         // run when the widget first appears.
   1237         //
   1238         mHandler.removeMessages(INCOMING_CALL_WIDGET_PING);
   1239         mHandler.sendEmptyMessageDelayed(
   1240                 INCOMING_CALL_WIDGET_PING,
   1241                 // Visual polish: add a small delay here, to make the
   1242                 // GlowPadView widget visible for a brief moment
   1243                 // *before* starting the ping animation.
   1244                 // This value doesn't need to be very precise.
   1245                 250 /* msec */);
   1246     }
   1247 
   1248     /**
   1249      * Handles state changes of the incoming-call widget.
   1250      *
   1251      * In previous releases (where we used a SlidingTab widget) we would
   1252      * display an onscreen hint depending on which "handle" the user was
   1253      * dragging.  But we now use a GlowPadView widget, which has only
   1254      * one handle, so for now we don't display a hint at all (see the TODO
   1255      * comment below.)
   1256      */
   1257     @Override
   1258     public void onGrabbedStateChange(View v, int grabbedState) {
   1259         if (mInCallScreen != null) {
   1260             // Look up the hint based on which handle is currently grabbed.
   1261             // (Note we don't simply pass grabbedState thru to the InCallScreen,
   1262             // since *this* class is the only place that knows that the left
   1263             // handle means "Answer" and the right handle means "Decline".)
   1264             int hintTextResId, hintColorResId;
   1265             switch (grabbedState) {
   1266                 case GlowPadView.OnTriggerListener.NO_HANDLE:
   1267                 case GlowPadView.OnTriggerListener.CENTER_HANDLE:
   1268                     hintTextResId = 0;
   1269                     hintColorResId = 0;
   1270                     break;
   1271                 default:
   1272                     Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
   1273                           + grabbedState);
   1274                     hintTextResId = 0;
   1275                     hintColorResId = 0;
   1276                     break;
   1277             }
   1278 
   1279             // Tell the InCallScreen to update the CallCard and force the
   1280             // screen to redraw.
   1281             mInCallScreen.updateIncomingCallWidgetHint(hintTextResId, hintColorResId);
   1282         }
   1283     }
   1284 
   1285     /**
   1286      * Handles an incoming RING event from the telephony layer.
   1287      */
   1288     public void onIncomingRing() {
   1289         if (ENABLE_PING_ON_RING_EVENTS) {
   1290             // Each RING from the telephony layer triggers a "ping" animation
   1291             // of the GlowPadView widget.  (The intent here is to make the
   1292             // pinging appear to be synchronized with the ringtone, although
   1293             // that only works for non-looping ringtones.)
   1294             triggerPing();
   1295         }
   1296     }
   1297 
   1298     /**
   1299      * Runs a single "ping" animation of the GlowPadView widget,
   1300      * or do nothing if the GlowPadView widget is no longer visible.
   1301      *
   1302      * Also, if ENABLE_PING_AUTO_REPEAT is true, schedule the next ping as
   1303      * well (but again, only if the GlowPadView widget is still visible.)
   1304      */
   1305     public void triggerPing() {
   1306         if (DBG) log("triggerPing: mIncomingCallWidget = " + mIncomingCallWidget);
   1307 
   1308         if (!mInCallScreen.isForegroundActivity()) {
   1309             // InCallScreen has been dismissed; no need to run a ping *or*
   1310             // schedule another one.
   1311             log("- triggerPing: InCallScreen no longer in foreground; ignoring...");
   1312             return;
   1313         }
   1314 
   1315         if (mIncomingCallWidget == null) {
   1316             // This shouldn't happen; the GlowPadView widget should
   1317             // always be present in our layout file.
   1318             Log.w(LOG_TAG, "- triggerPing: null mIncomingCallWidget!");
   1319             return;
   1320         }
   1321 
   1322         if (DBG) log("- triggerPing: mIncomingCallWidget visibility = "
   1323                      + mIncomingCallWidget.getVisibility());
   1324 
   1325         if (mIncomingCallWidget.getVisibility() != View.VISIBLE) {
   1326             if (DBG) log("- triggerPing: mIncomingCallWidget no longer visible; ignoring...");
   1327             return;
   1328         }
   1329 
   1330         // Ok, run a ping (and schedule the next one too, if desired...)
   1331 
   1332         mIncomingCallWidget.ping();
   1333 
   1334         if (ENABLE_PING_AUTO_REPEAT) {
   1335             // Schedule the next ping.  (ENABLE_PING_AUTO_REPEAT mode
   1336             // allows the ping animation to repeat much faster than in
   1337             // the ENABLE_PING_ON_RING_EVENTS case, since telephony RING
   1338             // events come fairly slowly (about 3 seconds apart.))
   1339 
   1340             // No need to check here if the call is still ringing, by
   1341             // the way, since we hide mIncomingCallWidget as soon as the
   1342             // ringing stops, or if the user answers.  (And at that
   1343             // point, any future triggerPing() call will be a no-op.)
   1344 
   1345             // TODO: Rather than having a separate timer here, maybe try
   1346             // having these pings synchronized with the vibrator (see
   1347             // VibratorThread in Ringer.java; we'd just need to get
   1348             // events routed from there to here, probably via the
   1349             // PhoneApp instance.)  (But watch out: make sure pings
   1350             // still work even if the Vibrate setting is turned off!)
   1351 
   1352             mHandler.sendEmptyMessageDelayed(INCOMING_CALL_WIDGET_PING,
   1353                                              PING_AUTO_REPEAT_DELAY_MSEC);
   1354         }
   1355     }
   1356 
   1357     // Debugging / testing code
   1358 
   1359     private void log(String msg) {
   1360         Log.d(LOG_TAG, msg);
   1361     }
   1362 }
   1363