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 
310     controls.addOnLayoutChangeListener(
311         new OnLayoutChangeListener() {
312           @Override
313           public void onLayoutChange(
314               View v,
315               int left,
316               int top,
317               int right,
318               int bottom,
319               int oldLeft,
320               int oldTop,
321               int oldRight,
322               int oldBottom) {
323             LogUtil.i("VideoCallFragment.onLayoutChange", "controls layout changed");
324             if (getActivity() != null && getView() != null) {
325               controls.removeOnLayoutChangeListener(this);
326               if (isInFullscreenMode) {
327                 enterFullscreenMode();
328               }
329             }
330           }
331         });
332 
333     return view;
334   }
335 
336   @Override
onViewCreated(View view, @Nullable Bundle bundle)337   public void onViewCreated(View view, @Nullable Bundle bundle) {
338     super.onViewCreated(view, bundle);
339     LogUtil.i("VideoCallFragment.onViewCreated", null);
340 
341     inCallScreenDelegate =
342         FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
343             .newInCallScreenDelegate();
344     videoCallScreenDelegate =
345         FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class)
346             .newVideoCallScreenDelegate(this);
347 
348     speakerButtonController =
349         new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate);
350     switchOnHoldCallController =
351         new SwitchOnHoldCallController(
352             switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate);
353 
354     videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this);
355 
356     inCallScreenDelegate.onInCallScreenDelegateInit(this);
357     inCallScreenDelegate.onInCallScreenReady();
358     inCallButtonUiDelegate.onInCallButtonUiReady(this);
359 
360     view.setOnSystemUiVisibilityChangeListener(this);
361 
362     if (videoCallScreenDelegate.isFullscreen()) {
363         controls.setVisibility(View.INVISIBLE);
364         contactGridManager.getContainerView().setVisibility(View.INVISIBLE);
365         endCallButton.setVisibility(View.INVISIBLE);
366     }
367   }
368 
369   @Override
onSaveInstanceState(Bundle outState)370   public void onSaveInstanceState(Bundle outState) {
371     super.onSaveInstanceState(outState);
372     inCallButtonUiDelegate.onSaveInstanceState(outState);
373   }
374 
375   @Override
onDestroyView()376   public void onDestroyView() {
377     super.onDestroyView();
378     LogUtil.i("VideoCallFragment.onDestroyView", null);
379     inCallButtonUiDelegate.onInCallButtonUiUnready();
380     inCallScreenDelegate.onInCallScreenUnready();
381   }
382 
383   @Override
onAttach(Context context)384   public void onAttach(Context context) {
385     super.onAttach(context);
386     if (savedSecondaryInfo != null) {
387       setSecondary(savedSecondaryInfo);
388     }
389   }
390 
391   @Override
onStart()392   public void onStart() {
393     super.onStart();
394     LogUtil.i("VideoCallFragment.onStart", null);
395     onVideoScreenStart();
396   }
397 
398   @Override
onVideoScreenStart()399   public void onVideoScreenStart() {
400     videoCallScreenDelegate.onVideoCallScreenUiReady();
401     getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
402     getView()
403         .postDelayed(videoChargesAlertDialogRunnable, VIDEO_CHARGES_ALERT_DIALOG_DELAY_IN_MILLIS);
404   }
405 
406   @Override
onResume()407   public void onResume() {
408     super.onResume();
409     LogUtil.i("VideoCallFragment.onResume", null);
410     inCallScreenDelegate.onInCallScreenResumed();
411   }
412 
413   @Override
onPause()414   public void onPause() {
415     super.onPause();
416     LogUtil.i("VideoCallFragment.onPause", null);
417     inCallScreenDelegate.onInCallScreenPaused();
418   }
419 
420   @Override
onStop()421   public void onStop() {
422     super.onStop();
423     LogUtil.i("VideoCallFragment.onStop", null);
424     onVideoScreenStop();
425   }
426 
427   @Override
onVideoScreenStop()428   public void onVideoScreenStop() {
429     getView().removeCallbacks(videoChargesAlertDialogRunnable);
430     getView().removeCallbacks(cameraPermissionDialogRunnable);
431     videoCallScreenDelegate.onVideoCallScreenUiUnready();
432   }
433 
exitFullscreenMode()434   private void exitFullscreenMode() {
435     LogUtil.i("VideoCallFragment.exitFullscreenMode", null);
436 
437     if (!getView().isAttachedToWindow()) {
438       LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached");
439       return;
440     }
441 
442     showSystemUI();
443 
444     LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
445 
446     // Animate the controls to the shown state.
447     controls
448         .animate()
449         .translationX(0)
450         .translationY(0)
451         .setInterpolator(linearOutSlowInInterpolator)
452         .alpha(1)
453         .withStartAction(
454             new Runnable() {
455               @Override
456               public void run() {
457                 controls.setVisibility(View.VISIBLE);
458               }
459             })
460         .start();
461 
462     // Animate onHold to the shown state.
463     switchOnHoldButton
464         .animate()
465         .translationX(0)
466         .translationY(0)
467         .setInterpolator(linearOutSlowInInterpolator)
468         .alpha(1)
469         .withStartAction(
470             new Runnable() {
471               @Override
472               public void run() {
473                 switchOnHoldCallController.setOnScreen();
474               }
475             });
476 
477     View contactGridView = contactGridManager.getContainerView();
478     // Animate contact grid to the shown state.
479     contactGridView
480         .animate()
481         .translationX(0)
482         .translationY(0)
483         .setInterpolator(linearOutSlowInInterpolator)
484         .alpha(1)
485         .withStartAction(
486             new Runnable() {
487               @Override
488               public void run() {
489                 contactGridManager.show();
490               }
491             });
492 
493     endCallButton
494         .animate()
495         .translationX(0)
496         .translationY(0)
497         .setInterpolator(linearOutSlowInInterpolator)
498         .alpha(1)
499         .withStartAction(
500             new Runnable() {
501               @Override
502               public void run() {
503                 endCallButton.setVisibility(View.VISIBLE);
504               }
505             })
506         .start();
507 
508     // Animate all the preview controls up to make room for the navigation bar.
509     // In green screen mode we don't need this because the preview takes up the whole screen and has
510     // a fixed position.
511     if (!isInGreenScreenMode) {
512       Point previewOffsetStartShown = getPreviewOffsetStartShown();
513       for (View view : getAllPreviewRelatedViews()) {
514         // Animate up with the preview offset above the navigation bar.
515         view.animate()
516             .translationX(previewOffsetStartShown.x)
517             .translationY(previewOffsetStartShown.y)
518             .setInterpolator(new AccelerateDecelerateInterpolator())
519             .start();
520       }
521     }
522 
523     updateOverlayBackground();
524   }
525 
showSystemUI()526   private void showSystemUI() {
527     View view = getView();
528     if (view != null) {
529       // Code is more expressive with all flags present, even though some may be combined
530       // noinspection PointlessBitwiseExpression
531       view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
532     }
533   }
534 
535   /** Set view flags to hide the system UI. System UI will return on any touch event */
hideSystemUI()536   private void hideSystemUI() {
537     View view = getView();
538     if (view != null) {
539       view.setSystemUiVisibility(
540           View.SYSTEM_UI_FLAG_FULLSCREEN
541               | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
542               | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
543     }
544   }
545 
getControlsOffsetEndHidden(View controls)546   private Point getControlsOffsetEndHidden(View controls) {
547     if (isLandscape()) {
548       return new Point(0, getOffsetBottom(controls));
549     } else {
550       return new Point(getOffsetStart(controls), 0);
551     }
552   }
553 
getSwitchOnHoldOffsetEndHidden(View swapCallButton)554   private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) {
555     if (isLandscape()) {
556       return new Point(0, getOffsetTop(swapCallButton));
557     } else {
558       return new Point(getOffsetEnd(swapCallButton), 0);
559     }
560   }
561 
getContactGridOffsetEndHidden(View view)562   private Point getContactGridOffsetEndHidden(View view) {
563     return new Point(0, getOffsetTop(view));
564   }
565 
getEndCallOffsetEndHidden(View endCallButton)566   private Point getEndCallOffsetEndHidden(View endCallButton) {
567     if (isLandscape()) {
568       return new Point(getOffsetEnd(endCallButton), 0);
569     } else {
570       return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin);
571     }
572   }
573 
getPreviewOffsetStartShown()574   private Point getPreviewOffsetStartShown() {
575     // No insets in multiwindow mode, and rootWindowInsets will get the display's insets.
576     if (getActivity().isInMultiWindowMode()) {
577       return new Point();
578     }
579     if (isLandscape()) {
580       int systemWindowInsetEnd =
581           getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
582               ? getView().getRootWindowInsets().getSystemWindowInsetLeft()
583               : -getView().getRootWindowInsets().getSystemWindowInsetRight();
584       return new Point(systemWindowInsetEnd, 0);
585     } else {
586       return new Point(0, -getView().getRootWindowInsets().getSystemWindowInsetBottom());
587     }
588   }
589 
getAllPreviewRelatedViews()590   private View[] getAllPreviewRelatedViews() {
591     return new View[] {
592       previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay,
593     };
594   }
595 
getOffsetTop(View view)596   private int getOffsetTop(View view) {
597     return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin);
598   }
599 
getOffsetBottom(View view)600   private int getOffsetBottom(View view) {
601     return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin;
602   }
603 
getOffsetStart(View view)604   private int getOffsetStart(View view) {
605     int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart();
606     if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
607       offset = -offset;
608     }
609     return -offset;
610   }
611 
getOffsetEnd(View view)612   private int getOffsetEnd(View view) {
613     int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd();
614     if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
615       offset = -offset;
616     }
617     return offset;
618   }
619 
enterFullscreenMode()620   private void enterFullscreenMode() {
621     LogUtil.i("VideoCallFragment.enterFullscreenMode", null);
622 
623     hideSystemUI();
624 
625     Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator();
626 
627     // Animate controls to the hidden state.
628     Point offset = getControlsOffsetEndHidden(controls);
629     controls
630         .animate()
631         .translationX(offset.x)
632         .translationY(offset.y)
633         .setInterpolator(fastOutLinearInInterpolator)
634         .alpha(0)
635         .start();
636 
637     // Animate onHold to the hidden state.
638     offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton);
639     switchOnHoldButton
640         .animate()
641         .translationX(offset.x)
642         .translationY(offset.y)
643         .setInterpolator(fastOutLinearInInterpolator)
644         .alpha(0);
645 
646     View contactGridView = contactGridManager.getContainerView();
647     // Animate contact grid to the hidden state.
648     offset = getContactGridOffsetEndHidden(contactGridView);
649     contactGridView
650         .animate()
651         .translationX(offset.x)
652         .translationY(offset.y)
653         .setInterpolator(fastOutLinearInInterpolator)
654         .alpha(0);
655 
656     offset = getEndCallOffsetEndHidden(endCallButton);
657     // Use a fast out interpolator to quickly fade out the button. This is important because the
658     // button can't draw under the navigation bar which means that it'll look weird if it just
659     // abruptly disappears when it reaches the edge of the naivgation bar.
660     endCallButton
661         .animate()
662         .translationX(offset.x)
663         .translationY(offset.y)
664         .setInterpolator(fastOutLinearInInterpolator)
665         .alpha(0)
666         .withEndAction(
667             new Runnable() {
668               @Override
669               public void run() {
670                 endCallButton.setVisibility(View.INVISIBLE);
671               }
672             })
673         .setInterpolator(new FastOutLinearInInterpolator())
674         .start();
675 
676     // Animate all the preview controls down now that the navigation bar is hidden.
677     // In green screen mode we don't need this because the preview takes up the whole screen and has
678     // a fixed position.
679     if (!isInGreenScreenMode) {
680       for (View view : getAllPreviewRelatedViews()) {
681         // Animate down with the navigation bar hidden.
682         view.animate()
683             .translationX(0)
684             .translationY(0)
685             .setInterpolator(new AccelerateDecelerateInterpolator())
686             .start();
687       }
688     }
689     updateOverlayBackground();
690   }
691 
692   @Override
onClick(View v)693   public void onClick(View v) {
694     if (v == endCallButton) {
695       LogUtil.i("VideoCallFragment.onClick", "end call button clicked");
696       inCallButtonUiDelegate.onEndCallClicked();
697       videoCallScreenDelegate.resetAutoFullscreenTimer();
698     } else if (v == swapCameraButton) {
699       if (swapCameraButton.getDrawable() instanceof Animatable) {
700         ((Animatable) swapCameraButton.getDrawable()).start();
701       }
702       inCallButtonUiDelegate.toggleCameraClicked();
703       videoCallScreenDelegate.resetAutoFullscreenTimer();
704     }
705   }
706 
707   @Override
onCheckedChanged(CheckableImageButton button, boolean isChecked)708   public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
709     if (button == cameraOffButton) {
710       if (!isChecked && !VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) {
711         LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog");
712         checkCameraPermission();
713       } else {
714         inCallButtonUiDelegate.pauseVideoClicked(isChecked);
715         videoCallScreenDelegate.resetAutoFullscreenTimer();
716       }
717     } else if (button == muteButton) {
718       inCallButtonUiDelegate.muteClicked(isChecked, true /* clickedByUser */);
719       videoCallScreenDelegate.resetAutoFullscreenTimer();
720     }
721   }
722 
723   @Override
showVideoViews( boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld)724   public void showVideoViews(
725       boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
726     LogUtil.i(
727         "VideoCallFragment.showVideoViews",
728         "showPreview: %b, shouldShowRemote: %b",
729         shouldShowPreview,
730         shouldShowRemote);
731 
732     videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView);
733     videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView);
734 
735     boolean updateRemoteOffView = false;
736     if (this.shouldShowRemote != shouldShowRemote) {
737       this.shouldShowRemote = shouldShowRemote;
738       updateRemoteOffView = true;
739     }
740     if (this.isRemotelyHeld != isRemotelyHeld) {
741       this.isRemotelyHeld = isRemotelyHeld;
742       updateRemoteOffView = true;
743     }
744 
745     if (updateRemoteOffView) {
746       updateRemoteOffView();
747     }
748     if (this.shouldShowPreview != shouldShowPreview) {
749       this.shouldShowPreview = shouldShowPreview;
750       updatePreviewOffView();
751     }
752   }
753 
754   @Override
onLocalVideoDimensionsChanged()755   public void onLocalVideoDimensionsChanged() {
756     LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null);
757     updatePreviewVideoScaling();
758   }
759 
760   @Override
onLocalVideoOrientationChanged()761   public void onLocalVideoOrientationChanged() {
762     LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null);
763     updatePreviewVideoScaling();
764   }
765 
766   /** Called when the remote video's dimensions change. */
767   @Override
onRemoteVideoDimensionsChanged()768   public void onRemoteVideoDimensionsChanged() {
769     LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null);
770     updateRemoteVideoScaling();
771   }
772 
773   @Override
updateFullscreenAndGreenScreenMode( boolean shouldShowFullscreen, boolean shouldShowGreenScreen)774   public void updateFullscreenAndGreenScreenMode(
775       boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {
776     LogUtil.i(
777         "VideoCallFragment.updateFullscreenAndGreenScreenMode",
778         "shouldShowFullscreen: %b, shouldShowGreenScreen: %b",
779         shouldShowFullscreen,
780         shouldShowGreenScreen);
781 
782     if (getActivity() == null) {
783       LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity");
784       return;
785     }
786 
787     // Check if anything is actually going to change. The first time this function is called we
788     // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen
789     // and green screen modes to update even if only one has changed. That's because they both
790     // depend on each other.
791     if (hasInitializedScreenModes
792         && shouldShowGreenScreen == isInGreenScreenMode
793         && shouldShowFullscreen == isInFullscreenMode) {
794       LogUtil.i(
795           "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes");
796       return;
797     }
798     hasInitializedScreenModes = true;
799     isInGreenScreenMode = shouldShowGreenScreen;
800     isInFullscreenMode = shouldShowFullscreen;
801 
802     if (getView().isAttachedToWindow() && !getActivity().isInMultiWindowMode()) {
803       controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets());
804     }
805     if (shouldShowGreenScreen) {
806       enterGreenScreenMode();
807     } else {
808       exitGreenScreenMode();
809     }
810     if (shouldShowFullscreen) {
811       enterFullscreenMode();
812     } else {
813       exitFullscreenMode();
814     }
815 
816     OnHoldFragment onHoldFragment =
817         ((OnHoldFragment)
818             getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner));
819     if (onHoldFragment != null) {
820       onHoldFragment.setPadTopInset(!isInFullscreenMode);
821     }
822   }
823 
824   @Override
getVideoCallScreenFragment()825   public Fragment getVideoCallScreenFragment() {
826     return this;
827   }
828 
829   @Override
830   @NonNull
getCallId()831   public String getCallId() {
832     return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
833   }
834 
835   @Override
onHandoverFromWiFiToLte()836   public void onHandoverFromWiFiToLte() {
837     getView().post(videoChargesAlertDialogRunnable);
838   }
839 
840   @Override
showButton(@nCallButtonIds int buttonId, boolean show)841   public void showButton(@InCallButtonIds int buttonId, boolean show) {
842     LogUtil.v(
843         "VideoCallFragment.showButton",
844         "buttonId: %s, show: %b",
845         InCallButtonIdsExtension.toString(buttonId),
846         show);
847     if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
848       speakerButtonController.setEnabled(show);
849     } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
850       muteButton.setEnabled(show);
851     } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
852       cameraOffButton.setEnabled(show);
853     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
854       switchOnHoldCallController.setVisible(show);
855     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
856       swapCameraButton.setEnabled(show);
857     }
858   }
859 
860   @Override
enableButton(@nCallButtonIds int buttonId, boolean enable)861   public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
862     LogUtil.v(
863         "VideoCallFragment.setEnabled",
864         "buttonId: %s, enable: %b",
865         InCallButtonIdsExtension.toString(buttonId),
866         enable);
867     if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
868       speakerButtonController.setEnabled(enable);
869     } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
870       muteButton.setEnabled(enable);
871     } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
872       cameraOffButton.setEnabled(enable);
873     } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
874       switchOnHoldCallController.setEnabled(enable);
875     }
876   }
877 
878   @Override
setEnabled(boolean enabled)879   public void setEnabled(boolean enabled) {
880     LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled);
881     speakerButtonController.setEnabled(enabled);
882     muteButton.setEnabled(enabled);
883     cameraOffButton.setEnabled(enabled);
884     switchOnHoldCallController.setEnabled(enabled);
885   }
886 
887   @Override
setHold(boolean value)888   public void setHold(boolean value) {
889     LogUtil.i("VideoCallFragment.setHold", "value: " + value);
890   }
891 
892   @Override
setCameraSwitched(boolean isBackFacingCamera)893   public void setCameraSwitched(boolean isBackFacingCamera) {
894     LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera);
895   }
896 
897   @Override
setVideoPaused(boolean isPaused)898   public void setVideoPaused(boolean isPaused) {
899     LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused);
900     cameraOffButton.setChecked(isPaused);
901   }
902 
903   @Override
setAudioState(CallAudioState audioState)904   public void setAudioState(CallAudioState audioState) {
905     LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState);
906     speakerButtonController.setAudioState(audioState);
907     muteButton.setChecked(audioState.isMuted());
908     updateMutePreviewOverlayVisibility();
909   }
910 
911   @Override
updateButtonStates()912   public void updateButtonStates() {
913     LogUtil.i("VideoCallFragment.updateButtonState", null);
914     speakerButtonController.updateButtonState();
915     switchOnHoldCallController.updateButtonState();
916   }
917 
918   @Override
updateInCallButtonUiColors(@olorInt int color)919   public void updateInCallButtonUiColors(@ColorInt int color) {}
920 
921   @Override
getInCallButtonUiFragment()922   public Fragment getInCallButtonUiFragment() {
923     return this;
924   }
925 
926   @Override
showAudioRouteSelector()927   public void showAudioRouteSelector() {
928     LogUtil.i("VideoCallFragment.showAudioRouteSelector", null);
929     AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
930         .show(getChildFragmentManager(), null);
931   }
932 
933   @Override
onAudioRouteSelected(int audioRoute)934   public void onAudioRouteSelected(int audioRoute) {
935     LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute);
936     inCallButtonUiDelegate.setAudioRoute(audioRoute);
937   }
938 
939   @Override
onAudioRouteSelectorDismiss()940   public void onAudioRouteSelectorDismiss() {}
941 
942   @Override
setPrimary(@onNull PrimaryInfo primaryInfo)943   public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
944     LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString());
945     contactGridManager.setPrimary(primaryInfo);
946   }
947 
948   @Override
setSecondary(@onNull SecondaryInfo secondaryInfo)949   public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
950     LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString());
951     if (!isAdded()) {
952       savedSecondaryInfo = secondaryInfo;
953       return;
954     }
955     savedSecondaryInfo = null;
956     switchOnHoldCallController.setSecondaryInfo(secondaryInfo);
957     updateButtonStates();
958     FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
959     Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner);
960     if (secondaryInfo.shouldShow()) {
961       OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo);
962       onHoldFragment.setPadTopInset(!isInFullscreenMode);
963       transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment);
964     } else {
965       if (oldBanner != null) {
966         transaction.remove(oldBanner);
967       }
968     }
969     transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
970     transaction.commitAllowingStateLoss();
971   }
972 
973   @Override
setCallState(@onNull PrimaryCallState primaryCallState)974   public void setCallState(@NonNull PrimaryCallState primaryCallState) {
975     LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString());
976     contactGridManager.setCallState(primaryCallState);
977   }
978 
979   @Override
setEndCallButtonEnabled(boolean enabled, boolean animate)980   public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
981     LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled);
982   }
983 
984   @Override
showManageConferenceCallButton(boolean visible)985   public void showManageConferenceCallButton(boolean visible) {
986     LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible);
987   }
988 
989   @Override
isManageConferenceVisible()990   public boolean isManageConferenceVisible() {
991     LogUtil.i("VideoCallFragment.isManageConferenceVisible", null);
992     return false;
993   }
994 
995   @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)996   public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
997     contactGridManager.dispatchPopulateAccessibilityEvent(event);
998   }
999 
1000   @Override
showNoteSentToast()1001   public void showNoteSentToast() {
1002     LogUtil.i("VideoCallFragment.showNoteSentToast", null);
1003   }
1004 
1005   @Override
updateInCallScreenColors()1006   public void updateInCallScreenColors() {
1007     LogUtil.i("VideoCallFragment.updateColors", null);
1008   }
1009 
1010   @Override
onInCallScreenDialpadVisibilityChange(boolean isShowing)1011   public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
1012     LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null);
1013   }
1014 
1015   @Override
getAnswerAndDialpadContainerResourceId()1016   public int getAnswerAndDialpadContainerResourceId() {
1017     return 0;
1018   }
1019 
1020   @Override
getInCallScreenFragment()1021   public Fragment getInCallScreenFragment() {
1022     return this;
1023   }
1024 
1025   @Override
isShowingLocationUi()1026   public boolean isShowingLocationUi() {
1027     return false;
1028   }
1029 
1030   @Override
showLocationUi(Fragment locationUi)1031   public void showLocationUi(Fragment locationUi) {
1032     LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported");
1033     // Do nothing
1034   }
1035 
updatePreviewVideoScaling()1036   private void updatePreviewVideoScaling() {
1037     if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) {
1038       LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet");
1039       return;
1040     }
1041     VideoSurfaceTexture localVideoSurfaceTexture =
1042         videoCallScreenDelegate.getLocalVideoSurfaceTexture();
1043     Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions();
1044     if (cameraDimensions == null) {
1045       LogUtil.i(
1046           "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set");
1047       return;
1048     }
1049     if (isLandscape()) {
1050       VideoSurfaceBindings.scaleVideoAndFillView(
1051           previewTextureView,
1052           cameraDimensions.x,
1053           cameraDimensions.y,
1054           videoCallScreenDelegate.getDeviceOrientation());
1055     } else {
1056       VideoSurfaceBindings.scaleVideoAndFillView(
1057           previewTextureView,
1058           cameraDimensions.y,
1059           cameraDimensions.x,
1060           videoCallScreenDelegate.getDeviceOrientation());
1061     }
1062   }
1063 
updateRemoteVideoScaling()1064   private void updateRemoteVideoScaling() {
1065     VideoSurfaceTexture remoteVideoSurfaceTexture =
1066         videoCallScreenDelegate.getRemoteVideoSurfaceTexture();
1067     Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions();
1068     if (videoSize == null) {
1069       LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null");
1070       return;
1071     }
1072     if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) {
1073       LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet");
1074       return;
1075     }
1076 
1077     // If the video and display aspect ratio's are close then scale video to fill display
1078     float videoAspectRatio = ((float) videoSize.x) / videoSize.y;
1079     float displayAspectRatio =
1080         ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight();
1081     float delta = Math.abs(videoAspectRatio - displayAspectRatio);
1082     float sum = videoAspectRatio + displayAspectRatio;
1083     if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) {
1084       VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0);
1085     } else {
1086       VideoSurfaceBindings.scaleVideoMaintainingAspectRatio(
1087           remoteTextureView, videoSize.x, videoSize.y);
1088     }
1089   }
1090 
isLandscape()1091   private boolean isLandscape() {
1092     // Choose orientation based on display orientation, not window orientation
1093     int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
1094     return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
1095   }
1096 
enterGreenScreenMode()1097   private void enterGreenScreenMode() {
1098     LogUtil.i("VideoCallFragment.enterGreenScreenMode", null);
1099     RelativeLayout.LayoutParams params =
1100         new RelativeLayout.LayoutParams(
1101             RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
1102     params.addRule(RelativeLayout.ALIGN_PARENT_START);
1103     params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
1104     previewTextureView.setLayoutParams(params);
1105     previewTextureView.setOutlineProvider(null);
1106     updateOverlayBackground();
1107     contactGridManager.setIsMiddleRowVisible(true);
1108     updateMutePreviewOverlayVisibility();
1109 
1110     previewOffBlurredImageView.setLayoutParams(params);
1111     previewOffBlurredImageView.setOutlineProvider(null);
1112     previewOffBlurredImageView.setClipToOutline(false);
1113   }
1114 
exitGreenScreenMode()1115   private void exitGreenScreenMode() {
1116     LogUtil.i("VideoCallFragment.exitGreenScreenMode", null);
1117     Resources resources = getResources();
1118     RelativeLayout.LayoutParams params =
1119         new RelativeLayout.LayoutParams(
1120             (int) resources.getDimension(R.dimen.videocall_preview_width),
1121             (int) resources.getDimension(R.dimen.videocall_preview_height));
1122     params.setMargins(
1123         0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom));
1124     if (isLandscape()) {
1125       params.addRule(RelativeLayout.ALIGN_PARENT_END);
1126       params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end));
1127     } else {
1128       params.addRule(RelativeLayout.ALIGN_PARENT_START);
1129       params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start));
1130     }
1131     params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
1132     previewTextureView.setLayoutParams(params);
1133     previewTextureView.setOutlineProvider(circleOutlineProvider);
1134     updateOverlayBackground();
1135     contactGridManager.setIsMiddleRowVisible(false);
1136     updateMutePreviewOverlayVisibility();
1137 
1138     previewOffBlurredImageView.setLayoutParams(params);
1139     previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider);
1140     previewOffBlurredImageView.setClipToOutline(true);
1141   }
1142 
updatePreviewOffView()1143   private void updatePreviewOffView() {
1144     LogUtil.enterBlock("VideoCallFragment.updatePreviewOffView");
1145 
1146     // Always hide the preview off and remote off views in green screen mode.
1147     boolean previewEnabled = isInGreenScreenMode || shouldShowPreview;
1148     previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE);
1149     updateBlurredImageView(
1150         previewTextureView,
1151         previewOffBlurredImageView,
1152         shouldShowPreview,
1153         BLUR_PREVIEW_RADIUS,
1154         BLUR_PREVIEW_SCALE_FACTOR);
1155   }
1156 
updateRemoteOffView()1157   private void updateRemoteOffView() {
1158     LogUtil.enterBlock("VideoCallFragment.updateRemoteOffView");
1159     boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote;
1160     boolean isResumed = remoteEnabled && !isRemotelyHeld;
1161     if (isResumed) {
1162       boolean wasRemoteVideoOff =
1163           TextUtils.equals(
1164               remoteVideoOff.getText(),
1165               remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off));
1166       // The text needs to be updated and hidden after enough delay in order to be announced by
1167       // talkback.
1168       remoteVideoOff.setText(
1169           wasRemoteVideoOff
1170               ? R.string.videocall_remote_video_on
1171               : R.string.videocall_remotely_resumed);
1172       remoteVideoOff.postDelayed(
1173           new Runnable() {
1174             @Override
1175             public void run() {
1176               remoteVideoOff.setVisibility(View.GONE);
1177             }
1178           },
1179           VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS);
1180     } else {
1181       remoteVideoOff.setText(
1182           isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off);
1183       remoteVideoOff.setVisibility(View.VISIBLE);
1184     }
1185     updateBlurredImageView(
1186         remoteTextureView,
1187         remoteOffBlurredImageView,
1188         shouldShowRemote,
1189         BLUR_REMOTE_RADIUS,
1190         BLUR_REMOTE_SCALE_FACTOR);
1191   }
1192 
1193   @VisibleForTesting
updateBlurredImageView( TextureView textureView, ImageView blurredImageView, boolean isVideoEnabled, float blurRadius, float scaleFactor)1194   void updateBlurredImageView(
1195       TextureView textureView,
1196       ImageView blurredImageView,
1197       boolean isVideoEnabled,
1198       float blurRadius,
1199       float scaleFactor) {
1200     Context context = getContext();
1201 
1202     if (isVideoEnabled || context == null) {
1203       blurredImageView.setImageBitmap(null);
1204       blurredImageView.setVisibility(View.GONE);
1205       return;
1206     }
1207 
1208     long startTimeMillis = SystemClock.elapsedRealtime();
1209     int width = Math.round(textureView.getWidth() * scaleFactor);
1210     int height = Math.round(textureView.getHeight() * scaleFactor);
1211 
1212     LogUtil.i("VideoCallFragment.updateBlurredImageView", "width: %d, height: %d", width, height);
1213 
1214     // This call takes less than 10 milliseconds.
1215     Bitmap bitmap = textureView.getBitmap(width, height);
1216 
1217     if (bitmap == null) {
1218       blurredImageView.setImageBitmap(null);
1219       blurredImageView.setVisibility(View.GONE);
1220       return;
1221     }
1222 
1223     // TODO(mdooley): When the view is first displayed after a rotation the bitmap is empty
1224     // and thus this blur has no effect.
1225     // This call can take 100 milliseconds.
1226     blur(getContext(), bitmap, blurRadius);
1227 
1228     // TODO(mdooley): Figure out why only have to apply the transform in landscape mode
1229     if (width > height) {
1230       bitmap =
1231           Bitmap.createBitmap(
1232               bitmap,
1233               0,
1234               0,
1235               bitmap.getWidth(),
1236               bitmap.getHeight(),
1237               textureView.getTransform(null),
1238               true);
1239     }
1240 
1241     blurredImageView.setImageBitmap(bitmap);
1242     blurredImageView.setVisibility(View.VISIBLE);
1243 
1244     LogUtil.i(
1245         "VideoCallFragment.updateBlurredImageView",
1246         "took %d millis",
1247         (SystemClock.elapsedRealtime() - startTimeMillis));
1248   }
1249 
updateOverlayBackground()1250   private void updateOverlayBackground() {
1251     if (isInGreenScreenMode) {
1252       // We want to darken the preview view to make text and buttons readable. The fullscreen
1253       // background is below the preview view so use the green screen background instead.
1254       animateSetVisibility(greenScreenBackgroundView, View.VISIBLE);
1255       animateSetVisibility(fullscreenBackgroundView, View.GONE);
1256     } else if (!isInFullscreenMode) {
1257       // We want to darken the remote view to make text and buttons readable. The green screen
1258       // background is above the preview view so it would darken the preview too. Use the fullscreen
1259       // background instead.
1260       animateSetVisibility(greenScreenBackgroundView, View.GONE);
1261       animateSetVisibility(fullscreenBackgroundView, View.VISIBLE);
1262     } else {
1263       animateSetVisibility(greenScreenBackgroundView, View.GONE);
1264       animateSetVisibility(fullscreenBackgroundView, View.GONE);
1265     }
1266   }
1267 
updateMutePreviewOverlayVisibility()1268   private void updateMutePreviewOverlayVisibility() {
1269     // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen
1270     // mode the preview is fullscreen so there's no where to anchor it.
1271     mutePreviewOverlay.setVisibility(
1272         muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE);
1273   }
1274 
animateSetVisibility(final View view, final int visibility)1275   private static void animateSetVisibility(final View view, final int visibility) {
1276     if (view.getVisibility() == visibility) {
1277       return;
1278     }
1279 
1280     int startAlpha;
1281     int endAlpha;
1282     if (visibility == View.GONE) {
1283       startAlpha = 1;
1284       endAlpha = 0;
1285     } else if (visibility == View.VISIBLE) {
1286       startAlpha = 0;
1287       endAlpha = 1;
1288     } else {
1289       Assert.fail();
1290       return;
1291     }
1292 
1293     view.setAlpha(startAlpha);
1294     view.setVisibility(View.VISIBLE);
1295     view.animate()
1296         .alpha(endAlpha)
1297         .withEndAction(
1298             new Runnable() {
1299               @Override
1300               public void run() {
1301                 view.setVisibility(visibility);
1302               }
1303             })
1304         .start();
1305   }
1306 
blur(Context context, Bitmap image, float blurRadius)1307   private static void blur(Context context, Bitmap image, float blurRadius) {
1308     RenderScript renderScript = RenderScript.create(context);
1309     ScriptIntrinsicBlur blurScript =
1310         ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
1311     Allocation allocationIn = Allocation.createFromBitmap(renderScript, image);
1312     Allocation allocationOut = Allocation.createFromBitmap(renderScript, image);
1313     blurScript.setRadius(blurRadius);
1314     blurScript.setInput(allocationIn);
1315     blurScript.forEach(allocationOut);
1316     allocationOut.copyTo(image);
1317     blurScript.destroy();
1318     allocationIn.destroy();
1319     allocationOut.destroy();
1320   }
1321 
1322   @Override
onSystemUiVisibilityChange(int visibility)1323   public void onSystemUiVisibilityChange(int visibility) {
1324     boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
1325     videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible);
1326   }
1327 
checkCameraPermission()1328   private void checkCameraPermission() {
1329     // Checks if user has consent of camera permission and the permission is granted.
1330     // If camera permission is revoked, shows system permission dialog.
1331     // If camera permission is granted but user doesn't have consent of camera permission
1332     // (which means it's first time making video call), shows custom dialog instead. This
1333     // will only be shown to user once.
1334     if (!VideoUtils.hasCameraPermissionAndShownPrivacyToast(getContext())) {
1335       videoCallScreenDelegate.onCameraPermissionDialogShown();
1336       if (!VideoUtils.hasCameraPermission(getContext())) {
1337         requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
1338       } else {
1339         PermissionsUtil.showCameraPermissionToast(getContext());
1340         videoCallScreenDelegate.onCameraPermissionGranted();
1341       }
1342     }
1343   }
1344 }
1345 
1346