diff options
Diffstat (limited to 'java/com/android/incallui/video/impl/VideoCallFragment.java')
-rw-r--r-- | java/com/android/incallui/video/impl/VideoCallFragment.java | 1215 |
1 files changed, 1215 insertions, 0 deletions
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java new file mode 100644 index 000000000..77a67d032 --- /dev/null +++ b/java/com/android/incallui/video/impl/VideoCallFragment.java @@ -0,0 +1,1215 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.incallui.video.impl; + +import android.Manifest.permission; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Outline; +import android.graphics.Point; +import android.graphics.drawable.Animatable; +import android.os.Bundle; +import android.os.SystemClock; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.telecom.CallAudioState; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnSystemUiVisibilityChangeListener; +import android.view.ViewGroup; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import com.android.dialer.common.Assert; +import com.android.dialer.common.FragmentUtils; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.ActivityCompat; +import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment; +import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter; +import com.android.incallui.call.VideoUtils; +import com.android.incallui.contactgrid.ContactGridManager; +import com.android.incallui.hold.OnHoldFragment; +import com.android.incallui.incall.protocol.InCallButtonIds; +import com.android.incallui.incall.protocol.InCallButtonIdsExtension; +import com.android.incallui.incall.protocol.InCallButtonUi; +import com.android.incallui.incall.protocol.InCallButtonUiDelegate; +import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory; +import com.android.incallui.incall.protocol.InCallScreen; +import com.android.incallui.incall.protocol.InCallScreenDelegate; +import com.android.incallui.incall.protocol.InCallScreenDelegateFactory; +import com.android.incallui.incall.protocol.PrimaryCallState; +import com.android.incallui.incall.protocol.PrimaryInfo; +import com.android.incallui.incall.protocol.SecondaryInfo; +import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener; +import com.android.incallui.video.protocol.VideoCallScreen; +import com.android.incallui.video.protocol.VideoCallScreenDelegate; +import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory; +import com.android.incallui.videosurface.bindings.VideoSurfaceBindings; +import com.android.incallui.videosurface.protocol.VideoSurfaceTexture; + +/** Contains UI elements for a video call. */ +public class VideoCallFragment extends Fragment + implements InCallScreen, + InCallButtonUi, + VideoCallScreen, + OnClickListener, + OnCheckedChangeListener, + AudioRouteSelectorPresenter, + OnSystemUiVisibilityChangeListener { + + private static final float BLUR_PREVIEW_RADIUS = 16.0f; + private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f; + private static final float BLUR_REMOTE_RADIUS = 25.0f; + private static final float BLUR_REMOTE_SCALE_FACTOR = 0.25f; + private static final float ASPECT_RATIO_MATCH_THRESHOLD = 0.2f; + + private static final int CAMERA_PERMISSION_REQUEST_CODE = 1; + private static final String CAMERA_PERMISSION_DIALOG_FRAMENT_TAG = + "CameraPermissionDialogFragment"; + private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L; + private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L; + + private final ViewOutlineProvider circleOutlineProvider = + new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + int x = view.getWidth() / 2; + int y = view.getHeight() / 2; + int radius = Math.min(x, y); + outline.setOval(x - radius, y - radius, x + radius, y + radius); + } + }; + private InCallScreenDelegate inCallScreenDelegate; + private VideoCallScreenDelegate videoCallScreenDelegate; + private InCallButtonUiDelegate inCallButtonUiDelegate; + private View endCallButton; + private CheckableImageButton speakerButton; + private SpeakerButtonController speakerButtonController; + private CheckableImageButton muteButton; + private CheckableImageButton cameraOffButton; + private ImageButton swapCameraButton; + private View switchOnHoldButton; + private View onHoldContainer; + private SwitchOnHoldCallController switchOnHoldCallController; + private TextView remoteVideoOff; + private ImageView remoteOffBlurredImageView; + private View mutePreviewOverlay; + private View previewOffOverlay; + private ImageView previewOffBlurredImageView; + private View controls; + private View controlsContainer; + private TextureView previewTextureView; + private TextureView remoteTextureView; + private View greenScreenBackgroundView; + private View fullscreenBackgroundView; + private boolean shouldShowRemote; + private boolean shouldShowPreview; + private boolean isInFullscreenMode; + private boolean isInGreenScreenMode; + private boolean hasInitializedScreenModes; + private boolean isRemotelyHeld; + private ContactGridManager contactGridManager; + private SecondaryInfo savedSecondaryInfo; + private final Runnable cameraPermissionDialogRunnable = + new Runnable() { + @Override + public void run() { + if (videoCallScreenDelegate.shouldShowCameraPermissionDialog()) { + LogUtil.i("VideoCallFragment.cameraPermissionDialogRunnable", "showing dialog"); + checkCameraPermission(); + } + } + }; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + LogUtil.i("VideoCallFragment.onCreate", null); + + inCallButtonUiDelegate = + FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class) + .newInCallButtonUiDelegate(); + if (savedInstanceState != null) { + inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission granted."); + videoCallScreenDelegate.onCameraPermissionGranted(); + } else { + LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission denied."); + } + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) { + LogUtil.i("VideoCallFragment.onCreateView", null); + + View view = + layoutInflater.inflate( + isLandscape() ? R.layout.frag_videocall_land : R.layout.frag_videocall, + viewGroup, + false); + contactGridManager = + new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */); + + controls = view.findViewById(R.id.videocall_video_controls); + controls.setVisibility( + ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE); + controlsContainer = view.findViewById(R.id.videocall_video_controls_container); + speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button); + muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button); + muteButton.setOnCheckedChangeListener(this); + mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay); + cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video); + cameraOffButton.setOnCheckedChangeListener(this); + previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay); + previewOffBlurredImageView = + (ImageView) view.findViewById(R.id.videocall_preview_off_blurred_image_view); + swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video); + swapCameraButton.setOnClickListener(this); + view.findViewById(R.id.videocall_switch_controls) + .setVisibility( + ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE); + switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold); + onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner); + remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off); + remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + remoteOffBlurredImageView = + (ImageView) view.findViewById(R.id.videocall_remote_off_blurred_image_view); + endCallButton = view.findViewById(R.id.videocall_end_call); + endCallButton.setOnClickListener(this); + previewTextureView = (TextureView) view.findViewById(R.id.videocall_video_preview); + previewTextureView.setClipToOutline(true); + previewOffOverlay.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + checkCameraPermission(); + } + }); + remoteTextureView = (TextureView) view.findViewById(R.id.videocall_video_remote); + greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background); + fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background); + + // We need the texture view size to be able to scale the remote video. At this point the view + // layout won't be complete so add a layout listener. + ViewTreeObserver observer = remoteTextureView.getViewTreeObserver(); + observer.addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + LogUtil.i("VideoCallFragment.onGlobalLayout", null); + updateRemoteVideoScaling(); + updatePreviewVideoScaling(); + updateVideoOffViews(); + // Remove the listener so we don't continually re-layout. + ViewTreeObserver observer = remoteTextureView.getViewTreeObserver(); + if (observer.isAlive()) { + observer.removeOnGlobalLayoutListener(this); + } + } + }); + + return view; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle bundle) { + super.onViewCreated(view, bundle); + LogUtil.i("VideoCallFragment.onViewCreated", null); + + inCallScreenDelegate = + FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class) + .newInCallScreenDelegate(); + videoCallScreenDelegate = + FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class) + .newVideoCallScreenDelegate(); + + speakerButtonController = + new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate); + switchOnHoldCallController = + new SwitchOnHoldCallController( + switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate); + + videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this); + + inCallScreenDelegate.onInCallScreenDelegateInit(this); + inCallScreenDelegate.onInCallScreenReady(); + inCallButtonUiDelegate.onInCallButtonUiReady(this); + + view.setOnSystemUiVisibilityChangeListener(this); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + inCallButtonUiDelegate.onSaveInstanceState(outState); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + LogUtil.i("VideoCallFragment.onDestroyView", null); + inCallButtonUiDelegate.onInCallButtonUiUnready(); + inCallScreenDelegate.onInCallScreenUnready(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (savedSecondaryInfo != null) { + setSecondary(savedSecondaryInfo); + } + } + + @Override + public void onResume() { + super.onResume(); + LogUtil.i("VideoCallFragment.onResume", null); + inCallScreenDelegate.onInCallScreenResumed(); + } + + @Override + public void onStart() { + super.onStart(); + LogUtil.i("VideoCallFragment.onStart", null); + inCallButtonUiDelegate.refreshMuteState(); + videoCallScreenDelegate.onVideoCallScreenUiReady(); + getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS); + } + + @Override + public void onPause() { + super.onPause(); + LogUtil.i("VideoCallFragment.onPause", null); + } + + @Override + public void onStop() { + super.onStop(); + LogUtil.i("VideoCallFragment.onStop", null); + getView().removeCallbacks(cameraPermissionDialogRunnable); + videoCallScreenDelegate.onVideoCallScreenUiUnready(); + } + + private void exitFullscreenMode() { + LogUtil.i("VideoCallFragment.exitFullscreenMode", null); + + if (!getView().isAttachedToWindow()) { + LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached"); + return; + } + + showSystemUI(); + + LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator(); + + // Animate the controls to the shown state. + controls + .animate() + .translationX(0) + .translationY(0) + .setInterpolator(linearOutSlowInInterpolator) + .alpha(1) + .start(); + + // Animate onHold to the shown state. + switchOnHoldButton + .animate() + .translationX(0) + .translationY(0) + .setInterpolator(linearOutSlowInInterpolator) + .alpha(1) + .withStartAction( + new Runnable() { + @Override + public void run() { + switchOnHoldCallController.setOnScreen(); + } + }); + + View contactGridView = contactGridManager.getContainerView(); + // Animate contact grid to the shown state. + contactGridView + .animate() + .translationX(0) + .translationY(0) + .setInterpolator(linearOutSlowInInterpolator) + .alpha(1) + .withStartAction( + new Runnable() { + @Override + public void run() { + contactGridManager.show(); + } + }); + + endCallButton + .animate() + .translationX(0) + .translationY(0) + .setInterpolator(linearOutSlowInInterpolator) + .alpha(1) + .withStartAction( + new Runnable() { + @Override + public void run() { + endCallButton.setVisibility(View.VISIBLE); + } + }) + .start(); + + // Animate all the preview controls up to make room for the navigation bar. + // In green screen mode we don't need this because the preview takes up the whole screen and has + // a fixed position. + if (!isInGreenScreenMode) { + Point previewOffsetStartShown = getPreviewOffsetStartShown(); + for (View view : getAllPreviewRelatedViews()) { + // Animate up with the preview offset above the navigation bar. + view.animate() + .translationX(previewOffsetStartShown.x) + .translationY(previewOffsetStartShown.y) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + } + } + + updateOverlayBackground(); + } + + private void showSystemUI() { + View view = getView(); + if (view != null) { + // Code is more expressive with all flags present, even though some may be combined + //noinspection PointlessBitwiseExpression + view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + + /** Set view flags to hide the system UI. System UI will return on any touch event */ + private void hideSystemUI() { + View view = getView(); + if (view != null) { + view.setSystemUiVisibility( + View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } + + private Point getControlsOffsetEndHidden(View controls) { + if (isLandscape()) { + return new Point(0, getOffsetBottom(controls)); + } else { + return new Point(getOffsetStart(controls), 0); + } + } + + private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) { + if (isLandscape()) { + return new Point(0, getOffsetTop(swapCallButton)); + } else { + return new Point(getOffsetEnd(swapCallButton), 0); + } + } + + private Point getContactGridOffsetEndHidden(View view) { + return new Point(0, getOffsetTop(view)); + } + + private Point getEndCallOffsetEndHidden(View endCallButton) { + if (isLandscape()) { + return new Point(getOffsetEnd(endCallButton), 0); + } else { + return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin); + } + } + + private Point getPreviewOffsetStartShown() { + // No insets in multiwindow mode, and rootWindowInsets will get the display's insets. + if (ActivityCompat.isInMultiWindowMode(getActivity())) { + return new Point(); + } + if (isLandscape()) { + int stableInsetEnd = + getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? getView().getRootWindowInsets().getStableInsetLeft() + : -getView().getRootWindowInsets().getStableInsetRight(); + return new Point(stableInsetEnd, 0); + } else { + return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom()); + } + } + + private View[] getAllPreviewRelatedViews() { + return new View[] { + previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay, + }; + } + + private int getOffsetTop(View view) { + return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin); + } + + private int getOffsetBottom(View view) { + return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin; + } + + private int getOffsetStart(View view) { + int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart(); + if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + offset = -offset; + } + return -offset; + } + + private int getOffsetEnd(View view) { + int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd(); + if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + offset = -offset; + } + return offset; + } + + private void enterFullscreenMode() { + LogUtil.i("VideoCallFragment.enterFullscreenMode", null); + + hideSystemUI(); + + Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator(); + + // Animate controls to the hidden state. + Point offset = getControlsOffsetEndHidden(controls); + controls + .animate() + .translationX(offset.x) + .translationY(offset.y) + .setInterpolator(fastOutLinearInInterpolator) + .alpha(0) + .start(); + + // Animate onHold to the hidden state. + offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton); + switchOnHoldButton + .animate() + .translationX(offset.x) + .translationY(offset.y) + .setInterpolator(fastOutLinearInInterpolator) + .alpha(0); + + View contactGridView = contactGridManager.getContainerView(); + // Animate contact grid to the hidden state. + offset = getContactGridOffsetEndHidden(contactGridView); + contactGridView + .animate() + .translationX(offset.x) + .translationY(offset.y) + .setInterpolator(fastOutLinearInInterpolator) + .alpha(0); + + offset = getEndCallOffsetEndHidden(endCallButton); + // Use a fast out interpolator to quickly fade out the button. This is important because the + // button can't draw under the navigation bar which means that it'll look weird if it just + // abruptly disappears when it reaches the edge of the naivgation bar. + endCallButton + .animate() + .translationX(offset.x) + .translationY(offset.y) + .setInterpolator(fastOutLinearInInterpolator) + .alpha(0) + .withEndAction( + new Runnable() { + @Override + public void run() { + endCallButton.setVisibility(View.INVISIBLE); + } + }) + .setInterpolator(new FastOutLinearInInterpolator()) + .start(); + + // Animate all the preview controls down now that the navigation bar is hidden. + // In green screen mode we don't need this because the preview takes up the whole screen and has + // a fixed position. + if (!isInGreenScreenMode) { + for (View view : getAllPreviewRelatedViews()) { + // Animate down with the navigation bar hidden. + view.animate() + .translationX(0) + .translationY(0) + .setInterpolator(new AccelerateDecelerateInterpolator()) + .start(); + } + } + updateOverlayBackground(); + } + + @Override + public void onClick(View v) { + if (v == endCallButton) { + LogUtil.i("VideoCallFragment.onClick", "end call button clicked"); + inCallButtonUiDelegate.onEndCallClicked(); + videoCallScreenDelegate.resetAutoFullscreenTimer(); + } else if (v == swapCameraButton) { + if (swapCameraButton.getDrawable() instanceof Animatable) { + ((Animatable) swapCameraButton.getDrawable()).start(); + } + inCallButtonUiDelegate.toggleCameraClicked(); + videoCallScreenDelegate.resetAutoFullscreenTimer(); + } + } + + @Override + public void onCheckedChanged(CheckableImageButton button, boolean isChecked) { + if (button == cameraOffButton) { + if (!isChecked && !VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) { + LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog"); + checkCameraPermission(); + } else { + inCallButtonUiDelegate.pauseVideoClicked(isChecked); + videoCallScreenDelegate.resetAutoFullscreenTimer(); + } + } else if (button == muteButton) { + inCallButtonUiDelegate.muteClicked(isChecked); + videoCallScreenDelegate.resetAutoFullscreenTimer(); + } + } + + @Override + public void showVideoViews( + boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) { + LogUtil.i( + "VideoCallFragment.showVideoViews", + "showPreview: %b, shouldShowRemote: %b", + shouldShowPreview, + shouldShowRemote); + this.shouldShowPreview = shouldShowPreview; + this.shouldShowRemote = shouldShowRemote; + this.isRemotelyHeld = isRemotelyHeld; + + videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView); + videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView); + + updateVideoOffViews(); + updateRemoteVideoScaling(); + } + + /** + * This method scales the video feed inside the texture view, it doesn't change the texture view's + * size. In the old UI we would change the view size to match the aspect ratio of the video. In + * the new UI the view is always square (with the circular clip) so we have to do additional work + * to make sure the non-square video doesn't look squished. + */ + @Override + public void onLocalVideoDimensionsChanged() { + LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null); + updatePreviewVideoScaling(); + } + + @Override + public void onLocalVideoOrientationChanged() { + LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null); + updatePreviewVideoScaling(); + } + + /** Called when the remote video's dimensions change. */ + @Override + public void onRemoteVideoDimensionsChanged() { + LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null); + updateRemoteVideoScaling(); + } + + @Override + public void updateFullscreenAndGreenScreenMode( + boolean shouldShowFullscreen, boolean shouldShowGreenScreen) { + LogUtil.i( + "VideoCallFragment.updateFullscreenAndGreenScreenMode", + "shouldShowFullscreen: %b, shouldShowGreenScreen: %b", + shouldShowFullscreen, + shouldShowGreenScreen); + + if (getActivity() == null) { + LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity"); + return; + } + + // Check if anything is actually going to change. The first time this function is called we + // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen + // and green screen modes to update even if only one has changed. That's because they both + // depend on each other. + if (hasInitializedScreenModes + && shouldShowGreenScreen == isInGreenScreenMode + && shouldShowFullscreen == isInFullscreenMode) { + LogUtil.i( + "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes"); + return; + } + hasInitializedScreenModes = true; + isInGreenScreenMode = shouldShowGreenScreen; + isInFullscreenMode = shouldShowFullscreen; + + if (getView().isAttachedToWindow() && !ActivityCompat.isInMultiWindowMode(getActivity())) { + controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets()); + } + if (shouldShowGreenScreen) { + enterGreenScreenMode(); + } else { + exitGreenScreenMode(); + } + if (shouldShowFullscreen) { + enterFullscreenMode(); + } else { + exitFullscreenMode(); + } + updateVideoOffViews(); + + OnHoldFragment onHoldFragment = + ((OnHoldFragment) + getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner)); + if (onHoldFragment != null) { + onHoldFragment.setPadTopInset(!isInFullscreenMode); + } + } + + @Override + public Fragment getVideoCallScreenFragment() { + return this; + } + + @Override + public void showButton(@InCallButtonIds int buttonId, boolean show) { + LogUtil.v( + "VideoCallFragment.showButton", + "buttonId: %s, show: %b", + InCallButtonIdsExtension.toString(buttonId), + show); + if (buttonId == InCallButtonIds.BUTTON_AUDIO) { + speakerButtonController.setEnabled(show); + } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { + muteButton.setEnabled(show); + } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { + cameraOffButton.setEnabled(show); + } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { + switchOnHoldCallController.setVisible(show); + } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) { + swapCameraButton.setEnabled(show); + } + } + + @Override + public void enableButton(@InCallButtonIds int buttonId, boolean enable) { + LogUtil.v( + "VideoCallFragment.setEnabled", + "buttonId: %s, enable: %b", + InCallButtonIdsExtension.toString(buttonId), + enable); + if (buttonId == InCallButtonIds.BUTTON_AUDIO) { + speakerButtonController.setEnabled(enable); + } else if (buttonId == InCallButtonIds.BUTTON_MUTE) { + muteButton.setEnabled(enable); + } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) { + cameraOffButton.setEnabled(enable); + } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) { + switchOnHoldCallController.setEnabled(enable); + } + } + + @Override + public void setEnabled(boolean enabled) { + LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled); + speakerButtonController.setEnabled(enabled); + muteButton.setEnabled(enabled); + cameraOffButton.setEnabled(enabled); + switchOnHoldCallController.setEnabled(enabled); + } + + @Override + public void setHold(boolean value) { + LogUtil.i("VideoCallFragment.setHold", "value: " + value); + } + + @Override + public void setCameraSwitched(boolean isBackFacingCamera) { + LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera); + } + + @Override + public void setVideoPaused(boolean isPaused) { + LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused); + cameraOffButton.setChecked(isPaused); + } + + @Override + public void setAudioState(CallAudioState audioState) { + LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState); + speakerButtonController.setAudioState(audioState); + muteButton.setChecked(audioState.isMuted()); + updateMutePreviewOverlayVisibility(); + } + + @Override + public void updateButtonStates() { + LogUtil.i("VideoCallFragment.updateButtonState", null); + speakerButtonController.updateButtonState(); + switchOnHoldCallController.updateButtonState(); + } + + @Override + public void updateInCallButtonUiColors() {} + + @Override + public Fragment getInCallButtonUiFragment() { + return this; + } + + @Override + public void showAudioRouteSelector() { + LogUtil.i("VideoCallFragment.showAudioRouteSelector", null); + AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState()) + .show(getChildFragmentManager(), null); + } + + @Override + public void onAudioRouteSelected(int audioRoute) { + LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute); + inCallButtonUiDelegate.setAudioRoute(audioRoute); + } + + @Override + public void setPrimary(@NonNull PrimaryInfo primaryInfo) { + LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString()); + contactGridManager.setPrimary(primaryInfo); + } + + @Override + public void setSecondary(@NonNull SecondaryInfo secondaryInfo) { + LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString()); + if (!isAdded()) { + savedSecondaryInfo = secondaryInfo; + return; + } + savedSecondaryInfo = null; + switchOnHoldCallController.setSecondaryInfo(secondaryInfo); + updateButtonStates(); + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner); + if (secondaryInfo.shouldShow) { + OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo); + onHoldFragment.setPadTopInset(!isInFullscreenMode); + transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment); + } else { + if (oldBanner != null) { + transaction.remove(oldBanner); + } + } + transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top); + transaction.commitAllowingStateLoss(); + } + + @Override + public void setCallState(@NonNull PrimaryCallState primaryCallState) { + LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString()); + contactGridManager.setCallState(primaryCallState); + } + + @Override + public void setEndCallButtonEnabled(boolean enabled, boolean animate) { + LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled); + } + + @Override + public void showManageConferenceCallButton(boolean visible) { + LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible); + } + + @Override + public boolean isManageConferenceVisible() { + LogUtil.i("VideoCallFragment.isManageConferenceVisible", null); + return false; + } + + @Override + public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + contactGridManager.dispatchPopulateAccessibilityEvent(event); + } + + @Override + public void showNoteSentToast() { + LogUtil.i("VideoCallFragment.showNoteSentToast", null); + } + + @Override + public void updateInCallScreenColors() { + LogUtil.i("VideoCallFragment.updateColors", null); + } + + @Override + public void onInCallScreenDialpadVisibilityChange(boolean isShowing) { + LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null); + } + + @Override + public int getAnswerAndDialpadContainerResourceId() { + return 0; + } + + @Override + public Fragment getInCallScreenFragment() { + return this; + } + + @Override + public boolean isShowingLocationUi() { + return false; + } + + @Override + public void showLocationUi(Fragment locationUi) { + LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported"); + // Do nothing + } + + private void updatePreviewVideoScaling() { + if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) { + LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet"); + return; + } + VideoSurfaceTexture localVideoSurfaceTexture = + videoCallScreenDelegate.getLocalVideoSurfaceTexture(); + Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions(); + if (cameraDimensions == null) { + LogUtil.i( + "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set"); + return; + } + if (isLandscape()) { + VideoSurfaceBindings.scaleVideoAndFillView( + previewTextureView, + cameraDimensions.x, + cameraDimensions.y, + videoCallScreenDelegate.getDeviceOrientation()); + } else { + VideoSurfaceBindings.scaleVideoAndFillView( + previewTextureView, + cameraDimensions.y, + cameraDimensions.x, + videoCallScreenDelegate.getDeviceOrientation()); + } + } + + private void updateRemoteVideoScaling() { + VideoSurfaceTexture remoteVideoSurfaceTexture = + videoCallScreenDelegate.getRemoteVideoSurfaceTexture(); + Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions(); + if (videoSize == null) { + LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null"); + return; + } + if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) { + LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet"); + return; + } + + // If the video and display aspect ratio's are close then scale video to fill display + float videoAspectRatio = ((float) videoSize.x) / videoSize.y; + float displayAspectRatio = + ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight(); + float delta = Math.abs(videoAspectRatio - displayAspectRatio); + float sum = videoAspectRatio + displayAspectRatio; + if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) { + VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0); + } else { + VideoSurfaceBindings.scaleVideoMaintainingAspectRatio( + remoteTextureView, videoSize.x, videoSize.y); + } + } + + private boolean isLandscape() { + // Choose orientation based on display orientation, not window orientation + int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation(); + return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270; + } + + private void enterGreenScreenMode() { + LogUtil.i("VideoCallFragment.enterGreenScreenMode", null); + RelativeLayout.LayoutParams params = + new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT); + params.addRule(RelativeLayout.ALIGN_PARENT_START); + params.addRule(RelativeLayout.ALIGN_PARENT_TOP); + previewTextureView.setLayoutParams(params); + previewTextureView.setOutlineProvider(null); + updatePreviewVideoScaling(); + updateOverlayBackground(); + contactGridManager.setIsMiddleRowVisible(true); + updateMutePreviewOverlayVisibility(); + + previewOffBlurredImageView.setLayoutParams(params); + previewOffBlurredImageView.setOutlineProvider(null); + previewOffBlurredImageView.setClipToOutline(false); + } + + private void exitGreenScreenMode() { + LogUtil.i("VideoCallFragment.exitGreenScreenMode", null); + Resources resources = getResources(); + RelativeLayout.LayoutParams params = + new RelativeLayout.LayoutParams( + (int) resources.getDimension(R.dimen.videocall_preview_width), + (int) resources.getDimension(R.dimen.videocall_preview_height)); + params.setMargins( + 0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom)); + if (isLandscape()) { + params.addRule(RelativeLayout.ALIGN_PARENT_END); + params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end)); + } else { + params.addRule(RelativeLayout.ALIGN_PARENT_START); + params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start)); + } + params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); + previewTextureView.setLayoutParams(params); + previewTextureView.setOutlineProvider(circleOutlineProvider); + updatePreviewVideoScaling(); + updateOverlayBackground(); + contactGridManager.setIsMiddleRowVisible(false); + updateMutePreviewOverlayVisibility(); + + previewOffBlurredImageView.setLayoutParams(params); + previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider); + previewOffBlurredImageView.setClipToOutline(true); + } + + private void updateVideoOffViews() { + // Always hide the preview off and remote off views in green screen mode. + boolean previewEnabled = isInGreenScreenMode || shouldShowPreview; + previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE); + updateBlurredImageView( + previewTextureView, + previewOffBlurredImageView, + shouldShowPreview, + BLUR_PREVIEW_RADIUS, + BLUR_PREVIEW_SCALE_FACTOR); + + boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote; + boolean isResumed = remoteEnabled && !isRemotelyHeld; + if (isResumed) { + boolean wasRemoteVideoOff = + TextUtils.equals( + remoteVideoOff.getText(), + remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off)); + // The text needs to be updated and hidden after enough delay in order to be announced by + // talkback. + remoteVideoOff.setText( + wasRemoteVideoOff + ? R.string.videocall_remote_video_on + : R.string.videocall_remotely_resumed); + remoteVideoOff.postDelayed( + new Runnable() { + @Override + public void run() { + remoteVideoOff.setVisibility(View.GONE); + } + }, + VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS); + } else { + remoteVideoOff.setText( + isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off); + remoteVideoOff.setVisibility(View.VISIBLE); + } + LogUtil.i("VideoCallFragment.updateVideoOffViews", "calling updateBlurredImageView"); + updateBlurredImageView( + remoteTextureView, + remoteOffBlurredImageView, + shouldShowRemote, + BLUR_REMOTE_RADIUS, + BLUR_REMOTE_SCALE_FACTOR); + } + + private void updateBlurredImageView( + TextureView textureView, + ImageView blurredImageView, + boolean isVideoEnabled, + float blurRadius, + float scaleFactor) { + boolean didBlur = false; + long startTimeMillis = SystemClock.elapsedRealtime(); + if (!isVideoEnabled) { + int width = Math.round(textureView.getWidth() * scaleFactor); + int height = Math.round(textureView.getHeight() * scaleFactor); + // This call takes less than 10 milliseconds. + Bitmap bitmap = textureView.getBitmap(width, height); + if (bitmap != null) { + // TODO: When the view is first displayed after a rotation the bitmap is empty + // and thus this blur has no effect. + // This call can take 100 milliseconds. + blur(getContext(), bitmap, blurRadius); + + // TODO: Figure out why only have to apply the transform in landscape mode + if (width > height) { + bitmap = + Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.getWidth(), + bitmap.getHeight(), + textureView.getTransform(null), + true); + } + + blurredImageView.setImageBitmap(bitmap); + blurredImageView.setVisibility(View.VISIBLE); + didBlur = true; + } + } + if (!didBlur) { + blurredImageView.setImageBitmap(null); + blurredImageView.setVisibility(View.GONE); + } + + LogUtil.i( + "VideoCallFragment.updateBlurredImageView", + "didBlur: %b, took %d millis", + didBlur, + (SystemClock.elapsedRealtime() - startTimeMillis)); + } + + private void updateOverlayBackground() { + if (isInGreenScreenMode) { + // We want to darken the preview view to make text and buttons readable. The fullscreen + // background is below the preview view so use the green screen background instead. + animateSetVisibility(greenScreenBackgroundView, View.VISIBLE); + animateSetVisibility(fullscreenBackgroundView, View.GONE); + } else if (!isInFullscreenMode) { + // We want to darken the remote view to make text and buttons readable. The green screen + // background is above the preview view so it would darken the preview too. Use the fullscreen + // background instead. + animateSetVisibility(greenScreenBackgroundView, View.GONE); + animateSetVisibility(fullscreenBackgroundView, View.VISIBLE); + } else { + animateSetVisibility(greenScreenBackgroundView, View.GONE); + animateSetVisibility(fullscreenBackgroundView, View.GONE); + } + } + + private void updateMutePreviewOverlayVisibility() { + // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen + // mode the preview is fullscreen so there's no where to anchor it. + mutePreviewOverlay.setVisibility( + muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE); + } + + private static void animateSetVisibility(final View view, final int visibility) { + if (view.getVisibility() == visibility) { + return; + } + + int startAlpha; + int endAlpha; + if (visibility == View.GONE) { + startAlpha = 1; + endAlpha = 0; + } else if (visibility == View.VISIBLE) { + startAlpha = 0; + endAlpha = 1; + } else { + Assert.fail(); + return; + } + + view.setAlpha(startAlpha); + view.setVisibility(View.VISIBLE); + view.animate() + .alpha(endAlpha) + .withEndAction( + new Runnable() { + @Override + public void run() { + view.setVisibility(visibility); + } + }) + .start(); + } + + private static void blur(Context context, Bitmap image, float blurRadius) { + RenderScript renderScript = RenderScript.create(context); + ScriptIntrinsicBlur blurScript = + ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)); + Allocation allocationIn = Allocation.createFromBitmap(renderScript, image); + Allocation allocationOut = Allocation.createFromBitmap(renderScript, image); + blurScript.setRadius(blurRadius); + blurScript.setInput(allocationIn); + blurScript.forEach(allocationOut); + allocationOut.copyTo(image); + } + + @Override + public void onSystemUiVisibilityChange(int visibility) { + boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible); + } + + protected void onCameraPermissionGranted() { + videoCallScreenDelegate.onCameraPermissionGranted(); + } + + private void checkCameraPermission() { + // Checks if user has consent of camera permission and the permission is granted. + // If camera permission is revoked, shows system permission dialog. + // If camera permission is granted but user doesn't have consent of camera permission + // (which means it's first time making video call), shows custom dialog instead. This + // will only be shown to user once. + if (!VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) { + videoCallScreenDelegate.onCameraPermissionDialogShown(); + if (!VideoUtils.hasCameraPermission(getContext())) { + requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE); + } else { + CameraPermissionDialogFragment.newInstance() + .show(getChildFragmentManager(), CAMERA_PERMISSION_DIALOG_FRAMENT_TAG); + } + } + } +} |