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