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