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