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