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