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