blob: 1d08671bd6588fc205c87f0b0cc82492c0d951a4 [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;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070032import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewPropertyAnimator;
35import android.view.ViewStub;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070036import android.widget.CompoundButton;
37import android.widget.FrameLayout;
38import android.widget.ImageButton;
39import android.widget.PopupMenu;
40import android.widget.TextView;
41import android.widget.Toast;
42
43import com.android.internal.telephony.Call;
44import com.android.internal.telephony.CallManager;
Santos Cordon7d4ddf62013-07-10 11:58:08 -070045import com.android.internal.telephony.PhoneConstants;
46import com.android.internal.widget.multiwaveview.GlowPadView;
47import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener;
48import 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 */
56public 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 Cordon593ab382013-08-06 21:58:23 -0700870
871 // TODO(klp): This is a compile stop-gap. This will all be deleted
872 final boolean usingHeadset = false; //mApp.isHeadsetPlugged();
873
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700874 earpieceItem.setVisible(!usingHeadset);
875 earpieceItem.setEnabled(!usingHeadset);
876 wiredHeadsetItem.setVisible(usingHeadset);
877 wiredHeadsetItem.setEnabled(usingHeadset);
878 // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
879 // as initially "selected" if inCallControlState.speakerOn and
880 // inCallControlState.bluetoothIndicatorOn are both false.
881
882 MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
883 bluetoothItem.setEnabled(inCallControlState.bluetoothEnabled);
884 // TODO: Show bluetoothItem as initially "selected" if
885 // inCallControlState.bluetoothIndicatorOn is true.
886
887 mAudioModePopup.show();
888
889 // Unfortunately we need to manually keep track of the popup menu's
890 // visiblity, since PopupMenu doesn't have an isShowing() method like
891 // Dialogs do.
892 mAudioModePopupVisible = true;
893 }
894
895 /**
896 * Dismisses the "Audio mode" popup if it's visible.
897 *
898 * This is safe to call even if the popup is already dismissed, or even if
899 * you never called showAudioModePopup() in the first place.
900 */
901 public void dismissAudioModePopup() {
902 if (mAudioModePopup != null) {
903 mAudioModePopup.dismiss(); // safe even if already dismissed
904 mAudioModePopup = null;
905 mAudioModePopupVisible = false;
906 }
907 }
908
909 /**
910 * Refreshes the "Audio mode" popup if it's visible. This is useful
911 * (for example) when a wired headset is plugged or unplugged,
912 * since we need to switch back and forth between the "earpiece"
913 * and "wired headset" items.
914 *
915 * This is safe to call even if the popup is already dismissed, or even if
916 * you never called showAudioModePopup() in the first place.
917 */
918 public void refreshAudioModePopup() {
919 if (mAudioModePopup != null && mAudioModePopupVisible) {
920 // Dismiss the previous one
921 mAudioModePopup.dismiss(); // safe even if already dismissed
922 // And bring up a fresh PopupMenu
923 showAudioModePopup();
924 }
925 }
926
927 // PopupMenu.OnMenuItemClickListener implementation; see showAudioModePopup()
928 @Override
929 public boolean onMenuItemClick(MenuItem item) {
930 if (DBG) log("- onMenuItemClick: " + item);
931 if (DBG) log(" id: " + item.getItemId());
932 if (DBG) log(" title: '" + item.getTitle() + "'");
933
934 if (mInCallScreen == null) {
935 Log.w(LOG_TAG, "onMenuItemClick(" + item + "), but null mInCallScreen!");
936 return true;
937 }
938
939 switch (item.getItemId()) {
940 case R.id.audio_mode_speaker:
941 mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.SPEAKER);
942 break;
943 case R.id.audio_mode_earpiece:
944 case R.id.audio_mode_wired_headset:
945 // InCallAudioMode.EARPIECE means either the handset earpiece,
946 // or the wired headset (if connected.)
947 mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.EARPIECE);
948 break;
949 case R.id.audio_mode_bluetooth:
950 mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.BLUETOOTH);
951 break;
952 default:
953 Log.wtf(LOG_TAG,
954 "onMenuItemClick: unexpected View ID " + item.getItemId()
955 + " (MenuItem = '" + item + "')");
956 break;
957 }
958 return true;
959 }
960
961 // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
962 // This gets called when the PopupMenu gets dismissed for *any* reason, like
963 // the user tapping outside its bounds, or pressing Back, or selecting one
964 // of the menu items.
965 @Override
966 public void onDismiss(PopupMenu menu) {
967 if (DBG) log("- onDismiss: " + menu);
968 mAudioModePopupVisible = false;
969 }
970
971 /**
972 * @return the amount of vertical space (in pixels) that needs to be
973 * reserved for the button cluster at the bottom of the screen.
974 * (The CallCard uses this measurement to determine how big
975 * the main "contact photo" area can be.)
976 *
977 * NOTE that this returns the "canonical height" of the main in-call
978 * button cluster, which may not match the amount of vertical space
979 * actually used. Specifically:
980 *
981 * - If an incoming call is ringing, the button cluster isn't
982 * visible at all. (And the GlowPadView widget is actually
983 * much taller than the button cluster.)
984 *
985 * - If the InCallTouchUi widget's "extra button row" is visible
986 * (in some rare phone states) the button cluster will actually
987 * be slightly taller than the "canonical height".
988 *
989 * In either of these cases, we allow the bottom edge of the contact
990 * photo to be covered up by whatever UI is actually onscreen.
991 */
992 public int getTouchUiHeight() {
993 // Add up the vertical space consumed by the various rows of buttons.
994 int height = 0;
995
996 // - The main row of buttons:
997 height += (int) getResources().getDimension(R.dimen.in_call_button_height);
998
999 // - The End button:
1000 height += (int) getResources().getDimension(R.dimen.in_call_end_button_height);
1001
1002 // - Note we *don't* consider the InCallTouchUi widget's "extra
1003 // button row" here.
1004
1005 //- And an extra bit of margin:
1006 height += (int) getResources().getDimension(R.dimen.in_call_touch_ui_upper_margin);
1007
1008 return height;
1009 }
1010
1011
1012 //
1013 // GlowPadView.OnTriggerListener implementation
1014 //
1015
1016 @Override
1017 public void onGrabbed(View v, int handle) {
1018
1019 }
1020
1021 @Override
1022 public void onReleased(View v, int handle) {
1023
1024 }
1025
1026 /**
1027 * Handles "Answer" and "Reject" actions for an incoming call.
1028 * We get this callback from the incoming call widget
1029 * when the user triggers an action.
1030 */
1031 @Override
1032 public void onTrigger(View view, int whichHandle) {
1033 if (DBG) log("onTrigger(whichHandle = " + whichHandle + ")...");
1034
1035 if (mInCallScreen == null) {
1036 Log.wtf(LOG_TAG, "onTrigger(" + whichHandle
1037 + ") from incoming-call widget, but null mInCallScreen!");
1038 return;
1039 }
1040
1041 // The InCallScreen actually implements all of these actions.
1042 // Each possible action from the incoming call widget corresponds
1043 // to an R.id value; we pass those to the InCallScreen's "button
1044 // click" handler (even though the UI elements aren't actually
1045 // buttons; see InCallScreen.handleOnscreenButtonClick().)
1046
1047 mShowInCallControlsDuringHidingAnimation = false;
1048 switch (whichHandle) {
1049 case ANSWER_CALL_ID:
1050 if (DBG) log("ANSWER_CALL_ID: answer!");
1051 mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer);
1052 mShowInCallControlsDuringHidingAnimation = true;
1053
1054 // ...and also prevent it from reappearing right away.
1055 // (This covers up a slow response from the radio for some
1056 // actions; see updateState().)
1057 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
1058 break;
1059
1060 case SEND_SMS_ID:
1061 if (DBG) log("SEND_SMS_ID!");
1062 mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms);
1063
1064 // Watch out: mLastIncomingCallActionTime should not be updated for this case.
1065 //
1066 // The variable is originally for avoiding a problem caused by delayed phone state
1067 // update; RINGING state may remain just after answering/declining an incoming
1068 // call, so we need to wait a bit (500ms) until we get the effective phone state.
1069 // For this case, we shouldn't rely on that hack.
1070 //
1071 // When the user selects this case, there are two possibilities, neither of which
1072 // should rely on the hack.
1073 //
1074 // 1. The first possibility is that, the device eventually sends one of canned
1075 // responses per the user's "send" request, and reject the call after sending it.
1076 // At that moment the code introducing the canned responses should handle the
1077 // case separately.
1078 //
1079 // 2. The second possibility is that, the device will show incoming call widget
1080 // again per the user's "cancel" request, where the incoming call will still
1081 // remain. At that moment the incoming call will keep its RINGING state.
1082 // The remaining phone state should never be ignored by the hack for
1083 // answering/declining calls because the RINGING state is legitimate. If we
1084 // use the hack for answer/decline cases, the user loses the incoming call
1085 // widget, until further screen update occurs afterward, which often results in
1086 // missed calls.
1087 break;
1088
1089 case DECLINE_CALL_ID:
1090 if (DBG) log("DECLINE_CALL_ID: reject!");
1091 mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject);
1092
1093 // Same as "answer" case.
1094 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
1095 break;
1096
1097 default:
1098 Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
1099 break;
1100 }
1101
1102 // On any action by the user, hide the widget.
1103 //
1104 // If requested above (i.e. if mShowInCallControlsDuringHidingAnimation is set to true),
1105 // in-call controls will start being shown too.
1106 //
1107 // TODO: The decision to hide this should be made by the controller
1108 // (InCallScreen), and not this view.
1109 hideIncomingCallWidget();
1110
1111 // Regardless of what action the user did, be sure to clear out
1112 // the hint text we were displaying while the user was dragging.
1113 mInCallScreen.updateIncomingCallWidgetHint(0, 0);
1114 }
1115
1116 public void onFinishFinalAnimation() {
1117 // Not used
1118 }
1119
1120 /**
1121 * Apply an animation to hide the incoming call widget.
1122 */
1123 private void hideIncomingCallWidget() {
1124 if (DBG) log("hideIncomingCallWidget()...");
1125 if (mIncomingCallWidget.getVisibility() != View.VISIBLE
1126 || mIncomingCallWidgetIsFadingOut) {
1127 if (DBG) log("Skipping hideIncomingCallWidget action");
1128 // Widget is already hidden or in the process of being hidden
1129 return;
1130 }
1131
1132 // Hide the incoming call screen with a transition
1133 mIncomingCallWidgetIsFadingOut = true;
1134 ViewPropertyAnimator animator = mIncomingCallWidget.animate();
1135 animator.cancel();
1136 animator.setDuration(AnimationUtils.ANIMATION_DURATION);
1137 animator.setListener(new AnimatorListenerAdapter() {
1138 @Override
1139 public void onAnimationStart(Animator animation) {
1140 if (mShowInCallControlsDuringHidingAnimation) {
1141 if (DBG) log("IncomingCallWidget's hiding animation started");
1142 updateInCallControls(mApp.mCM);
1143 mInCallControls.setVisibility(View.VISIBLE);
1144 }
1145 }
1146
1147 @Override
1148 public void onAnimationEnd(Animator animation) {
1149 if (DBG) log("IncomingCallWidget's hiding animation ended");
1150 mIncomingCallWidget.setAlpha(1);
1151 mIncomingCallWidget.setVisibility(View.GONE);
1152 mIncomingCallWidget.animate().setListener(null);
1153 mShowInCallControlsDuringHidingAnimation = false;
1154 mIncomingCallWidgetIsFadingOut = false;
1155 mIncomingCallWidgetShouldBeReset = true;
1156 }
1157
1158 @Override
1159 public void onAnimationCancel(Animator animation) {
1160 mIncomingCallWidget.animate().setListener(null);
1161 mShowInCallControlsDuringHidingAnimation = false;
1162 mIncomingCallWidgetIsFadingOut = false;
1163 mIncomingCallWidgetShouldBeReset = true;
1164
1165 // Note: the code which reset this animation should be responsible for
1166 // alpha and visibility.
1167 }
1168 });
1169 animator.alpha(0f);
1170 }
1171
1172 /**
1173 * Shows the incoming call widget and cancels any animation that may be fading it out.
1174 */
1175 private void showIncomingCallWidget(Call ringingCall) {
1176 if (DBG) log("showIncomingCallWidget()...");
1177
1178 // TODO: wouldn't be ok to suppress this whole request if the widget is already VISIBLE
1179 // and we don't need to reset it?
1180 // log("showIncomingCallWidget(). widget visibility: " + mIncomingCallWidget.getVisibility());
1181
1182 ViewPropertyAnimator animator = mIncomingCallWidget.animate();
1183 if (animator != null) {
1184 animator.cancel();
1185 // If animation is cancelled before it's running,
1186 // onAnimationCancel will not be called and mIncomingCallWidgetIsFadingOut
1187 // will be alway true. hideIncomingCallWidget() will not be excuted in this case.
1188 mIncomingCallWidgetIsFadingOut = false;
1189 }
1190 mIncomingCallWidget.setAlpha(1.0f);
1191
Santos Cordon7d4ddf62013-07-10 11:58:08 -07001192 // On an incoming call, if the layout is landscape, then align the "incoming call" text
1193 // to the left, because the incomingCallWidget (black background with glowing ring)
1194 // is aligned to the right and would cover the "incoming call" text.
1195 // Note that callStateLabel is within CallCard, outside of the context of InCallTouchUi
1196 if (PhoneUtils.isLandscape(this.getContext())) {
1197 TextView callStateLabel = (TextView) mIncomingCallWidget
1198 .getRootView().findViewById(R.id.callStateLabel);
1199 if (callStateLabel != null) callStateLabel.setGravity(Gravity.START);
1200 }
1201
1202 mIncomingCallWidget.setVisibility(View.VISIBLE);
1203
1204 // Finally, manually trigger a "ping" animation.
1205 //
1206 // Normally, the ping animation is triggered by RING events from
1207 // the telephony layer (see onIncomingRing().) But that *doesn't*
1208 // happen for the very first RING event of an incoming call, since
1209 // the incoming-call UI hasn't been set up yet at that point!
1210 //
1211 // So trigger an explicit ping() here, to force the animation to
1212 // run when the widget first appears.
1213 //
1214 mHandler.removeMessages(INCOMING_CALL_WIDGET_PING);
1215 mHandler.sendEmptyMessageDelayed(
1216 INCOMING_CALL_WIDGET_PING,
1217 // Visual polish: add a small delay here, to make the
1218 // GlowPadView widget visible for a brief moment
1219 // *before* starting the ping animation.
1220 // This value doesn't need to be very precise.
1221 250 /* msec */);
1222 }
1223
1224 /**
1225 * Handles state changes of the incoming-call widget.
1226 *
1227 * In previous releases (where we used a SlidingTab widget) we would
1228 * display an onscreen hint depending on which "handle" the user was
1229 * dragging. But we now use a GlowPadView widget, which has only
1230 * one handle, so for now we don't display a hint at all (see the TODO
1231 * comment below.)
1232 */
1233 @Override
1234 public void onGrabbedStateChange(View v, int grabbedState) {
1235 if (mInCallScreen != null) {
1236 // Look up the hint based on which handle is currently grabbed.
1237 // (Note we don't simply pass grabbedState thru to the InCallScreen,
1238 // since *this* class is the only place that knows that the left
1239 // handle means "Answer" and the right handle means "Decline".)
1240 int hintTextResId, hintColorResId;
1241 switch (grabbedState) {
1242 case GlowPadView.OnTriggerListener.NO_HANDLE:
1243 case GlowPadView.OnTriggerListener.CENTER_HANDLE:
1244 hintTextResId = 0;
1245 hintColorResId = 0;
1246 break;
1247 default:
1248 Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
1249 + grabbedState);
1250 hintTextResId = 0;
1251 hintColorResId = 0;
1252 break;
1253 }
1254
1255 // Tell the InCallScreen to update the CallCard and force the
1256 // screen to redraw.
1257 mInCallScreen.updateIncomingCallWidgetHint(hintTextResId, hintColorResId);
1258 }
1259 }
1260
1261 /**
1262 * Handles an incoming RING event from the telephony layer.
1263 */
1264 public void onIncomingRing() {
1265 if (ENABLE_PING_ON_RING_EVENTS) {
1266 // Each RING from the telephony layer triggers a "ping" animation
1267 // of the GlowPadView widget. (The intent here is to make the
1268 // pinging appear to be synchronized with the ringtone, although
1269 // that only works for non-looping ringtones.)
1270 triggerPing();
1271 }
1272 }
1273
1274 /**
1275 * Runs a single "ping" animation of the GlowPadView widget,
1276 * or do nothing if the GlowPadView widget is no longer visible.
1277 *
1278 * Also, if ENABLE_PING_AUTO_REPEAT is true, schedule the next ping as
1279 * well (but again, only if the GlowPadView widget is still visible.)
1280 */
1281 public void triggerPing() {
1282 if (DBG) log("triggerPing: mIncomingCallWidget = " + mIncomingCallWidget);
1283
1284 if (!mInCallScreen.isForegroundActivity()) {
1285 // InCallScreen has been dismissed; no need to run a ping *or*
1286 // schedule another one.
1287 log("- triggerPing: InCallScreen no longer in foreground; ignoring...");
1288 return;
1289 }
1290
1291 if (mIncomingCallWidget == null) {
1292 // This shouldn't happen; the GlowPadView widget should
1293 // always be present in our layout file.
1294 Log.w(LOG_TAG, "- triggerPing: null mIncomingCallWidget!");
1295 return;
1296 }
1297
1298 if (DBG) log("- triggerPing: mIncomingCallWidget visibility = "
1299 + mIncomingCallWidget.getVisibility());
1300
1301 if (mIncomingCallWidget.getVisibility() != View.VISIBLE) {
1302 if (DBG) log("- triggerPing: mIncomingCallWidget no longer visible; ignoring...");
1303 return;
1304 }
1305
1306 // Ok, run a ping (and schedule the next one too, if desired...)
1307
1308 mIncomingCallWidget.ping();
1309
1310 if (ENABLE_PING_AUTO_REPEAT) {
1311 // Schedule the next ping. (ENABLE_PING_AUTO_REPEAT mode
1312 // allows the ping animation to repeat much faster than in
1313 // the ENABLE_PING_ON_RING_EVENTS case, since telephony RING
1314 // events come fairly slowly (about 3 seconds apart.))
1315
1316 // No need to check here if the call is still ringing, by
1317 // the way, since we hide mIncomingCallWidget as soon as the
1318 // ringing stops, or if the user answers. (And at that
1319 // point, any future triggerPing() call will be a no-op.)
1320
1321 // TODO: Rather than having a separate timer here, maybe try
1322 // having these pings synchronized with the vibrator (see
1323 // VibratorThread in Ringer.java; we'd just need to get
1324 // events routed from there to here, probably via the
1325 // PhoneApp instance.) (But watch out: make sure pings
1326 // still work even if the Vibrate setting is turned off!)
1327
1328 mHandler.sendEmptyMessageDelayed(INCOMING_CALL_WIDGET_PING,
1329 PING_AUTO_REPEAT_DELAY_MSEC);
1330 }
1331 }
1332
1333 // Debugging / testing code
1334
1335 private void log(String msg) {
1336 Log.d(LOG_TAG, msg);
1337 }
1338}