1 /*
2  * Copyright (C) 2015 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 package com.android.car.dialer;
17 
18 import android.content.ContentResolver;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Color;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.support.car.ui.CircleBitmapDrawable;
25 import android.support.car.ui.FabDrawable;
26 import android.support.v4.app.Fragment;
27 import android.telecom.Call;
28 import android.telecom.CallAudioState;
29 import android.text.TextUtils;
30 import android.text.format.DateUtils;
31 import android.util.Log;
32 import android.view.KeyEvent;
33 import android.view.LayoutInflater;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.view.animation.AccelerateDecelerateInterpolator;
38 import android.view.animation.AccelerateInterpolator;
39 import android.view.animation.Animation;
40 import android.view.animation.Interpolator;
41 import android.view.animation.Transformation;
42 import android.widget.ImageButton;
43 import android.widget.ImageView;
44 import android.widget.LinearLayout;
45 import android.widget.TextView;
46 import com.android.car.dialer.bluetooth.UiBluetoothMonitor;
47 import com.android.car.dialer.telecom.TelecomUtils;
48 import com.android.car.dialer.telecom.UiCall;
49 import com.android.car.dialer.telecom.UiCallManager;
50 import com.android.car.dialer.telecom.UiCallManager.CallListener;
51 
52 import java.util.Arrays;
53 import java.util.HashMap;
54 import java.util.List;
55 
56 public class OngoingCallFragment extends Fragment {
57     private static final String TAG = "Em.OngoingCall";
58     private static final HashMap<Integer, Character> mDialpadButtonMap = new HashMap<>();
59 
60     static {
mDialpadButtonMap.put(R.id.one, '1')61         mDialpadButtonMap.put(R.id.one, '1');
mDialpadButtonMap.put(R.id.two, '2')62         mDialpadButtonMap.put(R.id.two, '2');
mDialpadButtonMap.put(R.id.three, '3')63         mDialpadButtonMap.put(R.id.three, '3');
mDialpadButtonMap.put(R.id.four, '4')64         mDialpadButtonMap.put(R.id.four, '4');
mDialpadButtonMap.put(R.id.five, '5')65         mDialpadButtonMap.put(R.id.five, '5');
mDialpadButtonMap.put(R.id.six, '6')66         mDialpadButtonMap.put(R.id.six, '6');
mDialpadButtonMap.put(R.id.seven, '7')67         mDialpadButtonMap.put(R.id.seven, '7');
mDialpadButtonMap.put(R.id.eight, '8')68         mDialpadButtonMap.put(R.id.eight, '8');
mDialpadButtonMap.put(R.id.nine, '9')69         mDialpadButtonMap.put(R.id.nine, '9');
mDialpadButtonMap.put(R.id.zero, '0')70         mDialpadButtonMap.put(R.id.zero, '0');
mDialpadButtonMap.put(R.id.star, '*')71         mDialpadButtonMap.put(R.id.star, '*');
mDialpadButtonMap.put(R.id.pound, '#')72         mDialpadButtonMap.put(R.id.pound, '#');
73     }
74 
75     private UiCall mPrimaryCall;
76     private UiCall mSecondaryCall;
77     private UiCall mLastRemovedCall;
78     private UiCallManager mUiCallManager;
79     private Handler mHandler;
80     private View mRingingCallControls;
81     private View mActiveCallControls;
82     private ImageButton mEndCallButton;
83     private ImageButton mUnholdCallButton;
84     private ImageButton mMuteButton;
85     private ImageButton mToggleDialpadButton;
86     private ImageButton mSwapButton;
87     private ImageButton mMergeButton;
88     private ImageButton mAnswerCallButton;
89     private ImageButton mRejectCallButton;
90     private TextView mNameTextView;
91     private TextView mSecondaryNameTextView;
92     private TextView mStateTextView;
93     private TextView mSecondaryStateTextView;
94     private ImageView mLargeContactPhotoView;
95     private ImageView mSmallContactPhotoView;
96     private View mDialpadContainer;
97     private View mSecondaryCallContainer;
98     private View mSecondaryCallControls;
99     private LinearLayout mRotaryDialpad;
100     private List<View> mDialpadViews;
101     private String mLoadedNumber;
102     private CharSequence mCallInfoLabel;
103     private boolean mIsHfpConnected;
104     private UiBluetoothMonitor mUiBluetoothMonitor;
105 
106     private final Interpolator
107             mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
108     private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator(10);
109 
110     @Override
onCreate(Bundle savedInstanceState)111     public void onCreate(Bundle savedInstanceState) {
112         super.onCreate(savedInstanceState);
113         mUiCallManager = UiCallManager.getInstance(getContext());
114         mUiBluetoothMonitor = UiBluetoothMonitor.getInstance();
115         mHandler = new Handler();
116     }
117 
118     @Override
onDestroy()119     public void onDestroy() {
120         super.onDestroy();
121         mHandler.removeCallbacks(mUpdateDurationRunnable);
122         mHandler.removeCallbacks(mStopDtmfToneRunnable);
123         mHandler = null;
124         mUiCallManager = null;
125         mLoadedNumber = null;
126         mUiBluetoothMonitor = null;
127     }
128 
129     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)130     public View onCreateView(LayoutInflater inflater, ViewGroup container,
131                              Bundle savedInstanceState) {
132         View view = inflater.inflate(R.layout.ongoing_call, container, false);
133         mRingingCallControls = view.findViewById(R.id.ringing_call_controls);
134         mActiveCallControls = view.findViewById(R.id.active_call_controls);
135         mEndCallButton = (ImageButton) view.findViewById(R.id.end_call);
136         mUnholdCallButton = (ImageButton) view.findViewById(R.id.unhold_call);
137         mMuteButton = (ImageButton) view.findViewById(R.id.mute);
138         mToggleDialpadButton = (ImageButton) view.findViewById(R.id.toggle_dialpad);
139         mDialpadContainer = view.findViewById(R.id.dialpad_container);
140         mNameTextView = (TextView) view.findViewById(R.id.name);
141         mSecondaryNameTextView = (TextView) view.findViewById(R.id.name_secondary);
142         mStateTextView = (TextView) view.findViewById(R.id.info);
143         mSecondaryStateTextView = (TextView) view.findViewById(R.id.info_secondary);
144         mLargeContactPhotoView = (ImageView) view.findViewById(R.id.large_contact_photo);
145         mSmallContactPhotoView = (ImageView) view.findViewById(R.id.small_contact_photo);
146         mSecondaryCallContainer = view.findViewById(R.id.secondary_call_container);
147         mSecondaryCallControls = view.findViewById(R.id.secondary_call_controls);
148         mRotaryDialpad = (LinearLayout) view.findViewById(R.id.rotary_dialpad);
149         mSwapButton = (ImageButton) view.findViewById(R.id.swap);
150         mMergeButton = (ImageButton) view.findViewById(R.id.merge);
151         mAnswerCallButton = (ImageButton) view.findViewById(R.id.answer_call_button);
152         mRejectCallButton = (ImageButton) view.findViewById(R.id.reject_call_button);
153 
154         boolean hasTouch = getResources().getBoolean(R.bool.has_touch);
155         View dialPadContainer = hasTouch ? mDialpadContainer : mRotaryDialpad;
156         mDialpadViews = Arrays.asList(
157                 dialPadContainer.findViewById(R.id.one),
158                 dialPadContainer.findViewById(R.id.two),
159                 dialPadContainer.findViewById(R.id.three),
160                 dialPadContainer.findViewById(R.id.four),
161                 dialPadContainer.findViewById(R.id.five),
162                 dialPadContainer.findViewById(R.id.six),
163                 dialPadContainer.findViewById(R.id.seven),
164                 dialPadContainer.findViewById(R.id.eight),
165                 dialPadContainer.findViewById(R.id.nine),
166                 dialPadContainer.findViewById(R.id.zero),
167                 dialPadContainer.findViewById(R.id.pound),
168                 dialPadContainer.findViewById(R.id.star)
169         );
170         if (hasTouch) {
171             // In touch screen, we need to adjust the InCall card for the narrow screen to show the
172             // full dial pad.
173             for (View dialpadView : mDialpadViews) {
174                 dialpadView.setOnTouchListener(mDialpadTouchListener);
175                 dialpadView.setOnKeyListener(mDialpadKeyListener);
176             }
177         } else {
178             for (View dialpadView : mDialpadViews) {
179                 dialpadView.setOnKeyListener(mDialpadKeyListener);
180             }
181             mToggleDialpadButton.setImageResource(R.drawable.ic_rotary_dialpad);
182         }
183         setDialPadFocusability(!hasTouch);
184         setInCallControllerFocusability(!hasTouch);
185 
186         mAnswerCallButton.setOnClickListener((unusedView) -> {
187             UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING);
188             if (call == null) {
189                 Log.w(TAG, "There is no incoming call to answer.");
190                 return;
191             }
192             mUiCallManager.answerCall(call);
193         });
194         Context context = getContext();
195         FabDrawable answerCallDrawable = new FabDrawable(context);
196         answerCallDrawable.setFabAndStrokeColor(getResources().getColor(R.color.phone_call));
197         mAnswerCallButton.setBackground(answerCallDrawable);
198 
199         mRejectCallButton.setOnClickListener((unusedView) -> {
200             UiCall call = mUiCallManager.getCallWithState(Call.STATE_RINGING);
201             if (call == null) {
202                 Log.w(TAG, "There is no incoming call to reject.");
203                 return;
204             }
205             mUiCallManager.rejectCall(call, false, null);
206         });
207 
208         mEndCallButton.setOnClickListener((unusedView) -> {
209             UiCall call = mUiCallManager.getPrimaryCall();
210             if (call == null) {
211                 Log.w(TAG, "There is no active call to end.");
212                 return;
213             }
214             mUiCallManager.disconnectCall(call);
215         });
216         FabDrawable endCallDrawable = new FabDrawable(context);
217         endCallDrawable.setFabAndStrokeColor(getResources().getColor(R.color.phone_end_call));
218         mEndCallButton.setBackground(endCallDrawable);
219 
220         mUnholdCallButton.setOnClickListener((unusedView) -> {
221             UiCall call = mUiCallManager.getPrimaryCall();
222             if (call == null) {
223                 Log.w(TAG, "There is no active call to unhold.");
224                 return;
225             }
226             mUiCallManager.unholdCall(call);
227         });
228         FabDrawable unholdCallDrawable = new FabDrawable(context);
229         unholdCallDrawable.setFabAndStrokeColor(getResources().getColor(R.color.phone_call));
230         mUnholdCallButton.setBackground(unholdCallDrawable);
231 
232         mMuteButton.setOnClickListener((unusedView) -> {
233             if (mUiCallManager.getMuted()) {
234                 mUiCallManager.setMuted(false);
235             } else {
236                 mUiCallManager.setMuted(true);
237             }
238         });
239 
240         mSwapButton.setOnClickListener((unusedView) -> {
241             UiCall call = mUiCallManager.getPrimaryCall();
242             if (call == null) {
243                 Log.w(TAG, "There is no active call to hold.");
244                 return;
245             }
246             if (call.getState() == Call.STATE_HOLDING) {
247                 mUiCallManager.unholdCall(call);
248             } else {
249                 mUiCallManager.holdCall(call);
250             }
251         });
252 
253         mMergeButton.setOnClickListener((unusedView) -> {
254             UiCall call = mUiCallManager.getPrimaryCall();
255             UiCall secondarycall = mUiCallManager.getSecondaryCall();
256             if (call == null || mSecondaryCall == null) {
257                 Log.w(TAG, "There aren't two call to merge.");
258                 return;
259             }
260 
261             mUiCallManager.conference(call, secondarycall);
262         });
263 
264         mToggleDialpadButton.setOnClickListener((unusedView) -> {
265             if (mToggleDialpadButton.isActivated()) {
266                 closeDialpad();
267             } else {
268                 openDialpad(true /*animate*/);
269             }
270         });
271 
272         mUiCallManager.addListener(mCallListener);
273 
274         // These must be called after the views are inflated because they have the side affect
275         // of updating the ui.
276         mUiBluetoothMonitor.addListener(mBluetoothListener);
277         mBluetoothListener.onStateChanged(); // Trigger state change to set initial state.
278 
279         updateCalls();
280         updateRotaryFocus();
281 
282         return view;
283     }
284 
285     @Override
onDestroyView()286     public void onDestroyView() {
287         super.onDestroyView();
288         mUiCallManager.removeListener(mCallListener);
289         mUiBluetoothMonitor.removeListener(mBluetoothListener);
290     }
291 
292     @Override
onStart()293     public void onStart() {
294         super.onStart();
295         trySpeakerAudioRouteIfNecessary();
296     }
297 
rebindViews()298     private void rebindViews() {
299         mHandler.removeCallbacks(mUpdateDurationRunnable);
300 
301         // Toggle the visibility between the active call controls, ringing call controls,
302         // and no controls.
303         CharSequence disconnectCauseLabel = mLastRemovedCall == null ?
304                 null : mLastRemovedCall.getDisconnectClause();
305         if (mPrimaryCall == null && !TextUtils.isEmpty(disconnectCauseLabel)) {
306             closeDialpad();
307             setStateText(disconnectCauseLabel);
308             return;
309         } else if (mPrimaryCall == null || mPrimaryCall.getState() == Call.STATE_DISCONNECTED) {
310             closeDialpad();
311             setStateText(getString(R.string.call_state_call_ended));
312             mRingingCallControls.setVisibility(View.GONE);
313             mActiveCallControls.setVisibility(View.GONE);
314             return;
315         } else if (mPrimaryCall.getState() == Call.STATE_RINGING) {
316             mRingingCallControls.setVisibility(View.VISIBLE);
317             mActiveCallControls.setVisibility(View.GONE);
318         } else {
319             mRingingCallControls.setVisibility(View.GONE);
320             mActiveCallControls.setVisibility(View.VISIBLE);
321         }
322 
323         // Show the primary contact photo in the large ImageView on the right if there is no
324         // secondary call. Otherwise, show it in the small ImageView that is inside the card.
325         Context context = getContext();
326         final ContentResolver cr = context.getContentResolver();
327         final String primaryNumber = mPrimaryCall.getNumber();
328         // Don't reload the image if the number is the same.
329         if ((primaryNumber != null && !primaryNumber.equals(mLoadedNumber))
330                 || (primaryNumber == null && mLoadedNumber != null)) {
331             BitmapWorkerTask.BitmapRunnable runnable = new BitmapWorkerTask.BitmapRunnable() {
332                 @Override
333                 public void run() {
334                     if (mBitmap != null) {
335                         Resources r = mSmallContactPhotoView.getResources();
336                         mSmallContactPhotoView.setImageDrawable(
337                                 new CircleBitmapDrawable(r, mBitmap));
338                         mLargeContactPhotoView.setImageBitmap(mBitmap);
339                         mLargeContactPhotoView.clearColorFilter();
340                     } else {
341                         mSmallContactPhotoView.setImageResource(R.drawable.logo_avatar);
342                         mLargeContactPhotoView.setImageResource(R.drawable.ic_avatar_bg);
343                     }
344 
345                     if (mSecondaryCall != null) {
346                         BitmapWorkerTask.BitmapRunnable secondCallContactPhotoHandler =
347                                 new BitmapWorkerTask.BitmapRunnable() {
348                                     @Override
349                                     public void run() {
350                                         if (mBitmap != null) {
351                                             mLargeContactPhotoView.setImageBitmap(mBitmap);
352                                         } else {
353                                             mLargeContactPhotoView.setImageResource(
354                                                     R.drawable.logo_avatar);
355                                         }
356                                     }
357                                 };
358 
359                         BitmapWorkerTask.loadBitmap(
360                                 cr, mLargeContactPhotoView, mSecondaryCall.getNumber(),
361                                 secondCallContactPhotoHandler);
362 
363                         int scrimColor = getResources().getColor(
364                                 R.color.phone_secondary_call_scrim);
365                         mLargeContactPhotoView.setColorFilter(scrimColor);
366                     }
367                     mLoadedNumber = primaryNumber;
368                 }
369             };
370             BitmapWorkerTask.loadBitmap(cr, mLargeContactPhotoView, primaryNumber, runnable);
371         }
372 
373         if (mSecondaryCall != null) {
374             mSecondaryCallContainer.setVisibility(View.VISIBLE);
375             if (mPrimaryCall.getState() == Call.STATE_ACTIVE
376                     && mSecondaryCall.getState() == Call.STATE_HOLDING) {
377                 mSecondaryCallControls.setVisibility(View.VISIBLE);
378             } else {
379                 mSecondaryCallControls.setVisibility(View.GONE);
380             }
381         } else {
382             mSecondaryCallContainer.setVisibility(View.GONE);
383             mSecondaryCallControls.setVisibility(View.GONE);
384         }
385 
386         String displayName = TelecomUtils.getDisplayName(context, mPrimaryCall);
387         mNameTextView.setText(displayName);
388         mNameTextView.setVisibility(TextUtils.isEmpty(displayName) ? View.GONE : View.VISIBLE);
389 
390         if (mSecondaryCall != null) {
391             mSecondaryNameTextView.setText(
392                     TelecomUtils.getDisplayName(context, mSecondaryCall));
393         }
394 
395         switch (mPrimaryCall.getState()) {
396             case Call.STATE_NEW:
397                 // Since the content resolver call is only cached when a contact is found,
398                 // this should only be called once on a new call to avoid jank.
399                 // TODO: consider moving TelecomUtils.getTypeFromNumber into a CursorLoader
400                 String number = mPrimaryCall.getNumber();
401                 mCallInfoLabel = TelecomUtils.getTypeFromNumber(context, number);
402             case Call.STATE_CONNECTING:
403             case Call.STATE_DIALING:
404             case Call.STATE_SELECT_PHONE_ACCOUNT:
405             case Call.STATE_HOLDING:
406             case Call.STATE_DISCONNECTED:
407                 mHandler.removeCallbacks(mUpdateDurationRunnable);
408                 String callInfoText = TelecomUtils.getCallInfoText(context,
409                         mPrimaryCall, mCallInfoLabel);
410                 setStateText(callInfoText);
411                 break;
412             case Call.STATE_ACTIVE:
413                 if (mIsHfpConnected) {
414                     mHandler.post(mUpdateDurationRunnable);
415                 }
416                 break;
417             case Call.STATE_RINGING:
418                 Log.w(TAG, "There should not be a ringing call in the ongoing call fragment.");
419                 break;
420             default:
421                 Log.w(TAG, "Unhandled call state: " + mPrimaryCall.getState());
422         }
423 
424         if (mSecondaryCall != null) {
425             mSecondaryStateTextView.setText(
426                     TelecomUtils.callStateToUiString(context, mSecondaryCall.getState()));
427         }
428 
429         // If it is a voicemail call, open the dialpad (with no animation).
430         if (primaryNumber != null && primaryNumber.equals(
431                 TelecomUtils.getVoicemailNumber(context))) {
432             if (getResources().getBoolean(R.bool.has_touch)) {
433                 openDialpad(false /*animate*/);
434                 mToggleDialpadButton.setVisibility(View.GONE);
435             } else {
436                 mToggleDialpadButton.setVisibility(View.VISIBLE);
437                 mToggleDialpadButton.requestFocus();
438             }
439         } else {
440             mToggleDialpadButton.setVisibility(View.VISIBLE);
441         }
442 
443         // Handle the holding case.
444         if (mPrimaryCall.getState() == Call.STATE_HOLDING) {
445             mEndCallButton.setVisibility(View.GONE);
446             mUnholdCallButton.setVisibility(View.VISIBLE);
447             mMuteButton.setVisibility(View.INVISIBLE);
448             mToggleDialpadButton.setVisibility(View.INVISIBLE);
449         } else {
450             mEndCallButton.setVisibility(View.VISIBLE);
451             mUnholdCallButton.setVisibility(View.GONE);
452             mMuteButton.setVisibility(View.VISIBLE);
453             mToggleDialpadButton.setVisibility(View.VISIBLE);
454         }
455     }
456 
setStateText(CharSequence stateText)457     private void setStateText(CharSequence stateText) {
458         mStateTextView.setText(stateText);
459         mStateTextView.setVisibility(TextUtils.isEmpty(stateText) ? View.GONE : View.VISIBLE);
460     }
461 
updateCalls()462     private void updateCalls() {
463         mPrimaryCall = mUiCallManager.getPrimaryCall();
464         if (mPrimaryCall != null && mPrimaryCall.getState() == Call.STATE_RINGING) {
465             // TODO: update when notifications will work
466         }
467         mSecondaryCall = mUiCallManager.getSecondaryCall();
468         if (Log.isLoggable(TAG, Log.DEBUG)) {
469             Log.d(TAG, "Primary call: " + mPrimaryCall + "\tSecondary call:" + mSecondaryCall);
470         }
471         rebindViews();
472     }
473 
474     /**
475      * If the phone is using bluetooth:
476      *     * Do nothing
477      * If the phone is not using bluetooth:
478      *     * If the phone supports bluetooth, use it.
479      *     * If the phone doesn't support bluetooth and support speaker, use speaker
480      *     * Otherwise, do nothing. Hopefully no phones won't have bt or speaker.
481      */
trySpeakerAudioRouteIfNecessary()482     private void trySpeakerAudioRouteIfNecessary() {
483         if (mUiCallManager == null) {
484             return;
485         }
486 
487         int supportedAudioRouteMask = mUiCallManager.getSupportedAudioRouteMask();
488         boolean supportsBluetooth = (supportedAudioRouteMask & CallAudioState.ROUTE_BLUETOOTH) != 0;
489         boolean supportsSpeaker = (supportedAudioRouteMask & CallAudioState.ROUTE_SPEAKER) != 0;
490         boolean isUsingBluetooth =
491                 mUiCallManager.getAudioRoute() == CallAudioState.ROUTE_BLUETOOTH;
492 
493         if (supportsBluetooth && !isUsingBluetooth) {
494             mUiCallManager.setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
495         } else if (!supportsBluetooth && supportsSpeaker) {
496             mUiCallManager.setAudioRoute(CallAudioState.ROUTE_SPEAKER);
497         }
498     }
499 
openDialpad(boolean animate)500     private void openDialpad(boolean animate) {
501         if (mToggleDialpadButton.isActivated()) {
502             return;
503         }
504         mToggleDialpadButton.setActivated(true);
505         if (getResources().getBoolean(R.bool.has_touch)) {
506             // This array of of size 2 because getLocationOnScreen returns (x,y) coordinates.
507             int[] location = new int[2];
508             mToggleDialpadButton.getLocationOnScreen(location);
509 
510             // The dialpad should be aligned with the right edge of mToggleDialpadButton.
511             int startingMargin = location[1] + mToggleDialpadButton.getWidth();
512 
513             ViewGroup.MarginLayoutParams layoutParams =
514                     (ViewGroup.MarginLayoutParams) mDialpadContainer.getLayoutParams();
515 
516             if (layoutParams.getMarginStart() != startingMargin) {
517                 layoutParams.setMarginStart(startingMargin);
518                 mDialpadContainer.setLayoutParams(layoutParams);
519             }
520 
521             Animation anim = new DialpadAnimation(getContext(), false /* reverse */, animate);
522             mDialpadContainer.startAnimation(anim);
523         } else {
524             final int toggleButtonImageOffset = getResources().getDimensionPixelSize(
525                     R.dimen.in_call_toggle_button_image_offset);
526             final int muteButtonLeftMargin =
527                     ((LinearLayout.LayoutParams) mMuteButton.getLayoutParams()).leftMargin;
528 
529             mEndCallButton.animate()
530                     .alpha(0)
531                     .setStartDelay(0)
532                     .setDuration(384)
533                     .setInterpolator(mAccelerateDecelerateInterpolator)
534                     .withEndAction(() -> {
535                             mEndCallButton.setVisibility(View.INVISIBLE);
536                             mEndCallButton.setFocusable(false);
537                         }).start();
538             mMuteButton.animate()
539                     .alpha(0)
540                     .setStartDelay(0)
541                     .setDuration(240)
542                     .setInterpolator(mAccelerateDecelerateInterpolator)
543                     .withEndAction(() -> {
544                             mMuteButton.setVisibility(View.INVISIBLE);
545                             mMuteButton.setFocusable(false);
546                         }).start();
547             mToggleDialpadButton.animate()
548                     .setStartDelay(0)
549                     .translationX(-(mEndCallButton.getWidth() + muteButtonLeftMargin
550                             + mMuteButton.getWidth() + toggleButtonImageOffset))
551                     .setDuration(480)
552                     .setInterpolator(mAccelerateDecelerateInterpolator)
553                     .start();
554 
555             mRotaryDialpad.setTranslationX(
556                     -(mEndCallButton.getWidth() + muteButtonLeftMargin + toggleButtonImageOffset));
557             mRotaryDialpad.animate()
558                     .translationX(-(mEndCallButton.getWidth() + muteButtonLeftMargin
559                             + mMuteButton.getWidth() + toggleButtonImageOffset))
560                     .setDuration(320)
561                     .setInterpolator(mAccelerateDecelerateInterpolator)
562                     .setStartDelay(240)
563                     .withStartAction(() -> {
564                             mRotaryDialpad.setVisibility(View.VISIBLE);
565                             int delay = 0;
566                             for (View dialpadView : mDialpadViews) {
567                                 dialpadView.setAlpha(0);
568                                 dialpadView.animate()
569                                         .alpha(1)
570                                         .setDuration(160)
571                                         .setStartDelay(delay)
572                                         .setInterpolator(mAccelerateInterpolator)
573                                         .start();
574                                 delay += 10;
575                             }
576                         }).start();
577         }
578     }
579 
closeDialpad()580     private void closeDialpad() {
581         if (!mToggleDialpadButton.isActivated()) {
582             return;
583         }
584         mToggleDialpadButton.setActivated(false);
585         if (getResources().getBoolean(R.bool.has_touch)) {
586             Animation anim = new DialpadAnimation(getContext(), true /* reverse */);
587             mDialpadContainer.startAnimation(anim);
588         } else {
589             final int toggleButtonImageOffset = getResources().getDimensionPixelSize(
590                     R.dimen.in_call_toggle_button_image_offset);
591             final int muteButtonLeftMargin =
592                     ((LinearLayout.LayoutParams) mMuteButton.getLayoutParams()).leftMargin;
593 
594             mRotaryDialpad.animate()
595                     .setStartDelay(0)
596                     .translationX(-(mEndCallButton.getWidth()
597                             + muteButtonLeftMargin + toggleButtonImageOffset))
598                     .setDuration(320)
599                     .setInterpolator(mAccelerateDecelerateInterpolator)
600                     .withStartAction(() -> {
601                             int delay = 0;
602                             for (int i = mDialpadViews.size() - 1; i >= 0; i--) {
603                                 View dialpadView = mDialpadViews.get(i);
604                                 dialpadView.animate()
605                                         .alpha(0)
606                                         .setDuration(160)
607                                         .setStartDelay(delay)
608                                         .setInterpolator(mAccelerateInterpolator)
609                                         .start();
610                                 delay += 10;
611                             }
612                         }).withEndAction(() -> {
613                             mRotaryDialpad.setVisibility(View.GONE);
614                             mRotaryDialpad.setTranslationX(0);
615                         }).start();
616             mToggleDialpadButton.animate()
617                     .translationX(0)
618                     .setDuration(480)
619                     .setStartDelay(80)
620                     .setInterpolator(mAccelerateDecelerateInterpolator)
621                     .start();
622             mMuteButton.animate()
623                     .alpha(1)
624                     .setDuration(176)
625                     .setInterpolator(mAccelerateDecelerateInterpolator)
626                     .setStartDelay(384)
627                     .withStartAction(() -> {
628                             mMuteButton.setVisibility(View.VISIBLE);
629                             mMuteButton.setFocusable(true);
630                         }).start();
631             mEndCallButton.animate()
632                     .alpha(1)
633                     .setDuration(320)
634                     .setInterpolator(mAccelerateDecelerateInterpolator)
635                     .setStartDelay(240)
636                     .withStartAction(() -> {
637                             mEndCallButton.setVisibility(View.VISIBLE);
638                             mEndCallButton.setFocusable(true);
639                         }).start();
640         }
641     }
642 
updateRotaryFocus()643     private void updateRotaryFocus() {
644         boolean hasTouch = getResources().getBoolean(R.bool.has_touch);
645         if (mPrimaryCall != null && !hasTouch) {
646             if (mPrimaryCall.getState() == Call.STATE_RINGING) {
647                 mRingingCallControls.requestFocus();
648             } else {
649                 mActiveCallControls.requestFocus();
650             }
651         }
652     }
653 
setInCallControllerFocusability(boolean focusable)654     private void setInCallControllerFocusability(boolean focusable) {
655         mSwapButton.setFocusable(focusable);
656         mMergeButton.setFocusable(focusable);
657 
658         mAnswerCallButton.setFocusable(focusable);
659         mRejectCallButton.setFocusable(focusable);
660 
661         mEndCallButton.setFocusable(focusable);
662         mUnholdCallButton.setFocusable(focusable);
663         mMuteButton.setFocusable(focusable);
664         mToggleDialpadButton.setFocusable(focusable);
665     }
666 
setDialPadFocusability(boolean focusable)667     private void setDialPadFocusability(boolean focusable) {
668         for (View dialPadView : mDialpadViews) {
669             dialPadView.setFocusable(focusable);
670         }
671     }
672 
673     private final View.OnTouchListener mDialpadTouchListener = new View.OnTouchListener() {
674 
675         @Override
676         public boolean onTouch(View v, MotionEvent event) {
677             Character digit = mDialpadButtonMap.get(v.getId());
678             if (digit == null) {
679                 Log.w(TAG, "Unknown dialpad button pressed.");
680                 return false;
681             }
682             if (event.getAction() == MotionEvent.ACTION_DOWN) {
683                 v.setPressed(true);
684                 mUiCallManager.playDtmfTone(mPrimaryCall, digit);
685                 return true;
686             } else if (event.getAction() == MotionEvent.ACTION_UP) {
687                 v.setPressed(false);
688                 v.performClick();
689                 mUiCallManager.stopDtmfTone(mPrimaryCall);
690                 return true;
691             }
692 
693             return false;
694         }
695     };
696 
697     private final View.OnKeyListener mDialpadKeyListener = new View.OnKeyListener() {
698         @Override
699         public boolean onKey(View v, int keyCode, KeyEvent event) {
700             Character digit = mDialpadButtonMap.get(v.getId());
701             if (digit == null) {
702                 Log.w(TAG, "Unknown dialpad button pressed.");
703                 return false;
704             }
705 
706             if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_CENTER) {
707                 return false;
708             }
709 
710             if (event.getAction() == KeyEvent.ACTION_DOWN) {
711                 v.setPressed(true);
712                 mUiCallManager.playDtmfTone(mPrimaryCall, digit);
713                 return true;
714             } else if (event.getAction() == KeyEvent.ACTION_UP) {
715                 v.setPressed(false);
716                 mUiCallManager.stopDtmfTone(mPrimaryCall);
717                 return true;
718             }
719 
720             return false;
721         }
722     };
723 
724     private final Runnable mUpdateDurationRunnable = new Runnable() {
725         @Override
726         public void run() {
727             if (mPrimaryCall.getState() != Call.STATE_ACTIVE) {
728                 return;
729             }
730             String callInfoText = TelecomUtils.getCallInfoText(getContext(),
731                     mPrimaryCall, mCallInfoLabel);
732             setStateText(callInfoText);
733             mHandler.postDelayed(this /* runnable */, DateUtils.SECOND_IN_MILLIS);
734         }
735     };
736 
737     private final Runnable mStopDtmfToneRunnable = new Runnable() {
738         @Override
739         public void run() {
740             mUiCallManager.stopDtmfTone(mPrimaryCall);
741         }
742     };
743 
744     private final class DialpadAnimation extends Animation {
745         private static final int DURATION = 300;
746         private static final float MAX_SCRIM_ALPHA = 0.6f;
747 
748         private final int mStartingTranslation;
749         private final int mScrimColor;
750         private final boolean mReverse;
751 
DialpadAnimation(Context context, boolean reverse)752         public DialpadAnimation(Context context, boolean reverse) {
753             this(context, reverse, true);
754         }
755 
DialpadAnimation(Context context, boolean reverse, boolean animate)756         public DialpadAnimation(Context context, boolean reverse, boolean animate) {
757             setDuration(animate ? DURATION : 0);
758             setInterpolator(new AccelerateDecelerateInterpolator());
759             Resources res = context.getResources();
760             mStartingTranslation =
761                     res.getDimensionPixelOffset(R.dimen.in_call_card_dialpad_translation_x);
762             mScrimColor = res.getColor(R.color.phone_theme);
763             mReverse = reverse;
764         }
765 
766         @Override
applyTransformation(float interpolatedTime, Transformation t)767         protected void applyTransformation(float interpolatedTime, Transformation t) {
768             if (mReverse) {
769                 interpolatedTime = 1f - interpolatedTime;
770             }
771             int translationX = (int) (mStartingTranslation * (1f - interpolatedTime));
772             mDialpadContainer.setTranslationX(translationX);
773             mDialpadContainer.setAlpha(interpolatedTime);
774             if (interpolatedTime == 0f) {
775                 mDialpadContainer.setVisibility(View.GONE);
776             } else {
777                 mDialpadContainer.setVisibility(View.VISIBLE);
778             }
779             float alpha = 255f * interpolatedTime * MAX_SCRIM_ALPHA;
780             mLargeContactPhotoView.setColorFilter(Color.argb((int) alpha, Color.red(mScrimColor),
781                     Color.green(mScrimColor), Color.blue(mScrimColor)));
782 
783             mSecondaryNameTextView.setAlpha(1f - interpolatedTime);
784             mSecondaryStateTextView.setAlpha(1f - interpolatedTime);
785         }
786     }
787 
788     private final CallListener mCallListener = new CallListener() {
789 
790         @Override
791         public void onCallAdded(UiCall call) {
792             if (Log.isLoggable(TAG, Log.DEBUG)) {
793                 Log.d(TAG, "on call added");
794             }
795             updateCalls();
796             trySpeakerAudioRouteIfNecessary();
797         }
798 
799         @Override
800         public void onCallRemoved(UiCall call) {
801             if (Log.isLoggable(TAG, Log.DEBUG)) {
802                 Log.d(TAG, "on call removed");
803             }
804             mLastRemovedCall = call;
805             updateCalls();
806         }
807 
808         @Override
809         public void onAudioStateChanged(boolean isMuted, int audioRoute,
810                 int supportedAudioRouteMask) {
811             if (Log.isLoggable(TAG, Log.DEBUG)) {
812                 Log.d(TAG, "on audio state changed");
813             }
814             mMuteButton.setActivated(isMuted);
815             trySpeakerAudioRouteIfNecessary();
816         }
817 
818         @Override
819         public void onStateChanged(UiCall call, int state) {
820             if (Log.isLoggable(TAG, Log.DEBUG)) {
821                 Log.d(TAG, "onStateChanged");
822             }
823             updateCalls();
824             //  this will reset the focus if any state of any call changes on pure rotary devices.
825             updateRotaryFocus();
826         }
827 
828         @Override
829         public void onCallUpdated(UiCall call) {
830             if (Log.isLoggable(TAG, Log.DEBUG)) {
831                 Log.d(TAG, "onCallUpdated");
832             }
833             updateCalls();
834         }
835     };
836 
837     private final UiBluetoothMonitor.Listener mBluetoothListener = () -> {
838         OngoingCallFragment.this.mIsHfpConnected =
839                 UiBluetoothMonitor.getInstance().isHfpConnected();
840     };
841 }
842