diff options
Diffstat (limited to 'java/com/android/incallui/VideoCallPresenter.java')
-rw-r--r-- | java/com/android/incallui/VideoCallPresenter.java | 1289 |
1 files changed, 1289 insertions, 0 deletions
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java new file mode 100644 index 000000000..971b6957a --- /dev/null +++ b/java/com/android/incallui/VideoCallPresenter.java @@ -0,0 +1,1289 @@ +/* + * 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.Connection; +import android.telecom.InCallService.VideoCall; +import android.telecom.VideoProfile; +import android.telecom.VideoProfile.CameraCapabilities; +import android.view.Surface; +import com.android.dialer.common.Assert; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +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.SessionModificationState; +import com.android.incallui.call.DialerCall.State; +import com.android.incallui.call.InCallVideoCallCallbackNotifier; +import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener; +import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener; +import com.android.incallui.call.VideoUtils; +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 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}. + * + * <p>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: + * + * <ul> + * <li>{@code VideoCallPresenter} creates and informs telephony of the display surface. + * <li>{@code VideoCallPresenter} creates the preview surface. + * <li>{@code VideoCallPresenter} informs telephony of the currently selected camera. + * <li>Telephony layer sends {@link CameraCapabilities}, including the dimensions of the video for + * the current camera. + * <li>{@code VideoCallPresenter} adjusts size of the preview surface to match the aspect ratio of + * the camera. + * <li>{@code VideoCallPresenter} informs telephony of the new preview surface. + * </ul> + * + * <p>When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both + * surfaces. + */ +public class VideoCallPresenter + implements IncomingCallListener, + InCallOrientationListener, + InCallStateListener, + InCallDetailsListener, + SurfaceChangeListener, + VideoEventListener, + InCallPresenter.InCallEventListener, + VideoCallScreenDelegate { + + private static boolean mIsVideoMode = false; + + private final Handler mHandler = new Handler(); + private VideoCallScreen mVideoCallScreen; + + /** The current context. */ + private Context mContext; + + @Override + public boolean shouldShowCameraPermissionDialog() { + if (mPrimaryCall == null) { + LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call"); + return false; + } + if (mPrimaryCall.didShowCameraPermission()) { + LogUtil.i( + "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call"); + return false; + } + if (!ConfigProviderBindings.get(mContext) + .getBoolean("camera_permission_dialog_allowed", true)) { + LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config"); + return false; + } + return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext); + } + + @Override + public void onCameraPermissionDialogShown() { + if (mPrimaryCall != null) { + mPrimaryCall.setDidShowCameraPermission(true); + } + } + + /** The call the video surfaces are currently related to */ + private DialerCall mPrimaryCall; + /** + * The {@link VideoCall} used to inform the video telephony layer of changes to the video + * surfaces. + */ + private VideoCall mVideoCall; + /** Determines if the current UI state represents a video call. */ + private int mCurrentVideoState; + /** DialerCall's current state */ + private int mCurrentCallState = DialerCall.State.INVALID; + /** Determines the device orientation (portrait/lanscape). */ + private int mDeviceOrientation = InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN; + /** Tracks the state of the preview surface negotiation with the telephony layer. */ + private int mPreviewSurfaceState = PreviewSurfaceState.NONE; + /** + * Determines whether video calls should automatically enter full screen mode after {@link + * #mAutoFullscreenTimeoutMillis} milliseconds. + */ + private boolean mIsAutoFullscreenEnabled = false; + /** + * Determines the number of milliseconds after which a video call will automatically enter + * fullscreen mode. Requires {@link #mIsAutoFullscreenEnabled} to be {@code true}. + */ + private int mAutoFullscreenTimeoutMillis = 0; + /** + * Determines if the countdown is currently running to automatically enter full screen video mode. + */ + private boolean mAutoFullScreenPending = false; + /** Whether if the call is remotely held. */ + private boolean mIsRemotelyHeld = 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 mAutoFullscreenRunnable = + new Runnable() { + @Override + public void run() { + if (mAutoFullScreenPending + && !InCallPresenter.getInstance().isDialpadVisible() + && mIsVideoMode) { + + LogUtil.v("VideoCallPresenter.mAutoFullScreenRunnable", "entering fullscreen mode"); + InCallPresenter.getInstance().setFullScreen(true); + mAutoFullScreenPending = 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, + * 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; + + return !isPaused && isCallActive && 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.hasCameraPermissionAndAllowedByUser(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 = DialerCall.VideoSettings.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 (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) { + cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN; + call.getVideoSettings().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 (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) { + cameraDir = activeCall.getVideoSettings().getCameraDir(); + } + + // Infer the camera direction from the video state and store it, + // if this is an outgoing video call. + else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) { + cameraDir = toCameraDirection(call.getVideoState()); + call.getVideoSettings().setCameraDir(cameraDir); + } + + // Use the stored camera dir if this is an outgoing video call for which camera direction + // is set. + else if (VideoUtils.isOutgoingVideoCall(call)) { + cameraDir = call.getVideoSettings().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 (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) { + cameraDir = toCameraDirection(call.getVideoState()); + call.getVideoSettings().setCameraDir(cameraDir); + } + + // Use the stored camera dir if this is an active video call for which camera direction + // is set. + else if (VideoUtils.isActiveVideoCall(call)) { + cameraDir = call.getVideoSettings().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 == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING); + } + + private static int toCameraDirection(int videoState) { + return VideoProfile.isTransmissionEnabled(videoState) + && !VideoProfile.isBidirectional(videoState) + ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING + : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING; + } + + private static boolean isCameraDirectionSet(DialerCall call) { + return VideoUtils.isVideoCall(call) + && call.getVideoSettings().getCameraDir() + != DialerCall.VideoSettings.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) { + mContext = context; + mVideoCallScreen = videoCallScreen; + mIsAutoFullscreenEnabled = + mContext.getResources().getBoolean(R.bool.video_call_auto_fullscreen); + mAutoFullscreenTimeoutMillis = + mContext.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; + } + + mDeviceOrientation = 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); + InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this); + mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; + mCurrentCallState = 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); + InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(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 b/32957114. + if (mPrimaryCall != null) { + updateCameraSelection(mPrimaryCall); + } + + 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(mPrimaryCall); + // 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(mPrimaryCall); + } + } + + @Override + public VideoSurfaceTexture getLocalVideoSurfaceTexture() { + return InCallPresenter.getInstance().getLocalVideoSurfaceTexture(); + } + + @Override + public VideoSurfaceTexture getRemoteVideoSurfaceTexture() { + return InCallPresenter.getInstance().getRemoteVideoSurfaceTexture(); + } + + @Override + public int getDeviceOrientation() { + return mDeviceOrientation; + } + + /** + * 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", ""); + VideoUtils.setCameraAllowedByUser(mContext); + enableCamera(mPrimaryCall.getVideoCall(), isCameraRequired()); + showVideoUi( + mPrimaryCall.getVideoState(), + mPrimaryCall.getState(), + mPrimaryCall.getSessionModificationState(), + mPrimaryCall.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 (mAutoFullScreenPending) { + LogUtil.i("VideoCallPresenter.resetAutoFullscreenTimer", "resetting"); + mHandler.removeCallbacks(mAutoFullscreenRunnable); + mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis); + } + } + + /** + * 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) { + // 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 (!VideoUtils.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(mPrimaryCall, primary); + LogUtil.i( + "VideoCallPresenter.onStateChange", + "primaryChanged: %b, primary: %s, mPrimaryCall: %s", + primaryChanged, + primary, + mPrimaryCall); + if (primaryChanged) { + onPrimaryCallChanged(primary); + } else if (mPrimaryCall != 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 (mPrimaryCall != null) { + updateFullscreenAndGreenScreenMode( + mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState()); + } else { + updateFullscreenAndGreenScreenMode( + State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST); + } + } + + private void checkForVideoStateChange(DialerCall call) { + final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call); + final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState(); + + LogUtil.v( + "VideoCallPresenter.checkForVideoStateChange", + "shouldShowVideoUi: %b, hasVideoStateChanged: %b, isVideoMode: %b, previousVideoState: %s," + + " newVideoState: %s", + shouldShowVideoUi, + hasVideoStateChanged, + isVideoMode(), + VideoProfile.videoStateToString(mCurrentVideoState), + 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 = + mCurrentCallState != call.getState() || mIsRemotelyHeld != call.isRemotelyHeld(); + mIsRemotelyHeld = 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) && VideoUtils.isActiveVideoCall(call)) { + enableCamera(call.getVideoCall(), true); + } + } + + // Make sure we hide or show the video UI if needed. + showVideoUi( + call.getVideoState(), + call.getState(), + call.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 mIsVideoMode; + } + + private void updateCallCache(DialerCall call) { + if (call == null) { + mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; + mCurrentCallState = DialerCall.State.INVALID; + mVideoCall = null; + mPrimaryCall = null; + } else { + mCurrentVideoState = call.getVideoState(); + mVideoCall = call.getVideoCall(); + mCurrentCallState = call.getState(); + mPrimaryCall = 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, + mPrimaryCall); + if (call == null) { + return; + } + // If the details change is not for the currently active call no update is required. + if (!call.equals(mPrimaryCall)) { + 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.getSessionModificationState()); + } + + private void checkForOrientationAllowedChange(@Nullable DialerCall call) { + InCallPresenter.getInstance() + .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call)); + } + + private void updateFullscreenAndGreenScreenMode( + int callState, @SessionModificationState int sessionModificationState) { + if (mVideoCallScreen != null) { + boolean shouldShowFullscreen = InCallPresenter.getInstance().isFullscreen(); + boolean shouldShowGreenScreen = + callState == State.DIALING + || callState == State.CONNECTING + || callState == State.INCOMING + || isVideoUpgrade(sessionModificationState); + mVideoCallScreen.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, + mVideoCall); + if (!Objects.equals(videoCall, mVideoCall)) { + 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, + mVideoCall); + final boolean hasChanged = mVideoCall == null && videoCall != null; + + mVideoCall = videoCall; + if (mVideoCall == null) { + LogUtil.v("VideoCallPresenter.changeVideoCall", "video call or primary call is null. Return"); + return; + } + + if (shouldShowVideoUiForCall(call) && hasChanged) { + adjustVideoMode(call); + } + } + + private boolean isCameraRequired() { + return mPrimaryCall != null + && isCameraRequired( + mPrimaryCall.getVideoState(), mPrimaryCall.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: 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 (mVideoCallScreen == null) { + LogUtil.e("VideoCallPresenter.adjustVideoMode", "error VideoCallScreen is null so returning"); + return; + } + + showVideoUi( + newVideoState, call.getState(), call.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( + mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN); + videoCall.setDeviceOrientation(mDeviceOrientation); + enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState())); + } + int previousVideoState = mCurrentVideoState; + mCurrentVideoState = newVideoState; + mIsVideoMode = 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 (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) { + maybeAutoEnterFullscreen(call); + } + } + + private static boolean shouldShowVideoUiForCall(@Nullable DialerCall call) { + if (call == null) { + return false; + } + + if (VideoUtils.isVideoCall(call)) { + return true; + } + + if (isVideoUpgrade(call)) { + return true; + } + + return false; + } + + private void enableCamera(VideoCall videoCall, boolean isCameraRequired) { + LogUtil.v( + "VideoCallPresenter.enableCamera", + "videoCall: %s, enabling: %b", + videoCall, + isCameraRequired); + if (videoCall == null) { + LogUtil.i("VideoCallPresenter.enableCamera", "videoCall is null."); + return; + } + + boolean hasCameraPermission = VideoUtils.hasCameraPermissionAndAllowedByUser(mContext); + if (!hasCameraPermission) { + videoCall.setCamera(null); + mPreviewSurfaceState = PreviewSurfaceState.NONE; + // TODO: Inform remote party that the video is off. This is similar to b/30256571. + } else if (isCameraRequired) { + InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager(); + videoCall.setCamera(cameraManager.getActiveCameraId()); + mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET; + videoCall.requestCameraCapabilities(); + } else { + mPreviewSurfaceState = PreviewSurfaceState.NONE; + videoCall.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, + DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST, + false /* isRemotelyHeld */); + enableCamera(mVideoCall, false); + InCallPresenter.getInstance().setFullScreen(false); + + mIsVideoMode = 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 (mVideoCallScreen == null) { + LogUtil.e("VideoCallPresenter.showVideoUi", "videoCallScreen is null returning"); + return; + } + boolean showIncomingVideo = showIncomingVideo(videoState, callState); + boolean showOutgoingVideo = showOutgoingVideo(mContext, videoState, sessionModificationState); + LogUtil.i( + "VideoCallPresenter.showVideoUi", + "showIncoming: %b, showOutgoing: %b, isRemotelyHeld: %b", + showIncomingVideo, + showOutgoingVideo, + isRemotelyHeld); + updateRemoteVideoSurfaceDimensions(); + mVideoCallScreen.showVideoViews(showOutgoingVideo, showIncomingVideo, isRemotelyHeld); + + InCallPresenter.getInstance().enableScreenTimeout(VideoProfile.isAudioOnly(videoState)); + updateFullscreenAndGreenScreenMode(callState, sessionModificationState); + } + + /** + * Handles peer video pause state changes. + * + * @param call The call which paused or un-pausedvideo transmission. + * @param paused {@code True} when the video transmission is paused, {@code false} when video + * transmission resumes. + */ + @Override + public void onPeerPauseStateChanged(DialerCall call, boolean paused) { + if (!call.equals(mPrimaryCall)) { + return; + } + } + + /** + * 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 (mVideoCallScreen == null) { + LogUtil.e("VideoCallPresenter.onUpdatePeerDimensions", "videoCallScreen is null"); + return; + } + if (!call.equals(mPrimaryCall)) { + 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 && mVideoCallScreen != null) { + getRemoteVideoSurfaceTexture().setSourceVideoDimensions(new Point(width, height)); + mVideoCallScreen.onRemoteVideoDimensionsChanged(); + } + } + + /** + * Handles any video quality changes in the call. + * + * @param call The call which experienced a video quality change. + * @param videoQuality The new video call quality. + */ + @Override + public void onVideoQualityChanged(DialerCall call, int videoQuality) { + // No-op + } + + /** + * 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 (mVideoCallScreen == null) { + LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "ui is null"); + return; + } + + if (!call.equals(mPrimaryCall)) { + LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "not the primary call"); + return; + } + + mPreviewSurfaceState = 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) { + mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; + mVideoCall.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 (mVideoCallScreen == null) { + return; + } + + // Resize the surface used to display the preview video + getLocalVideoSurfaceTexture().setSurfaceDimensions(new Point(width, height)); + mVideoCallScreen.onLocalVideoDimensionsChanged(); + } + + /** + * Called when call session event is raised. + * + * @param event The call session event. + */ + @Override + public void onCallSessionEvent(int event) { + switch (event) { + case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE: + LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause"); + break; + case Connection.VideoProvider.SESSION_EVENT_RX_RESUME: + LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume"); + break; + case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE: + LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure"); + break; + case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY: + LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready"); + break; + default: + LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event); + break; + } + } + + /** + * Handles a change to the call data usage + * + * @param dataUsage call data usage value + */ + @Override + public void onCallDataUsageChange(long dataUsage) { + LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage); + } + + /** + * 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", + mDeviceOrientation, + orientation); + mDeviceOrientation = orientation; + + if (mVideoCallScreen == 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); + + mVideoCallScreen.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 (!VideoUtils.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 (!mIsAutoFullscreenEnabled) { + return; + } + + if (call == null + || call.getState() != DialerCall.State.ACTIVE + || !VideoUtils.isBidirectionalVideoCall(call) + || InCallPresenter.getInstance().isFullscreen() + || (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) { + // Ensure any previously scheduled attempt to enter fullscreen is cancelled. + cancelAutoFullScreen(); + return; + } + + if (mAutoFullScreenPending) { + LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "already pending."); + return; + } + LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "scheduled"); + mAutoFullScreenPending = true; + mHandler.removeCallbacks(mAutoFullscreenRunnable); + mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis); + } + + /** Cancels pending auto fullscreen mode. */ + @Override + public void cancelAutoFullScreen() { + if (!mAutoFullScreenPending) { + LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "none pending."); + return; + } + LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "cancelling pending"); + mAutoFullScreenPending = false; + mHandler.removeCallbacks(mAutoFullscreenRunnable); + } + + private void updateRemoteVideoSurfaceDimensions() { + Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity(); + if (activity != null) { + Point screenSize = new Point(); + activity.getWindowManager().getDefaultDisplay().getSize(screenSize); + getRemoteVideoSurfaceTexture().setSurfaceDimensions(screenSize); + } + } + + private static boolean isVideoUpgrade(DialerCall call) { + return VideoUtils.hasSentVideoUpgradeRequest(call) + || VideoUtils.hasReceivedVideoUpgradeRequest(call); + } + + 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 (mVideoCallScreen == null) { + LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no UI"); + return; + } + if (mVideoCall == 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 (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) { + mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; + mVideoCall.setPreviewSurface(videoCallSurface.getSavedSurface()); + } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()) { + enableCamera(mVideoCall, true); + } + } + + @Override + public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) { + if (mVideoCall == null) { + LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceReleased", "no video call"); + return; + } + + mVideoCall.setPreviewSurface(null); + enableCamera(mVideoCall, false); + } + + @Override + public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) { + if (mVideoCall == null) { + LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceDestroyed", "no video call"); + return; + } + + boolean isChangingConfigurations = InCallPresenter.getInstance().isChangingConfigurations(); + if (!isChangingConfigurations) { + enableCamera(mVideoCall, 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 (mVideoCallScreen == null) { + LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no UI"); + return; + } + if (mVideoCall == null) { + LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no video call"); + return; + } + mVideoCall.setDisplaySurface(videoCallSurface.getSavedSurface()); + } + + @Override + public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) { + if (mVideoCall == null) { + LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceReleased", "no video call"); + return; + } + mVideoCall.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; + } +} |