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