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.content.Context;
     20 import android.graphics.drawable.Drawable;
     21 import android.os.SystemClock;
     22 import android.util.AttributeSet;
     23 import android.util.Log;
     24 import android.view.LayoutInflater;
     25 import android.view.MotionEvent;
     26 import android.view.View;
     27 import android.view.animation.AlphaAnimation;
     28 import android.view.animation.Animation;
     29 import android.view.animation.Animation.AnimationListener;
     30 import android.widget.Button;
     31 import android.widget.FrameLayout;
     32 import android.widget.ImageButton;
     33 import android.widget.TextView;
     34 import android.widget.ToggleButton;
     35 
     36 import com.android.internal.telephony.Call;
     37 import com.android.internal.telephony.Phone;
     38 import com.android.internal.widget.SlidingTab;
     39 import com.android.internal.telephony.CallManager;
     40 
     41 
     42 /**
     43  * In-call onscreen touch UI elements, used on some platforms.
     44  *
     45  * This widget is a fullscreen overlay, drawn on top of the
     46  * non-touch-sensitive parts of the in-call UI (i.e. the call card).
     47  */
     48 public class InCallTouchUi extends FrameLayout
     49         implements View.OnClickListener, SlidingTab.OnTriggerListener {
     50     private static final int IN_CALL_WIDGET_TRANSITION_TIME = 250; // in ms
     51     private static final String LOG_TAG = "InCallTouchUi";
     52     private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2);
     53 
     54     /**
     55      * Reference to the InCallScreen activity that owns us.  This may be
     56      * null if we haven't been initialized yet *or* after the InCallScreen
     57      * activity has been destroyed.
     58      */
     59     private InCallScreen mInCallScreen;
     60 
     61     // Phone app instance
     62     private PhoneApp mApplication;
     63 
     64     // UI containers / elements
     65     private SlidingTab mIncomingCallWidget;  // UI used for an incoming call
     66     private View mInCallControls;  // UI elements while on a regular call
     67     //
     68     private Button mAddButton;
     69     private Button mMergeButton;
     70     private Button mEndButton;
     71     private Button mDialpadButton;
     72     private ToggleButton mBluetoothButton;
     73     private ToggleButton mMuteButton;
     74     private ToggleButton mSpeakerButton;
     75     //
     76     private View mHoldButtonContainer;
     77     private ImageButton mHoldButton;
     78     private TextView mHoldButtonLabel;
     79     private View mSwapButtonContainer;
     80     private ImageButton mSwapButton;
     81     private TextView mSwapButtonLabel;
     82     private View mCdmaMergeButtonContainer;
     83     private ImageButton mCdmaMergeButton;
     84     //
     85     private Drawable mHoldIcon;
     86     private Drawable mUnholdIcon;
     87     private Drawable mShowDialpadIcon;
     88     private Drawable mHideDialpadIcon;
     89 
     90     // Time of the most recent "answer" or "reject" action (see updateState())
     91     private long mLastIncomingCallActionTime;  // in SystemClock.uptimeMillis() time base
     92 
     93     // Overall enabledness of the "touch UI" features
     94     private boolean mAllowIncomingCallTouchUi;
     95     private boolean mAllowInCallTouchUi;
     96 
     97     public InCallTouchUi(Context context, AttributeSet attrs) {
     98         super(context, attrs);
     99 
    100         if (DBG) log("InCallTouchUi constructor...");
    101         if (DBG) log("- this = " + this);
    102         if (DBG) log("- context " + context + ", attrs " + attrs);
    103 
    104         // Inflate our contents, and add it (to ourself) as a child.
    105         LayoutInflater inflater = LayoutInflater.from(context);
    106         inflater.inflate(
    107                 R.layout.incall_touch_ui,  // resource
    108                 this,                      // root
    109                 true);
    110 
    111         mApplication = PhoneApp.getInstance();
    112 
    113         // The various touch UI features are enabled on a per-product
    114         // basis.  (These flags in config.xml may be overridden by
    115         // product-specific overlay files.)
    116 
    117         mAllowIncomingCallTouchUi = getResources().getBoolean(R.bool.allow_incoming_call_touch_ui);
    118         if (DBG) log("- incoming call touch UI: "
    119                      + (mAllowIncomingCallTouchUi ? "ENABLED" : "DISABLED"));
    120         mAllowInCallTouchUi = getResources().getBoolean(R.bool.allow_in_call_touch_ui);
    121         if (DBG) log("- regular in-call touch UI: "
    122                      + (mAllowInCallTouchUi ? "ENABLED" : "DISABLED"));
    123     }
    124 
    125     void setInCallScreenInstance(InCallScreen inCallScreen) {
    126         mInCallScreen = inCallScreen;
    127     }
    128 
    129     @Override
    130     protected void onFinishInflate() {
    131         super.onFinishInflate();
    132         if (DBG) log("InCallTouchUi onFinishInflate(this = " + this + ")...");
    133 
    134         // Look up the various UI elements.
    135 
    136         // "Dial-to-answer" widget for incoming calls.
    137         mIncomingCallWidget = (SlidingTab) findViewById(R.id.incomingCallWidget);
    138         mIncomingCallWidget.setLeftTabResources(
    139                 R.drawable.ic_jog_dial_answer,
    140                 com.android.internal.R.drawable.jog_tab_target_green,
    141                 com.android.internal.R.drawable.jog_tab_bar_left_answer,
    142                 com.android.internal.R.drawable.jog_tab_left_answer
    143                 );
    144         mIncomingCallWidget.setRightTabResources(
    145                 R.drawable.ic_jog_dial_decline,
    146                 com.android.internal.R.drawable.jog_tab_target_red,
    147                 com.android.internal.R.drawable.jog_tab_bar_right_decline,
    148                 com.android.internal.R.drawable.jog_tab_right_decline
    149                 );
    150 
    151         // For now, we only need to show two states: answer and decline.
    152         mIncomingCallWidget.setLeftHintText(R.string.slide_to_answer_hint);
    153         mIncomingCallWidget.setRightHintText(R.string.slide_to_decline_hint);
    154 
    155         mIncomingCallWidget.setOnTriggerListener(this);
    156 
    157         // Container for the UI elements shown while on a regular call.
    158         mInCallControls = findViewById(R.id.inCallControls);
    159 
    160         // Regular (single-tap) buttons, where we listen for click events:
    161         // Main cluster of buttons:
    162         mAddButton = (Button) mInCallControls.findViewById(R.id.addButton);
    163         mAddButton.setOnClickListener(this);
    164         mMergeButton = (Button) mInCallControls.findViewById(R.id.mergeButton);
    165         mMergeButton.setOnClickListener(this);
    166         mEndButton = (Button) mInCallControls.findViewById(R.id.endButton);
    167         mEndButton.setOnClickListener(this);
    168         mDialpadButton = (Button) mInCallControls.findViewById(R.id.dialpadButton);
    169         mDialpadButton.setOnClickListener(this);
    170         mBluetoothButton = (ToggleButton) mInCallControls.findViewById(R.id.bluetoothButton);
    171         mBluetoothButton.setOnClickListener(this);
    172         mMuteButton = (ToggleButton) mInCallControls.findViewById(R.id.muteButton);
    173         mMuteButton.setOnClickListener(this);
    174         mSpeakerButton = (ToggleButton) mInCallControls.findViewById(R.id.speakerButton);
    175         mSpeakerButton.setOnClickListener(this);
    176 
    177         // Upper corner buttons:
    178         mHoldButtonContainer = mInCallControls.findViewById(R.id.holdButtonContainer);
    179         mHoldButton = (ImageButton) mInCallControls.findViewById(R.id.holdButton);
    180         mHoldButton.setOnClickListener(this);
    181         mHoldButtonLabel = (TextView) mInCallControls.findViewById(R.id.holdButtonLabel);
    182         //
    183         mSwapButtonContainer = mInCallControls.findViewById(R.id.swapButtonContainer);
    184         mSwapButton = (ImageButton) mInCallControls.findViewById(R.id.swapButton);
    185         mSwapButton.setOnClickListener(this);
    186         mSwapButtonLabel = (TextView) mInCallControls.findViewById(R.id.swapButtonLabel);
    187         if (PhoneApp.getPhone().getPhoneType() == Phone.PHONE_TYPE_CDMA) {
    188             // In CDMA we use a generalized text - "Manage call", as behavior on selecting
    189             // this option depends entirely on what the current call state is.
    190             mSwapButtonLabel.setText(R.string.onscreenManageCallsText);
    191         } else {
    192             mSwapButtonLabel.setText(R.string.onscreenSwapCallsText);
    193         }
    194         //
    195         mCdmaMergeButtonContainer = mInCallControls.findViewById(R.id.cdmaMergeButtonContainer);
    196         mCdmaMergeButton = (ImageButton) mInCallControls.findViewById(R.id.cdmaMergeButton);
    197         mCdmaMergeButton.setOnClickListener(this);
    198 
    199         // Add a custom OnTouchListener to manually shrink the "hit
    200         // target" of some buttons.
    201         // (We do this for a few specific buttons which are vulnerable to
    202         // "false touches" because either (1) they're near the edge of the
    203         // screen and might be unintentionally touched while holding the
    204         // device in your hand, or (2) they're in the upper corners and might
    205         // be touched by the user's ear before the prox sensor has a chance to
    206         // kick in.)
    207         View.OnTouchListener smallerHitTargetTouchListener = new SmallerHitTargetTouchListener();
    208         mAddButton.setOnTouchListener(smallerHitTargetTouchListener);
    209         mMergeButton.setOnTouchListener(smallerHitTargetTouchListener);
    210         mDialpadButton.setOnTouchListener(smallerHitTargetTouchListener);
    211         mBluetoothButton.setOnTouchListener(smallerHitTargetTouchListener);
    212         mSpeakerButton.setOnTouchListener(smallerHitTargetTouchListener);
    213         mHoldButton.setOnTouchListener(smallerHitTargetTouchListener);
    214         mSwapButton.setOnTouchListener(smallerHitTargetTouchListener);
    215         mCdmaMergeButton.setOnTouchListener(smallerHitTargetTouchListener);
    216         mSpeakerButton.setOnTouchListener(smallerHitTargetTouchListener);
    217 
    218         // Icons we need to change dynamically.  (Most other icons are specified
    219         // directly in incall_touch_ui.xml.)
    220         mHoldIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_round_hold);
    221         mUnholdIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_round_unhold);
    222         mShowDialpadIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_dialpad);
    223         mHideDialpadIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_dialpad_close);
    224     }
    225 
    226     /**
    227      * Updates the visibility and/or state of our UI elements, based on
    228      * the current state of the phone.
    229      */
    230     void updateState(CallManager cm) {
    231         if (DBG) log("updateState( CallManager" + cm + ")...");
    232 
    233         if (mInCallScreen == null) {
    234             log("- updateState: mInCallScreen has been destroyed; bailing out...");
    235             return;
    236         }
    237 
    238         Phone.State state = cm.getState();  // IDLE, RINGING, or OFFHOOK
    239         if (DBG) log("- updateState: CallManager state is " + state);
    240 
    241         boolean showIncomingCallControls = false;
    242         boolean showInCallControls = false;
    243 
    244         final Call ringingCall = cm.getFirstActiveRingingCall();
    245         // If the FG call is dialing/alerting, we should display for that call
    246         // and ignore the ringing call. This case happens when the telephony
    247         // layer rejects the ringing call while the FG call is dialing/alerting,
    248         // but the incoming call *does* briefly exist in the DISCONNECTING or
    249         // DISCONNECTED state.
    250         if ((ringingCall.getState() != Call.State.IDLE)
    251                 && !cm.getActiveFgCallState().isDialing()) {
    252             // A phone call is ringing *or* call waiting.
    253             if (mAllowIncomingCallTouchUi) {
    254                 // Watch out: even if the phone state is RINGING, it's
    255                 // possible for the ringing call to be in the DISCONNECTING
    256                 // state.  (This typically happens immediately after the user
    257                 // rejects an incoming call, and in that case we *don't* show
    258                 // the incoming call controls.)
    259                 if (ringingCall.getState().isAlive()) {
    260                     if (DBG) log("- updateState: RINGING!  Showing incoming call controls...");
    261                     showIncomingCallControls = true;
    262                 }
    263 
    264                 // Ugly hack to cover up slow response from the radio:
    265                 // if we attempted to answer or reject an incoming call
    266                 // within the last 500 msec, *don't* show the incoming call
    267                 // UI even if the phone is still in the RINGING state.
    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                 // TODO: UI design issue: if the device is NOT currently
    275                 // locked, we probably don't need to make the user
    276                 // double-tap the "incoming call" buttons.  (The device
    277                 // presumably isn't in a pocket or purse, so we don't need
    278                 // to worry about false touches while it's ringing.)
    279                 // But OTOH having "inconsistent" buttons might just make
    280                 // it *more* confusing.
    281             }
    282         } else {
    283             if (mAllowInCallTouchUi) {
    284                 // Ok, the in-call touch UI is available on this platform,
    285                 // so make it visible (with some exceptions):
    286                 if (mInCallScreen.okToShowInCallTouchUi()) {
    287                     showInCallControls = true;
    288                 } else {
    289                     if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
    290                 }
    291             }
    292         }
    293 
    294         if (showInCallControls) {
    295             // TODO change the phone to CallManager
    296             updateInCallControls(cm.getActiveFgCall().getPhone());
    297         }
    298 
    299         if (showIncomingCallControls && showInCallControls) {
    300             throw new IllegalStateException(
    301                 "'Incoming' and 'in-call' touch controls visible at the same time!");
    302         }
    303 
    304         if (showIncomingCallControls) {
    305             showIncomingCallWidget();
    306         } else {
    307             hideIncomingCallWidget();
    308         }
    309 
    310         mInCallControls.setVisibility(showInCallControls ? View.VISIBLE : View.GONE);
    311 
    312         // TODO: As an optimization, also consider setting the visibility
    313         // of the overall InCallTouchUi widget to GONE if *nothing at all*
    314         // is visible right now.
    315     }
    316 
    317     // View.OnClickListener implementation
    318     public void onClick(View view) {
    319         int id = view.getId();
    320         if (DBG) log("onClick(View " + view + ", id " + id + ")...");
    321 
    322         switch (id) {
    323             case R.id.addButton:
    324             case R.id.mergeButton:
    325             case R.id.endButton:
    326             case R.id.dialpadButton:
    327             case R.id.bluetoothButton:
    328             case R.id.muteButton:
    329             case R.id.speakerButton:
    330             case R.id.holdButton:
    331             case R.id.swapButton:
    332             case R.id.cdmaMergeButton:
    333                 // Clicks on the regular onscreen buttons get forwarded
    334                 // straight to the InCallScreen.
    335                 mInCallScreen.handleOnscreenButtonClick(id);
    336                 break;
    337 
    338             default:
    339                 Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
    340                 break;
    341         }
    342     }
    343 
    344     /**
    345      * Updates the enabledness and "checked" state of the buttons on the
    346      * "inCallControls" panel, based on the current telephony state.
    347      */
    348     void updateInCallControls(Phone phone) {
    349         int phoneType = phone.getPhoneType();
    350         // Note we do NOT need to worry here about cases where the entire
    351         // in-call touch UI is disabled, like during an OTA call or if the
    352         // dtmf dialpad is up.  (That's handled by updateState(), which
    353         // calls InCallScreen.okToShowInCallTouchUi().)
    354         //
    355         // If we get here, it *is* OK to show the in-call touch UI, so we
    356         // now need to update the enabledness and/or "checked" state of
    357         // each individual button.
    358         //
    359 
    360         // The InCallControlState object tells us the enabledness and/or
    361         // state of the various onscreen buttons:
    362         InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
    363 
    364         // "Add" or "Merge":
    365         // These two buttons occupy the same space onscreen, so only
    366         // one of them should be available at a given moment.
    367         if (inCallControlState.canAddCall) {
    368             mAddButton.setVisibility(View.VISIBLE);
    369             mAddButton.setEnabled(true);
    370             mMergeButton.setVisibility(View.GONE);
    371         } else if (inCallControlState.canMerge) {
    372             if (phoneType == Phone.PHONE_TYPE_CDMA) {
    373                 // In CDMA "Add" option is always given to the user and the
    374                 // "Merge" option is provided as a button on the top left corner of the screen,
    375                 // we always set the mMergeButton to GONE
    376                 mMergeButton.setVisibility(View.GONE);
    377             } else if ((phoneType == Phone.PHONE_TYPE_GSM)
    378                     || (phoneType == Phone.PHONE_TYPE_SIP)) {
    379                 mMergeButton.setVisibility(View.VISIBLE);
    380                 mMergeButton.setEnabled(true);
    381                 mAddButton.setVisibility(View.GONE);
    382             } else {
    383                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
    384             }
    385         } else {
    386             // Neither "Add" nor "Merge" is available.  (This happens in
    387             // some transient states, like while dialing an outgoing call,
    388             // and in other rare cases like if you have both lines in use
    389             // *and* there are already 5 people on the conference call.)
    390             // Since the common case here is "while dialing", we show the
    391             // "Add" button in a disabled state so that there won't be any
    392             // jarring change in the UI when the call finally connects.
    393             mAddButton.setVisibility(View.VISIBLE);
    394             mAddButton.setEnabled(false);
    395             mMergeButton.setVisibility(View.GONE);
    396         }
    397         if (inCallControlState.canAddCall && inCallControlState.canMerge) {
    398             if ((phoneType == Phone.PHONE_TYPE_GSM)
    399                     || (phoneType == Phone.PHONE_TYPE_SIP)) {
    400                 // Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
    401                 // should both be available right now.  This *should* never
    402                 // happen with GSM, but if it's possible on any
    403                 // future devices we may need to re-layout Add and Merge so
    404                 // they can both be visible at the same time...
    405                 Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
    406                         " but can't show both!");
    407             } else if (phoneType == Phone.PHONE_TYPE_CDMA) {
    408                 // In CDMA "Add" option is always given to the user and the hence
    409                 // in this case both "Add" and "Merge" options would be available to user
    410                 if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
    411             } else {
    412                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
    413             }
    414         }
    415 
    416         // "End call": this button has no state and it's always enabled.
    417         mEndButton.setEnabled(true);
    418 
    419         // "Dialpad": Enabled only when it's OK to use the dialpad in the
    420         // first place.
    421         mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
    422         //
    423         if (inCallControlState.dialpadVisible) {
    424             // Show the "hide dialpad" state.
    425             mDialpadButton.setText(R.string.onscreenHideDialpadText);
    426             mDialpadButton.setCompoundDrawablesWithIntrinsicBounds(
    427                 null, mHideDialpadIcon, null, null);
    428         } else {
    429             // Show the "show dialpad" state.
    430             mDialpadButton.setText(R.string.onscreenShowDialpadText);
    431             mDialpadButton.setCompoundDrawablesWithIntrinsicBounds(
    432                     null, mShowDialpadIcon, null, null);
    433         }
    434 
    435         // "Bluetooth"
    436         mBluetoothButton.setEnabled(inCallControlState.bluetoothEnabled);
    437         mBluetoothButton.setChecked(inCallControlState.bluetoothIndicatorOn);
    438 
    439         // "Mute"
    440         mMuteButton.setEnabled(inCallControlState.canMute);
    441         mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
    442 
    443         // "Speaker"
    444         mSpeakerButton.setEnabled(inCallControlState.speakerEnabled);
    445         mSpeakerButton.setChecked(inCallControlState.speakerOn);
    446 
    447         // "Hold"
    448         // (Note "Hold" and "Swap" are never both available at
    449         // the same time.  That's why it's OK for them to both be in the
    450         // same position onscreen.)
    451         // This button is totally hidden (rather than just disabled)
    452         // when the operation isn't available.
    453         mHoldButtonContainer.setVisibility(
    454                 inCallControlState.canHold ? View.VISIBLE : View.GONE);
    455         if (inCallControlState.canHold) {
    456             // The Hold button icon and label (either "Hold" or "Unhold")
    457             // depend on the current Hold state.
    458             if (inCallControlState.onHold) {
    459                 mHoldButton.setImageDrawable(mUnholdIcon);
    460                 mHoldButtonLabel.setText(R.string.onscreenUnholdText);
    461             } else {
    462                 mHoldButton.setImageDrawable(mHoldIcon);
    463                 mHoldButtonLabel.setText(R.string.onscreenHoldText);
    464             }
    465         }
    466 
    467         // "Swap"
    468         // This button is totally hidden (rather than just disabled)
    469         // when the operation isn't available.
    470         mSwapButtonContainer.setVisibility(
    471                 inCallControlState.canSwap ? View.VISIBLE : View.GONE);
    472 
    473         if (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
    474             // "Merge"
    475             // This button is totally hidden (rather than just disabled)
    476             // when the operation isn't available.
    477             mCdmaMergeButtonContainer.setVisibility(
    478                     inCallControlState.canMerge ? View.VISIBLE : View.GONE);
    479         }
    480 
    481         if (inCallControlState.canSwap && inCallControlState.canHold) {
    482             // Uh oh, the InCallControlState thinks that Swap *and* Hold
    483             // should both be available.  This *should* never happen with
    484             // either GSM or CDMA, but if it's possible on any future
    485             // devices we may need to re-layout Hold and Swap so they can
    486             // both be visible at the same time...
    487             Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
    488         }
    489 
    490         if (phoneType == Phone.PHONE_TYPE_CDMA) {
    491             if (inCallControlState.canSwap && inCallControlState.canMerge) {
    492                 // Uh oh, the InCallControlState thinks that Swap *and* Merge
    493                 // should both be available.  This *should* never happen with
    494                 // CDMA, but if it's possible on any future
    495                 // devices we may need to re-layout Merge and Swap so they can
    496                 // both be visible at the same time...
    497                 Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
    498                         "enabled, but can't show both!");
    499             }
    500         }
    501 
    502         // One final special case: if the dialpad is visible, that trumps
    503         // *any* of the upper corner buttons:
    504         if (inCallControlState.dialpadVisible) {
    505             mHoldButtonContainer.setVisibility(View.GONE);
    506             mSwapButtonContainer.setVisibility(View.GONE);
    507             mCdmaMergeButtonContainer.setVisibility(View.GONE);
    508         }
    509     }
    510 
    511     //
    512     // InCallScreen API
    513     //
    514 
    515     /**
    516      * @return true if the onscreen touch UI is enabled (for regular
    517      * "ongoing call" states) on the current device.
    518      */
    519     /* package */ boolean isTouchUiEnabled() {
    520         return mAllowInCallTouchUi;
    521     }
    522 
    523     /**
    524      * @return true if the onscreen touch UI is enabled for
    525      * the "incoming call" state on the current device.
    526      */
    527     /* package */ boolean isIncomingCallTouchUiEnabled() {
    528         return mAllowIncomingCallTouchUi;
    529     }
    530 
    531     //
    532     // SlidingTab.OnTriggerListener implementation
    533     //
    534 
    535     /**
    536      * Handles "Answer" and "Reject" actions for an incoming call.
    537      * We get this callback from the SlidingTab
    538      * when the user triggers an action.
    539      *
    540      * To answer or reject the incoming call, we call
    541      * InCallScreen.handleOnscreenButtonClick() and pass one of the
    542      * special "virtual button" IDs:
    543      *   - R.id.answerButton to answer the call
    544      * or
    545      *   - R.id.rejectButton to reject the call.
    546      */
    547     public void onTrigger(View v, int whichHandle) {
    548         log("onDialTrigger(whichHandle = " + whichHandle + ")...");
    549 
    550         switch (whichHandle) {
    551             case SlidingTab.OnTriggerListener.LEFT_HANDLE:
    552                 if (DBG) log("LEFT_HANDLE: answer!");
    553 
    554                 hideIncomingCallWidget();
    555 
    556                 // ...and also prevent it from reappearing right away.
    557                 // (This covers up a slow response from the radio; see updateState().)
    558                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
    559 
    560                 // Do the appropriate action.
    561                 if (mInCallScreen != null) {
    562                     // Send this to the InCallScreen as a virtual "button click" event:
    563                     mInCallScreen.handleOnscreenButtonClick(R.id.answerButton);
    564                 } else {
    565                     Log.e(LOG_TAG, "answer trigger: mInCallScreen is null");
    566                 }
    567                 break;
    568 
    569             case SlidingTab.OnTriggerListener.RIGHT_HANDLE:
    570                 if (DBG) log("RIGHT_HANDLE: reject!");
    571 
    572                 hideIncomingCallWidget();
    573 
    574                 // ...and also prevent it from reappearing right away.
    575                 // (This covers up a slow response from the radio; see updateState().)
    576                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
    577 
    578                 // Do the appropriate action.
    579                 if (mInCallScreen != null) {
    580                     // Send this to the InCallScreen as a virtual "button click" event:
    581                     mInCallScreen.handleOnscreenButtonClick(R.id.rejectButton);
    582                 } else {
    583                     Log.e(LOG_TAG, "reject trigger: mInCallScreen is null");
    584                 }
    585                 break;
    586 
    587             default:
    588                 Log.e(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
    589                 break;
    590         }
    591 
    592         // Regardless of what action the user did, be sure to clear out
    593         // the hint text we were displaying while the user was dragging.
    594         mInCallScreen.updateSlidingTabHint(0, 0);
    595     }
    596 
    597     /**
    598      * Apply an animation to hide the incoming call widget.
    599      */
    600     private void hideIncomingCallWidget() {
    601         if (mIncomingCallWidget.getVisibility() != View.VISIBLE
    602                 || mIncomingCallWidget.getAnimation() != null) {
    603             // Widget is already hidden or in the process of being hidden
    604             return;
    605         }
    606         // Hide the incoming call screen with a transition
    607         AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f);
    608         anim.setDuration(IN_CALL_WIDGET_TRANSITION_TIME);
    609         anim.setAnimationListener(new AnimationListener() {
    610 
    611             public void onAnimationStart(Animation animation) {
    612 
    613             }
    614 
    615             public void onAnimationRepeat(Animation animation) {
    616 
    617             }
    618 
    619             public void onAnimationEnd(Animation animation) {
    620                 // hide the incoming call UI.
    621                 mIncomingCallWidget.clearAnimation();
    622                 mIncomingCallWidget.setVisibility(View.GONE);
    623             }
    624         });
    625         mIncomingCallWidget.startAnimation(anim);
    626     }
    627 
    628     /**
    629      * Shows the incoming call widget and cancels any animation that may be fading it out.
    630      */
    631     private void showIncomingCallWidget() {
    632         Animation anim = mIncomingCallWidget.getAnimation();
    633         if (anim != null) {
    634             anim.reset();
    635             mIncomingCallWidget.clearAnimation();
    636         }
    637         mIncomingCallWidget.reset(false);
    638         mIncomingCallWidget.setVisibility(View.VISIBLE);
    639     }
    640 
    641     /**
    642      * Handles state changes of the SlidingTabSelector widget.  While the user
    643      * is dragging one of the handles, we display an onscreen hint; see
    644      * CallCard.getRotateWidgetHint().
    645      */
    646     public void onGrabbedStateChange(View v, int grabbedState) {
    647         if (mInCallScreen != null) {
    648             // Look up the hint based on which handle is currently grabbed.
    649             // (Note we don't simply pass grabbedState thru to the InCallScreen,
    650             // since *this* class is the only place that knows that the left
    651             // handle means "Answer" and the right handle means "Decline".)
    652             int hintTextResId, hintColorResId;
    653             switch (grabbedState) {
    654                 case SlidingTab.OnTriggerListener.NO_HANDLE:
    655                     hintTextResId = 0;
    656                     hintColorResId = 0;
    657                     break;
    658                 case SlidingTab.OnTriggerListener.LEFT_HANDLE:
    659                     // TODO: Use different variants of "Slide to answer" in some cases
    660                     // depending on the phone state, like slide_to_answer_and_hold
    661                     // for a call waiting call, or slide_to_answer_and_end_active or
    662                     // slide_to_answer_and_end_onhold for the 2-lines-in-use case.
    663                     // (Note these are GSM-only cases, though.)
    664                     hintTextResId = R.string.slide_to_answer;
    665                     hintColorResId = R.color.incall_textConnected;  // green
    666                     break;
    667                 case SlidingTab.OnTriggerListener.RIGHT_HANDLE:
    668                     hintTextResId = R.string.slide_to_decline;
    669                     hintColorResId = R.color.incall_textEnded;  // red
    670                     break;
    671                 default:
    672                     Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
    673                           + grabbedState);
    674                     hintTextResId = 0;
    675                     hintColorResId = 0;
    676                     break;
    677             }
    678 
    679             // Tell the InCallScreen to update the CallCard and force the
    680             // screen to redraw.
    681             mInCallScreen.updateSlidingTabHint(hintTextResId, hintColorResId);
    682         }
    683     }
    684 
    685 
    686     /**
    687      * OnTouchListener used to shrink the "hit target" of some onscreen
    688      * buttons.
    689      */
    690     class SmallerHitTargetTouchListener implements View.OnTouchListener {
    691         /**
    692          * Width of the allowable "hit target" as a percentage of
    693          * the total width of this button.
    694          */
    695         private static final int HIT_TARGET_PERCENT_X = 50;
    696 
    697         /**
    698          * Height of the allowable "hit target" as a percentage of
    699          * the total height of this button.
    700          *
    701          * This is larger than HIT_TARGET_PERCENT_X because some of
    702          * the onscreen buttons are wide but not very tall and we don't
    703          * want to make the vertical hit target *too* small.
    704          */
    705         private static final int HIT_TARGET_PERCENT_Y = 80;
    706 
    707         // Size (percentage-wise) of the "edge" area that's *not* touch-sensitive.
    708         private static final int X_EDGE = (100 - HIT_TARGET_PERCENT_X) / 2;
    709         private static final int Y_EDGE = (100 - HIT_TARGET_PERCENT_Y) / 2;
    710         // Min/max values (percentage-wise) of the touch-sensitive hit target.
    711         private static final int X_HIT_MIN = X_EDGE;
    712         private static final int X_HIT_MAX = 100 - X_EDGE;
    713         private static final int Y_HIT_MIN = Y_EDGE;
    714         private static final int Y_HIT_MAX = 100 - Y_EDGE;
    715 
    716         // True if the most recent DOWN event was a "hit".
    717         boolean mDownEventHit;
    718 
    719         /**
    720          * Called when a touch event is dispatched to a view. This allows listeners to
    721          * get a chance to respond before the target view.
    722          *
    723          * @return True if the listener has consumed the event, false otherwise.
    724          *         (In other words, we return true when the touch is *outside*
    725          *         the "smaller hit target", which will prevent the actual
    726          *         button from handling these events.)
    727          */
    728         public boolean onTouch(View v, MotionEvent event) {
    729             // if (DBG) log("SmallerHitTargetTouchListener: " + v + ", event " + event);
    730 
    731             if (event.getAction() == MotionEvent.ACTION_DOWN) {
    732                 // Note that event.getX() and event.getY() are already
    733                 // translated into the View's coordinates.  (In other words,
    734                 // "0,0" is a touch on the upper-left-most corner of the view.)
    735                 int touchX = (int) event.getX();
    736                 int touchY = (int) event.getY();
    737 
    738                 int viewWidth = v.getWidth();
    739                 int viewHeight = v.getHeight();
    740 
    741                 // Touch location as a percentage of the total button width or height.
    742                 int touchXPercent = (int) ((float) (touchX * 100) / (float) viewWidth);
    743                 int touchYPercent = (int) ((float) (touchY * 100) / (float) viewHeight);
    744                 // if (DBG) log("- percentage:  x = " + touchXPercent + ",  y = " + touchYPercent);
    745 
    746                 // TODO: user research: add event logging here of the actual
    747                 // hit location (and button ID), and enable it for dogfooders
    748                 // for a few days.  That'll give us a good idea of how close
    749                 // to the center of the button(s) most touch events are, to
    750                 // help us fine-tune the HIT_TARGET_PERCENT_* constants.
    751 
    752                 if (touchXPercent < X_HIT_MIN || touchXPercent > X_HIT_MAX
    753                         || touchYPercent < Y_HIT_MIN || touchYPercent > Y_HIT_MAX) {
    754                     // Missed!
    755                     // if (DBG) log("  -> MISSED!");
    756                     mDownEventHit = false;
    757                     return true;  // Consume this event; don't let the button see it
    758                 } else {
    759                     // Hit!
    760                     // if (DBG) log("  -> HIT!");
    761                     mDownEventHit = true;
    762                     return false;  // Let this event through to the actual button
    763                 }
    764             } else {
    765                 // This is a MOVE, UP or CANCEL event.
    766                 //
    767                 // We only do the "smaller hit target" check on DOWN events.
    768                 // For the subsequent MOVE/UP/CANCEL events, we let them
    769                 // through to the actual button IFF the previous DOWN event
    770                 // got through to the actual button (i.e. it was a "hit".)
    771                 return !mDownEventHit;
    772             }
    773         }
    774     }
    775 
    776 
    777     // Debugging / testing code
    778 
    779     private void log(String msg) {
    780         Log.d(LOG_TAG, msg);
    781     }
    782 }
    783