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