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