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