• 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.content.Context;
20 import android.graphics.drawable.Drawable;
21 import android.os.SystemClock;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.view.LayoutInflater;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.animation.AlphaAnimation;
28 import android.view.animation.Animation;
29 import android.view.animation.Animation.AnimationListener;
30 import android.widget.Button;
31 import android.widget.FrameLayout;
32 import android.widget.ImageButton;
33 import android.widget.TextView;
34 import android.widget.ToggleButton;
35 
36 import com.android.internal.telephony.Call;
37 import com.android.internal.telephony.Phone;
38 import com.android.internal.widget.SlidingTab;
39 import com.android.internal.telephony.CallManager;
40 
41 
42 /**
43  * In-call onscreen touch UI elements, used on some platforms.
44  *
45  * This widget is a fullscreen overlay, drawn on top of the
46  * non-touch-sensitive parts of the in-call UI (i.e. the call card).
47  */
48 public class InCallTouchUi extends FrameLayout
49         implements View.OnClickListener, SlidingTab.OnTriggerListener {
50     private static final int IN_CALL_WIDGET_TRANSITION_TIME = 250; // in ms
51     private static final String LOG_TAG = "InCallTouchUi";
52     private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2);
53 
54     /**
55      * Reference to the InCallScreen activity that owns us.  This may be
56      * null if we haven't been initialized yet *or* after the InCallScreen
57      * activity has been destroyed.
58      */
59     private InCallScreen mInCallScreen;
60 
61     // Phone app instance
62     private PhoneApp mApplication;
63 
64     // UI containers / elements
65     private SlidingTab mIncomingCallWidget;  // UI used for an incoming call
66     private View mInCallControls;  // UI elements while on a regular call
67     //
68     private Button mAddButton;
69     private Button mMergeButton;
70     private Button mEndButton;
71     private Button mDialpadButton;
72     private ToggleButton mBluetoothButton;
73     private ToggleButton mMuteButton;
74     private ToggleButton mSpeakerButton;
75     //
76     private View mHoldButtonContainer;
77     private ImageButton mHoldButton;
78     private TextView mHoldButtonLabel;
79     private View mSwapButtonContainer;
80     private ImageButton mSwapButton;
81     private TextView mSwapButtonLabel;
82     private View mCdmaMergeButtonContainer;
83     private ImageButton mCdmaMergeButton;
84     //
85     private Drawable mHoldIcon;
86     private Drawable mUnholdIcon;
87     private Drawable mShowDialpadIcon;
88     private Drawable mHideDialpadIcon;
89 
90     // Time of the most recent "answer" or "reject" action (see updateState())
91     private long mLastIncomingCallActionTime;  // in SystemClock.uptimeMillis() time base
92 
93     // Overall enabledness of the "touch UI" features
94     private boolean mAllowIncomingCallTouchUi;
95     private boolean mAllowInCallTouchUi;
96 
InCallTouchUi(Context context, AttributeSet attrs)97     public InCallTouchUi(Context context, AttributeSet attrs) {
98         super(context, attrs);
99 
100         if (DBG) log("InCallTouchUi constructor...");
101         if (DBG) log("- this = " + this);
102         if (DBG) log("- context " + context + ", attrs " + attrs);
103 
104         // Inflate our contents, and add it (to ourself) as a child.
105         LayoutInflater inflater = LayoutInflater.from(context);
106         inflater.inflate(
107                 R.layout.incall_touch_ui,  // resource
108                 this,                      // root
109                 true);
110 
111         mApplication = PhoneApp.getInstance();
112 
113         // The various touch UI features are enabled on a per-product
114         // basis.  (These flags in config.xml may be overridden by
115         // product-specific overlay files.)
116 
117         mAllowIncomingCallTouchUi = getResources().getBoolean(R.bool.allow_incoming_call_touch_ui);
118         if (DBG) log("- incoming call touch UI: "
119                      + (mAllowIncomingCallTouchUi ? "ENABLED" : "DISABLED"));
120         mAllowInCallTouchUi = getResources().getBoolean(R.bool.allow_in_call_touch_ui);
121         if (DBG) log("- regular in-call touch UI: "
122                      + (mAllowInCallTouchUi ? "ENABLED" : "DISABLED"));
123     }
124 
setInCallScreenInstance(InCallScreen inCallScreen)125     void setInCallScreenInstance(InCallScreen inCallScreen) {
126         mInCallScreen = inCallScreen;
127     }
128 
129     @Override
onFinishInflate()130     protected void onFinishInflate() {
131         super.onFinishInflate();
132         if (DBG) log("InCallTouchUi onFinishInflate(this = " + this + ")...");
133 
134         // Look up the various UI elements.
135 
136         // "Dial-to-answer" widget for incoming calls.
137         mIncomingCallWidget = (SlidingTab) findViewById(R.id.incomingCallWidget);
138         mIncomingCallWidget.setLeftTabResources(
139                 R.drawable.ic_jog_dial_answer,
140                 com.android.internal.R.drawable.jog_tab_target_green,
141                 com.android.internal.R.drawable.jog_tab_bar_left_answer,
142                 com.android.internal.R.drawable.jog_tab_left_answer
143                 );
144         mIncomingCallWidget.setRightTabResources(
145                 R.drawable.ic_jog_dial_decline,
146                 com.android.internal.R.drawable.jog_tab_target_red,
147                 com.android.internal.R.drawable.jog_tab_bar_right_decline,
148                 com.android.internal.R.drawable.jog_tab_right_decline
149                 );
150 
151         // For now, we only need to show two states: answer and decline.
152         mIncomingCallWidget.setLeftHintText(R.string.slide_to_answer_hint);
153         mIncomingCallWidget.setRightHintText(R.string.slide_to_decline_hint);
154 
155         mIncomingCallWidget.setOnTriggerListener(this);
156 
157         // Container for the UI elements shown while on a regular call.
158         mInCallControls = findViewById(R.id.inCallControls);
159 
160         // Regular (single-tap) buttons, where we listen for click events:
161         // Main cluster of buttons:
162         mAddButton = (Button) mInCallControls.findViewById(R.id.addButton);
163         mAddButton.setOnClickListener(this);
164         mMergeButton = (Button) mInCallControls.findViewById(R.id.mergeButton);
165         mMergeButton.setOnClickListener(this);
166         mEndButton = (Button) mInCallControls.findViewById(R.id.endButton);
167         mEndButton.setOnClickListener(this);
168         mDialpadButton = (Button) mInCallControls.findViewById(R.id.dialpadButton);
169         mDialpadButton.setOnClickListener(this);
170         mBluetoothButton = (ToggleButton) mInCallControls.findViewById(R.id.bluetoothButton);
171         mBluetoothButton.setOnClickListener(this);
172         mMuteButton = (ToggleButton) mInCallControls.findViewById(R.id.muteButton);
173         mMuteButton.setOnClickListener(this);
174         mSpeakerButton = (ToggleButton) mInCallControls.findViewById(R.id.speakerButton);
175         mSpeakerButton.setOnClickListener(this);
176 
177         // Upper corner buttons:
178         mHoldButtonContainer = mInCallControls.findViewById(R.id.holdButtonContainer);
179         mHoldButton = (ImageButton) mInCallControls.findViewById(R.id.holdButton);
180         mHoldButton.setOnClickListener(this);
181         mHoldButtonLabel = (TextView) mInCallControls.findViewById(R.id.holdButtonLabel);
182         //
183         mSwapButtonContainer = mInCallControls.findViewById(R.id.swapButtonContainer);
184         mSwapButton = (ImageButton) mInCallControls.findViewById(R.id.swapButton);
185         mSwapButton.setOnClickListener(this);
186         mSwapButtonLabel = (TextView) mInCallControls.findViewById(R.id.swapButtonLabel);
187         if (PhoneApp.getPhone().getPhoneType() == Phone.PHONE_TYPE_CDMA) {
188             // In CDMA we use a generalized text - "Manage call", as behavior on selecting
189             // this option depends entirely on what the current call state is.
190             mSwapButtonLabel.setText(R.string.onscreenManageCallsText);
191         } else {
192             mSwapButtonLabel.setText(R.string.onscreenSwapCallsText);
193         }
194         //
195         mCdmaMergeButtonContainer = mInCallControls.findViewById(R.id.cdmaMergeButtonContainer);
196         mCdmaMergeButton = (ImageButton) mInCallControls.findViewById(R.id.cdmaMergeButton);
197         mCdmaMergeButton.setOnClickListener(this);
198 
199         // Add a custom OnTouchListener to manually shrink the "hit
200         // target" of some buttons.
201         // (We do this for a few specific buttons which are vulnerable to
202         // "false touches" because either (1) they're near the edge of the
203         // screen and might be unintentionally touched while holding the
204         // device in your hand, or (2) they're in the upper corners and might
205         // be touched by the user's ear before the prox sensor has a chance to
206         // kick in.)
207         View.OnTouchListener smallerHitTargetTouchListener = new SmallerHitTargetTouchListener();
208         mAddButton.setOnTouchListener(smallerHitTargetTouchListener);
209         mMergeButton.setOnTouchListener(smallerHitTargetTouchListener);
210         mDialpadButton.setOnTouchListener(smallerHitTargetTouchListener);
211         mBluetoothButton.setOnTouchListener(smallerHitTargetTouchListener);
212         mSpeakerButton.setOnTouchListener(smallerHitTargetTouchListener);
213         mHoldButton.setOnTouchListener(smallerHitTargetTouchListener);
214         mSwapButton.setOnTouchListener(smallerHitTargetTouchListener);
215         mCdmaMergeButton.setOnTouchListener(smallerHitTargetTouchListener);
216         mSpeakerButton.setOnTouchListener(smallerHitTargetTouchListener);
217 
218         // Icons we need to change dynamically.  (Most other icons are specified
219         // directly in incall_touch_ui.xml.)
220         mHoldIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_round_hold);
221         mUnholdIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_round_unhold);
222         mShowDialpadIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_dialpad);
223         mHideDialpadIcon = getResources().getDrawable(R.drawable.ic_in_call_touch_dialpad_close);
224     }
225 
226     /**
227      * Updates the visibility and/or state of our UI elements, based on
228      * the current state of the phone.
229      */
updateState(CallManager cm)230     void updateState(CallManager cm) {
231         if (mInCallScreen == null) {
232             log("- updateState: mInCallScreen has been destroyed; bailing out...");
233             return;
234         }
235 
236         Phone.State state = cm.getState();  // IDLE, RINGING, or OFFHOOK
237         if (DBG) log("- updateState: CallManager state is " + state);
238 
239         boolean showIncomingCallControls = false;
240         boolean showInCallControls = false;
241 
242         final Call ringingCall = cm.getFirstActiveRingingCall();
243         // If the FG call is dialing/alerting, we should display for that call
244         // and ignore the ringing call. This case happens when the telephony
245         // layer rejects the ringing call while the FG call is dialing/alerting,
246         // but the incoming call *does* briefly exist in the DISCONNECTING or
247         // DISCONNECTED state.
248         if ((ringingCall.getState() != Call.State.IDLE)
249                 && !cm.getActiveFgCallState().isDialing()) {
250             // A phone call is ringing *or* call waiting.
251             if (mAllowIncomingCallTouchUi) {
252                 // Watch out: even if the phone state is RINGING, it's
253                 // possible for the ringing call to be in the DISCONNECTING
254                 // state.  (This typically happens immediately after the user
255                 // rejects an incoming call, and in that case we *don't* show
256                 // the incoming call controls.)
257                 if (ringingCall.getState().isAlive()) {
258                     if (DBG) log("- updateState: RINGING!  Showing incoming call controls...");
259                     showIncomingCallControls = true;
260                 }
261 
262                 // Ugly hack to cover up slow response from the radio:
263                 // if we attempted to answer or reject an incoming call
264                 // within the last 500 msec, *don't* show the incoming call
265                 // UI even if the phone is still in the RINGING state.
266                 long now = SystemClock.uptimeMillis();
267                 if (now < mLastIncomingCallActionTime + 500) {
268                     log("updateState: Too soon after last action; not drawing!");
269                     showIncomingCallControls = false;
270                 }
271 
272                 // TODO: UI design issue: if the device is NOT currently
273                 // locked, we probably don't need to make the user
274                 // double-tap the "incoming call" buttons.  (The device
275                 // presumably isn't in a pocket or purse, so we don't need
276                 // to worry about false touches while it's ringing.)
277                 // But OTOH having "inconsistent" buttons might just make
278                 // it *more* confusing.
279             }
280         } else {
281             if (mAllowInCallTouchUi) {
282                 // Ok, the in-call touch UI is available on this platform,
283                 // so make it visible (with some exceptions):
284                 if (mInCallScreen.okToShowInCallTouchUi()) {
285                     showInCallControls = true;
286                 } else {
287                     if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
288                 }
289             }
290         }
291 
292         if (showInCallControls) {
293             // TODO change the phone to CallManager
294             updateInCallControls(cm.getActiveFgCall().getPhone());
295         }
296 
297         if (showIncomingCallControls && showInCallControls) {
298             throw new IllegalStateException(
299                 "'Incoming' and 'in-call' touch controls visible at the same time!");
300         }
301 
302         if (showIncomingCallControls) {
303             showIncomingCallWidget();
304         } else {
305             hideIncomingCallWidget();
306         }
307 
308         mInCallControls.setVisibility(showInCallControls ? View.VISIBLE : View.GONE);
309 
310         // TODO: As an optimization, also consider setting the visibility
311         // of the overall InCallTouchUi widget to GONE if *nothing at all*
312         // is visible right now.
313     }
314 
315     // View.OnClickListener implementation
onClick(View view)316     public void onClick(View view) {
317         int id = view.getId();
318         if (DBG) log("onClick(View " + view + ", id " + id + ")...");
319 
320         switch (id) {
321             case R.id.addButton:
322             case R.id.mergeButton:
323             case R.id.endButton:
324             case R.id.dialpadButton:
325             case R.id.bluetoothButton:
326             case R.id.muteButton:
327             case R.id.speakerButton:
328             case R.id.holdButton:
329             case R.id.swapButton:
330             case R.id.cdmaMergeButton:
331                 // Clicks on the regular onscreen buttons get forwarded
332                 // straight to the InCallScreen.
333                 mInCallScreen.handleOnscreenButtonClick(id);
334                 break;
335 
336             default:
337                 Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
338                 break;
339         }
340     }
341 
342     /**
343      * Updates the enabledness and "checked" state of the buttons on the
344      * "inCallControls" panel, based on the current telephony state.
345      */
updateInCallControls(Phone phone)346     void updateInCallControls(Phone phone) {
347         int phoneType = phone.getPhoneType();
348         // Note we do NOT need to worry here about cases where the entire
349         // in-call touch UI is disabled, like during an OTA call or if the
350         // dtmf dialpad is up.  (That's handled by updateState(), which
351         // calls InCallScreen.okToShowInCallTouchUi().)
352         //
353         // If we get here, it *is* OK to show the in-call touch UI, so we
354         // now need to update the enabledness and/or "checked" state of
355         // each individual button.
356         //
357 
358         // The InCallControlState object tells us the enabledness and/or
359         // state of the various onscreen buttons:
360         InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
361 
362         // "Add" or "Merge":
363         // These two buttons occupy the same space onscreen, so only
364         // one of them should be available at a given moment.
365         if (inCallControlState.canAddCall) {
366             mAddButton.setVisibility(View.VISIBLE);
367             mAddButton.setEnabled(true);
368             mMergeButton.setVisibility(View.GONE);
369         } else if (inCallControlState.canMerge) {
370             if (phoneType == Phone.PHONE_TYPE_CDMA) {
371                 // In CDMA "Add" option is always given to the user and the
372                 // "Merge" option is provided as a button on the top left corner of the screen,
373                 // we always set the mMergeButton to GONE
374                 mMergeButton.setVisibility(View.GONE);
375             } else if ((phoneType == Phone.PHONE_TYPE_GSM)
376                     || (phoneType == Phone.PHONE_TYPE_SIP)) {
377                 mMergeButton.setVisibility(View.VISIBLE);
378                 mMergeButton.setEnabled(true);
379                 mAddButton.setVisibility(View.GONE);
380             } else {
381                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
382             }
383         } else {
384             // Neither "Add" nor "Merge" is available.  (This happens in
385             // some transient states, like while dialing an outgoing call,
386             // and in other rare cases like if you have both lines in use
387             // *and* there are already 5 people on the conference call.)
388             // Since the common case here is "while dialing", we show the
389             // "Add" button in a disabled state so that there won't be any
390             // jarring change in the UI when the call finally connects.
391             mAddButton.setVisibility(View.VISIBLE);
392             mAddButton.setEnabled(false);
393             mMergeButton.setVisibility(View.GONE);
394         }
395         if (inCallControlState.canAddCall && inCallControlState.canMerge) {
396             if ((phoneType == Phone.PHONE_TYPE_GSM)
397                     || (phoneType == Phone.PHONE_TYPE_SIP)) {
398                 // Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
399                 // should both be available right now.  This *should* never
400                 // happen with GSM, but if it's possible on any
401                 // future devices we may need to re-layout Add and Merge so
402                 // they can both be visible at the same time...
403                 Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
404                         " but can't show both!");
405             } else if (phoneType == Phone.PHONE_TYPE_CDMA) {
406                 // In CDMA "Add" option is always given to the user and the hence
407                 // in this case both "Add" and "Merge" options would be available to user
408                 if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
409             } else {
410                 throw new IllegalStateException("Unexpected phone type: " + phoneType);
411             }
412         }
413 
414         // "End call": this button has no state and it's always enabled.
415         mEndButton.setEnabled(true);
416 
417         // "Dialpad": Enabled only when it's OK to use the dialpad in the
418         // first place.
419         mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
420         //
421         if (inCallControlState.dialpadVisible) {
422             // Show the "hide dialpad" state.
423             mDialpadButton.setText(R.string.onscreenHideDialpadText);
424             mDialpadButton.setCompoundDrawablesWithIntrinsicBounds(
425                 null, mHideDialpadIcon, null, null);
426         } else {
427             // Show the "show dialpad" state.
428             mDialpadButton.setText(R.string.onscreenShowDialpadText);
429             mDialpadButton.setCompoundDrawablesWithIntrinsicBounds(
430                     null, mShowDialpadIcon, null, null);
431         }
432 
433         // "Bluetooth"
434         mBluetoothButton.setEnabled(inCallControlState.bluetoothEnabled);
435         mBluetoothButton.setChecked(inCallControlState.bluetoothIndicatorOn);
436 
437         // "Mute"
438         mMuteButton.setEnabled(inCallControlState.canMute);
439         mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
440 
441         // "Speaker"
442         mSpeakerButton.setEnabled(inCallControlState.speakerEnabled);
443         mSpeakerButton.setChecked(inCallControlState.speakerOn);
444 
445         // "Hold"
446         // (Note "Hold" and "Swap" are never both available at
447         // the same time.  That's why it's OK for them to both be in the
448         // same position onscreen.)
449         // This button is totally hidden (rather than just disabled)
450         // when the operation isn't available.
451         mHoldButtonContainer.setVisibility(
452                 inCallControlState.canHold ? View.VISIBLE : View.GONE);
453         if (inCallControlState.canHold) {
454             // The Hold button icon and label (either "Hold" or "Unhold")
455             // depend on the current Hold state.
456             if (inCallControlState.onHold) {
457                 mHoldButton.setImageDrawable(mUnholdIcon);
458                 mHoldButtonLabel.setText(R.string.onscreenUnholdText);
459             } else {
460                 mHoldButton.setImageDrawable(mHoldIcon);
461                 mHoldButtonLabel.setText(R.string.onscreenHoldText);
462             }
463         }
464 
465         // "Swap"
466         // This button is totally hidden (rather than just disabled)
467         // when the operation isn't available.
468         mSwapButtonContainer.setVisibility(
469                 inCallControlState.canSwap ? View.VISIBLE : View.GONE);
470 
471         if (phone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
472             // "Merge"
473             // This button is totally hidden (rather than just disabled)
474             // when the operation isn't available.
475             mCdmaMergeButtonContainer.setVisibility(
476                     inCallControlState.canMerge ? View.VISIBLE : View.GONE);
477         }
478 
479         if (inCallControlState.canSwap && inCallControlState.canHold) {
480             // Uh oh, the InCallControlState thinks that Swap *and* Hold
481             // should both be available.  This *should* never happen with
482             // either GSM or CDMA, but if it's possible on any future
483             // devices we may need to re-layout Hold and Swap so they can
484             // both be visible at the same time...
485             Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
486         }
487 
488         if (phoneType == Phone.PHONE_TYPE_CDMA) {
489             if (inCallControlState.canSwap && inCallControlState.canMerge) {
490                 // Uh oh, the InCallControlState thinks that Swap *and* Merge
491                 // should both be available.  This *should* never happen with
492                 // CDMA, but if it's possible on any future
493                 // devices we may need to re-layout Merge and Swap so they can
494                 // both be visible at the same time...
495                 Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
496                         "enabled, but can't show both!");
497             }
498         }
499 
500         // One final special case: if the dialpad is visible, that trumps
501         // *any* of the upper corner buttons:
502         if (inCallControlState.dialpadVisible) {
503             mHoldButtonContainer.setVisibility(View.GONE);
504             mSwapButtonContainer.setVisibility(View.GONE);
505             mCdmaMergeButtonContainer.setVisibility(View.GONE);
506         }
507     }
508 
509     //
510     // InCallScreen API
511     //
512 
513     /**
514      * @return true if the onscreen touch UI is enabled (for regular
515      * "ongoing call" states) on the current device.
516      */
isTouchUiEnabled()517     /* package */ boolean isTouchUiEnabled() {
518         return mAllowInCallTouchUi;
519     }
520 
521     /**
522      * @return true if the onscreen touch UI is enabled for
523      * the "incoming call" state on the current device.
524      */
isIncomingCallTouchUiEnabled()525     /* package */ boolean isIncomingCallTouchUiEnabled() {
526         return mAllowIncomingCallTouchUi;
527     }
528 
529     //
530     // SlidingTab.OnTriggerListener implementation
531     //
532 
533     /**
534      * Handles "Answer" and "Reject" actions for an incoming call.
535      * We get this callback from the SlidingTab
536      * when the user triggers an action.
537      *
538      * To answer or reject the incoming call, we call
539      * InCallScreen.handleOnscreenButtonClick() and pass one of the
540      * special "virtual button" IDs:
541      *   - R.id.answerButton to answer the call
542      * or
543      *   - R.id.rejectButton to reject the call.
544      */
onTrigger(View v, int whichHandle)545     public void onTrigger(View v, int whichHandle) {
546         log("onDialTrigger(whichHandle = " + whichHandle + ")...");
547 
548         switch (whichHandle) {
549             case SlidingTab.OnTriggerListener.LEFT_HANDLE:
550                 if (DBG) log("LEFT_HANDLE: answer!");
551 
552                 hideIncomingCallWidget();
553 
554                 // ...and also prevent it from reappearing right away.
555                 // (This covers up a slow response from the radio; see updateState().)
556                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
557 
558                 // Do the appropriate action.
559                 if (mInCallScreen != null) {
560                     // Send this to the InCallScreen as a virtual "button click" event:
561                     mInCallScreen.handleOnscreenButtonClick(R.id.answerButton);
562                 } else {
563                     Log.e(LOG_TAG, "answer trigger: mInCallScreen is null");
564                 }
565                 break;
566 
567             case SlidingTab.OnTriggerListener.RIGHT_HANDLE:
568                 if (DBG) log("RIGHT_HANDLE: reject!");
569 
570                 hideIncomingCallWidget();
571 
572                 // ...and also prevent it from reappearing right away.
573                 // (This covers up a slow response from the radio; see updateState().)
574                 mLastIncomingCallActionTime = SystemClock.uptimeMillis();
575 
576                 // Do the appropriate action.
577                 if (mInCallScreen != null) {
578                     // Send this to the InCallScreen as a virtual "button click" event:
579                     mInCallScreen.handleOnscreenButtonClick(R.id.rejectButton);
580                 } else {
581                     Log.e(LOG_TAG, "reject trigger: mInCallScreen is null");
582                 }
583                 break;
584 
585             default:
586                 Log.e(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
587                 break;
588         }
589 
590         // Regardless of what action the user did, be sure to clear out
591         // the hint text we were displaying while the user was dragging.
592         mInCallScreen.updateSlidingTabHint(0, 0);
593     }
594 
595     /**
596      * Apply an animation to hide the incoming call widget.
597      */
hideIncomingCallWidget()598     private void hideIncomingCallWidget() {
599         if (mIncomingCallWidget.getVisibility() != View.VISIBLE
600                 || mIncomingCallWidget.getAnimation() != null) {
601             // Widget is already hidden or in the process of being hidden
602             return;
603         }
604         // Hide the incoming call screen with a transition
605         AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f);
606         anim.setDuration(IN_CALL_WIDGET_TRANSITION_TIME);
607         anim.setAnimationListener(new AnimationListener() {
608 
609             public void onAnimationStart(Animation animation) {
610 
611             }
612 
613             public void onAnimationRepeat(Animation animation) {
614 
615             }
616 
617             public void onAnimationEnd(Animation animation) {
618                 // hide the incoming call UI.
619                 mIncomingCallWidget.clearAnimation();
620                 mIncomingCallWidget.setVisibility(View.GONE);
621             }
622         });
623         mIncomingCallWidget.startAnimation(anim);
624     }
625 
626     /**
627      * Shows the incoming call widget and cancels any animation that may be fading it out.
628      */
showIncomingCallWidget()629     private void showIncomingCallWidget() {
630         Animation anim = mIncomingCallWidget.getAnimation();
631         if (anim != null) {
632             anim.reset();
633             mIncomingCallWidget.clearAnimation();
634         }
635         mIncomingCallWidget.reset(false);
636         mIncomingCallWidget.setVisibility(View.VISIBLE);
637     }
638 
639     /**
640      * Handles state changes of the SlidingTabSelector widget.  While the user
641      * is dragging one of the handles, we display an onscreen hint; see
642      * CallCard.getRotateWidgetHint().
643      */
onGrabbedStateChange(View v, int grabbedState)644     public void onGrabbedStateChange(View v, int grabbedState) {
645         if (mInCallScreen != null) {
646             // Look up the hint based on which handle is currently grabbed.
647             // (Note we don't simply pass grabbedState thru to the InCallScreen,
648             // since *this* class is the only place that knows that the left
649             // handle means "Answer" and the right handle means "Decline".)
650             int hintTextResId, hintColorResId;
651             switch (grabbedState) {
652                 case SlidingTab.OnTriggerListener.NO_HANDLE:
653                     hintTextResId = 0;
654                     hintColorResId = 0;
655                     break;
656                 case SlidingTab.OnTriggerListener.LEFT_HANDLE:
657                     // TODO: Use different variants of "Slide to answer" in some cases
658                     // depending on the phone state, like slide_to_answer_and_hold
659                     // for a call waiting call, or slide_to_answer_and_end_active or
660                     // slide_to_answer_and_end_onhold for the 2-lines-in-use case.
661                     // (Note these are GSM-only cases, though.)
662                     hintTextResId = R.string.slide_to_answer;
663                     hintColorResId = R.color.incall_textConnected;  // green
664                     break;
665                 case SlidingTab.OnTriggerListener.RIGHT_HANDLE:
666                     hintTextResId = R.string.slide_to_decline;
667                     hintColorResId = R.color.incall_textEnded;  // red
668                     break;
669                 default:
670                     Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
671                           + grabbedState);
672                     hintTextResId = 0;
673                     hintColorResId = 0;
674                     break;
675             }
676 
677             // Tell the InCallScreen to update the CallCard and force the
678             // screen to redraw.
679             mInCallScreen.updateSlidingTabHint(hintTextResId, hintColorResId);
680         }
681     }
682 
683 
684     /**
685      * OnTouchListener used to shrink the "hit target" of some onscreen
686      * buttons.
687      */
688     class SmallerHitTargetTouchListener implements View.OnTouchListener {
689         /**
690          * Width of the allowable "hit target" as a percentage of
691          * the total width of this button.
692          */
693         private static final int HIT_TARGET_PERCENT_X = 50;
694 
695         /**
696          * Height of the allowable "hit target" as a percentage of
697          * the total height of this button.
698          *
699          * This is larger than HIT_TARGET_PERCENT_X because some of
700          * the onscreen buttons are wide but not very tall and we don't
701          * want to make the vertical hit target *too* small.
702          */
703         private static final int HIT_TARGET_PERCENT_Y = 80;
704 
705         // Size (percentage-wise) of the "edge" area that's *not* touch-sensitive.
706         private static final int X_EDGE = (100 - HIT_TARGET_PERCENT_X) / 2;
707         private static final int Y_EDGE = (100 - HIT_TARGET_PERCENT_Y) / 2;
708         // Min/max values (percentage-wise) of the touch-sensitive hit target.
709         private static final int X_HIT_MIN = X_EDGE;
710         private static final int X_HIT_MAX = 100 - X_EDGE;
711         private static final int Y_HIT_MIN = Y_EDGE;
712         private static final int Y_HIT_MAX = 100 - Y_EDGE;
713 
714         // True if the most recent DOWN event was a "hit".
715         boolean mDownEventHit;
716 
717         /**
718          * Called when a touch event is dispatched to a view. This allows listeners to
719          * get a chance to respond before the target view.
720          *
721          * @return True if the listener has consumed the event, false otherwise.
722          *         (In other words, we return true when the touch is *outside*
723          *         the "smaller hit target", which will prevent the actual
724          *         button from handling these events.)
725          */
onTouch(View v, MotionEvent event)726         public boolean onTouch(View v, MotionEvent event) {
727             // if (DBG) log("SmallerHitTargetTouchListener: " + v + ", event " + event);
728 
729             if (event.getAction() == MotionEvent.ACTION_DOWN) {
730                 // Note that event.getX() and event.getY() are already
731                 // translated into the View's coordinates.  (In other words,
732                 // "0,0" is a touch on the upper-left-most corner of the view.)
733                 int touchX = (int) event.getX();
734                 int touchY = (int) event.getY();
735 
736                 int viewWidth = v.getWidth();
737                 int viewHeight = v.getHeight();
738 
739                 // Touch location as a percentage of the total button width or height.
740                 int touchXPercent = (int) ((float) (touchX * 100) / (float) viewWidth);
741                 int touchYPercent = (int) ((float) (touchY * 100) / (float) viewHeight);
742                 // if (DBG) log("- percentage:  x = " + touchXPercent + ",  y = " + touchYPercent);
743 
744                 // TODO: user research: add event logging here of the actual
745                 // hit location (and button ID), and enable it for dogfooders
746                 // for a few days.  That'll give us a good idea of how close
747                 // to the center of the button(s) most touch events are, to
748                 // help us fine-tune the HIT_TARGET_PERCENT_* constants.
749 
750                 if (touchXPercent < X_HIT_MIN || touchXPercent > X_HIT_MAX
751                         || touchYPercent < Y_HIT_MIN || touchYPercent > Y_HIT_MAX) {
752                     // Missed!
753                     // if (DBG) log("  -> MISSED!");
754                     mDownEventHit = false;
755                     return true;  // Consume this event; don't let the button see it
756                 } else {
757                     // Hit!
758                     // if (DBG) log("  -> HIT!");
759                     mDownEventHit = true;
760                     return false;  // Let this event through to the actual button
761                 }
762             } else {
763                 // This is a MOVE, UP or CANCEL event.
764                 //
765                 // We only do the "smaller hit target" check on DOWN events.
766                 // For the subsequent MOVE/UP/CANCEL events, we let them
767                 // through to the actual button IFF the previous DOWN event
768                 // got through to the actual button (i.e. it was a "hit".)
769                 return !mDownEventHit;
770             }
771         }
772     }
773 
774 
775     // Debugging / testing code
776 
log(String msg)777     private void log(String msg) {
778         Log.d(LOG_TAG, msg);
779     }
780 }
781