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