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
17package com.android.phone;
18
19import android.content.Context;
20import android.graphics.drawable.Drawable;
21import android.graphics.drawable.LayerDrawable;
22import android.os.Handler;
23import android.os.Message;
24import android.os.SystemClock;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.LayoutInflater;
28import android.view.Menu;
29import android.view.MenuItem;
30import android.view.MotionEvent;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.animation.AlphaAnimation;
34import android.view.animation.Animation;
35import android.view.animation.Animation.AnimationListener;
36import android.widget.CompoundButton;
37import android.widget.FrameLayout;
38import android.widget.ImageButton;
39import android.widget.PopupMenu;
40
41import com.android.internal.telephony.Call;
42import com.android.internal.telephony.Phone;
43import com.android.internal.widget.multiwaveview.MultiWaveView;
44import com.android.internal.widget.multiwaveview.MultiWaveView.OnTriggerListener;
45import 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 */
54public 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