/* * Copyright (C) 2014 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; import android.app.Activity; import android.content.Context; import android.graphics.Point; import android.os.Handler; import android.support.annotation.Nullable; import android.telecom.InCallService.VideoCall; import android.telecom.VideoProfile; import android.telecom.VideoProfile.CameraCapabilities; import android.view.Surface; import android.view.SurfaceView; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.CompatUtils; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.util.PermissionsUtil; import com.android.incallui.InCallPresenter.InCallDetailsListener; import com.android.incallui.InCallPresenter.InCallOrientationListener; import com.android.incallui.InCallPresenter.InCallStateListener; import com.android.incallui.InCallPresenter.IncomingCallListener; import com.android.incallui.call.CallList; import com.android.incallui.call.DialerCall; import com.android.incallui.call.DialerCall.CameraDirection; import com.android.incallui.call.DialerCall.State; import com.android.incallui.call.InCallVideoCallCallbackNotifier; import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener; import com.android.incallui.util.AccessibilityUtil; import com.android.incallui.video.protocol.VideoCallScreen; import com.android.incallui.video.protocol.VideoCallScreenDelegate; import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate; import com.android.incallui.videosurface.protocol.VideoSurfaceTexture; import com.android.incallui.videotech.utils.SessionModificationState; import com.android.incallui.videotech.utils.VideoUtils; import java.util.Objects; /** * Logic related to the {@link VideoCallScreen} and for managing changes to the video calling * surfaces based on other user interface events and incoming events from the {@class * VideoCallListener}. * *

When a call's video state changes to bi-directional video, the {@link * com.android.incallui.VideoCallPresenter} performs the following negotiation with the telephony * layer: * *

* *

When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both * surfaces. */ public class VideoCallPresenter implements IncomingCallListener, InCallOrientationListener, InCallStateListener, InCallDetailsListener, SurfaceChangeListener, InCallPresenter.InCallEventListener, VideoCallScreenDelegate { private static boolean isVideoMode = false; private final Handler handler = new Handler(); private VideoCallScreen videoCallScreen; /** The current context. */ private Context context; /** The call the video surfaces are currently related to */ private DialerCall primaryCall; /** * The {@link VideoCall} used to inform the video telephony layer of changes to the video * surfaces. */ private VideoCall videoCall; /** Determines if the current UI state represents a video call. */ private int currentVideoState; /** DialerCall's current state */ private int currentCallState = DialerCall.State.INVALID; /** Determines the device orientation (portrait/lanscape). */ private int deviceOrientation = InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN; /** Tracks the state of the preview surface negotiation with the telephony layer. */ private int previewSurfaceState = PreviewSurfaceState.NONE; /** * Determines whether video calls should automatically enter full screen mode after {@link * #autoFullscreenTimeoutMillis} milliseconds. */ private boolean isAutoFullscreenEnabled = false; /** * Determines the number of milliseconds after which a video call will automatically enter * fullscreen mode. Requires {@link #isAutoFullscreenEnabled} to be {@code true}. */ private int autoFullscreenTimeoutMillis = 0; /** * Determines if the countdown is currently running to automatically enter full screen video mode. */ private boolean autoFullScreenPending = false; /** Whether if the call is remotely held. */ private boolean isRemotelyHeld = false; /** * Runnable which is posted to schedule automatically entering fullscreen mode. Will not auto * enter fullscreen mode if the dialpad is visible (doing so would make it impossible to exit the * dialpad). */ private Runnable autoFullscreenRunnable = new Runnable() { @Override public void run() { if (autoFullScreenPending && !InCallPresenter.getInstance().isDialpadVisible() && isVideoMode) { LogUtil.v("VideoCallPresenter.mAutoFullScreenRunnable", "entering fullscreen mode"); InCallPresenter.getInstance().setFullScreen(true); autoFullScreenPending = false; } else { LogUtil.v( "VideoCallPresenter.mAutoFullScreenRunnable", "skipping scheduled fullscreen mode."); } } }; private boolean isVideoCallScreenUiReady; private static boolean isCameraRequired(int videoState, int sessionModificationState) { return VideoProfile.isBidirectional(videoState) || VideoProfile.isTransmissionEnabled(videoState) || isVideoUpgrade(sessionModificationState); } /** * Determines if the incoming video surface should be shown based on the current videoState and * callState. The video surface is shown when incoming video is not paused, the call is active or * dialing and video reception is enabled. * * @param videoState The current video state. * @param callState The current call state. * @return {@code true} if the incoming video surface should be shown, {@code false} otherwise. */ public static boolean showIncomingVideo(int videoState, int callState) { if (!CompatUtils.isVideoCompatible()) { return false; } boolean isPaused = VideoProfile.isPaused(videoState); boolean isCallActive = callState == DialerCall.State.ACTIVE; // Show incoming Video for dialing calls to support early media boolean isCallOutgoingPending = DialerCall.State.isDialing(callState) || callState == DialerCall.State.CONNECTING; return !isPaused && (isCallActive || isCallOutgoingPending) && VideoProfile.isReceptionEnabled(videoState); } /** * Determines if the outgoing video surface should be shown based on the current videoState. The * video surface is shown if video transmission is enabled. * * @return {@code true} if the the outgoing video surface should be shown, {@code false} * otherwise. */ public static boolean showOutgoingVideo( Context context, int videoState, int sessionModificationState) { if (!VideoUtils.hasCameraPermissionAndShownPrivacyToast(context)) { LogUtil.i("VideoCallPresenter.showOutgoingVideo", "Camera permission is disabled by user."); return false; } if (!CompatUtils.isVideoCompatible()) { return false; } return VideoProfile.isTransmissionEnabled(videoState) || isVideoUpgrade(sessionModificationState); } private static void updateCameraSelection(DialerCall call) { LogUtil.v("VideoCallPresenter.updateCameraSelection", "call=" + call); LogUtil.v("VideoCallPresenter.updateCameraSelection", "call=" + toSimpleString(call)); final DialerCall activeCall = CallList.getInstance().getActiveCall(); int cameraDir; // this function should never be called with null call object, however if it happens we // should handle it gracefully. if (call == null) { cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN; LogUtil.e( "VideoCallPresenter.updateCameraSelection", "call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)"); } // Clear camera direction if this is not a video call. else if (isAudioCall(call) && !isVideoUpgrade(call)) { cameraDir = CameraDirection.CAMERA_DIRECTION_UNKNOWN; call.setCameraDir(cameraDir); } // If this is a waiting video call, default to active call's camera, // since we don't want to change the current camera for waiting call // without user's permission. else if (isVideoCall(activeCall) && isIncomingVideoCall(call)) { cameraDir = activeCall.getCameraDir(); } // Infer the camera direction from the video state and store it, // if this is an outgoing video call. else if (isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) { cameraDir = toCameraDirection(call.getVideoState()); call.setCameraDir(cameraDir); } // Use the stored camera dir if this is an outgoing video call for which camera direction // is set. else if (isOutgoingVideoCall(call)) { cameraDir = call.getCameraDir(); } // Infer the camera direction from the video state and store it, // if this is an active video call and camera direction is not set. else if (isActiveVideoCall(call) && !isCameraDirectionSet(call)) { cameraDir = toCameraDirection(call.getVideoState()); call.setCameraDir(cameraDir); } // Use the stored camera dir if this is an active video call for which camera direction // is set. else if (isActiveVideoCall(call)) { cameraDir = call.getCameraDir(); } // For all other cases infer the camera direction but don't store it in the call object. else { cameraDir = toCameraDirection(call.getVideoState()); } LogUtil.i( "VideoCallPresenter.updateCameraSelection", "setting camera direction to %d, call: %s", cameraDir, call); final InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); cameraManager.setUseFrontFacingCamera( cameraDir == CameraDirection.CAMERA_DIRECTION_FRONT_FACING); } private static int toCameraDirection(int videoState) { return VideoProfile.isTransmissionEnabled(videoState) && !VideoProfile.isBidirectional(videoState) ? CameraDirection.CAMERA_DIRECTION_BACK_FACING : CameraDirection.CAMERA_DIRECTION_FRONT_FACING; } private static boolean isCameraDirectionSet(DialerCall call) { return isVideoCall(call) && call.getCameraDir() != CameraDirection.CAMERA_DIRECTION_UNKNOWN; } private static String toSimpleString(DialerCall call) { return call == null ? null : call.toSimpleString(); } /** * Initializes the presenter. * * @param context The current context. */ @Override public void initVideoCallScreenDelegate(Context context, VideoCallScreen videoCallScreen) { this.context = context; this.videoCallScreen = videoCallScreen; isAutoFullscreenEnabled = this.context.getResources().getBoolean(R.bool.video_call_auto_fullscreen); autoFullscreenTimeoutMillis = this.context.getResources().getInteger(R.integer.video_call_auto_fullscreen_timeout); } /** Called when the user interface is ready to be used. */ @Override public void onVideoCallScreenUiReady() { LogUtil.v("VideoCallPresenter.onVideoCallScreenUiReady", ""); Assert.checkState(!isVideoCallScreenUiReady); // Do not register any listeners if video calling is not compatible to safeguard against // any accidental calls of video calling code. if (!CompatUtils.isVideoCompatible()) { return; } deviceOrientation = InCallOrientationEventListener.getCurrentOrientation(); // Register for call state changes last InCallPresenter.getInstance().addListener(this); InCallPresenter.getInstance().addDetailsListener(this); InCallPresenter.getInstance().addIncomingCallListener(this); InCallPresenter.getInstance().addOrientationListener(this); // To get updates of video call details changes InCallPresenter.getInstance().addInCallEventListener(this); InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(new LocalDelegate()); InCallPresenter.getInstance().getRemoteVideoSurfaceTexture().setDelegate(new RemoteDelegate()); // Register for surface and video events from {@link InCallVideoCallListener}s. InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this); currentVideoState = VideoProfile.STATE_AUDIO_ONLY; currentCallState = DialerCall.State.INVALID; InCallPresenter.InCallState inCallState = InCallPresenter.getInstance().getInCallState(); onStateChange(inCallState, inCallState, CallList.getInstance()); isVideoCallScreenUiReady = true; } /** Called when the user interface is no longer ready to be used. */ @Override public void onVideoCallScreenUiUnready() { LogUtil.v("VideoCallPresenter.onVideoCallScreenUiUnready", ""); Assert.checkState(isVideoCallScreenUiReady); if (!CompatUtils.isVideoCompatible()) { return; } cancelAutoFullScreen(); InCallPresenter.getInstance().removeListener(this); InCallPresenter.getInstance().removeDetailsListener(this); InCallPresenter.getInstance().removeIncomingCallListener(this); InCallPresenter.getInstance().removeOrientationListener(this); InCallPresenter.getInstance().removeInCallEventListener(this); InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null); InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this); // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this // happens after any call state changes but we're unregistering from InCallPresenter above so // we won't get any more call state changes. See a bug. if (primaryCall != null) { updateCameraSelection(primaryCall); } isVideoCallScreenUiReady = false; } /** * Handles clicks on the video surfaces. If not currently in fullscreen mode, will set fullscreen. */ private void onSurfaceClick() { LogUtil.i("VideoCallPresenter.onSurfaceClick", ""); cancelAutoFullScreen(); if (!InCallPresenter.getInstance().isFullscreen()) { InCallPresenter.getInstance().setFullScreen(true); } else { InCallPresenter.getInstance().setFullScreen(false); maybeAutoEnterFullscreen(primaryCall); // If Activity is not multiwindow, fullscreen will be driven by SystemUI visibility changes // instead. See #onSystemUiVisibilityChange(boolean) // TODO (keyboardr): onSystemUiVisibilityChange isn't being called the first time // visibility changes after orientation change, so this is currently always done as a backup. } } @Override public void onSystemUiVisibilityChange(boolean visible) { // If the SystemUI has changed to be visible, take us out of fullscreen mode LogUtil.i("VideoCallPresenter.onSystemUiVisibilityChange", "visible: " + visible); if (visible) { InCallPresenter.getInstance().setFullScreen(false); maybeAutoEnterFullscreen(primaryCall); } } @Override public VideoSurfaceTexture getLocalVideoSurfaceTexture() { return InCallPresenter.getInstance().getLocalVideoSurfaceTexture(); } @Override public VideoSurfaceTexture getRemoteVideoSurfaceTexture() { return InCallPresenter.getInstance().getRemoteVideoSurfaceTexture(); } @Override public void setSurfaceViews(SurfaceView preview, SurfaceView remote) { throw Assert.createUnsupportedOperationFailException(); } @Override public int getDeviceOrientation() { return deviceOrientation; } /** * This should only be called when user approved the camera permission, which is local action and * does NOT change any call states. */ @Override public void onCameraPermissionGranted() { LogUtil.i("VideoCallPresenter.onCameraPermissionGranted", ""); PermissionsUtil.setCameraPrivacyToastShown(context); enableCamera(primaryCall, isCameraRequired()); showVideoUi( primaryCall.getVideoState(), primaryCall.getState(), primaryCall.getVideoTech().getSessionModificationState(), primaryCall.isRemotelyHeld()); InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted(); } /** * Called when the user interacts with the UI. If a fullscreen timer is pending then we start the * timer from scratch to avoid having the UI disappear while the user is interacting with it. */ @Override public void resetAutoFullscreenTimer() { if (autoFullScreenPending) { LogUtil.i("VideoCallPresenter.resetAutoFullscreenTimer", "resetting"); handler.removeCallbacks(autoFullscreenRunnable); handler.postDelayed(autoFullscreenRunnable, autoFullscreenTimeoutMillis); } } /** * Handles incoming calls. * * @param oldState The old in call state. * @param newState The new in call state. * @param call The call. */ @Override public void onIncomingCall( InCallPresenter.InCallState oldState, InCallPresenter.InCallState newState, DialerCall call) { // If video call screen ui is already destroyed, this shouldn't be called. But the UI may be // updated synchronized by {@link CallCardPresenter#onIncomingCall} before this is called, this // could still be called. Thus just do nothing in this case. if (!isVideoCallScreenUiReady) { LogUtil.i("VideoCallPresenter.onIncomingCall", "UI is not ready"); return; } // same logic should happen as with onStateChange() onStateChange(oldState, newState, CallList.getInstance()); } /** * Handles state changes (including incoming calls) * * @param newState The in call state. * @param callList The call list. */ @Override public void onStateChange( InCallPresenter.InCallState oldState, InCallPresenter.InCallState newState, CallList callList) { LogUtil.v( "VideoCallPresenter.onStateChange", "oldState: %s, newState: %s, isVideoMode: %b", oldState, newState, isVideoMode()); if (newState == InCallPresenter.InCallState.NO_CALLS) { if (isVideoMode()) { exitVideoMode(); } InCallPresenter.getInstance().cleanupSurfaces(); } // Determine the primary active call). DialerCall primary = null; // Determine the call which is the focus of the user's attention. In the case of an // incoming call waiting call, the primary call is still the active video call, however // the determination of whether we should be in fullscreen mode is based on the type of the // incoming call, not the active video call. DialerCall currentCall = null; if (newState == InCallPresenter.InCallState.INCOMING) { // We don't want to replace active video call (primary call) // with a waiting call, since user may choose to ignore/decline the waiting call and // this should have no impact on current active video call, that is, we should not // change the camera or UI unless the waiting VT call becomes active. primary = callList.getActiveCall(); currentCall = callList.getIncomingCall(); if (!isActiveVideoCall(primary)) { primary = callList.getIncomingCall(); } } else if (newState == InCallPresenter.InCallState.OUTGOING) { currentCall = primary = callList.getOutgoingCall(); } else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) { currentCall = primary = callList.getPendingOutgoingCall(); } else if (newState == InCallPresenter.InCallState.INCALL) { currentCall = primary = callList.getActiveCall(); } final boolean primaryChanged = !Objects.equals(primaryCall, primary); LogUtil.i( "VideoCallPresenter.onStateChange", "primaryChanged: %b, primary: %s, mPrimaryCall: %s", primaryChanged, primary, primaryCall); if (primaryChanged) { onPrimaryCallChanged(primary); } else if (primaryCall != null) { updateVideoCall(primary); } updateCallCache(primary); // If the call context changed, potentially exit fullscreen or schedule auto enter of // fullscreen mode. // If the current call context is no longer a video call, exit fullscreen mode. maybeExitFullscreen(currentCall); // Schedule auto-enter of fullscreen mode if the current call context is a video call maybeAutoEnterFullscreen(currentCall); } /** * Handles a change to the fullscreen mode of the app. * * @param isFullscreenMode {@code true} if the app is now fullscreen, {@code false} otherwise. */ @Override public void onFullscreenModeChanged(boolean isFullscreenMode) { cancelAutoFullScreen(); if (primaryCall != null) { updateFullscreenAndGreenScreenMode( primaryCall.getState(), primaryCall.getVideoTech().getSessionModificationState()); } else { updateFullscreenAndGreenScreenMode(State.INVALID, SessionModificationState.NO_REQUEST); } } private void checkForVideoStateChange(DialerCall call) { final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call); final boolean hasVideoStateChanged = currentVideoState != call.getVideoState(); LogUtil.v( "VideoCallPresenter.checkForVideoStateChange", "shouldShowVideoUi: %b, hasVideoStateChanged: %b, isVideoMode: %b, previousVideoState: %s," + " newVideoState: %s", shouldShowVideoUi, hasVideoStateChanged, isVideoMode(), VideoProfile.videoStateToString(currentVideoState), VideoProfile.videoStateToString(call.getVideoState())); if (!hasVideoStateChanged) { return; } updateCameraSelection(call); if (shouldShowVideoUi) { adjustVideoMode(call); } else if (isVideoMode()) { exitVideoMode(); } } private void checkForCallStateChange(DialerCall call) { final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call); final boolean hasCallStateChanged = currentCallState != call.getState() || isRemotelyHeld != call.isRemotelyHeld(); isRemotelyHeld = call.isRemotelyHeld(); LogUtil.v( "VideoCallPresenter.checkForCallStateChange", "shouldShowVideoUi: %b, hasCallStateChanged: %b, isVideoMode: %b", shouldShowVideoUi, hasCallStateChanged, isVideoMode()); if (!hasCallStateChanged) { return; } if (shouldShowVideoUi) { final InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); String prevCameraId = cameraManager.getActiveCameraId(); updateCameraSelection(call); String newCameraId = cameraManager.getActiveCameraId(); if (!Objects.equals(prevCameraId, newCameraId) && isActiveVideoCall(call)) { enableCamera(call, true); } } // Make sure we hide or show the video UI if needed. showVideoUi( call.getVideoState(), call.getState(), call.getVideoTech().getSessionModificationState(), call.isRemotelyHeld()); } private void onPrimaryCallChanged(DialerCall newPrimaryCall) { final boolean shouldShowVideoUi = shouldShowVideoUiForCall(newPrimaryCall); final boolean isVideoMode = isVideoMode(); LogUtil.v( "VideoCallPresenter.onPrimaryCallChanged", "shouldShowVideoUi: %b, isVideoMode: %b", shouldShowVideoUi, isVideoMode); if (!shouldShowVideoUi && isVideoMode) { // Terminate video mode if new primary call is not a video call // and we are currently in video mode. LogUtil.i("VideoCallPresenter.onPrimaryCallChanged", "exiting video mode..."); exitVideoMode(); } else if (shouldShowVideoUi) { LogUtil.i("VideoCallPresenter.onPrimaryCallChanged", "entering video mode..."); updateCameraSelection(newPrimaryCall); adjustVideoMode(newPrimaryCall); } checkForOrientationAllowedChange(newPrimaryCall); } private boolean isVideoMode() { return isVideoMode; } private void updateCallCache(DialerCall call) { if (call == null) { currentVideoState = VideoProfile.STATE_AUDIO_ONLY; currentCallState = DialerCall.State.INVALID; videoCall = null; primaryCall = null; } else { currentVideoState = call.getVideoState(); videoCall = call.getVideoCall(); currentCallState = call.getState(); primaryCall = call; } } /** * Handles changes to the details of the call. The {@link VideoCallPresenter} is interested in * changes to the video state. * * @param call The call for which the details changed. * @param details The new call details. */ @Override public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) { LogUtil.v( "VideoCallPresenter.onDetailsChanged", "call: %s, details: %s, mPrimaryCall: %s", call, details, primaryCall); if (call == null) { return; } // If the details change is not for the currently active call no update is required. if (!call.equals(primaryCall)) { LogUtil.v("VideoCallPresenter.onDetailsChanged", "details not for current active call"); return; } updateVideoCall(call); updateCallCache(call); } private void updateVideoCall(DialerCall call) { checkForVideoCallChange(call); checkForVideoStateChange(call); checkForCallStateChange(call); checkForOrientationAllowedChange(call); updateFullscreenAndGreenScreenMode( call.getState(), call.getVideoTech().getSessionModificationState()); } private void checkForOrientationAllowedChange(@Nullable DialerCall call) { InCallPresenter.getInstance() .setInCallAllowsOrientationChange(isVideoCall(call) || isVideoUpgrade(call)); } private void updateFullscreenAndGreenScreenMode( int callState, @SessionModificationState int sessionModificationState) { if (videoCallScreen != null) { boolean shouldShowFullscreen = InCallPresenter.getInstance().isFullscreen(); boolean shouldShowGreenScreen = callState == State.DIALING || callState == State.CONNECTING || callState == State.INCOMING || isVideoUpgrade(sessionModificationState); videoCallScreen.updateFullscreenAndGreenScreenMode( shouldShowFullscreen, shouldShowGreenScreen); } } /** Checks for a change to the video call and changes it if required. */ private void checkForVideoCallChange(DialerCall call) { final VideoCall videoCall = call.getVideoCall(); LogUtil.v( "VideoCallPresenter.checkForVideoCallChange", "videoCall: %s, mVideoCall: %s", videoCall, this.videoCall); if (!Objects.equals(videoCall, this.videoCall)) { changeVideoCall(call); } } /** * Handles a change to the video call. Sets the surfaces on the previous call to null and sets the * surfaces on the new video call accordingly. * * @param call The new video call. */ private void changeVideoCall(DialerCall call) { final VideoCall videoCall = call == null ? null : call.getVideoCall(); LogUtil.i( "VideoCallPresenter.changeVideoCall", "videoCall: %s, mVideoCall: %s", videoCall, this.videoCall); final boolean hasChanged = this.videoCall == null && videoCall != null; this.videoCall = videoCall; if (this.videoCall == null) { LogUtil.v("VideoCallPresenter.changeVideoCall", "video call or primary call is null. Return"); return; } if (shouldShowVideoUiForCall(call) && hasChanged) { adjustVideoMode(call); } } private boolean isCameraRequired() { return primaryCall != null && isCameraRequired( primaryCall.getVideoState(), primaryCall.getVideoTech().getSessionModificationState()); } /** * Adjusts the current video mode by setting up the preview and display surfaces as necessary. * Expected to be called whenever the video state associated with a call changes (e.g. a user * turns their camera on or off) to ensure the correct surfaces are shown/hidden. TODO(vt): Need * to adjust size and orientation of preview surface here. */ private void adjustVideoMode(DialerCall call) { VideoCall videoCall = call.getVideoCall(); int newVideoState = call.getVideoState(); LogUtil.i( "VideoCallPresenter.adjustVideoMode", "videoCall: %s, videoState: %d", videoCall, newVideoState); if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.adjustVideoMode", "error VideoCallScreen is null so returning"); return; } showVideoUi( newVideoState, call.getState(), call.getVideoTech().getSessionModificationState(), call.isRemotelyHeld()); // Communicate the current camera to telephony and make a request for the camera // capabilities. if (videoCall != null) { Surface surface = getRemoteVideoSurfaceTexture().getSavedSurface(); if (surface != null) { LogUtil.v( "VideoCallPresenter.adjustVideoMode", "calling setDisplaySurface with: " + surface); videoCall.setDisplaySurface(surface); } Assert.checkState( deviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN); videoCall.setDeviceOrientation(deviceOrientation); enableCamera( call, isCameraRequired(newVideoState, call.getVideoTech().getSessionModificationState())); } int previousVideoState = currentVideoState; currentVideoState = newVideoState; isVideoMode = true; // adjustVideoMode may be called if we are already in a 1-way video state. In this case // we do not want to trigger auto-fullscreen mode. if (!isVideoCall(previousVideoState) && isVideoCall(newVideoState)) { maybeAutoEnterFullscreen(call); } } private static boolean shouldShowVideoUiForCall(@Nullable DialerCall call) { if (call == null) { return false; } if (isVideoCall(call)) { return true; } if (isVideoUpgrade(call)) { return true; } return false; } private void enableCamera(DialerCall call, boolean isCameraRequired) { LogUtil.v("VideoCallPresenter.enableCamera", "call: %s, enabling: %b", call, isCameraRequired); if (call == null) { LogUtil.i("VideoCallPresenter.enableCamera", "call is null"); return; } boolean hasCameraPermission = VideoUtils.hasCameraPermissionAndShownPrivacyToast(context); if (!hasCameraPermission) { call.getVideoTech().setCamera(null); previewSurfaceState = PreviewSurfaceState.NONE; // TODO(wangqi): Inform remote party that the video is off. This is similar to a bug. } else if (isCameraRequired) { InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); call.getVideoTech().setCamera(cameraManager.getActiveCameraId()); previewSurfaceState = PreviewSurfaceState.CAMERA_SET; } else { previewSurfaceState = PreviewSurfaceState.NONE; call.getVideoTech().setCamera(null); } } /** Exits video mode by hiding the video surfaces and making other adjustments (eg. audio). */ private void exitVideoMode() { LogUtil.i("VideoCallPresenter.exitVideoMode", ""); showVideoUi( VideoProfile.STATE_AUDIO_ONLY, DialerCall.State.ACTIVE, SessionModificationState.NO_REQUEST, false /* isRemotelyHeld */); enableCamera(primaryCall, false); InCallPresenter.getInstance().setFullScreen(false); InCallPresenter.getInstance().enableScreenTimeout(false); isVideoMode = false; } /** * Based on the current video state and call state, show or hide the incoming and outgoing video * surfaces. The outgoing video surface is shown any time video is transmitting. The incoming * video surface is shown whenever the video is un-paused and active. * * @param videoState The video state. * @param callState The call state. */ private void showVideoUi( int videoState, int callState, @SessionModificationState int sessionModificationState, boolean isRemotelyHeld) { if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.showVideoUi", "videoCallScreen is null returning"); return; } boolean showIncomingVideo = showIncomingVideo(videoState, callState); boolean showOutgoingVideo = showOutgoingVideo(context, videoState, sessionModificationState); LogUtil.i( "VideoCallPresenter.showVideoUi", "showIncoming: %b, showOutgoing: %b, isRemotelyHeld: %b", showIncomingVideo, showOutgoingVideo, isRemotelyHeld); updateRemoteVideoSurfaceDimensions(); videoCallScreen.showVideoViews(showOutgoingVideo, showIncomingVideo, isRemotelyHeld); InCallPresenter.getInstance().enableScreenTimeout(VideoProfile.isAudioOnly(videoState)); updateFullscreenAndGreenScreenMode(callState, sessionModificationState); } /** * Handles peer video dimension changes. * * @param call The call which experienced a peer video dimension change. * @param width The new peer video width . * @param height The new peer video height. */ @Override public void onUpdatePeerDimensions(DialerCall call, int width, int height) { LogUtil.i("VideoCallPresenter.onUpdatePeerDimensions", "width: %d, height: %d", width, height); if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.onUpdatePeerDimensions", "videoCallScreen is null"); return; } if (!call.equals(primaryCall)) { LogUtil.e( "VideoCallPresenter.onUpdatePeerDimensions", "current call is not equal to primary"); return; } // Change size of display surface to match the peer aspect ratio if (width > 0 && height > 0 && videoCallScreen != null) { getRemoteVideoSurfaceTexture().setSourceVideoDimensions(new Point(width, height)); videoCallScreen.onRemoteVideoDimensionsChanged(); } } /** * Handles a change to the dimensions of the local camera. Receiving the camera capabilities * triggers the creation of the video * * @param call The call which experienced the camera dimension change. * @param width The new camera video width. * @param height The new camera video height. */ @Override public void onCameraDimensionsChange(DialerCall call, int width, int height) { LogUtil.i( "VideoCallPresenter.onCameraDimensionsChange", "call: %s, width: %d, height: %d", call, width, height); if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "ui is null"); return; } if (!call.equals(primaryCall)) { LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "not the primary call"); return; } previewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED; changePreviewDimensions(width, height); // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}. // If it not yet ready, it will be set when when creation completes. Surface surface = getLocalVideoSurfaceTexture().getSavedSurface(); if (surface != null) { previewSurfaceState = PreviewSurfaceState.SURFACE_SET; videoCall.setPreviewSurface(surface); } } /** * Changes the dimensions of the preview surface. * * @param width The new width. * @param height The new height. */ private void changePreviewDimensions(int width, int height) { if (videoCallScreen == null) { return; } // Resize the surface used to display the preview video getLocalVideoSurfaceTexture().setSurfaceDimensions(new Point(width, height)); videoCallScreen.onLocalVideoDimensionsChanged(); } /** * Handles changes to the device orientation. * * @param orientation The screen orientation of the device (one of: {@link * InCallOrientationEventListener#SCREEN_ORIENTATION_0}, {@link * InCallOrientationEventListener#SCREEN_ORIENTATION_90}, {@link * InCallOrientationEventListener#SCREEN_ORIENTATION_180}, {@link * InCallOrientationEventListener#SCREEN_ORIENTATION_270}). */ @Override public void onDeviceOrientationChanged(int orientation) { LogUtil.i( "VideoCallPresenter.onDeviceOrientationChanged", "orientation: %d -> %d", deviceOrientation, orientation); deviceOrientation = orientation; if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.onDeviceOrientationChanged", "videoCallScreen is null"); return; } Point previewDimensions = getLocalVideoSurfaceTexture().getSurfaceDimensions(); if (previewDimensions == null) { return; } LogUtil.v( "VideoCallPresenter.onDeviceOrientationChanged", "orientation: %d, size: %s", orientation, previewDimensions); changePreviewDimensions(previewDimensions.x, previewDimensions.y); videoCallScreen.onLocalVideoOrientationChanged(); } /** * Exits fullscreen mode if the current call context has changed to a non-video call. * * @param call The call. */ protected void maybeExitFullscreen(DialerCall call) { if (call == null) { return; } if (!isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) { LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen"); InCallPresenter.getInstance().setFullScreen(false); } } /** * Schedules auto-entering of fullscreen mode. Will not enter full screen mode if any of the * following conditions are met: 1. No call 2. DialerCall is not active 3. The current video state * is not bi-directional. 4. Already in fullscreen mode 5. In accessibility mode * * @param call The current call. */ protected void maybeAutoEnterFullscreen(DialerCall call) { if (!isAutoFullscreenEnabled) { return; } if (call == null || call.getState() != DialerCall.State.ACTIVE || !isBidirectionalVideoCall(call) || InCallPresenter.getInstance().isFullscreen() || (context != null && AccessibilityUtil.isTouchExplorationEnabled(context))) { // Ensure any previously scheduled attempt to enter fullscreen is cancelled. cancelAutoFullScreen(); return; } if (autoFullScreenPending) { LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "already pending."); return; } LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "scheduled"); autoFullScreenPending = true; handler.removeCallbacks(autoFullscreenRunnable); handler.postDelayed(autoFullscreenRunnable, autoFullscreenTimeoutMillis); } /** Cancels pending auto fullscreen mode. */ @Override public void cancelAutoFullScreen() { if (!autoFullScreenPending) { LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "none pending."); return; } LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "cancelling pending"); autoFullScreenPending = false; handler.removeCallbacks(autoFullscreenRunnable); } @Override public boolean shouldShowCameraPermissionToast() { if (primaryCall == null) { LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionToast", "null call"); return false; } if (primaryCall.didShowCameraPermission()) { LogUtil.i( "VideoCallPresenter.shouldShowCameraPermissionToast", "already shown for this call"); return false; } if (!ConfigProviderBindings.get(context).getBoolean("camera_permission_dialog_allowed", true)) { LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionToast", "disabled by config"); return false; } return !VideoUtils.hasCameraPermission(context) || !PermissionsUtil.hasCameraPrivacyToastShown(context); } @Override public void onCameraPermissionDialogShown() { if (primaryCall != null) { primaryCall.setDidShowCameraPermission(true); } } private void updateRemoteVideoSurfaceDimensions() { Activity activity = videoCallScreen.getVideoCallScreenFragment().getActivity(); if (activity != null) { Point screenSize = new Point(); activity.getWindowManager().getDefaultDisplay().getSize(screenSize); getRemoteVideoSurfaceTexture().setSurfaceDimensions(screenSize); } } private static boolean isVideoUpgrade(DialerCall call) { return call != null && (call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest()); } private static boolean isVideoUpgrade(@SessionModificationState int state) { return VideoUtils.hasSentVideoUpgradeRequest(state) || VideoUtils.hasReceivedVideoUpgradeRequest(state); } private class LocalDelegate implements VideoSurfaceDelegate { @Override public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) { if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no UI"); return; } if (videoCall == null) { LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no video call"); return; } // If the preview surface has just been created and we have already received camera // capabilities, but not yet set the surface, we will set the surface now. if (previewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) { previewSurfaceState = PreviewSurfaceState.SURFACE_SET; videoCall.setPreviewSurface(videoCallSurface.getSavedSurface()); } else if (previewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()) { enableCamera(primaryCall, true); } } @Override public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) { if (videoCall == null) { LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceReleased", "no video call"); return; } videoCall.setPreviewSurface(null); enableCamera(primaryCall, false); } @Override public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) { if (videoCall == null) { LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceDestroyed", "no video call"); return; } boolean isChangingConfigurations = InCallPresenter.getInstance().isChangingConfigurations(); if (!isChangingConfigurations) { enableCamera(primaryCall, false); } else { LogUtil.i( "VideoCallPresenter.LocalDelegate.onSurfaceDestroyed", "activity is being destroyed due to configuration changes. Not closing the camera."); } } @Override public void onSurfaceClick(VideoSurfaceTexture videoCallSurface) { VideoCallPresenter.this.onSurfaceClick(); } } private class RemoteDelegate implements VideoSurfaceDelegate { @Override public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) { if (videoCallScreen == null) { LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no UI"); return; } if (videoCall == null) { LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no video call"); return; } videoCall.setDisplaySurface(videoCallSurface.getSavedSurface()); } @Override public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) { if (videoCall == null) { LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceReleased", "no video call"); return; } videoCall.setDisplaySurface(null); } @Override public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) {} @Override public void onSurfaceClick(VideoSurfaceTexture videoCallSurface) { VideoCallPresenter.this.onSurfaceClick(); } } /** Defines the state of the preview surface negotiation with the telephony layer. */ private static class PreviewSurfaceState { /** * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet started. */ private static final int NONE = 0; /** * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet been * received. */ private static final int CAMERA_SET = 1; /** * The camera capabilties have been received from telephony, but the surface has not yet been * set on the {@link VideoCall}. */ private static final int CAPABILITIES_RECEIVED = 2; /** The surface has been set on the {@link VideoCall}. */ private static final int SURFACE_SET = 3; } private static boolean isBidirectionalVideoCall(DialerCall call) { return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState()); } private static boolean isIncomingVideoCall(DialerCall call) { if (!isVideoCall(call)) { return false; } final int state = call.getState(); return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING); } private static boolean isActiveVideoCall(DialerCall call) { return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE; } private static boolean isOutgoingVideoCall(DialerCall call) { if (!isVideoCall(call)) { return false; } final int state = call.getState(); return DialerCall.State.isDialing(state) || state == DialerCall.State.CONNECTING || state == DialerCall.State.SELECT_PHONE_ACCOUNT; } private static boolean isAudioCall(DialerCall call) { if (!CompatUtils.isVideoCompatible()) { return true; } return call != null && VideoProfile.isAudioOnly(call.getVideoState()); } private static boolean isVideoCall(@Nullable DialerCall call) { return call != null && call.isVideoCall(); } private static boolean isVideoCall(int videoState) { return CompatUtils.isVideoCompatible() && (VideoProfile.isTransmissionEnabled(videoState) || VideoProfile.isReceptionEnabled(videoState)); } }