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