/* * 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.content.Context; import android.database.Cursor; import android.graphics.Point; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.provider.ContactsContract; import android.telecom.CallAudioState; import android.telecom.Connection; import android.telecom.InCallService.VideoCall; import android.telecom.VideoProfile; import android.telecom.VideoProfile.CameraCapabilities; import android.view.Surface; import android.widget.ImageView; import com.android.contacts.common.ContactPhotoManager; 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.InCallVideoCallCallbackNotifier.SurfaceChangeListener; import com.android.incallui.InCallVideoCallCallbackNotifier.VideoEventListener; import java.util.Objects; /** * Logic related to the {@link VideoCallFragment} 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 extends Presenter implements IncomingCallListener, InCallOrientationListener, InCallStateListener, InCallDetailsListener, SurfaceChangeListener, VideoEventListener, InCallVideoCallCallbackNotifier.SessionModificationListener, InCallPresenter.InCallEventListener { public static final String TAG = "VideoCallPresenter"; public static final boolean DEBUG = false; /** * Runnable which is posted to schedule automatically entering fullscreen mode. */ private Runnable mAutoFullscreenRunnable = new Runnable() { @Override public void run() { if (mAutoFullScreenPending) { Log.v(this, "Automatically entering fullscreen mode."); InCallPresenter.getInstance().setFullScreen(true); mAutoFullScreenPending = false; } else { Log.v(this, "Skipping scheduled fullscreen mode."); } } }; /** * Defines the state of the preview surface negotiation with the telephony layer. */ private 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; } /** * The minimum width or height of the preview surface. Used when re-sizing the preview surface * to match the aspect ratio of the currently selected camera. */ private float mMinimumVideoDimension; /** * The current context. */ private Context mContext; /** * The call the video surfaces are currently related to */ private Call 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; /** * Call's current state */ private int mCurrentCallState = Call.State.INVALID; /** * Determines the device orientation (portrait/lanscape). */ private int mDeviceOrientation; /** * Tracks the state of the preview surface negotiation with the telephony layer. */ private int mPreviewSurfaceState = PreviewSurfaceState.NONE; /** * Saves the audio mode which was selected prior to going into a video call. */ private static int sPrevVideoAudioMode = AudioModeProvider.AUDIO_MODE_INVALID; private static boolean mIsVideoMode = false; /** * Contact photo manager to retrieve cached contact photo information. */ private ContactPhotoManager mContactPhotoManager = null; /** * The URI for the user's profile photo, or {@code null} if not specified. */ private ContactInfoCache.ContactCacheEntry mProfileInfo = null; /** * UI thread handler used for delayed task execution. */ private Handler mHandler; /** * 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; /** * Initializes the presenter. * * @param context The current context. */ public void init(Context context) { mContext = context; mMinimumVideoDimension = mContext.getResources().getDimension( R.dimen.video_preview_small_dimension); mHandler = new Handler(Looper.getMainLooper()); 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. * * @param ui The Ui implementation that is now ready to be used. */ @Override public void onUiReady(VideoCallUi ui) { super.onUiReady(ui); Log.d(this, "onUiReady:"); // 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().addDetailsListener(this); // Register for surface and video events from {@link InCallVideoCallListener}s. InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this); InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this); InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this); mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; mCurrentCallState = Call.State.INVALID; } /** * Called when the user interface is no longer ready to be used. * * @param ui The Ui implementation that is no longer ready to be used. */ @Override public void onUiUnready(VideoCallUi ui) { super.onUiUnready(ui); Log.d(this, "onUiUnready:"); InCallPresenter.getInstance().removeListener(this); InCallPresenter.getInstance().removeDetailsListener(this); InCallPresenter.getInstance().removeIncomingCallListener(this); InCallPresenter.getInstance().removeOrientationListener(this); InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this); InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this); InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this); } /** * Handles the creation of a surface in the {@link VideoCallFragment}. * * @param surface The surface which was created. */ public void onSurfaceCreated(int surface) { Log.d(this, "onSurfaceCreated surface=" + surface + " mVideoCall=" + mVideoCall); Log.d(this, "onSurfaceCreated PreviewSurfaceState=" + mPreviewSurfaceState); Log.d(this, "onSurfaceCreated presenter=" + this); final VideoCallUi ui = getUi(); if (ui == null || mVideoCall == null) { Log.w(this, "onSurfaceCreated: Error bad state VideoCallUi=" + ui + " mVideoCall=" + mVideoCall); 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 (surface == VideoCallFragment.SURFACE_PREVIEW ) { if (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) { mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface()); } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()){ enableCamera(mVideoCall, true); } } else if (surface == VideoCallFragment.SURFACE_DISPLAY) { mVideoCall.setDisplaySurface(ui.getDisplayVideoSurface()); } } /** * Handles structural changes (format or size) to a surface. * * @param surface The surface which changed. * @param format The new PixelFormat of the surface. * @param width The new width of the surface. * @param height The new height of the surface. */ public void onSurfaceChanged(int surface, int format, int width, int height) { //Do stuff } /** * Handles the destruction of a surface in the {@link VideoCallFragment}. * Note: The surface is being released, that is, it is no longer valid. * * @param surface The surface which was destroyed. */ public void onSurfaceReleased(int surface) { Log.d(this, "onSurfaceReleased: mSurfaceId=" + surface); if ( mVideoCall == null) { Log.w(this, "onSurfaceReleased: VideoCall is null. mSurfaceId=" + surface); return; } if (surface == VideoCallFragment.SURFACE_DISPLAY) { mVideoCall.setDisplaySurface(null); } else if (surface == VideoCallFragment.SURFACE_PREVIEW) { mVideoCall.setPreviewSurface(null); enableCamera(mVideoCall, false); } } /** * Called by {@link VideoCallFragment} when the surface is detached from UI (TextureView). * Note: The surface will be cached by {@link VideoCallFragment}, so we don't immediately * null out incoming video surface. * @see VideoCallPresenter#onSurfaceReleased(int) * * @param surface The surface which was detached. */ public void onSurfaceDestroyed(int surface) { Log.d(this, "onSurfaceDestroyed: mSurfaceId=" + surface); if (mVideoCall == null) { return; } final boolean isChangingConfigurations = InCallPresenter.getInstance().isChangingConfigurations(); Log.d(this, "onSurfaceDestroyed: isChangingConfigurations=" + isChangingConfigurations); if (surface == VideoCallFragment.SURFACE_PREVIEW) { if (!isChangingConfigurations) { enableCamera(mVideoCall, false); } else { Log.w(this, "onSurfaceDestroyed: Activity is being destroyed due " + "to configuration changes. Not closing the camera."); } } } /** * Handles clicks on the video surfaces by toggling full screen state. * Informs the {@link InCallPresenter} of the change so that it can inform the * {@link CallCardPresenter} of the change. * * @param surfaceId The video surface receiving the click. */ public void onSurfaceClick(int surfaceId) { boolean isFullscreen = InCallPresenter.getInstance().toggleFullscreenMode(); Log.v(this, "toggleFullScreen = " + isFullscreen); } /** * 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, Call 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) { Log.d(this, "onStateChange oldState" + oldState + " newState=" + newState + " isVideoMode=" + isVideoMode()); if (newState == InCallPresenter.InCallState.NO_CALLS) { updateAudioMode(false); if (isVideoMode()) { exitVideoMode(); } cleanupSurfaces(); } // Determine the primary active call). Call 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. Call 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 (!CallUtils.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); Log.d(this, "onStateChange primaryChanged=" + primaryChanged); Log.d(this, "onStateChange primary= " + primary); Log.d(this, "onStateChange mPrimaryCall = " + 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(); } private void checkForVideoStateChange(Call call) { final boolean isVideoCall = CallUtils.isVideoCall(call); final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState(); Log.d(this, "checkForVideoStateChange: isVideoCall= " + isVideoCall + " hasVideoStateChanged=" + hasVideoStateChanged + " isVideoMode=" + isVideoMode() + " previousVideoState: " + VideoProfile.videoStateToString(mCurrentVideoState) + " newVideoState: " + VideoProfile.videoStateToString(call.getVideoState())); if (!hasVideoStateChanged) { return; } updateCameraSelection(call); if (isVideoCall) { enterVideoMode(call); } else if (isVideoMode()) { exitVideoMode(); } } private void checkForCallStateChange(Call call) { final boolean isVideoCall = CallUtils.isVideoCall(call); final boolean hasCallStateChanged = mCurrentCallState != call.getState(); Log.d(this, "checkForCallStateChange: isVideoCall= " + isVideoCall + " hasCallStateChanged=" + hasCallStateChanged + " isVideoMode=" + isVideoMode()); if (!hasCallStateChanged) { return; } if (isVideoCall) { final InCallCameraManager cameraManager = InCallPresenter.getInstance(). getInCallCameraManager(); String prevCameraId = cameraManager.getActiveCameraId(); updateCameraSelection(call); String newCameraId = cameraManager.getActiveCameraId(); if (!Objects.equals(prevCameraId, newCameraId) && CallUtils.isActiveVideoCall(call)) { enableCamera(call.getVideoCall(), true); } } // Make sure we hide or show the video UI if needed. showVideoUi(call.getVideoState(), call.getState()); } private void cleanupSurfaces() { final VideoCallUi ui = getUi(); if (ui == null) { Log.w(this, "cleanupSurfaces"); return; } ui.cleanupSurfaces(); } private void onPrimaryCallChanged(Call newPrimaryCall) { final boolean isVideoCall = CallUtils.isVideoCall(newPrimaryCall); final boolean isVideoMode = isVideoMode(); Log.d(this, "onPrimaryCallChanged: isVideoCall=" + isVideoCall + " isVideoMode=" + isVideoMode); if (!isVideoCall && isVideoMode) { // Terminate video mode if new primary call is not a video call // and we are currently in video mode. Log.d(this, "onPrimaryCallChanged: Exiting video mode..."); exitVideoMode(); } else if (isVideoCall) { Log.d(this, "onPrimaryCallChanged: Entering video mode..."); updateCameraSelection(newPrimaryCall); enterVideoMode(newPrimaryCall); } } private boolean isVideoMode() { return mIsVideoMode; } private void updateCallCache(Call call) { if (call == null) { mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY; mCurrentCallState = Call.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(Call call, android.telecom.Call.Details details) { Log.d(this, " onDetailsChanged call=" + call + " details=" + details + " mPrimaryCall=" + mPrimaryCall); if (call == null) { return; } // If the details change is not for the currently active call no update is required. if (!call.equals(mPrimaryCall)) { Log.d(this," onDetailsChanged: Details not for current active call so returning. "); return; } updateVideoCall(call); updateCallCache(call); } private void updateVideoCall(Call call) { checkForVideoCallChange(call); checkForVideoStateChange(call); checkForCallStateChange(call); checkForOrientationAllowedChange(call); } private void checkForOrientationAllowedChange(Call call) { InCallPresenter.getInstance().setInCallAllowsOrientationChange(CallUtils.isVideoCall(call)); } /** * Checks for a change to the video call and changes it if required. */ private void checkForVideoCallChange(Call call) { final VideoCall videoCall = call.getTelecommCall().getVideoCall(); Log.d(this, "checkForVideoCallChange: videoCall=" + videoCall + " mVideoCall=" + 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 videoCall The new video call. */ private void changeVideoCall(Call call) { final VideoCall videoCall = call.getTelecommCall().getVideoCall(); Log.d(this, "changeVideoCall to videoCall=" + videoCall + " mVideoCall=" + mVideoCall); // Null out the surfaces on the previous video call. if (mVideoCall != null) { // Log.d(this, "Null out the surfaces on the previous video call."); // mVideoCall.setDisplaySurface(null); // mVideoCall.setPreviewSurface(null); } final boolean hasChanged = mVideoCall == null && videoCall != null; mVideoCall = videoCall; if (mVideoCall == null || call == null) { Log.d(this, "Video call or primary call is null. Return"); return; } if (CallUtils.isVideoCall(call) && hasChanged) { enterVideoMode(call); } } private static boolean isCameraRequired(int videoState) { return VideoProfile.isBidirectional(videoState) || VideoProfile.isTransmissionEnabled(videoState); } private boolean isCameraRequired() { return mPrimaryCall != null ? isCameraRequired(mPrimaryCall.getVideoState()) : false; } /** * Enters video mode by showing the video surfaces and making other adjustments (eg. audio). * TODO(vt): Need to adjust size and orientation of preview surface here. */ private void enterVideoMode(Call call) { VideoCall videoCall = call.getVideoCall(); int newVideoState = call.getVideoState(); Log.d(this, "enterVideoMode videoCall= " + videoCall + " videoState: " + newVideoState); VideoCallUi ui = getUi(); if (ui == null) { Log.e(this, "Error VideoCallUi is null so returning"); return; } showVideoUi(newVideoState, call.getState()); // Communicate the current camera to telephony and make a request for the camera // capabilities. if (videoCall != null) { if (ui.isDisplayVideoSurfaceCreated()) { Log.d(this, "Calling setDisplaySurface with " + ui.getDisplayVideoSurface()); videoCall.setDisplaySurface(ui.getDisplayVideoSurface()); } final int rotation = ui.getCurrentRotation(); if (rotation != VideoCallFragment.ORIENTATION_UNKNOWN) { videoCall.setDeviceOrientation(InCallPresenter.toRotationAngle(rotation)); } enableCamera(videoCall, isCameraRequired(newVideoState)); } mCurrentVideoState = newVideoState; updateAudioMode(true); mIsVideoMode = true; maybeAutoEnterFullscreen(call); } //TODO: Move this into Telecom. InCallUI should not be this close to audio functionality. private void updateAudioMode(boolean enableSpeaker) { if (!isSpeakerEnabledForVideoCalls()) { Log.d(this, "Speaker is disabled. Can't update audio mode"); return; } final TelecomAdapter telecomAdapter = TelecomAdapter.getInstance(); final boolean isPrevAudioModeValid = sPrevVideoAudioMode != AudioModeProvider.AUDIO_MODE_INVALID; Log.d(this, "Is previous audio mode valid = " + isPrevAudioModeValid + " enableSpeaker is " + enableSpeaker); // Set audio mode to previous mode if enableSpeaker is false. if (isPrevAudioModeValid && !enableSpeaker) { telecomAdapter.setAudioRoute(sPrevVideoAudioMode); sPrevVideoAudioMode = AudioModeProvider.AUDIO_MODE_INVALID; return; } int currentAudioMode = AudioModeProvider.getInstance().getAudioMode(); // Set audio mode to speaker if enableSpeaker is true and bluetooth or headset are not // connected and it's a video call. if (!isAudioRouteEnabled(currentAudioMode, CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET) && !isPrevAudioModeValid && enableSpeaker && CallUtils.isVideoCall(mPrimaryCall)) { sPrevVideoAudioMode = currentAudioMode; Log.d(this, "Routing audio to speaker"); telecomAdapter.setAudioRoute(CallAudioState.ROUTE_SPEAKER); } } private static boolean isSpeakerEnabledForVideoCalls() { // TODO: Make this a carrier configurable setting. For now this is always true. b/20090407 return true; } private void enableCamera(VideoCall videoCall, boolean isCameraRequired) { Log.d(this, "enableCamera: VideoCall=" + videoCall + " enabling=" + isCameraRequired); if (videoCall == null) { Log.w(this, "enableCamera: VideoCall is null."); return; } 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() { Log.d(this, "exitVideoMode"); showVideoUi(VideoProfile.STATE_AUDIO_ONLY, Call.State.ACTIVE); 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) { VideoCallUi ui = getUi(); if (ui == null) { Log.e(this, "showVideoUi, VideoCallUi is null returning"); return; } boolean isPaused = VideoProfile.isPaused(videoState); boolean isCallActive = callState == Call.State.ACTIVE; if (VideoProfile.isBidirectional(videoState)) { ui.showVideoViews(true, !isPaused && isCallActive); } else if (VideoProfile.isTransmissionEnabled(videoState)) { ui.showVideoViews(true, false); } else if (VideoProfile.isReceptionEnabled(videoState)) { ui.showVideoViews(false, !isPaused && isCallActive); loadProfilePhotoAsync(); } else { ui.hideVideoUi(); } InCallPresenter.getInstance().enableScreenTimeout( VideoProfile.isAudioOnly(videoState)); } /** * 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(Call call, boolean paused) { if (!call.equals(mPrimaryCall)) { return; } // TODO(vt): Show/hide the peer contact photo. } /** * 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(Call call, int width, int height) { Log.d(this, "onUpdatePeerDimensions: width= " + width + " height= " + height); VideoCallUi ui = getUi(); if (ui == null) { Log.e(this, "VideoCallUi is null. Bail out"); return; } if (!call.equals(mPrimaryCall)) { Log.e(this, "Current call is not equal to primary call. Bail out"); return; } // Change size of display surface to match the peer aspect ratio if (width > 0 && height > 0) { setDisplayVideoSize(width, height); } } /** * 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(Call 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(Call call, int width, int height) { Log.d(this, "onCameraDimensionsChange call=" + call + " width=" + width + " height=" + height); VideoCallUi ui = getUi(); if (ui == null) { Log.e(this, "onCameraDimensionsChange ui is null"); return; } if (!call.equals(mPrimaryCall)) { Log.e(this, "Call is not 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. if (ui.isPreviewVideoSurfaceCreated()) { mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET; mVideoCall.setPreviewSurface(ui.getPreviewVideoSurface()); } } /** * Changes the dimensions of the preview surface. * * @param width The new width. * @param height The new height. */ private void changePreviewDimensions(int width, int height) { VideoCallUi ui = getUi(); if (ui == null) { return; } // Resize the surface used to display the preview video ui.setPreviewSurfaceSize(width, height); // Configure the preview surface to the correct aspect ratio. float aspectRatio = 1.0f; if (width > 0 && height > 0) { aspectRatio = (float) width / (float) height; } // Resize the textureview housing the preview video and rotate it appropriately based on // the device orientation setPreviewSize(mDeviceOrientation, aspectRatio); } /** * Called when call session event is raised. * * @param event The call session event. */ @Override public void onCallSessionEvent(int event) { StringBuilder sb = new StringBuilder(); sb.append("onCallSessionEvent = "); switch (event) { case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE: sb.append("rx_pause"); break; case Connection.VideoProvider.SESSION_EVENT_RX_RESUME: sb.append("rx_resume"); break; case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE: sb.append("camera_failure"); break; case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY: sb.append("camera_ready"); break; default: sb.append("unknown event = "); sb.append(event); break; } Log.d(this, sb.toString()); } /** * Handles a change to the call data usage * * @param dataUsage call data usage value */ @Override public void onCallDataUsageChange(long dataUsage) { Log.d(this, "onCallDataUsageChange dataUsage=" + dataUsage); } /** * Handles changes to the device orientation. * * @param orientation The device orientation (one of: {@link Surface#ROTATION_0}, * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, * {@link Surface#ROTATION_270}). */ @Override public void onDeviceOrientationChanged(int orientation) { mDeviceOrientation = orientation; Point previewDimensions = getUi().getPreviewSize(); if (previewDimensions == null) { return; } Log.d(this, "onDeviceOrientationChanged: orientation=" + orientation + " size: " + previewDimensions); changePreviewDimensions(previewDimensions.x, previewDimensions.y); } @Override public void onUpgradeToVideoRequest(Call call, int videoState) { Log.d(this, "onUpgradeToVideoRequest call = " + call + " new video state = " + videoState); if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) { Log.w(this, "UpgradeToVideoRequest received for non-primary call"); } if (call == null) { return; } call.setSessionModificationTo(videoState); } @Override public void onUpgradeToVideoSuccess(Call call) { Log.d(this, "onUpgradeToVideoSuccess call=" + call); if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) { Log.w(this, "UpgradeToVideoSuccess received for non-primary call"); } if (call == null) { return; } } @Override public void onUpgradeToVideoFail(int status, Call call) { Log.d(this, "onUpgradeToVideoFail call=" + call); if (mPrimaryCall == null || !Call.areSame(mPrimaryCall, call)) { Log.w(this, "UpgradeToVideoFail received for non-primary call"); } if (call == null) { return; } } @Override public void onDowngradeToAudio(Call call) { call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); // exit video mode exitVideoMode(); } /** * Sets the preview surface size based on the current device orientation. * * @param orientation The device orientation (one of: {@link Surface#ROTATION_0}, * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, * {@link Surface#ROTATION_270}). * @param aspectRatio The aspect ratio of the camera (width / height). */ private void setPreviewSize(int orientation, float aspectRatio) { VideoCallUi ui = getUi(); if (ui == null) { return; } int height; int width; if (orientation == Surface.ROTATION_90 || orientation == Surface.ROTATION_270) { // Landscape or reverse landscape orientation. width = (int) (mMinimumVideoDimension * aspectRatio); height = (int) mMinimumVideoDimension; } else { // Portrait or reverse portrait orientation. width = (int) mMinimumVideoDimension; height = (int) (mMinimumVideoDimension * aspectRatio); } ui.setPreviewSize(width, height); } /** * Sets the display video surface size based on peer width and height * * @param width peer width * @param height peer height */ private void setDisplayVideoSize(int width, int height) { Log.d(this, "setDisplayVideoSize:Received peer width=" + width + " peer height=" + height); VideoCallUi ui = getUi(); if (ui == null) { return; } // Get current display size Point size = ui.getScreenSize(); Log.d("VideoCallPresenter", "setDisplayVideoSize: windowmgr width=" + size.x + " windowmgr height=" + size.y); if (size.y * width > size.x * height) { // current display height is too much. Correct it size.y = (int) (size.x * height / width); } else if (size.y * width < size.x * height) { // current display width is too much. Correct it size.x = (int) (size.y * width / height); } ui.setDisplayVideoSize(size.x, size.y); } /** * Exits fullscreen mode if the current call context has changed to a non-video call. * * @param call The call. */ protected void maybeExitFullscreen(Call call) { if (call == null) { return; } if (!CallUtils.isVideoCall(call) || call.getState() == Call.State.INCOMING) { 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. Call is not active * 3. Call is not video call * 4. Already in fullscreen mode * * @param call The current call. */ protected void maybeAutoEnterFullscreen(Call call) { if (!mIsAutoFullscreenEnabled) { return; } if (call == null || ( call != null && (call.getState() != Call.State.ACTIVE || !CallUtils.isVideoCall(call)) || InCallPresenter.getInstance().isFullscreen())) { // Ensure any previously scheduled attempt to enter fullscreen is cancelled. cancelAutoFullScreen(); return; } if (mAutoFullScreenPending) { Log.v(this, "maybeAutoEnterFullscreen : already pending."); return; } Log.v(this, "maybeAutoEnterFullscreen : scheduled"); mAutoFullScreenPending = true; mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis); } /** * Cancels pending auto fullscreen mode. */ public void cancelAutoFullScreen() { if (!mAutoFullScreenPending) { Log.v(this, "cancelAutoFullScreen : none pending."); return; } Log.v(this, "cancelAutoFullScreen : cancelling pending"); mAutoFullScreenPending = false; } private static boolean isAudioRouteEnabled(int audioRoute, int audioRouteMask) { return ((audioRoute & audioRouteMask) != 0); } private static void updateCameraSelection(Call call) { Log.d(TAG, "updateCameraSelection: call=" + call); Log.d(TAG, "updateCameraSelection: call=" + toSimpleString(call)); final Call activeCall = CallList.getInstance().getActiveCall(); int cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; // this function should never be called with null call object, however if it happens we // should handle it gracefully. if (call == null) { cameraDir = Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; com.android.incallui.Log.e(TAG, "updateCameraSelection: Call object is null." + " Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)"); } // Clear camera direction if this is not a video call. else if (CallUtils.isAudioCall(call)) { cameraDir = Call.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 (CallUtils.isVideoCall(activeCall) && CallUtils.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 (CallUtils.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 (CallUtils.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 (CallUtils.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 (CallUtils.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()); } com.android.incallui.Log.d(TAG, "updateCameraSelection: Setting camera direction to " + cameraDir + " Call=" + call); final InCallCameraManager cameraManager = InCallPresenter.getInstance(). getInCallCameraManager(); cameraManager.setUseFrontFacingCamera(cameraDir == Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING); } private static int toCameraDirection(int videoState) { return VideoProfile.isTransmissionEnabled(videoState) && !VideoProfile.isBidirectional(videoState) ? Call.VideoSettings.CAMERA_DIRECTION_BACK_FACING : Call.VideoSettings.CAMERA_DIRECTION_FRONT_FACING; } private static boolean isCameraDirectionSet(Call call) { return CallUtils.isVideoCall(call) && call.getVideoSettings().getCameraDir() != Call.VideoSettings.CAMERA_DIRECTION_UNKNOWN; } private static String toSimpleString(Call call) { return call == null ? null : call.toSimpleString(); } /** * Starts an asynchronous load of the user's profile photo. */ public void loadProfilePhotoAsync() { final VideoCallUi ui = getUi(); if (ui == null) { return; } final AsyncTask task = new AsyncTask() { /** * Performs asynchronous load of the user profile information. * * @param params The parameters of the task. * * @return {@code null}. */ @Override protected Void doInBackground(Void... params) { if (mProfileInfo == null) { // Try and read the photo URI from the local profile. mProfileInfo = new ContactInfoCache.ContactCacheEntry(); final Cursor cursor = mContext.getContentResolver().query( ContactsContract.Profile.CONTENT_URI, new String[]{ ContactsContract.CommonDataKinds.Phone._ID, ContactsContract.CommonDataKinds.Phone.PHOTO_URI, ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, }, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { mProfileInfo.lookupKey = cursor.getString(cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY)); String photoUri = cursor.getString(cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.PHOTO_URI)); mProfileInfo.displayPhotoUri = photoUri == null ? null : Uri.parse(photoUri); mProfileInfo.name = cursor.getString(cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); } } finally { cursor.close(); } } } return null; } @Override protected void onPostExecute(Void result) { // If user profile information was found, issue an async request to load the user's // profile photo. if (mProfileInfo != null) { if (mContactPhotoManager == null) { mContactPhotoManager = ContactPhotoManager.getInstance(mContext); } ContactPhotoManager.DefaultImageRequest imageRequest = (mProfileInfo != null) ? null : new ContactPhotoManager.DefaultImageRequest(mProfileInfo.name, mProfileInfo.lookupKey, false /* isCircularPhoto */); ImageView photoView = ui.getPreviewPhotoView(); if (photoView == null) { return; } mContactPhotoManager.loadDirectoryPhoto(photoView, mProfileInfo.displayPhotoUri, false /* darkTheme */, false /* isCircular */, imageRequest); } } }; task.execute(); } /** * Defines the VideoCallUI interactions. */ public interface VideoCallUi extends Ui { void showVideoViews(boolean showPreview, boolean showIncoming); void hideVideoUi(); boolean isDisplayVideoSurfaceCreated(); boolean isPreviewVideoSurfaceCreated(); Surface getDisplayVideoSurface(); Surface getPreviewVideoSurface(); int getCurrentRotation(); void setPreviewSize(int width, int height); void setPreviewSurfaceSize(int width, int height); void setDisplayVideoSize(int width, int height); Point getScreenSize(); Point getPreviewSize(); void cleanupSurfaces(); ImageView getPreviewPhotoView(); } }