• 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.content.res.Resources;
23 import android.graphics.Bitmap;
24 import android.graphics.Outline;
25 import android.graphics.Point;
26 import android.graphics.drawable.Animatable;
27 import android.os.Bundle;
28 import android.os.SystemClock;
29 import android.renderscript.Allocation;
30 import android.renderscript.Element;
31 import android.renderscript.RenderScript;
32 import android.renderscript.ScriptIntrinsicBlur;
33 import android.support.annotation.ColorInt;
34 import android.support.annotation.NonNull;
35 import android.support.annotation.Nullable;
36 import android.support.annotation.VisibleForTesting;
37 import android.support.v4.app.Fragment;
38 import android.support.v4.app.FragmentTransaction;
39 import android.support.v4.view.animation.FastOutLinearInInterpolator;
40 import android.support.v4.view.animation.LinearOutSlowInInterpolator;
41 import android.telecom.CallAudioState;
42 import android.text.TextUtils;
43 import android.view.LayoutInflater;
44 import android.view.Surface;
45 import android.view.TextureView;
46 import android.view.View;
47 import android.view.View.OnClickListener;
48 import android.view.View.OnLayoutChangeListener;
49 import android.view.View.OnSystemUiVisibilityChangeListener;
50 import android.view.ViewGroup;
51 import android.view.ViewGroup.MarginLayoutParams;
52 import android.view.ViewOutlineProvider;
53 import android.view.accessibility.AccessibilityEvent;
54 import android.view.animation.AccelerateDecelerateInterpolator;
55 import android.view.animation.Interpolator;
56 import android.widget.ImageButton;
57 import android.widget.ImageView;
58 import android.widget.RelativeLayout;
59 import android.widget.TextView;
60 import com.android.dialer.common.Assert;
61 import com.android.dialer.common.FragmentUtils;
62 import com.android.dialer.common.LogUtil;
63 import com.android.dialer.util.PermissionsUtil;
64 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
65 import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
66 import com.android.incallui.contactgrid.ContactGridManager;
67 import com.android.incallui.hold.OnHoldFragment;
68 import com.android.incallui.incall.protocol.InCallButtonIds;
69 import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
70 import com.android.incallui.incall.protocol.InCallButtonUi;
71 import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
72 import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
73 import com.android.incallui.incall.protocol.InCallScreen;
74 import com.android.incallui.incall.protocol.InCallScreenDelegate;
75 import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
76 import com.android.incallui.incall.protocol.PrimaryCallState;
77 import com.android.incallui.incall.protocol.PrimaryInfo;
78 import com.android.incallui.incall.protocol.SecondaryInfo;
79 import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
80 import com.android.incallui.video.protocol.VideoCallScreen;
81 import com.android.incallui.video.protocol.VideoCallScreenDelegate;
82 import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
83 import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
84 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
85 import com.android.incallui.videotech.utils.VideoUtils;
86 
87 /** Contains UI elements for a video call. */
88 
89 public class VideoCallFragment extends Fragment
90     implements InCallScreen,
91         InCallButtonUi,
92         VideoCallScreen,
93         OnClickListener,
94         OnCheckedChangeListener,
95         AudioRouteSelectorPresenter,
96         OnSystemUiVisibilityChangeListener {
97 
98   @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
99   static final String ARG_CALL_ID = "call_id";
100 
101   private static final String TAG_VIDEO_CHARGES_ALERT = "tag_video_charges_alert";
102 
103   @VisibleForTesting static final float BLUR_PREVIEW_RADIUS = 16.0f;
104   @VisibleForTesting static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
105   private static final float BLUR_REMOTE_RADIUS = 25.0f;
106   private static final float BLUR_REMOTE_SCALE_FACTOR = 0.25f;
107   private static final float ASPECT_RATIO_MATCH_THRESHOLD = 0.2f;
108 
109   private static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
110   private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L;
111   private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L;
112   private static final long VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS = 500L;
113 
114   private final ViewOutlineProvider circleOutlineProvider =
115       new ViewOutlineProvider() {
116         @Override
117         public void getOutline(View view, Outline outline) {
118           int x = view.getWidth() / 2;
119           int y = view.getHeight() / 2;
120           int radius = Math.min(x, y);
121           outline.setOval(x - radius, y - radius, x + radius, y + radius);
122         }
123       };
124 
125   private InCallScreenDelegate inCallScreenDelegate;
126   private VideoCallScreenDelegate videoCallScreenDelegate;
127   private InCallButtonUiDelegate inCallButtonUiDelegate;
128   private View endCallButton;
129   private CheckableImageButton speakerButton;
130   private SpeakerButtonController speakerButtonController;
131   private CheckableImageButton muteButton;
132   private CheckableImageButton cameraOffButton;
133   private ImageButton swapCameraButton;
134   private View switchOnHoldButton;
135   private View onHoldContainer;
136   private SwitchOnHoldCallController switchOnHoldCallController;
137   private TextView remoteVideoOff;
138   private ImageView remoteOffBlurredImageView;
139   private View mutePreviewOverlay;
140   private View previewOffOverlay;
141   private ImageView previewOffBlurredImageView;
142   private View controls;
143   private View controlsContainer;
144   private TextureView previewTextureView;
145   private TextureView remoteTextureView;
146   private View greenScreenBackgroundView;
147   private View fullscreenBackgroundView;
148   private boolean shouldShowRemote;
149   private boolean shouldShowPreview;
150   private boolean isInFullscreenMode;
151   private boolean isInGreenScreenMode;
152   private boolean hasInitializedScreenModes;
153   private boolean isRemotelyHeld;
154   private ContactGridManager contactGridManager;
155   private SecondaryInfo savedSecondaryInfo;
156   private final Runnable cameraPermissionDialogRunnable =
157       new Runnable() {
158         @Override
159         public void run() {
160           if (videoCallScreenDelegate.shouldShowCameraPermissionToast()) {
161             LogUtil.i("VideoCallFragment.cameraPermissionDialogRunnable", "showing dialog");
162             checkCameraPermission();
163           }
164         }
165       };
166 
167   private final Runnable videoChargesAlertDialogRunnable =
168       () -> {
169         VideoChargesAlertDialogFragment existingVideoChargesAlertFragment =
170             (VideoChargesAlertDialogFragment)
171                 getChildFragmentManager().findFragmentByTag(TAG_VIDEO_CHARGES_ALERT);
172         if (existingVideoChargesAlertFragment != null) {
173           LogUtil.i(
174               "VideoCallFragment.videoChargesAlertDialogRunnable", "already shown for this call");
175           return;
176         }
177 
178         if (VideoChargesAlertDialogFragment.shouldShow(getContext(), getCallId())) {
179           LogUtil.i("VideoCallFragment.videoChargesAlertDialogRunnable", "showing dialog");
180           VideoChargesAlertDialogFragment.newInstance(getCallId())
181               .show(getChildFragmentManager(), TAG_VIDEO_CHARGES_ALERT);
182         }
183       };
184 
newInstance(String callId)185   public static VideoCallFragment newInstance(String callId) {
186     Bundle bundle = new Bundle();
187     bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
188 
189     VideoCallFragment instance = new VideoCallFragment();
190     instance.setArguments(bundle);
191     return instance;
192   }
193 
194   @Override
onCreate(@ullable Bundle savedInstanceState)195   public void onCreate(@Nullable Bundle savedInstanceState) {
196     super.onCreate(savedInstanceState);
197     LogUtil.i("VideoCallFragment.onCreate", null);
198 
199     inCallButtonUiDelegate =
200         FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
201             .newInCallButtonUiDelegate();
202     if (savedInstanceState != null) {
203       inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
204     }
205   }
206 
207   @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)208   public void onRequestPermissionsResult(
209       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
210     if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
211       if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
212         LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission granted.");
213         videoCallScreenDelegate.onCameraPermissionGranted();
214       } else {
215         LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission denied.");
216       }
217     }
218     super.onRequestPermissionsResult(requestCode, permissions, grantResults);
219   }
220 
221   @Nullable
222   @Override
onCreateView( LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle)223   public View onCreateView(
224       LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
225     LogUtil.i("VideoCallFragment.onCreateView", null);
226 
227     View view =
228         layoutInflater.inflate(
229             isLandscape() ? R.layout.frag_videocall_land : R.layout.frag_videocall,
230             viewGroup,
231             false);
232     contactGridManager =
233         new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */);
234 
235     controls = view.findViewById(R.id.videocall_video_controls);
236     controls.setVisibility(getActivity().isInMultiWindowMode() ? View.GONE : View.VISIBLE);
237     controlsContainer = view.findViewById(R.id.videocall_video_controls_container);
238     speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button);
239     muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button);
240     muteButton.setOnCheckedChangeListener(this);
241     mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay);
242     cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video);
243     cameraOffButton.setOnCheckedChangeListener(this);
244     previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay);
245     previewOffBlurredImageView =
246         (ImageView) view.findViewById(R.id.videocall_preview_off_blurred_image_view);
247     swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video);
248     swapCameraButton.setOnClickListener(this);
249     view.findViewById(R.id.videocall_switch_controls)
250         .setVisibility(getActivity().isInMultiWindowMode() ? View.GONE : View.VISIBLE);
251     switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold);
252     onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner);
253     remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off);
254     remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
255     remoteOffBlurredImageView =
256         (ImageView) view.findViewById(R.id.videocall_remote_off_blurred_image_view);
257     endCallButton = view.findViewById(R.id.videocall_end_call);
258     endCallButton.setOnClickListener(this);
259     previewTextureView = (TextureView) view.findViewById(R.id.videocall_video_preview);
260     previewTextureView.setClipToOutline(true);
261     previewOffOverlay.setOnClickListener(
262         new OnClickListener() {
263           @Override
264           public void onClick(View v) {
265             checkCameraPermission();
266           }
267         });
268     remoteTextureView = (TextureView) view.findViewById(R.id.videocall_video_remote);
269     greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background);
270     fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background);
271 
272     remoteTextureView.addOnLayoutChangeListener(
273         new OnLayoutChangeListener() {
274           @Override
275           public void onLayoutChange(
276               View v,
277               int left,
278               int top,
279               int right,
280               int bottom,
281               int oldLeft,
282               int oldTop,
283               int oldRight,
284               int oldBottom) {
285             LogUtil.i("VideoCallFragment.onLayoutChange", "remoteTextureView layout changed");
286             updateRemoteVideoScaling();
287             updateRemoteOffView();
288           }
289         });
290 
291     previewTextureView.addOnLayoutChangeListener(
292         new OnLayoutChangeListener() {
293           @Override
294           public void onLayoutChange(
295               View v,
296               int left,
297               int top,
298               int right,
299               int bottom,
300               int oldLeft,
301               int oldTop,
302               int oldRight,
303               int oldBottom) {
304             LogUtil.i("VideoCallFragment.onLayoutChange", "previewTextureView layout changed");
305             updatePreviewVideoScaling();
306             updatePreviewOffView();
307           }
308         });
309     return view;
310   }
311 
312   @Override
onViewCreated(View view, @Nullable Bundle bundle)313   public void onViewCreated(View view, @Nullable Bundle bundle) {
314     super.onViewCreated(view, bundle);
315     LogUtil.i("VideoCallFragment.onViewCreated", null);
316 
317     inCallScreenDelegate =
318         FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
319             .newInCallScreenDelegate();
320     videoCallScreenDelegate =
321         FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class)
322             .newVideoCallScreenDelegate(this);
323 
324     speakerButtonController =
325         new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate);
326     switchOnHoldCallController =
327         new SwitchOnHoldCallController(
328             switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate);
329 
330     videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this);
331 
332     inCallScreenDelegate.onInCallScreenDelegateInit(this);
333     inCallScreenDelegate.onInCallScreenReady();
334     inCallButtonUiDelegate.onInCallButtonUiReady(this);
335 
336     view.setOnSystemUiVisibilityChangeListener(this);
337   }
338 
339   @Override
onSaveInstanceState(Bundle outState)340   public void onSaveInstanceState(Bundle outState) {
341     super.onSaveInstanceState(outState);
342     inCallButtonUiDelegate.onSaveInstanceState(outState);
343   }
344 
345   @Override
onDestroyView()346   public void onDestroyView() {
347     super.onDestroyView();
348     LogUtil.i("VideoCallFragment.onDestroyView", null);
349     inCallButtonUiDelegate.onInCallButtonUiUnready();
350     inCallScreenDelegate.onInCallScreenUnready();
351   }
352 
353   @Override
onAttach(Context context)354   public void onAttach(Context context) {
355     super.onAttach(context);
356     if (savedSecondaryInfo != null) {
357       setSecondary(savedSecondaryInfo);
358     }
359   }
360 
361   @Override
onStart()362   public void onStart() {
363     super.onStart();
364     LogUtil.i("VideoCallFragment.onStart", null);
365     onVideoScreenStart();
366   }
367 
368   @Override
onVideoScreenStart()369   public void onVideoScreenStart() {
370     inCallButtonUiDelegate.refreshMuteState();
371     videoCallScreenDelegate.onVideoCallScreenUiReady();
372     getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
373     getView()
374         .postDelayed(videoChargesAlertDialogRunnable, VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS);
375   }
376 
377   @Override
onResume()378   public void onResume() {
379     super.onResume();
380     LogUtil.i("VideoCallFragment.onResume", null);
381     inCallScreenDelegate.onInCallScreenResumed();
382   }
383 
384   @Override
onPause()385   public void onPause() {
386     super.onPause();
387     LogUtil.i("VideoCallFragment.onPause", null);
388     inCallScreenDelegate.onInCallScreenPaused();
389   }
390 
391   @Override
onStop()392   public void onStop() {
393     super.onStop();
394     LogUtil.i("VideoCallFragment.onStop", null);
395     onVideoScreenStop();
396   }
397 
398   @Override
onVideoScreenStop()399   public void onVideoScreenStop() {
400     getView().removeCallbacks(videoChargesAlertDialogRunnable);
401     getView().removeCallbacks(cameraPermissionDialogRunnable);
402     videoCallScreenDelegate.onVideoCallScreenUiUnready();
403   }
404 
exitFullscreenMode()405   private void exitFullscreenMode() {
406     LogUtil.i("VideoCallFragment.exitFullscreenMode", null);
407 
408     if (!getView().isAttachedToWindow()) {
409       LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached");
410       return;
411     }
412 
413     showSystemUI();
414 
415     LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
416 
417     // Animate the controls to the shown state.
418     controls
419         .animate()
420         .translationX(0)
421         .translationY(0)
422         .setInterpolator(linearOutSlowInInterpolator)
423         .alpha(1)
424         .start();
425 
426     // Animate onHold to the shown state.
427     switchOnHoldButton
428         .animate()
429         .translationX(0)
430         .translationY(0)
431         .setInterpolator(linearOutSlowInInterpolator)
432         .alpha(1)
433         .withStartAction(
434             new Runnable() {
435               @Override
436               public void run() {
437                 switchOnHoldCallController.setOnScreen();
438               }
439             });
440 
441     View contactGridView = contactGridManager.getContainerView();
442     // Animate contact grid to the shown state.
443     contactGridView
444         .animate()
445         .translationX(0)
446         .translationY(0)
447         .setInterpolator(linearOutSlowInInterpolator)
448         .alpha(1)
449         .withStartAction(
450             new Runnable() {
451               @Override
452               public void run() {
453                 contactGridManager.show();
454               }
455             });
456 
457     endCallButton
458         .animate()
459         .translationX(0)
460         .translationY(0)
461         .setInterpolator(linearOutSlowInInterpolator)
462         .alpha(1)
463         .withStartAction(
464             new Runnable() {
465               @Override
466               public void run() {
467                 endCallButton.setVisibility(View.VISIBLE);
468               }
469             })
470         .start();
471 
472     // Animate all the preview controls up to make room for the navigation bar.
473     // In green screen mode we don't need this because the preview takes up the whole screen and has
474     // a fixed position.
475     if (!isInGreenScreenMode) {
476       Point previewOffsetStartShown = getPreviewOffsetStartShown();
477       for (View view : getAllPreviewRelatedViews()) {
478         // Animate up with the preview offset above the navigation bar.
479         view.animate()
480             .translationX(previewOffsetStartShown.x)
481             .translationY(previewOffsetStartShown.y)
482             .setInterpolator(new AccelerateDecelerateInterpolator())
483             .start();
484       }
485     }
486 
487     updateOverlayBackground();
488   }
489 
showSystemUI()490   private void showSystemUI() {
491     View view = getView();
492     if (view != null) {
493       // Code is more expressive with all flags present, even though some may be combined
494       // noinspection PointlessBitwiseExpression
495       view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
496     }
497   }
498 
499   /** Set view flags to hide the system UI. System UI will return on any touch event */
hideSystemUI()500   private void hideSystemUI() {
501     View view = getView();
502     if (view != null) {
503       view.setSystemUiVisibility(
504           View.SYSTEM_UI_FLAG_FULLSCREEN
505               | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
506               | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
507     }
508   }
509 
getControlsOffsetEndHidden(View controls)510   private Point getControlsOffsetEndHidden(View controls) {
511     if (isLandscape()) {
512       return new Point(0, getOffsetBottom(controls));
513     } else {
514       return new Point(getOffsetStart(controls), 0);
515     }
516   }
517 
getSwitchOnHoldOffsetEndHidden(View swapCallButton)518   private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) {
519     if (isLandscape()) {
520       return new Point(0, getOffsetTop(swapCallButton));
521     } else {
522       return new Point(getOffsetEnd(swapCallButton), 0);
523     }
524   }
525 
getContactGridOffsetEndHidden(View view)526   private Point getContactGridOffsetEndHidden(View view) {
527     return new Point(0, getOffsetTop(view));
528   }
529 
getEndCallOffsetEndHidden(View endCallButton)530   private Point getEndCallOffsetEndHidden(View endCallButton) {
531     if (isLandscape()) {
532       return new Point(getOffsetEnd(endCallButton), 0);
533     } else {
534       return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin);
535     }
536   }
537 
getPreviewOffsetStartShown()538   private Point getPreviewOffsetStartShown() {
539     // No insets in multiwindow mode, and rootWindowInsets will get the display's insets.
540     if (getActivity().isInMultiWindowMode()) {
541       return new Point();
542     }
543     if (isLandscape()) {
544       int stableInsetEnd =
545           getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
546               ? getView().getRootWindowInsets().getStableInsetLeft()
547               : -getView().getRootWindowInsets().getStableInsetRight();
548       return new Point(stableInsetEnd, 0);
549     } else {
550       return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom());
551     }
552   }
553 
getAllPreviewRelatedViews()554   private View[] getAllPreviewRelatedViews() {
555     return new View[] {
556       previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay,
557     };
558   }
559 
getOffsetTop(View view)560   private int getOffsetTop(View view) {
561     return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin);
562   }
563 
getOffsetBottom(View view)564   private int getOffsetBottom(View view) {
565     return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin;
566   }
567 
getOffsetStart(View view)568   private int getOffsetStart(View view) {
569     int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart();
570     if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
571       offset = -offset;
572     }
573     return -offset;
574   }
575 
getOffsetEnd(View view)576   private int getOffsetEnd(View view) {
577     int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd();
578     if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
579       offset = -offset;
580     }
581     return offset;
582   }
583 
enterFullscreenMode()584   private void enterFullscreenMode() {
585     LogUtil.i("VideoCallFragment.enterFullscreenMode", null);
586 
587     hideSystemUI();
588 
589     Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator();
590 
591     // Animate controls to the hidden state.
592     Point offset = getControlsOffsetEndHidden(controls);
593     controls
594         .animate()
595         .translationX(offset.x)
596         .translationY(offset.y)
597         .setInterpolator(fastOutLinearInInterpolator)
598         .alpha(0)
599         .start();
600 
601     // Animate onHold to the hidden state.
602     offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton);
603     switchOnHoldButton
604         .animate()
605         .translationX(offset.x)
606         .translationY(offset.y)
607         .setInterpolator(fastOutLinearInInterpolator)
608         .alpha(0);
609 
610     View contactGridView = contactGridManager.getContainerView();
611     // Animate contact grid to the hidden state.
612     offset = getContactGridOffsetEndHidden(contactGridView);
613     contactGridView
614         .animate()
615         .translationX(offset.x)
616         .translationY(offset.y)
617         .setInterpolator(fastOutLinearInInterpolator)
618         .alpha(0);
619 
620     offset = getEndCallOffsetEndHidden(endCallButton);
621     // Use a fast out interpolator to quickly fade out the button. This is important because the
622     // button can't draw under the navigation bar which means that it'll look weird if it just
623     // abruptly disappears when it reaches the edge of the naivgation bar.
624     endCallButton
625         .animate()
626         .translationX(offset.x)
627         .translationY(offset.y)
628         .setInterpolator(fastOutLinearInInterpolator)
629         .alpha(0)
630         .withEndAction(
631             new Runnable() {
632               @Override
633               public void run() {
634                 endCallButton.setVisibility(View.INVISIBLE);
635               }
636             })
637         .setInterpolator(new FastOutLinearInInterpolator())
638         .start();
639 
640     // Animate all the preview controls down now that the navigation bar is hidden.
641     // In green screen mode we don't need this because the preview takes up the whole screen and has
642     // a fixed position.
643     if (!isInGreenScreenMode) {
644       for (View view : getAllPreviewRelatedViews()) {
645         // Animate down with the navigation bar hidden.
646         view.animate()
647             .translationX(0)
648             .translationY(0)
649             .setInterpolator(new AccelerateDecelerateInterpolator())
650             .start();
651       }
652     }
653     updateOverlayBackground();
654   }
655 
656   @Override
onClick(View v)657   public void onClick(View v) {
658     if (v == endCallButton) {
659       LogUtil.i("VideoCallFragment.onClick", "end call button clicked");
660       inCallButtonUiDelegate.onEndCallClicked();
661       videoCallScreenDelegate.resetAutoFullscreenTimer();
662     } else if (v == swapCameraButton) {
663       if (swapCameraButton.getDrawable() instanceof Animatable) {
664         ((Animatable) swapCameraButton.getDrawable()).start();
665       }
666       inCallButtonUiDelegate.toggleCameraClicked();
667       videoCallScreenDelegate.resetAutoFullscreenTimer();
668     }
669   }
670 
671   @Override
onCheckedChanged(CheckableImageButton button, boolean isChecked)672   public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
673     if (button == cameraOffButton) {
674       if (!isChecked && !VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) {
675         LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog");
676         checkCameraPermission();
677       } else {
678         inCallButtonUiDelegate.pauseVideoClicked(isChecked);
679         videoCallScreenDelegate.resetAutoFullscreenTimer();
680       }
681     } else if (button == muteButton) {
682       inCallButtonUiDelegate.muteClicked(isChecked, true /* clickedByUser */);
683       videoCallScreenDelegate.resetAutoFullscreenTimer();
684     }
685   }
686 
687   @Override
showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)688   public void showVideoViews(
689       boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
690     LogUtil.i(
691         "VideoCallFragment.showVideoViews",
692         "showPreview: %b, shouldShowRemote: %b",
693         shouldShowPreview,
694         shouldShowRemote);
695 
696     videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView);
697     videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView);
698 
699     this.isRemotelyHeld = isRemotelyHeld;
700     if (this.shouldShowRemote != shouldShowRemote) {
701       this.shouldShowRemote = shouldShowRemote;
702       updateRemoteOffView();
703     }
704     if (this.shouldShowPreview != shouldShowPreview) {
705       this.shouldShowPreview = shouldShowPreview;
706       updatePreviewOffView();
707     }
708   }
709 
710   @Override
onLocalVideoDimensionsChanged()711   public void onLocalVideoDimensionsChanged() {
712     LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null);
713     updatePreviewVideoScaling();
714   }
715 
716   @Override
onLocalVideoOrientationChanged()717   public void onLocalVideoOrientationChanged() {
718     LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null);
719     updatePreviewVideoScaling();
720   }
721 
722   /** Called when the remote video's dimensions change. */
723   @Override
onRemoteVideoDimensionsChanged()724   public void onRemoteVideoDimensionsChanged() {
725     LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null);
726     updateRemoteVideoScaling();
727   }
728 
729   @Override
updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)730   public void updateFullscreenAndGreenScreenMode(
731       boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {
732     LogUtil.i(
733         "VideoCallFragment.updateFullscreenAndGreenScreenMode",
734         "shouldShowFullscreen: %b, shouldShowGreenScreen: %b",
735         shouldShowFullscreen,
736         shouldShowGreenScreen);
737 
738     if (getActivity() == null) {
739       LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity");
740       return;
741     }
742 
743     // Check if anything is actually going to change. The first time this function is called we
744     // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen
745     // and green screen modes to update even if only one has changed. That's because they both
746     // depend on each other.
747     if (hasInitializedScreenModes
748         && shouldShowGreenScreen == isInGreenScreenMode
749         && shouldShowFullscreen == isInFullscreenMode) {
750       LogUtil.i(
751           "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes");
752       return;
753     }
754     hasInitializedScreenModes = true;
755     isInGreenScreenMode = shouldShowGreenScreen;
756     isInFullscreenMode = shouldShowFullscreen;
757 
758     if (getView().isAttachedToWindow() && !getActivity().isInMultiWindowMode()) {
759       controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets());
760     }
761     if (shouldShowGreenScreen) {
762       enterGreenScreenMode();
763     } else {
764       exitGreenScreenMode();
765     }
766     if (shouldShowFullscreen) {
767       enterFullscreenMode();
768     } else {
769       exitFullscreenMode();
770     }
771 
772     OnHoldFragment onHoldFragment =
773         ((OnHoldFragment)
774             getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner));
775     if (onHoldFragment != null) {
776       onHoldFragment.setPadTopInset(!isInFullscreenMode);
777     }
778   }
779 
780   @Override
getVideoCallScreenFragment()781   public Fragment getVideoCallScreenFragment() {
782     return this;
783   }
784 
785   @Override
786   @NonNull
getCallId()787   public String getCallId() {
788     return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
789   }
790 
791   @Override
onHandoverFromWiFiToLte()792   public void onHandoverFromWiFiToLte() {
793     getView().post(videoChargesAlertDialogRunnable);
794   }
795 
796   @Override
showButton(@nCallButtonIds int buttonId, boolean show)797   public void showButton(@InCallButtonIds int buttonId, boolean show) {
798     LogUtil.v(
799         "VideoCallFragment.showButton",
800         "buttonId: %s, show: %b",
801         InCallButtonIdsExtension.toString(buttonId),
802         show);
803     if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
804       speakerButtonController.setEnabled(show);
805     } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
806       muteButton.setEnabled(show);
807     } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
808       cameraOffButton.setEnabled(show);
809     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
810       switchOnHoldCallController.setVisible(show);
811     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
812       swapCameraButton.setEnabled(show);
813     }
814   }
815 
816   @Override
enableButton(@nCallButtonIds int buttonId, boolean enable)817   public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
818     LogUtil.v(
819         "VideoCallFragment.setEnabled",
820         "buttonId: %s, enable: %b",
821         InCallButtonIdsExtension.toString(buttonId),
822         enable);
823     if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
824       speakerButtonController.setEnabled(enable);
825     } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
826       muteButton.setEnabled(enable);
827     } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
828       cameraOffButton.setEnabled(enable);
829     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
830       switchOnHoldCallController.setEnabled(enable);
831     }
832   }
833 
834   @Override
setEnabled(boolean enabled)835   public void setEnabled(boolean enabled) {
836     LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled);
837     speakerButtonController.setEnabled(enabled);
838     muteButton.setEnabled(enabled);
839     cameraOffButton.setEnabled(enabled);
840     switchOnHoldCallController.setEnabled(enabled);
841   }
842 
843   @Override
setHold(boolean value)844   public void setHold(boolean value) {
845     LogUtil.i("VideoCallFragment.setHold", "value: " + value);
846   }
847 
848   @Override
setCameraSwitched(boolean isBackFacingCamera)849   public void setCameraSwitched(boolean isBackFacingCamera) {
850     LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera);
851   }
852 
853   @Override
setVideoPaused(boolean isPaused)854   public void setVideoPaused(boolean isPaused) {
855     LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused);
856     cameraOffButton.setChecked(isPaused);
857   }
858 
859   @Override
setAudioState(CallAudioState audioState)860   public void setAudioState(CallAudioState audioState) {
861     LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState);
862     speakerButtonController.setAudioState(audioState);
863     muteButton.setChecked(audioState.isMuted());
864     updateMutePreviewOverlayVisibility();
865   }
866 
867   @Override
updateButtonStates()868   public void updateButtonStates() {
869     LogUtil.i("VideoCallFragment.updateButtonState", null);
870     speakerButtonController.updateButtonState();
871     switchOnHoldCallController.updateButtonState();
872   }
873 
874   @Override
updateInCallButtonUiColors(@olorInt int color)875   public void updateInCallButtonUiColors(@ColorInt int color) {}
876 
877   @Override
getInCallButtonUiFragment()878   public Fragment getInCallButtonUiFragment() {
879     return this;
880   }
881 
882   @Override
showAudioRouteSelector()883   public void showAudioRouteSelector() {
884     LogUtil.i("VideoCallFragment.showAudioRouteSelector", null);
885     AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
886         .show(getChildFragmentManager(), null);
887   }
888 
889   @Override
onAudioRouteSelected(int audioRoute)890   public void onAudioRouteSelected(int audioRoute) {
891     LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute);
892     inCallButtonUiDelegate.setAudioRoute(audioRoute);
893   }
894 
895   @Override
onAudioRouteSelectorDismiss()896   public void onAudioRouteSelectorDismiss() {}
897 
898   @Override
setPrimary(@onNull PrimaryInfo primaryInfo)899   public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
900     LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString());
901     contactGridManager.setPrimary(primaryInfo);
902   }
903 
904   @Override
setSecondary(@onNull SecondaryInfo secondaryInfo)905   public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
906     LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString());
907     if (!isAdded()) {
908       savedSecondaryInfo = secondaryInfo;
909       return;
910     }
911     savedSecondaryInfo = null;
912     switchOnHoldCallController.setSecondaryInfo(secondaryInfo);
913     updateButtonStates();
914     FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
915     Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner);
916     if (secondaryInfo.shouldShow()) {
917       OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo);
918       onHoldFragment.setPadTopInset(!isInFullscreenMode);
919       transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment);
920     } else {
921       if (oldBanner != null) {
922         transaction.remove(oldBanner);
923       }
924     }
925     transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
926     transaction.commitAllowingStateLoss();
927   }
928 
929   @Override
setCallState(@onNull PrimaryCallState primaryCallState)930   public void setCallState(@NonNull PrimaryCallState primaryCallState) {
931     LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString());
932     contactGridManager.setCallState(primaryCallState);
933   }
934 
935   @Override
setEndCallButtonEnabled(boolean enabled, boolean animate)936   public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
937     LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled);
938   }
939 
940   @Override
showManageConferenceCallButton(boolean visible)941   public void showManageConferenceCallButton(boolean visible) {
942     LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible);
943   }
944 
945   @Override
isManageConferenceVisible()946   public boolean isManageConferenceVisible() {
947     LogUtil.i("VideoCallFragment.isManageConferenceVisible", null);
948     return false;
949   }
950 
951   @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)952   public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
953     contactGridManager.dispatchPopulateAccessibilityEvent(event);
954   }
955 
956   @Override
showNoteSentToast()957   public void showNoteSentToast() {
958     LogUtil.i("VideoCallFragment.showNoteSentToast", null);
959   }
960 
961   @Override
updateInCallScreenColors()962   public void updateInCallScreenColors() {
963     LogUtil.i("VideoCallFragment.updateColors", null);
964   }
965 
966   @Override
onInCallScreenDialpadVisibilityChange(boolean isShowing)967   public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
968     LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null);
969   }
970 
971   @Override
getAnswerAndDialpadContainerResourceId()972   public int getAnswerAndDialpadContainerResourceId() {
973     return 0;
974   }
975 
976   @Override
getInCallScreenFragment()977   public Fragment getInCallScreenFragment() {
978     return this;
979   }
980 
981   @Override
isShowingLocationUi()982   public boolean isShowingLocationUi() {
983     return false;
984   }
985 
986   @Override
showLocationUi(Fragment locationUi)987   public void showLocationUi(Fragment locationUi) {
988     LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported");
989     // Do nothing
990   }
991 
updatePreviewVideoScaling()992   private void updatePreviewVideoScaling() {
993     if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) {
994       LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet");
995       return;
996     }
997     VideoSurfaceTexture localVideoSurfaceTexture =
998         videoCallScreenDelegate.getLocalVideoSurfaceTexture();
999     Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions();
1000     if (cameraDimensions == null) {
1001       LogUtil.i(
1002           "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set");
1003       return;
1004     }
1005     if (isLandscape()) {
1006       VideoSurfaceBindings.scaleVideoAndFillView(
1007           previewTextureView,
1008           cameraDimensions.x,
1009           cameraDimensions.y,
1010           videoCallScreenDelegate.getDeviceOrientation());
1011     } else {
1012       VideoSurfaceBindings.scaleVideoAndFillView(
1013           previewTextureView,
1014           cameraDimensions.y,
1015           cameraDimensions.x,
1016           videoCallScreenDelegate.getDeviceOrientation());
1017     }
1018   }
1019 
updateRemoteVideoScaling()1020   private void updateRemoteVideoScaling() {
1021     VideoSurfaceTexture remoteVideoSurfaceTexture =
1022         videoCallScreenDelegate.getRemoteVideoSurfaceTexture();
1023     Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions();
1024     if (videoSize == null) {
1025       LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null");
1026       return;
1027     }
1028     if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) {
1029       LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet");
1030       return;
1031     }
1032 
1033     // If the video and display aspect ratio's are close then scale video to fill display
1034     float videoAspectRatio = ((float) videoSize.x) / videoSize.y;
1035     float displayAspectRatio =
1036         ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight();
1037     float delta = Math.abs(videoAspectRatio - displayAspectRatio);
1038     float sum = videoAspectRatio + displayAspectRatio;
1039     if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) {
1040       VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0);
1041     } else {
1042       VideoSurfaceBindings.scaleVideoMaintainingAspectRatio(
1043           remoteTextureView, videoSize.x, videoSize.y);
1044     }
1045   }
1046 
isLandscape()1047   private boolean isLandscape() {
1048     // Choose orientation based on display orientation, not window orientation
1049     int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
1050     return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
1051   }
1052 
enterGreenScreenMode()1053   private void enterGreenScreenMode() {
1054     LogUtil.i("VideoCallFragment.enterGreenScreenMode", null);
1055     RelativeLayout.LayoutParams params =
1056         new RelativeLayout.LayoutParams(
1057             RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
1058     params.addRule(RelativeLayout.ALIGN_PARENT_START);
1059     params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
1060     previewTextureView.setLayoutParams(params);
1061     previewTextureView.setOutlineProvider(null);
1062     updateOverlayBackground();
1063     contactGridManager.setIsMiddleRowVisible(true);
1064     updateMutePreviewOverlayVisibility();
1065 
1066     previewOffBlurredImageView.setLayoutParams(params);
1067     previewOffBlurredImageView.setOutlineProvider(null);
1068     previewOffBlurredImageView.setClipToOutline(false);
1069   }
1070 
exitGreenScreenMode()1071   private void exitGreenScreenMode() {
1072     LogUtil.i("VideoCallFragment.exitGreenScreenMode", null);
1073     Resources resources = getResources();
1074     RelativeLayout.LayoutParams params =
1075         new RelativeLayout.LayoutParams(
1076             (int) resources.getDimension(R.dimen.videocall_preview_width),
1077             (int) resources.getDimension(R.dimen.videocall_preview_height));
1078     params.setMargins(
1079         0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom));
1080     if (isLandscape()) {
1081       params.addRule(RelativeLayout.ALIGN_PARENT_END);
1082       params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end));
1083     } else {
1084       params.addRule(RelativeLayout.ALIGN_PARENT_START);
1085       params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start));
1086     }
1087     params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
1088     previewTextureView.setLayoutParams(params);
1089     previewTextureView.setOutlineProvider(circleOutlineProvider);
1090     updateOverlayBackground();
1091     contactGridManager.setIsMiddleRowVisible(false);
1092     updateMutePreviewOverlayVisibility();
1093 
1094     previewOffBlurredImageView.setLayoutParams(params);
1095     previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider);
1096     previewOffBlurredImageView.setClipToOutline(true);
1097   }
1098 
updatePreviewOffView()1099   private void updatePreviewOffView() {
1100     LogUtil.enterBlock("VideoCallFragment.updatePreviewOffView");
1101 
1102     // Always hide the preview off and remote off views in green screen mode.
1103     boolean previewEnabled = isInGreenScreenMode || shouldShowPreview;
1104     previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE);
1105     updateBlurredImageView(
1106         previewTextureView,
1107         previewOffBlurredImageView,
1108         shouldShowPreview,
1109         BLUR_PREVIEW_RADIUS,
1110         BLUR_PREVIEW_SCALE_FACTOR);
1111   }
1112 
updateRemoteOffView()1113   private void updateRemoteOffView() {
1114     LogUtil.enterBlock("VideoCallFragment.updateRemoteOffView");
1115     boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote;
1116     boolean isResumed = remoteEnabled && !isRemotelyHeld;
1117     if (isResumed) {
1118       boolean wasRemoteVideoOff =
1119           TextUtils.equals(
1120               remoteVideoOff.getText(),
1121               remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off));
1122       // The text needs to be updated and hidden after enough delay in order to be announced by
1123       // talkback.
1124       remoteVideoOff.setText(
1125           wasRemoteVideoOff
1126               ? R.string.videocall_remote_video_on
1127               : R.string.videocall_remotely_resumed);
1128       remoteVideoOff.postDelayed(
1129           new Runnable() {
1130             @Override
1131             public void run() {
1132               remoteVideoOff.setVisibility(View.GONE);
1133             }
1134           },
1135           VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS);
1136     } else {
1137       remoteVideoOff.setText(
1138           isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off);
1139       remoteVideoOff.setVisibility(View.VISIBLE);
1140     }
1141     updateBlurredImageView(
1142         remoteTextureView,
1143         remoteOffBlurredImageView,
1144         shouldShowRemote,
1145         BLUR_REMOTE_RADIUS,
1146         BLUR_REMOTE_SCALE_FACTOR);
1147   }
1148 
1149   @VisibleForTesting
updateBlurredImageView( TextureView textureView, ImageView blurredImageView, boolean isVideoEnabled, float blurRadius, float scaleFactor)1150   void updateBlurredImageView(
1151       TextureView textureView,
1152       ImageView blurredImageView,
1153       boolean isVideoEnabled,
1154       float blurRadius,
1155       float scaleFactor) {
1156     Context context = getContext();
1157 
1158     if (isVideoEnabled || context == null) {
1159       blurredImageView.setImageBitmap(null);
1160       blurredImageView.setVisibility(View.GONE);
1161       return;
1162     }
1163 
1164     long startTimeMillis = SystemClock.elapsedRealtime();
1165     int width = Math.round(textureView.getWidth() * scaleFactor);
1166     int height = Math.round(textureView.getHeight() * scaleFactor);
1167 
1168     LogUtil.i("VideoCallFragment.updateBlurredImageView", "width: %d, height: %d", width, height);
1169 
1170     // This call takes less than 10 milliseconds.
1171     Bitmap bitmap = textureView.getBitmap(width, height);
1172 
1173     if (bitmap == null) {
1174       blurredImageView.setImageBitmap(null);
1175       blurredImageView.setVisibility(View.GONE);
1176       return;
1177     }
1178 
1179     // TODO(mdooley): When the view is first displayed after a rotation the bitmap is empty
1180     // and thus this blur has no effect.
1181     // This call can take 100 milliseconds.
1182     blur(getContext(), bitmap, blurRadius);
1183 
1184     // TODO(mdooley): Figure out why only have to apply the transform in landscape mode
1185     if (width > height) {
1186       bitmap =
1187           Bitmap.createBitmap(
1188               bitmap,
1189               0,
1190               0,
1191               bitmap.getWidth(),
1192               bitmap.getHeight(),
1193               textureView.getTransform(null),
1194               true);
1195     }
1196 
1197     blurredImageView.setImageBitmap(bitmap);
1198     blurredImageView.setVisibility(View.VISIBLE);
1199 
1200     LogUtil.i(
1201         "VideoCallFragment.updateBlurredImageView",
1202         "took %d millis",
1203         (SystemClock.elapsedRealtime() - startTimeMillis));
1204   }
1205 
updateOverlayBackground()1206   private void updateOverlayBackground() {
1207     if (isInGreenScreenMode) {
1208       // We want to darken the preview view to make text and buttons readable. The fullscreen
1209       // background is below the preview view so use the green screen background instead.
1210       animateSetVisibility(greenScreenBackgroundView, View.VISIBLE);
1211       animateSetVisibility(fullscreenBackgroundView, View.GONE);
1212     } else if (!isInFullscreenMode) {
1213       // We want to darken the remote view to make text and buttons readable. The green screen
1214       // background is above the preview view so it would darken the preview too. Use the fullscreen
1215       // background instead.
1216       animateSetVisibility(greenScreenBackgroundView, View.GONE);
1217       animateSetVisibility(fullscreenBackgroundView, View.VISIBLE);
1218     } else {
1219       animateSetVisibility(greenScreenBackgroundView, View.GONE);
1220       animateSetVisibility(fullscreenBackgroundView, View.GONE);
1221     }
1222   }
1223 
updateMutePreviewOverlayVisibility()1224   private void updateMutePreviewOverlayVisibility() {
1225     // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen
1226     // mode the preview is fullscreen so there's no where to anchor it.
1227     mutePreviewOverlay.setVisibility(
1228         muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE);
1229   }
1230 
animateSetVisibility(final View view, final int visibility)1231   private static void animateSetVisibility(final View view, final int visibility) {
1232     if (view.getVisibility() == visibility) {
1233       return;
1234     }
1235 
1236     int startAlpha;
1237     int endAlpha;
1238     if (visibility == View.GONE) {
1239       startAlpha = 1;
1240       endAlpha = 0;
1241     } else if (visibility == View.VISIBLE) {
1242       startAlpha = 0;
1243       endAlpha = 1;
1244     } else {
1245       Assert.fail();
1246       return;
1247     }
1248 
1249     view.setAlpha(startAlpha);
1250     view.setVisibility(View.VISIBLE);
1251     view.animate()
1252         .alpha(endAlpha)
1253         .withEndAction(
1254             new Runnable() {
1255               @Override
1256               public void run() {
1257                 view.setVisibility(visibility);
1258               }
1259             })
1260         .start();
1261   }
1262 
blur(Context context, Bitmap image, float blurRadius)1263   private static void blur(Context context, Bitmap image, float blurRadius) {
1264     RenderScript renderScript = RenderScript.create(context);
1265     ScriptIntrinsicBlur blurScript =
1266         ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
1267     Allocation allocationIn = Allocation.createFromBitmap(renderScript, image);
1268     Allocation allocationOut = Allocation.createFromBitmap(renderScript, image);
1269     blurScript.setRadius(blurRadius);
1270     blurScript.setInput(allocationIn);
1271     blurScript.forEach(allocationOut);
1272     allocationOut.copyTo(image);
1273     blurScript.destroy();
1274     allocationIn.destroy();
1275     allocationOut.destroy();
1276   }
1277 
1278   @Override
onSystemUiVisibilityChange(int visibility)1279   public void onSystemUiVisibilityChange(int visibility) {
1280     boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
1281     videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible);
1282   }
1283 
checkCameraPermission()1284   private void checkCameraPermission() {
1285     // Checks if user has consent of camera permission and the permission is granted.
1286     // If camera permission is revoked, shows system permission dialog.
1287     // If camera permission is granted but user doesn't have consent of camera permission
1288     // (which means it's first time making video call), shows custom dialog instead. This
1289     // will only be shown to user once.
1290     if (!VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) {
1291       videoCallScreenDelegate.onCameraPermissionDialogShown();
1292       if (!VideoUtils.hasCameraPermission(getContext())) {
1293         requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
1294       } else {
1295         PermissionsUtil.showCameraPermissionToast(getContext());
1296         videoCallScreenDelegate.onCameraPermissionGranted();
1297       }
1298     }
1299   }
1300 }
1301 
1302