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