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