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 (mInCallScreen == null) {
    232             log("- updateState: mInCallScreen has been destroyed; bailing out...");
    233             return;
    234         }
    235 
    236         Phone.State state = cm.getState();  // IDLE, RINGING, or OFFHOOK
    237         if (DBG) log("- updateState: CallManager state is " + state);
    238 
    239         boolean showIncomingCallControls = false;
    240         boolean showInCallControls = false;
    241 
    242         final Call ringingCall = cm.getFirstActiveRingingCall();
    243         // If the FG call is dialing/alerting, we should display for that call
    244         // and ignore the ringing call. This case happens when the telephony
    245         // layer rejects the ringing call while the FG call is dialing/alerting,
    246         // but the incoming call *does* briefly exist in the DISCONNECTING or
    247         // DISCONNECTED state.
    248         if ((ringingCall.getState() != Call.State.IDLE)
    249                 && !cm.getActiveFgCallState().isDialing()) {
    250             // A phone call is ringing *or* call waiting.
    251             if (mAllowIncomingCallTouchUi) {
    252                 // Watch out: even if the phone state is RINGING, it's
    253                 // possible for the ringing call to be in the DISCONNECTING
    254                 // state.  (This typically happens immediately after the user
    255                 // rejects an incoming call, and in that case we *don't* show
    256                 // the incoming call controls.)
    257                 if (ringingCall.getState().isAlive()) {
    258                     if (DBG) log("- updateState: RINGING!  Showing incoming call controls...");
    259                     showIncomingCallControls = true;
    260                 }
    261 
    262                 // Ugly hack to cover up slow response from the radio:
    263                 // if we attempted to answer or reject an incoming call
    264                 // within the last 500 msec, *don't* show the incoming call
    265                 // UI even if the phone is still in the RINGING state.
    266                 long now = SystemClock.uptimeMillis();
    267                 if (now < mLastIncomingCallActionTime + 500) {
    268                     log("updateState: Too soon after last action; not drawing!");
    269                     showIncomingCallControls = false;
    270                 }
    271 
    272                 // TODO: UI design issue: if the device is NOT currently
    273                 // locked, we probably don't need to make the user
    274                 // double-tap the "incoming call" buttons.  (The device
    275                 // presumably isn't in a pocket or purse, so we don't need
    276                 // to worry about false touches while it's ringing.)
    277                 // But OTOH having "inconsistent" buttons might just make
    278                 // it *more* confusing.
    279             }
    280         } else {
    281             if (mAllowInCallTouchUi) {
    282                 // Ok, the in-call touch UI is available on this platform,
    283                 // so make it visible (with some exceptions):
    284                 if (mInCallScreen.okToShowInCallTouchUi()) {
    285                     showInCallControls = true;
    286                 } else {
    287                     if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
    288                 }
    289             }
    290         }
    291 
    292         if (showInCallControls) {
    293             // TODO change the phone to CallManager
    294             updateInCallControls(cm.getActiveFgCall().getPhone());
    295         }
    296 
    297         if (showIncomingCallControls && showInCallControls) {
    298             throw new IllegalStateException(
    299                 "'Incoming' and 'in-call' touch controls visible at the same time!");
    300         }
    301 
    302         if (showIncomingCallControls) {
    303             showIncomingCallWidget();
    304         } else {
    305             hideIncomingCallWidget();
    306         }
    307 
    308         mInCallControls.setVisibility(showInCallControls ? View.VISIBLE : View.GONE);
    309 
    310         // TODO: As an optimization, also consider setting the visibility
    311         // of the overall InCallTouchUi widget to GONE if *nothing at all*
    312         // is visible right now.
    313     }
    314 
    315     // View.OnClickListener implementation
    316     public void onClick(View view) {
    317         int id = view.getId();
    318         if (DBG) log("onClick(View " + view + ", id " + id + ")...");
    319 
    320         switch (id) {
    321             case R.id.addButton:
    322             case R.id.mergeButton:
    323             case R.id.endButton:
    324             case R.id.dialpadButton:
    325             case R.id.bluetoothButton:
    326             case R.id.muteButton:
    327             case R.id.speakerButton:
    328             case R.id.holdButton:
    329             case R.id.swapButton:
    330             case R.id.cdmaMergeButton:
    331                 // Clicks on the regular onscreen buttons get forwarded
    332                 // straight to the InCallScreen.
    333                 mInCallScreen.handleOnscreenButtonClick(id);
    334                 break;
    335 
    336             default:
    337                 Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
    338                 break;
    339         }
    340     }
    341 
    342     /**
    343      * Updates the enabledness and "checked" state of the buttons on the
    344      * "inCallControls" panel, based on the current telephony state.
    345      */
    346     void updateInCallControls(Phone phone) {
    347         int phoneType = phone.getPhoneType();
    348         // Note we do NOT need to worry here about cases where the entire
    349         // in-call touch UI is disabled, like during an OTA call or if the
    350         // dtmf dialpad is up.  (That's handled by updateState(), which
    351         // calls InCallScreen.okToShowInCallTouchUi().)
    352         //
    353         // If we get here, it *is* OK to show the in-call touch UI, so we
    354         // now need to update the enabledness and/or "checked" state of
    355         // each individual button.
    356         //
    357 
    358         // The InCallControlState object tells us the enabledness and/or
    359         // state of the various onscreen buttons:
    360         InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
    361 
    362         // "Add" or "Merge":
    363         // These two buttons occupy the same space onscreen, so only
    364         // one of them should be available at a given moment.
    365         if (inCallControlState.canAddCall) {
    366             mAddButton.setVisibility(View.VISIBLE);
    367             mAddButton.setEnabled(true);
    368             mMergeButton.setVisibility(View.GONE);
    369         } else if (inCallControlState.canMerge) {
    370             if (phoneType == Phone.PHONE_TYPE_CDMA) {
    371                 // In CDMA "Add" option is always given to the user and the
    372                 // "Merge" option is provided as a button on the top left corner of the screen,
    373                 // we always set the mMergeButton to GONE
    374                 mMergeButton.setVisibility(View.GONE);
    375             } else if ((phoneType == Phone.PHONE_TYPE_GSM)
    376                     || (phoneType == Phone.PHONE_TYPE_SIP)) {
    377                 mMergeButton.setVisibility(View.VISIBLE);
    378                 mMergeButton.setEnabled(true);
    379                 mAddButton.setVisibility(View.GONE);
    380             } else {
    381                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
    382             }
    383         } else {
    384             // Neither "Add" nor "Merge" is available.  (This happens in
    385             // some transient states, like while dialing an outgoing call,
    386             // and in other rare cases like if you have both lines in use
    387             // *and* there are already 5 people on the conference call.)
    388             // Since the common case here is "while dialing", we show the
    389             // "Add" button in a disabled state so that there won't be any
    390             // jarring change in the UI when the call finally connects.
    391             mAddButton.setVisibility(View.VISIBLE);
    392             mAddButton.setEnabled(false);
    393             mMergeButton.setVisibility(View.GONE);
    394         }
    395         if (inCallControlState.canAddCall && inCallControlState.canMerge) {
    396             if ((phoneType == Phone.PHONE_TYPE_GSM)
    397                     || (phoneType == Phone.PHONE_TYPE_SIP)) {
    398                 // Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
    399                 // should both be available right now.  This *should* never
    400                 // happen with GSM, but if it's possible on any
    401                 // future devices we may need to re-layout Add and Merge so
    402                 // they can both be visible at the same time...
    403                 Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
    404                         " but can't show both!");
    405             } else if (phoneType == Phone.PHONE_TYPE_CDMA) {
    406                 // In CDMA "Add" option is always given to the user and the hence
    407                 // in this case both "Add" and "Merge" options would be available to user
    408                 if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
    409             } else {
    410                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
    411             }
    412         }
    413 
    414         // "End call": this button has no state and it's always enabled.
    415         mEndButton.setEnabled(true);
    416 
    417         // "Dialpad": Enabled only when it's OK to use the dialpad in the
    418         // first place.
    419         mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
    420         //
    421         if (inCallControlState.dialpadVisible) {
    422             // Show the "hide dialpad" state.
    423             mDialpadButton.setText(R.string.onscreenHideDialpadText);
    424             mDialpadButton.setCompoundDrawablesWithIntrinsicBounds(
    425                 null, mHideDialpadIcon, null, null);
    426         } else {
    427             // Show the "show dialpad" state.
    428             mDialpadButton.setText(R.string.onscreenShowDialpadText);
    429             mDialpadButton.setCompoundDrawablesWithIntrinsicBounds(
    430                     null, mShowDialpadIcon, null, null);
    431         }
    432 
    433         // "Bluetooth"
    434         mBluetoothButton.setEnabled(inCallControlState.bluetoothEnabled);
    435         mBluetoothButton.setChecked(inCallControlState.bluetoothIndicatorOn);
    436 
    437         // "Mute"
    438         mMuteButton.setEnabled(inCallControlState.canMute);
    439         mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
    440 
    441         // "Speaker"
    442         mSpeakerButton.setEnabled(inCallControlState.speakerEnabled);
    443         mSpeakerButton.setChecked(inCallControlState.speakerOn);
    444 
    445         // "Hold"
    446         // (Note "Hold" and "Swap" are never both available at
    447         // the same time.  That's why it's OK for them to both be in the
    448         // same position onscreen.)
    449         // This button is totally hidden (rather than just disabled)
    450         // when the operation isn't available.
    451         mHoldButtonContainer.setVisibility(
    452                 inCallControlState.canHold ? View.VISIBLE : View.GONE);
    453         if (inCallControlState.canHold) {
    454             // The Hold button icon and label (either "Hold" or "Unhold")
    455             // depend on the current Hold state.
    456             if (inCallControlState.onHold) {
    457                 mHoldButton.setImageDrawable(mUnholdIcon);
    458                 mHoldButtonLabel.setText(R.string.onscreenUnholdText);
    459             } else {
    460                 mHoldButton.setImageDrawable(mHoldIcon);
    461                 mHoldButtonLabel.setText(R.string.onscreenHoldText);
    462             }
    463         }
    464 
    465         // "Swap"
    466         // This button is totally hidden (rather than just disabled)
    467         // when the operation isn't available.
    468         mSwapButtonContainer.setVisibility(
    469                 inCallControlState.canSwap ? View.VISIBLE : View.GONE);
    470 
    471         if (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
    472             // "Merge"
    473             // This button is totally hidden (rather than just disabled)
    474             // when the operation isn't available.
    475             mCdmaMergeButtonContainer.setVisibility(
    476                     inCallControlState.canMerge ? View.VISIBLE : View.GONE);
    477         }
    478 
    479         if (inCallControlState.canSwap && inCallControlState.canHold) {
    480             // Uh oh, the InCallControlState thinks that Swap *and* Hold
    481             // should both be available.  This *should* never happen with
    482             // either GSM or CDMA, but if it's possible on any future
    483             // devices we may need to re-layout Hold and Swap so they can
    484             // both be visible at the same time...
    485             Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
    486         }
    487 
    488         if (phoneType == Phone.PHONE_TYPE_CDMA) {
    489             if (inCallControlState.canSwap && inCallControlState.canMerge) {
    490                 // Uh oh, the InCallControlState thinks that Swap *and* Merge
    491                 // should both be available.  This *should* never happen with
    492                 // CDMA, but if it's possible on any future
    493                 // devices we may need to re-layout Merge and Swap so they can
    494                 // both be visible at the same time...
    495                 Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
    496                         "enabled, but can't show both!");
    497             }
    498         }
    499 
    500         // One final special case: if the dialpad is visible, that trumps
    501         // *any* of the upper corner buttons:
    502         if (inCallControlState.dialpadVisible) {
    503             mHoldButtonContainer.setVisibility(View.GONE);
    504             mSwapButtonContainer.setVisibility(View.GONE);
    505             mCdmaMergeButtonContainer.setVisibility(View.GONE);
    506         }
    507     }
    508 
    509     //
    510     // InCallScreen API
    511     //
    512 
    513     /**
    514      * @return true if the onscreen touch UI is enabled (for regular
    515      * "ongoing call" states) on the current device.
    516      */
    517     /* package */ boolean isTouchUiEnabled() {
    518         return mAllowInCallTouchUi;
    519     }
    520 
    521     /**
    522      * @return true if the onscreen touch UI is enabled for
    523      * the "incoming call" state on the current device.
    524      */
    525     /* package */ boolean isIncomingCallTouchUiEnabled() {
    526         return mAllowIncomingCallTouchUi;
    527     }
    528 
    529     //
    530     // SlidingTab.OnTriggerListener implementation
    531     //
    532 
    533     /**
    534      * Handles "Answer" and "Reject" actions for an incoming call.
    535      * We get this callback from the SlidingTab
    536      * when the user triggers an action.
    537      *
    538      * To answer or reject the incoming call, we call
    539      * InCallScreen.handleOnscreenButtonClick() and pass one of the
    540      * special "virtual button" IDs:
    541      *   - R.id.answerButton to answer the call
    542      * or
    543      *   - R.id.rejectButton to reject the call.
    544      */
    545     public void onTrigger(View v, int whichHandle) {
    546         log("onDialTrigger(whichHandle = " + whichHandle + ")...");
    547 
    548         switch (whichHandle) {
    549             case SlidingTab.OnTriggerListener.LEFT_HANDLE:
    550                 if (DBG) log("LEFT_HANDLE: answer!");
    551 
    552                 hideIncomingCallWidget();
    553 
    554                 // ...and also prevent it from reappearing right away.
    555                 // (This covers up a slow response from the radio; see updateState().)
    556                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
    557 
    558                 // Do the appropriate action.
    559                 if (mInCallScreen != null) {
    560                     // Send this to the InCallScreen as a virtual "button click" event:
    561                     mInCallScreen.handleOnscreenButtonClick(R.id.answerButton);
    562                 } else {
    563                     Log.e(LOG_TAG, "answer trigger: mInCallScreen is null");
    564                 }
    565                 break;
    566 
    567             case SlidingTab.OnTriggerListener.RIGHT_HANDLE:
    568                 if (DBG) log("RIGHT_HANDLE: reject!");
    569 
    570                 hideIncomingCallWidget();
    571 
    572                 // ...and also prevent it from reappearing right away.
    573                 // (This covers up a slow response from the radio; see updateState().)
    574                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
    575 
    576                 // Do the appropriate action.
    577                 if (mInCallScreen != null) {
    578                     // Send this to the InCallScreen as a virtual "button click" event:
    579                     mInCallScreen.handleOnscreenButtonClick(R.id.rejectButton);
    580                 } else {
    581                     Log.e(LOG_TAG, "reject trigger: mInCallScreen is null");
    582                 }
    583                 break;
    584 
    585             default:
    586                 Log.e(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
    587                 break;
    588         }
    589 
    590         // Regardless of what action the user did, be sure to clear out
    591         // the hint text we were displaying while the user was dragging.
    592         mInCallScreen.updateSlidingTabHint(0, 0);
    593     }
    594 
    595     /**
    596      * Apply an animation to hide the incoming call widget.
    597      */
    598     private void hideIncomingCallWidget() {
    599         if (mIncomingCallWidget.getVisibility() != View.VISIBLE
    600                 || mIncomingCallWidget.getAnimation() != null) {
    601             // Widget is already hidden or in the process of being hidden
    602             return;
    603         }
    604         // Hide the incoming call screen with a transition
    605         AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f);
    606         anim.setDuration(IN_CALL_WIDGET_TRANSITION_TIME);
    607         anim.setAnimationListener(new AnimationListener() {
    608 
    609             public void onAnimationStart(Animation animation) {
    610 
    611             }
    612 
    613             public void onAnimationRepeat(Animation animation) {
    614 
    615             }
    616 
    617             public void onAnimationEnd(Animation animation) {
    618                 // hide the incoming call UI.
    619                 mIncomingCallWidget.clearAnimation();
    620                 mIncomingCallWidget.setVisibility(View.GONE);
    621             }
    622         });
    623         mIncomingCallWidget.startAnimation(anim);
    624     }
    625 
    626     /**
    627      * Shows the incoming call widget and cancels any animation that may be fading it out.
    628      */
    629     private void showIncomingCallWidget() {
    630         Animation anim = mIncomingCallWidget.getAnimation();
    631         if (anim != null) {
    632             anim.reset();
    633             mIncomingCallWidget.clearAnimation();
    634         }
    635         mIncomingCallWidget.reset(false);
    636         mIncomingCallWidget.setVisibility(View.VISIBLE);
    637     }
    638 
    639     /**
    640      * Handles state changes of the SlidingTabSelector widget.  While the user
    641      * is dragging one of the handles, we display an onscreen hint; see
    642      * CallCard.getRotateWidgetHint().
    643      */
    644     public void onGrabbedStateChange(View v, int grabbedState) {
    645         if (mInCallScreen != null) {
    646             // Look up the hint based on which handle is currently grabbed.
    647             // (Note we don't simply pass grabbedState thru to the InCallScreen,
    648             // since *this* class is the only place that knows that the left
    649             // handle means "Answer" and the right handle means "Decline".)
    650             int hintTextResId, hintColorResId;
    651             switch (grabbedState) {
    652                 case SlidingTab.OnTriggerListener.NO_HANDLE:
    653                     hintTextResId = 0;
    654                     hintColorResId = 0;
    655                     break;
    656                 case SlidingTab.OnTriggerListener.LEFT_HANDLE:
    657                     // TODO: Use different variants of "Slide to answer" in some cases
    658                     // depending on the phone state, like slide_to_answer_and_hold
    659                     // for a call waiting call, or slide_to_answer_and_end_active or
    660                     // slide_to_answer_and_end_onhold for the 2-lines-in-use case.
    661                     // (Note these are GSM-only cases, though.)
    662                     hintTextResId = R.string.slide_to_answer;
    663                     hintColorResId = R.color.incall_textConnected;  // green
    664                     break;
    665                 case SlidingTab.OnTriggerListener.RIGHT_HANDLE:
    666                     hintTextResId = R.string.slide_to_decline;
    667                     hintColorResId = R.color.incall_textEnded;  // red
    668                     break;
    669                 default:
    670                     Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
    671                           + grabbedState);
    672                     hintTextResId = 0;
    673                     hintColorResId = 0;
    674                     break;
    675             }
    676 
    677             // Tell the InCallScreen to update the CallCard and force the
    678             // screen to redraw.
    679             mInCallScreen.updateSlidingTabHint(hintTextResId, hintColorResId);
    680         }
    681     }
    682 
    683 
    684     /**
    685      * OnTouchListener used to shrink the "hit target" of some onscreen
    686      * buttons.
    687      */
    688     class SmallerHitTargetTouchListener implements View.OnTouchListener {
    689         /**
    690          * Width of the allowable "hit target" as a percentage of
    691          * the total width of this button.
    692          */
    693         private static final int HIT_TARGET_PERCENT_X = 50;
    694 
    695         /**
    696          * Height of the allowable "hit target" as a percentage of
    697          * the total height of this button.
    698          *
    699          * This is larger than HIT_TARGET_PERCENT_X because some of
    700          * the onscreen buttons are wide but not very tall and we don't
    701          * want to make the vertical hit target *too* small.
    702          */
    703         private static final int HIT_TARGET_PERCENT_Y = 80;
    704 
    705         // Size (percentage-wise) of the "edge" area that's *not* touch-sensitive.
    706         private static final int X_EDGE = (100 - HIT_TARGET_PERCENT_X) / 2;
    707         private static final int Y_EDGE = (100 - HIT_TARGET_PERCENT_Y) / 2;
    708         // Min/max values (percentage-wise) of the touch-sensitive hit target.
    709         private static final int X_HIT_MIN = X_EDGE;
    710         private static final int X_HIT_MAX = 100 - X_EDGE;
    711         private static final int Y_HIT_MIN = Y_EDGE;
    712         private static final int Y_HIT_MAX = 100 - Y_EDGE;
    713 
    714         // True if the most recent DOWN event was a "hit".
    715         boolean mDownEventHit;
    716 
    717         /**
    718          * Called when a touch event is dispatched to a view. This allows listeners to
    719          * get a chance to respond before the target view.
    720          *
    721          * @return True if the listener has consumed the event, false otherwise.
    722          *         (In other words, we return true when the touch is *outside*
    723          *         the "smaller hit target", which will prevent the actual
    724          *         button from handling these events.)
    725          */
    726         public boolean onTouch(View v, MotionEvent event) {
    727             // if (DBG) log("SmallerHitTargetTouchListener: " + v + ", event " + event);
    728 
    729             if (event.getAction() == MotionEvent.ACTION_DOWN) {
    730                 // Note that event.getX() and event.getY() are already
    731                 // translated into the View's coordinates.  (In other words,
    732                 // "0,0" is a touch on the upper-left-most corner of the view.)
    733                 int touchX = (int) event.getX();
    734                 int touchY = (int) event.getY();
    735 
    736                 int viewWidth = v.getWidth();
    737                 int viewHeight = v.getHeight();
    738 
    739                 // Touch location as a percentage of the total button width or height.
    740                 int touchXPercent = (int) ((float) (touchX * 100) / (float) viewWidth);
    741                 int touchYPercent = (int) ((float) (touchY * 100) / (float) viewHeight);
    742                 // if (DBG) log("- percentage:  x = " + touchXPercent + ",  y = " + touchYPercent);
    743 
    744                 // TODO: user research: add event logging here of the actual
    745                 // hit location (and button ID), and enable it for dogfooders
    746                 // for a few days.  That'll give us a good idea of how close
    747                 // to the center of the button(s) most touch events are, to
    748                 // help us fine-tune the HIT_TARGET_PERCENT_* constants.
    749 
    750                 if (touchXPercent < X_HIT_MIN || touchXPercent > X_HIT_MAX
    751                         || touchYPercent < Y_HIT_MIN || touchYPercent > Y_HIT_MAX) {
    752                     // Missed!
    753                     // if (DBG) log("  -> MISSED!");
    754                     mDownEventHit = false;
    755                     return true;  // Consume this event; don't let the button see it
    756                 } else {
    757                     // Hit!
    758                     // if (DBG) log("  -> HIT!");
    759                     mDownEventHit = true;
    760                     return false;  // Let this event through to the actual button
    761                 }
    762             } else {
    763                 // This is a MOVE, UP or CANCEL event.
    764                 //
    765                 // We only do the "smaller hit target" check on DOWN events.
    766                 // For the subsequent MOVE/UP/CANCEL events, we let them
    767                 // through to the actual button IFF the previous DOWN event
    768                 // got through to the actual button (i.e. it was a "hit".)
    769                 return !mDownEventHit;
    770             }
    771         }
    772     }
    773 
    774 
    775     // Debugging / testing code
    776 
    777     private void log(String msg) {
    778         Log.d(LOG_TAG, msg);
    779     }
    780 }
    781