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