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