1 /*
2  * Copyright (C) 2016 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.incallui.video.impl;
18 
19 import android.Manifest.permission;
20 import android.content.Context;
21 import android.content.pm.PackageManager;
22 import android.graphics.Point;
23 import android.graphics.drawable.Animatable;
24 import android.os.Bundle;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.annotation.VisibleForTesting;
28 import android.support.v4.app.Fragment;
29 import android.support.v4.app.FragmentTransaction;
30 import android.support.v4.view.animation.FastOutLinearInInterpolator;
31 import android.support.v4.view.animation.LinearOutSlowInInterpolator;
32 import android.telecom.CallAudioState;
33 import android.text.TextUtils;
34 import android.view.LayoutInflater;
35 import android.view.Surface;
36 import android.view.SurfaceView;
37 import android.view.View;
38 import android.view.View.OnClickListener;
39 import android.view.View.OnSystemUiVisibilityChangeListener;
40 import android.view.ViewGroup;
41 import android.view.ViewGroup.MarginLayoutParams;
42 import android.view.ViewTreeObserver;
43 import android.view.accessibility.AccessibilityEvent;
44 import android.view.animation.AccelerateDecelerateInterpolator;
45 import android.view.animation.Interpolator;
46 import android.widget.FrameLayout;
47 import android.widget.ImageButton;
48 import android.widget.TextView;
49 import com.android.dialer.common.Assert;
50 import com.android.dialer.common.FragmentUtils;
51 import com.android.dialer.common.LogUtil;
52 import com.android.dialer.compat.ActivityCompat;
53 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
54 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
55 import com.android.incallui.contactgrid.ContactGridManager;
56 import com.android.incallui.hold.OnHoldFragment;
57 import com.android.incallui.incall.protocol.InCallButtonIds;
58 import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
59 import com.android.incallui.incall.protocol.InCallButtonUi;
60 import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
61 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
62 import com.android.incallui.incall.protocol.InCallScreen;
63 import com.android.incallui.incall.protocol.InCallScreenDelegate;
64 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
65 import com.android.incallui.incall.protocol.PrimaryCallState;
66 import com.android.incallui.incall.protocol.PrimaryInfo;
67 import com.android.incallui.incall.protocol.SecondaryInfo;
68 import com.android.incallui.video.impl.CameraPermissionDialogFragment.CameraPermissionDialogCallback;
69 import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
70 import com.android.incallui.video.protocol.VideoCallScreen;
71 import com.android.incallui.video.protocol.VideoCallScreenDelegate;
72 import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
73 import com.android.incallui.videotech.utils.VideoUtils;
74 
75 /**
76  * Contains UI elements for a video call.
77  *
78  * <p>This version is used by RCS Video Share since Dreamchip requires a SurfaceView instead of the
79  * TextureView, which is present in {@link VideoCallFragment} and used by IMS.
80  */
81 public class SurfaceViewVideoCallFragment extends Fragment
82     implements InCallScreen,
83         InCallButtonUi,
84         VideoCallScreen,
85         OnClickListener,
86         OnCheckedChangeListener,
87         AudioRouteSelectorPresenter,
88         OnSystemUiVisibilityChangeListener,
89         CameraPermissionDialogCallback {
90 
91   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
92   static final String ARG_CALL_ID = "call_id";
93 
94   private static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
95   private static final String CAMERA_PERMISSION_DIALOG_FRAMENT_TAG =
96       "CameraPermissionDialogFragment";
97   private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L;
98   private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L;
99 
100   private InCallScreenDelegate inCallScreenDelegate;
101   private VideoCallScreenDelegate videoCallScreenDelegate;
102   private InCallButtonUiDelegate inCallButtonUiDelegate;
103   private View endCallButton;
104   private CheckableImageButton speakerButton;
105   private SpeakerButtonController speakerButtonController;
106   private CheckableImageButton muteButton;
107   private CheckableImageButton cameraOffButton;
108   private ImageButton swapCameraButton;
109   private View switchOnHoldButton;
110   private View onHoldContainer;
111   private SwitchOnHoldCallController switchOnHoldCallController;
112   private TextView remoteVideoOff;
113   private View mutePreviewOverlay;
114   private View previewOffOverlay;
115   private View controls;
116   private View controlsContainer;
117   private SurfaceView previewSurfaceView;
118   private SurfaceView remoteSurfaceView;
119   private View greenScreenBackgroundView;
120   private View fullscreenBackgroundView;
121   private FrameLayout previewRoot;
122   private boolean shouldShowRemote;
123   private boolean shouldShowPreview;
124   private boolean isInFullscreenMode;
125   private boolean isInGreenScreenMode;
126   private boolean hasInitializedScreenModes;
127   private boolean isRemotelyHeld;
128   private ContactGridManager contactGridManager;
129   private SecondaryInfo savedSecondaryInfo;
130   private final Runnable cameraPermissionDialogRunnable =
131       new Runnable() {
132         @Override
133         public void run() {
134           if (videoCallScreenDelegate.shouldShowCameraPermissionDialog()) {
135             LogUtil.i(
136                 "SurfaceViewVideoCallFragment.cameraPermissionDialogRunnable", "showing dialog");
137             checkCameraPermission();
138           }
139         }
140       };
141 
newInstance(String callId)142   public static SurfaceViewVideoCallFragment newInstance(String callId) {
143     Bundle bundle = new Bundle();
144     bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
145 
146     SurfaceViewVideoCallFragment instance = new SurfaceViewVideoCallFragment();
147     instance.setArguments(bundle);
148     return instance;
149   }
150 
151   @Override
onCreate(@ullable Bundle savedInstanceState)152   public void onCreate(@Nullable Bundle savedInstanceState) {
153     super.onCreate(savedInstanceState);
154     LogUtil.i("SurfaceViewVideoCallFragment.onCreate", null);
155 
156     inCallButtonUiDelegate =
157         FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
158             .newInCallButtonUiDelegate();
159     if (savedInstanceState != null) {
160       inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
161     }
162   }
163 
164   @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)165   public void onRequestPermissionsResult(
166       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
167     if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
168       if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
169         LogUtil.i(
170             "SurfaceViewVideoCallFragment.onRequestPermissionsResult",
171             "Camera permission granted.");
172         videoCallScreenDelegate.onCameraPermissionGranted();
173       } else {
174         LogUtil.i(
175             "SurfaceViewVideoCallFragment.onRequestPermissionsResult", "Camera permission denied.");
176       }
177     }
178     super.onRequestPermissionsResult(requestCode, permissions, grantResults);
179   }
180 
181   @Nullable
182   @Override
onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)183   public View onCreateView(
184       LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
185     LogUtil.i("SurfaceViewVideoCallFragment.onCreateView", null);
186 
187     View view = layoutInflater.inflate(R.layout.frag_videocall_surfaceview, viewGroup, false);
188     contactGridManager =
189         new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */);
190 
191     controls = view.findViewById(R.id.videocall_video_controls);
192     controls.setVisibility(
193         ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
194     controlsContainer = view.findViewById(R.id.videocall_video_controls_container);
195     speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button);
196     muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button);
197     muteButton.setOnCheckedChangeListener(this);
198     mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay);
199     cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video);
200     cameraOffButton.setOnCheckedChangeListener(this);
201     previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay);
202     swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video);
203     swapCameraButton.setOnClickListener(this);
204     view.findViewById(R.id.videocall_switch_controls)
205         .setVisibility(
206             ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
207     switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold);
208     onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner);
209     remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off);
210     remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
211     endCallButton = view.findViewById(R.id.videocall_end_call);
212     endCallButton.setOnClickListener(this);
213     previewSurfaceView = (SurfaceView) view.findViewById(R.id.videocall_video_preview);
214     previewSurfaceView.setZOrderMediaOverlay(true);
215     previewOffOverlay.setOnClickListener(
216         new OnClickListener() {
217           @Override
218           public void onClick(View v) {
219             checkCameraPermission();
220           }
221         });
222     remoteSurfaceView = (SurfaceView) view.findViewById(R.id.videocall_video_remote);
223     remoteSurfaceView.setOnClickListener(
224         surfaceView -> {
225           videoCallScreenDelegate.resetAutoFullscreenTimer();
226           if (isInFullscreenMode) {
227             updateFullscreenAndGreenScreenMode(
228                 false /* shouldShowFullscreen */, false /* shouldShowGreenScreen */);
229           } else {
230             updateFullscreenAndGreenScreenMode(
231                 true /* shouldShowFullscreen */, false /* shouldShowGreenScreen */);
232           }
233         });
234     greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background);
235     fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background);
236     previewRoot = (FrameLayout) view.findViewById(R.id.videocall_preview_root);
237 
238     // We need the texture view size to be able to scale the remote video. At this point the view
239     // layout won't be complete so add a layout listener.
240     ViewTreeObserver observer = remoteSurfaceView.getViewTreeObserver();
241     observer.addOnGlobalLayoutListener(
242         new ViewTreeObserver.OnGlobalLayoutListener() {
243           @Override
244           public void onGlobalLayout() {
245             LogUtil.i("SurfaceViewVideoCallFragment.onGlobalLayout", null);
246             updateVideoOffViews();
247             // Remove the listener so we don't continually re-layout.
248             ViewTreeObserver observer = remoteSurfaceView.getViewTreeObserver();
249             if (observer.isAlive()) {
250               observer.removeOnGlobalLayoutListener(this);
251             }
252           }
253         });
254 
255     return view;
256   }
257 
258   @Override
onViewCreated(View view, @Nullable Bundle bundle)259   public void onViewCreated(View view, @Nullable Bundle bundle) {
260     super.onViewCreated(view, bundle);
261     LogUtil.i("SurfaceViewVideoCallFragment.onViewCreated", null);
262 
263     inCallScreenDelegate =
264         FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
265             .newInCallScreenDelegate();
266     videoCallScreenDelegate =
267         FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class)
268             .newVideoCallScreenDelegate(this);
269 
270     speakerButtonController =
271         new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate);
272     switchOnHoldCallController =
273         new SwitchOnHoldCallController(
274             switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate);
275 
276     videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this);
277 
278     inCallScreenDelegate.onInCallScreenDelegateInit(this);
279     inCallScreenDelegate.onInCallScreenReady();
280     inCallButtonUiDelegate.onInCallButtonUiReady(this);
281 
282     view.setOnSystemUiVisibilityChangeListener(this);
283   }
284 
285   @Override
onSaveInstanceState(Bundle outState)286   public void onSaveInstanceState(Bundle outState) {
287     super.onSaveInstanceState(outState);
288     inCallButtonUiDelegate.onSaveInstanceState(outState);
289   }
290 
291   @Override
onDestroyView()292   public void onDestroyView() {
293     super.onDestroyView();
294     LogUtil.i("SurfaceViewVideoCallFragment.onDestroyView", null);
295     inCallButtonUiDelegate.onInCallButtonUiUnready();
296     inCallScreenDelegate.onInCallScreenUnready();
297   }
298 
299   @Override
onAttach(Context context)300   public void onAttach(Context context) {
301     super.onAttach(context);
302     if (savedSecondaryInfo != null) {
303       setSecondary(savedSecondaryInfo);
304     }
305   }
306 
307   @Override
onStart()308   public void onStart() {
309     super.onStart();
310     LogUtil.i("SurfaceViewVideoCallFragment.onStart", null);
311     onVideoScreenStart();
312   }
313 
314   @Override
onVideoScreenStart()315   public void onVideoScreenStart() {
316     inCallButtonUiDelegate.refreshMuteState();
317     videoCallScreenDelegate.onVideoCallScreenUiReady();
318     getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
319   }
320 
321   @Override
onResume()322   public void onResume() {
323     super.onResume();
324     LogUtil.i("SurfaceViewVideoCallFragment.onResume", null);
325     inCallScreenDelegate.onInCallScreenResumed();
326   }
327 
328   @Override
onPause()329   public void onPause() {
330     super.onPause();
331     LogUtil.i("SurfaceViewVideoCallFragment.onPause", null);
332     inCallScreenDelegate.onInCallScreenPaused();
333   }
334 
335   @Override
onStop()336   public void onStop() {
337     super.onStop();
338     LogUtil.i("SurfaceViewVideoCallFragment.onStop", null);
339     onVideoScreenStop();
340   }
341 
342   @Override
onVideoScreenStop()343   public void onVideoScreenStop() {
344     getView().removeCallbacks(cameraPermissionDialogRunnable);
345     videoCallScreenDelegate.onVideoCallScreenUiUnready();
346   }
347 
exitFullscreenMode()348   private void exitFullscreenMode() {
349     LogUtil.i("SurfaceViewVideoCallFragment.exitFullscreenMode", null);
350 
351     if (!getView().isAttachedToWindow()) {
352       LogUtil.i("SurfaceViewVideoCallFragment.exitFullscreenMode", "not attached");
353       return;
354     }
355 
356     showSystemUI();
357 
358     LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
359 
360     // Animate the controls to the shown state.
361     controls
362         .animate()
363         .translationX(0)
364         .translationY(0)
365         .setInterpolator(linearOutSlowInInterpolator)
366         .alpha(1)
367         .start();
368 
369     // Animate onHold to the shown state.
370     switchOnHoldButton
371         .animate()
372         .translationX(0)
373         .translationY(0)
374         .setInterpolator(linearOutSlowInInterpolator)
375         .alpha(1)
376         .withStartAction(
377             new Runnable() {
378               @Override
379               public void run() {
380                 switchOnHoldCallController.setOnScreen();
381               }
382             });
383 
384     View contactGridView = contactGridManager.getContainerView();
385     // Animate contact grid to the shown state.
386     contactGridView
387         .animate()
388         .translationX(0)
389         .translationY(0)
390         .setInterpolator(linearOutSlowInInterpolator)
391         .alpha(1)
392         .withStartAction(
393             new Runnable() {
394               @Override
395               public void run() {
396                 contactGridManager.show();
397               }
398             });
399 
400     endCallButton
401         .animate()
402         .translationX(0)
403         .translationY(0)
404         .setInterpolator(linearOutSlowInInterpolator)
405         .alpha(1)
406         .withStartAction(
407             new Runnable() {
408               @Override
409               public void run() {
410                 endCallButton.setVisibility(View.VISIBLE);
411               }
412             })
413         .start();
414 
415     // Animate all the preview controls up to make room for the navigation bar.
416     // In green screen mode we don't need this because the preview takes up the whole screen and has
417     // a fixed position.
418     if (!isInGreenScreenMode) {
419       Point previewOffsetStartShown = getPreviewOffsetStartShown();
420       for (View view : getAllPreviewRelatedViews()) {
421         // Animate up with the preview offset above the navigation bar.
422         view.animate()
423             .translationX(previewOffsetStartShown.x)
424             .translationY(previewOffsetStartShown.y)
425             .setInterpolator(new AccelerateDecelerateInterpolator())
426             .start();
427       }
428     }
429 
430     updateOverlayBackground();
431   }
432 
showSystemUI()433   private void showSystemUI() {
434     View view = getView();
435     if (view != null) {
436       // Code is more expressive with all flags present, even though some may be combined
437       //noinspection PointlessBitwiseExpression
438       view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
439     }
440   }
441 
442   /** Set view flags to hide the system UI. System UI will return on any touch event */
hideSystemUI()443   private void hideSystemUI() {
444     View view = getView();
445     if (view != null) {
446       view.setSystemUiVisibility(
447           View.SYSTEM_UI_FLAG_FULLSCREEN
448               | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
449               | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
450     }
451   }
452 
getControlsOffsetEndHidden(View controls)453   private Point getControlsOffsetEndHidden(View controls) {
454     if (isLandscape()) {
455       return new Point(0, getOffsetBottom(controls));
456     } else {
457       return new Point(getOffsetStart(controls), 0);
458     }
459   }
460 
getSwitchOnHoldOffsetEndHidden(View swapCallButton)461   private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) {
462     if (isLandscape()) {
463       return new Point(0, getOffsetTop(swapCallButton));
464     } else {
465       return new Point(getOffsetEnd(swapCallButton), 0);
466     }
467   }
468 
getContactGridOffsetEndHidden(View view)469   private Point getContactGridOffsetEndHidden(View view) {
470     return new Point(0, getOffsetTop(view));
471   }
472 
getEndCallOffsetEndHidden(View endCallButton)473   private Point getEndCallOffsetEndHidden(View endCallButton) {
474     if (isLandscape()) {
475       return new Point(getOffsetEnd(endCallButton), 0);
476     } else {
477       return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin);
478     }
479   }
480 
getPreviewOffsetStartShown()481   private Point getPreviewOffsetStartShown() {
482     // No insets in multiwindow mode, and rootWindowInsets will get the display's insets.
483     if (ActivityCompat.isInMultiWindowMode(getActivity())) {
484       return new Point();
485     }
486     if (isLandscape()) {
487       int stableInsetEnd =
488           getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
489               ? getView().getRootWindowInsets().getStableInsetLeft()
490               : -getView().getRootWindowInsets().getStableInsetRight();
491       return new Point(stableInsetEnd, 0);
492     } else {
493       return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom());
494     }
495   }
496 
getAllPreviewRelatedViews()497   private View[] getAllPreviewRelatedViews() {
498     return new View[] {previewRoot};
499   }
500 
getOffsetTop(View view)501   private int getOffsetTop(View view) {
502     return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin);
503   }
504 
getOffsetBottom(View view)505   private int getOffsetBottom(View view) {
506     return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin;
507   }
508 
getOffsetStart(View view)509   private int getOffsetStart(View view) {
510     int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart();
511     if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
512       offset = -offset;
513     }
514     return -offset;
515   }
516 
getOffsetEnd(View view)517   private int getOffsetEnd(View view) {
518     int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd();
519     if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
520       offset = -offset;
521     }
522     return offset;
523   }
524 
enterFullscreenMode()525   private void enterFullscreenMode() {
526     LogUtil.i("SurfaceViewVideoCallFragment.enterFullscreenMode", null);
527 
528     hideSystemUI();
529 
530     Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator();
531 
532     // Animate controls to the hidden state.
533     Point offset = getControlsOffsetEndHidden(controls);
534     controls
535         .animate()
536         .translationX(offset.x)
537         .translationY(offset.y)
538         .setInterpolator(fastOutLinearInInterpolator)
539         .alpha(0)
540         .start();
541 
542     // Animate onHold to the hidden state.
543     offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton);
544     switchOnHoldButton
545         .animate()
546         .translationX(offset.x)
547         .translationY(offset.y)
548         .setInterpolator(fastOutLinearInInterpolator)
549         .alpha(0);
550 
551     View contactGridView = contactGridManager.getContainerView();
552     // Animate contact grid to the hidden state.
553     offset = getContactGridOffsetEndHidden(contactGridView);
554     contactGridView
555         .animate()
556         .translationX(offset.x)
557         .translationY(offset.y)
558         .setInterpolator(fastOutLinearInInterpolator)
559         .alpha(0);
560 
561     offset = getEndCallOffsetEndHidden(endCallButton);
562     // Use a fast out interpolator to quickly fade out the button. This is important because the
563     // button can't draw under the navigation bar which means that it'll look weird if it just
564     // abruptly disappears when it reaches the edge of the naivgation bar.
565     endCallButton
566         .animate()
567         .translationX(offset.x)
568         .translationY(offset.y)
569         .setInterpolator(fastOutLinearInInterpolator)
570         .alpha(0)
571         .withEndAction(
572             new Runnable() {
573               @Override
574               public void run() {
575                 endCallButton.setVisibility(View.INVISIBLE);
576               }
577             })
578         .setInterpolator(new FastOutLinearInInterpolator())
579         .start();
580 
581     // Animate all the preview controls down now that the navigation bar is hidden.
582     // In green screen mode we don't need this because the preview takes up the whole screen and has
583     // a fixed position.
584     if (!isInGreenScreenMode) {
585       for (View view : getAllPreviewRelatedViews()) {
586         // Animate down with the navigation bar hidden.
587         view.animate()
588             .translationX(0)
589             .translationY(0)
590             .setInterpolator(new AccelerateDecelerateInterpolator())
591             .start();
592       }
593     }
594     updateOverlayBackground();
595   }
596 
597   @Override
onClick(View v)598   public void onClick(View v) {
599     if (v == endCallButton) {
600       LogUtil.i("SurfaceViewVideoCallFragment.onClick", "end call button clicked");
601       inCallButtonUiDelegate.onEndCallClicked();
602       videoCallScreenDelegate.resetAutoFullscreenTimer();
603     } else if (v == swapCameraButton) {
604       if (swapCameraButton.getDrawable() instanceof Animatable) {
605         ((Animatable) swapCameraButton.getDrawable()).start();
606       }
607       inCallButtonUiDelegate.toggleCameraClicked();
608       videoCallScreenDelegate.resetAutoFullscreenTimer();
609     }
610   }
611 
612   @Override
onCheckedChanged(CheckableImageButton button, boolean isChecked)613   public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
614     if (button == cameraOffButton) {
615       if (!isChecked && !VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
616         LogUtil.i("SurfaceViewVideoCallFragment.onCheckedChanged", "show camera permission dialog");
617         checkCameraPermission();
618       } else {
619         inCallButtonUiDelegate.pauseVideoClicked(isChecked);
620         videoCallScreenDelegate.resetAutoFullscreenTimer();
621       }
622     } else if (button == muteButton) {
623       inCallButtonUiDelegate.muteClicked(isChecked, true /* clickedByUser */);
624       videoCallScreenDelegate.resetAutoFullscreenTimer();
625     }
626   }
627 
628   @Override
showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)629   public void showVideoViews(
630       boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
631     LogUtil.i(
632         "SurfaceViewVideoCallFragment.showVideoViews",
633         "showPreview: %b, shouldShowRemote: %b",
634         shouldShowPreview,
635         shouldShowRemote);
636 
637     this.shouldShowPreview = shouldShowPreview;
638     this.shouldShowRemote = shouldShowRemote;
639     this.isRemotelyHeld = isRemotelyHeld;
640 
641     previewSurfaceView.setVisibility(shouldShowPreview ? View.VISIBLE : View.INVISIBLE);
642 
643     videoCallScreenDelegate.setSurfaceViews(previewSurfaceView, remoteSurfaceView);
644     updateVideoOffViews();
645   }
646 
647   /**
648    * This method scales the video feed inside the texture view, it doesn't change the texture view's
649    * size. In the old UI we would change the view size to match the aspect ratio of the video. In
650    * the new UI the view is always square (with the circular clip) so we have to do additional work
651    * to make sure the non-square video doesn't look squished.
652    */
653   @Override
onLocalVideoDimensionsChanged()654   public void onLocalVideoDimensionsChanged() {
655     LogUtil.i("SurfaceViewVideoCallFragment.onLocalVideoDimensionsChanged", null);
656   }
657 
658   @Override
onLocalVideoOrientationChanged()659   public void onLocalVideoOrientationChanged() {
660     LogUtil.i("SurfaceViewVideoCallFragment.onLocalVideoOrientationChanged", null);
661   }
662 
663   /** Called when the remote video's dimensions change. */
664   @Override
onRemoteVideoDimensionsChanged()665   public void onRemoteVideoDimensionsChanged() {
666     LogUtil.i("SurfaceViewVideoCallFragment.onRemoteVideoDimensionsChanged", null);
667   }
668 
669   @Override
updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)670   public void updateFullscreenAndGreenScreenMode(
671       boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {
672     LogUtil.i(
673         "SurfaceViewVideoCallFragment.updateFullscreenAndGreenScreenMode",
674         "shouldShowFullscreen: %b, shouldShowGreenScreen: %b",
675         shouldShowFullscreen,
676         shouldShowGreenScreen);
677 
678     if (getActivity() == null) {
679       LogUtil.i(
680           "SurfaceViewVideoCallFragment.updateFullscreenAndGreenScreenMode",
681           "not attached to activity");
682       return;
683     }
684 
685     // Check if anything is actually going to change. The first time this function is called we
686     // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen
687     // and green screen modes to update even if only one has changed. That's because they both
688     // depend on each other.
689     if (hasInitializedScreenModes
690         && shouldShowGreenScreen == isInGreenScreenMode
691         && shouldShowFullscreen == isInFullscreenMode) {
692       LogUtil.i(
693           "SurfaceViewVideoCallFragment.updateFullscreenAndGreenScreenMode",
694           "no change to screen modes");
695       return;
696     }
697     hasInitializedScreenModes = true;
698     isInGreenScreenMode = shouldShowGreenScreen;
699     isInFullscreenMode = shouldShowFullscreen;
700 
701     if (getView().isAttachedToWindow() && !ActivityCompat.isInMultiWindowMode(getActivity())) {
702       controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets());
703     }
704     if (shouldShowGreenScreen) {
705       enterGreenScreenMode();
706     } else {
707       exitGreenScreenMode();
708     }
709     if (shouldShowFullscreen) {
710       enterFullscreenMode();
711     } else {
712       exitFullscreenMode();
713     }
714     updateVideoOffViews();
715 
716     OnHoldFragment onHoldFragment =
717         ((OnHoldFragment)
718             getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner));
719     if (onHoldFragment != null) {
720       onHoldFragment.setPadTopInset(!isInFullscreenMode);
721     }
722   }
723 
724   @Override
getVideoCallScreenFragment()725   public Fragment getVideoCallScreenFragment() {
726     return this;
727   }
728 
729   @Override
730   @NonNull
getCallId()731   public String getCallId() {
732     return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
733   }
734 
735   @Override
showButton(@nCallButtonIds int buttonId, boolean show)736   public void showButton(@InCallButtonIds int buttonId, boolean show) {
737     LogUtil.v(
738         "SurfaceViewVideoCallFragment.showButton",
739         "buttonId: %s, show: %b",
740         InCallButtonIdsExtension.toString(buttonId),
741         show);
742     if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
743       speakerButtonController.setEnabled(show);
744     } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
745       muteButton.setEnabled(show);
746     } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
747       cameraOffButton.setEnabled(show);
748     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
749       switchOnHoldCallController.setVisible(show);
750     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
751       swapCameraButton.setEnabled(show);
752     }
753   }
754 
755   @Override
enableButton(@nCallButtonIds int buttonId, boolean enable)756   public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
757     LogUtil.v(
758         "SurfaceViewVideoCallFragment.setEnabled",
759         "buttonId: %s, enable: %b",
760         InCallButtonIdsExtension.toString(buttonId),
761         enable);
762     if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
763       speakerButtonController.setEnabled(enable);
764     } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
765       muteButton.setEnabled(enable);
766     } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
767       cameraOffButton.setEnabled(enable);
768     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
769       switchOnHoldCallController.setEnabled(enable);
770     }
771   }
772 
773   @Override
setEnabled(boolean enabled)774   public void setEnabled(boolean enabled) {
775     LogUtil.v("SurfaceViewVideoCallFragment.setEnabled", "enabled: " + enabled);
776     speakerButtonController.setEnabled(enabled);
777     muteButton.setEnabled(enabled);
778     cameraOffButton.setEnabled(enabled);
779     switchOnHoldCallController.setEnabled(enabled);
780   }
781 
782   @Override
setHold(boolean value)783   public void setHold(boolean value) {
784     LogUtil.i("SurfaceViewVideoCallFragment.setHold", "value: " + value);
785   }
786 
787   @Override
setCameraSwitched(boolean isBackFacingCamera)788   public void setCameraSwitched(boolean isBackFacingCamera) {
789     LogUtil.i(
790         "SurfaceViewVideoCallFragment.setCameraSwitched",
791         "isBackFacingCamera: " + isBackFacingCamera);
792   }
793 
794   @Override
setVideoPaused(boolean isPaused)795   public void setVideoPaused(boolean isPaused) {
796     LogUtil.i("SurfaceViewVideoCallFragment.setVideoPaused", "isPaused: " + isPaused);
797     cameraOffButton.setChecked(isPaused);
798   }
799 
800   @Override
setAudioState(CallAudioState audioState)801   public void setAudioState(CallAudioState audioState) {
802     LogUtil.i("SurfaceViewVideoCallFragment.setAudioState", "audioState: " + audioState);
803     speakerButtonController.setAudioState(audioState);
804     muteButton.setChecked(audioState.isMuted());
805     updateMutePreviewOverlayVisibility();
806   }
807 
808   @Override
updateButtonStates()809   public void updateButtonStates() {
810     LogUtil.i("SurfaceViewVideoCallFragment.updateButtonState", null);
811     speakerButtonController.updateButtonState();
812     switchOnHoldCallController.updateButtonState();
813   }
814 
815   @Override
updateInCallButtonUiColors()816   public void updateInCallButtonUiColors() {}
817 
818   @Override
getInCallButtonUiFragment()819   public Fragment getInCallButtonUiFragment() {
820     return this;
821   }
822 
823   @Override
showAudioRouteSelector()824   public void showAudioRouteSelector() {
825     LogUtil.i("SurfaceViewVideoCallFragment.showAudioRouteSelector", null);
826     AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
827         .show(getChildFragmentManager(), null);
828   }
829 
830   @Override
onAudioRouteSelected(int audioRoute)831   public void onAudioRouteSelected(int audioRoute) {
832     LogUtil.i("SurfaceViewVideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute);
833     inCallButtonUiDelegate.setAudioRoute(audioRoute);
834   }
835 
836   @Override
setPrimary(@onNull PrimaryInfo primaryInfo)837   public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
838     LogUtil.i("SurfaceViewVideoCallFragment.setPrimary", primaryInfo.toString());
839     contactGridManager.setPrimary(primaryInfo);
840   }
841 
842   @Override
setSecondary(@onNull SecondaryInfo secondaryInfo)843   public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
844     LogUtil.i("SurfaceViewVideoCallFragment.setSecondary", secondaryInfo.toString());
845     if (!isAdded()) {
846       savedSecondaryInfo = secondaryInfo;
847       return;
848     }
849     savedSecondaryInfo = null;
850     switchOnHoldCallController.setSecondaryInfo(secondaryInfo);
851     updateButtonStates();
852     FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
853     Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner);
854     if (secondaryInfo.shouldShow) {
855       OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo);
856       onHoldFragment.setPadTopInset(!isInFullscreenMode);
857       transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment);
858     } else {
859       if (oldBanner != null) {
860         transaction.remove(oldBanner);
861       }
862     }
863     transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
864     transaction.commitAllowingStateLoss();
865   }
866 
867   @Override
setCallState(@onNull PrimaryCallState primaryCallState)868   public void setCallState(@NonNull PrimaryCallState primaryCallState) {
869     LogUtil.i("SurfaceViewVideoCallFragment.setCallState", primaryCallState.toString());
870     contactGridManager.setCallState(primaryCallState);
871   }
872 
873   @Override
setEndCallButtonEnabled(boolean enabled, boolean animate)874   public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
875     LogUtil.i("SurfaceViewVideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled);
876   }
877 
878   @Override
showManageConferenceCallButton(boolean visible)879   public void showManageConferenceCallButton(boolean visible) {
880     LogUtil.i("SurfaceViewVideoCallFragment.showManageConferenceCallButton", "visible: " + visible);
881   }
882 
883   @Override
isManageConferenceVisible()884   public boolean isManageConferenceVisible() {
885     LogUtil.i("SurfaceViewVideoCallFragment.isManageConferenceVisible", null);
886     return false;
887   }
888 
889   @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)890   public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
891     contactGridManager.dispatchPopulateAccessibilityEvent(event);
892   }
893 
894   @Override
showNoteSentToast()895   public void showNoteSentToast() {
896     LogUtil.i("SurfaceViewVideoCallFragment.showNoteSentToast", null);
897   }
898 
899   @Override
updateInCallScreenColors()900   public void updateInCallScreenColors() {
901     LogUtil.i("SurfaceViewVideoCallFragment.updateColors", null);
902   }
903 
904   @Override
onInCallScreenDialpadVisibilityChange(boolean isShowing)905   public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
906     LogUtil.i("SurfaceViewVideoCallFragment.onInCallScreenDialpadVisibilityChange", null);
907   }
908 
909   @Override
getAnswerAndDialpadContainerResourceId()910   public int getAnswerAndDialpadContainerResourceId() {
911     return 0;
912   }
913 
914   @Override
getInCallScreenFragment()915   public Fragment getInCallScreenFragment() {
916     return this;
917   }
918 
919   @Override
isShowingLocationUi()920   public boolean isShowingLocationUi() {
921     return false;
922   }
923 
924   @Override
showLocationUi(Fragment locationUi)925   public void showLocationUi(Fragment locationUi) {
926     LogUtil.e(
927         "SurfaceViewVideoCallFragment.showLocationUi", "Emergency video calling not supported");
928     // Do nothing
929   }
930 
isLandscape()931   private boolean isLandscape() {
932     // Choose orientation based on display orientation, not window orientation
933     int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
934     return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
935   }
936 
enterGreenScreenMode()937   private void enterGreenScreenMode() {
938     LogUtil.i("SurfaceViewVideoCallFragment.enterGreenScreenMode", null);
939     updateOverlayBackground();
940     contactGridManager.setIsMiddleRowVisible(true);
941     updateMutePreviewOverlayVisibility();
942   }
943 
exitGreenScreenMode()944   private void exitGreenScreenMode() {
945     LogUtil.i("SurfaceViewVideoCallFragment.exitGreenScreenMode", null);
946     updateOverlayBackground();
947     contactGridManager.setIsMiddleRowVisible(false);
948     updateMutePreviewOverlayVisibility();
949   }
950 
updateVideoOffViews()951   private void updateVideoOffViews() {
952     // Always hide the preview off and remote off views in green screen mode.
953     boolean previewEnabled = isInGreenScreenMode || shouldShowPreview;
954     previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE);
955 
956     boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote;
957     boolean isResumed = remoteEnabled && !isRemotelyHeld;
958     if (isResumed) {
959       boolean wasRemoteVideoOff =
960           TextUtils.equals(
961               remoteVideoOff.getText(),
962               remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off));
963       // The text needs to be updated and hidden after enough delay in order to be announced by
964       // talkback.
965       remoteVideoOff.setText(
966           wasRemoteVideoOff
967               ? R.string.videocall_remote_video_on
968               : R.string.videocall_remotely_resumed);
969       remoteVideoOff.postDelayed(
970           new Runnable() {
971             @Override
972             public void run() {
973               remoteVideoOff.setVisibility(View.GONE);
974             }
975           },
976           VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS);
977     } else {
978       remoteVideoOff.setText(
979           isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off);
980       remoteVideoOff.setVisibility(View.VISIBLE);
981     }
982   }
983 
updateOverlayBackground()984   private void updateOverlayBackground() {
985     if (isInGreenScreenMode) {
986       // We want to darken the preview view to make text and buttons readable. The fullscreen
987       // background is below the preview view so use the green screen background instead.
988       animateSetVisibility(greenScreenBackgroundView, View.VISIBLE);
989       animateSetVisibility(fullscreenBackgroundView, View.GONE);
990     } else if (!isInFullscreenMode) {
991       // We want to darken the remote view to make text and buttons readable. The green screen
992       // background is above the preview view so it would darken the preview too. Use the fullscreen
993       // background instead.
994       animateSetVisibility(greenScreenBackgroundView, View.GONE);
995       animateSetVisibility(fullscreenBackgroundView, View.VISIBLE);
996     } else {
997       animateSetVisibility(greenScreenBackgroundView, View.GONE);
998       animateSetVisibility(fullscreenBackgroundView, View.GONE);
999     }
1000   }
1001 
updateMutePreviewOverlayVisibility()1002   private void updateMutePreviewOverlayVisibility() {
1003     // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen
1004     // mode the preview is fullscreen so there's no where to anchor it.
1005     mutePreviewOverlay.setVisibility(
1006         muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE);
1007   }
1008 
animateSetVisibility(final View view, final int visibility)1009   private static void animateSetVisibility(final View view, final int visibility) {
1010     if (view.getVisibility() == visibility) {
1011       return;
1012     }
1013 
1014     int startAlpha;
1015     int endAlpha;
1016     if (visibility == View.GONE) {
1017       startAlpha = 1;
1018       endAlpha = 0;
1019     } else if (visibility == View.VISIBLE) {
1020       startAlpha = 0;
1021       endAlpha = 1;
1022     } else {
1023       Assert.fail();
1024       return;
1025     }
1026 
1027     view.setAlpha(startAlpha);
1028     view.setVisibility(View.VISIBLE);
1029     view.animate()
1030         .alpha(endAlpha)
1031         .withEndAction(
1032             new Runnable() {
1033               @Override
1034               public void run() {
1035                 view.setVisibility(visibility);
1036               }
1037             })
1038         .start();
1039   }
1040 
1041   @Override
onSystemUiVisibilityChange(int visibility)1042   public void onSystemUiVisibilityChange(int visibility) {
1043     boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
1044     videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible);
1045     if (navBarVisible) {
1046       updateFullscreenAndGreenScreenMode(
1047           false /* shouldShowFullscreen */, false /* shouldShowGreenScreen */);
1048     } else {
1049       updateFullscreenAndGreenScreenMode(
1050           true /* shouldShowFullscreen */, false /* shouldShowGreenScreen */);
1051     }
1052   }
1053 
1054   @Override
onCameraPermissionGranted()1055   public void onCameraPermissionGranted() {
1056     videoCallScreenDelegate.onCameraPermissionGranted();
1057   }
1058 
checkCameraPermission()1059   private void checkCameraPermission() {
1060     // Checks if user has consent of camera permission and the permission is granted.
1061     // If camera permission is revoked, shows system permission dialog.
1062     // If camera permission is granted but user doesn't have consent of camera permission
1063     // (which means it's first time making video call), shows custom dialog instead. This
1064     // will only be shown to user once.
1065     if (!VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
1066       videoCallScreenDelegate.onCameraPermissionDialogShown();
1067       if (!VideoUtils.hasCameraPermission(getContext())) {
1068         requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
1069       } else {
1070         CameraPermissionDialogFragment.newInstance()
1071             .show(getChildFragmentManager(), CAMERA_PERMISSION_DIALOG_FRAMENT_TAG);
1072       }
1073     }
1074   }
1075 }
1076