/*
* 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.content.res.Configuration;
import android.graphics.Point;
import android.os.Handler;
import android.telecom.AudioState;
import android.telecom.CameraCapabilities;
import android.telecom.Connection;
import android.telecom.Connection.VideoProvider;
import android.telecom.InCallService.VideoCall;
import android.telecom.VideoProfile;
import android.view.Surface;
import com.android.contacts.common.CallUtil;
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.InCallVideoCallListenerNotifier.SurfaceChangeListener;
import com.android.incallui.InCallVideoCallListenerNotifier.VideoEventListener;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.TelephonyProperties;
import com.google.common.base.Preconditions;
import java.util.Objects;
import android.os.SystemProperties;
/**
* 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:
*
* - {@code VideoCallPresenter} creates and informs telephony of the display surface.
* - {@code VideoCallPresenter} creates the preview surface.
* - {@code VideoCallPresenter} informs telephony of the currently selected camera.
* - Telephony layer sends {@link CameraCapabilities}, including the
* dimensions of the video for the current camera.
* - {@code VideoCallPresenter} adjusts size of the preview surface to match the aspect
* ratio of the camera.
* - {@code VideoCallPresenter} informs telephony of the new preview surface.
*
*
* 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,
InCallVideoCallListenerNotifier.SessionModificationListener {
public static final String TAG = "VideoCallPresenter";
/**
* Determines the device orientation (portrait/lanscape).
*/
public int getDeviceOrientation() {
return mDeviceOrientation;
}
/**
* 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;
/**
* Determines whether the video surface is in full-screen mode.
*/
private boolean mIsFullScreen = false;
/**
* 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;
/**
* Stores the current call substate.
*/
private int mCurrentCallSubstate;
/** Handler which resets request state to NO_REQUEST after an interval. */
private Handler mSessionModificationResetHandler;
private static final long SESSION_MODIFICATION_RESET_DELAY_MS = 3000;
/**
* Initializes the presenter.
*
* @param context The current context.
*/
public void init(Context context) {
mContext = Preconditions.checkNotNull(context);
mMinimumVideoDimension = mContext.getResources().getDimension(
R.dimen.video_preview_small_dimension);
mSessionModificationResetHandler = new Handler();
}
/**
* 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.
InCallVideoCallListenerNotifier.getInstance().addSurfaceChangeListener(this);
InCallVideoCallListenerNotifier.getInstance().addVideoEventListener(this);
InCallVideoCallListenerNotifier.getInstance().addSessionModificationListener(this);
mCurrentVideoState = VideoProfile.VideoState.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);
InCallVideoCallListenerNotifier.getInstance().removeSurfaceChangeListener(this);
InCallVideoCallListenerNotifier.getInstance().removeVideoEventListener(this);
InCallVideoCallListenerNotifier.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.");
}
}
}
private void toggleFullScreen() {
mIsFullScreen = !mIsFullScreen;
InCallPresenter.getInstance().setFullScreenVideoState(mIsFullScreen);
}
/**
* 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) {
toggleFullScreen();
}
/**
* Handles incoming calls.
*
* @param state The 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;
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();
if (!CallUtils.isActiveVideoCall(primary)) {
primary = callList.getIncomingCall();
}
} else if (newState == InCallPresenter.InCallState.OUTGOING) {
primary = callList.getOutgoingCall();
} else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) {
primary = callList.getPendingOutgoingCall();
} else if (newState == InCallPresenter.InCallState.INCALL) {
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);
}
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());
if (!hasVideoStateChanged) { return;}
updateCameraSelection(call);
if (isVideoCall) {
enterVideoMode(call.getVideoCall(), call.getVideoState());
} 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; }
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);
}
}
private void checkForCallSubstateChange(Call call) {
if (mCurrentCallSubstate != call.getCallSubstate()) {
VideoCallUi ui = getUi();
if (ui == null) {
Log.e(this, "Error VideoCallUi is null. Return.");
return;
}
mCurrentCallSubstate = call.getCallSubstate();
// Display a call substate changed message on UI.
ui.showCallSubstateChanged(mCurrentCallSubstate);
}
}
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.getVideoCall(), newPrimaryCall.getVideoState());
}
}
private boolean isVideoMode() {
return mIsVideoMode;
}
private void updateCallCache(Call call) {
if (call == null) {
mCurrentVideoState = VideoProfile.VideoState.AUDIO_ONLY;
mCurrentCallSubstate = Connection.SUBSTATE_NONE;
mCurrentCallState = Call.State.INVALID;
mVideoCall = null;
mPrimaryCall = null;
} else {
mCurrentVideoState = call.getVideoState();
mCurrentCallSubstate = call.getCallSubstate();
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 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);
checkForCallSubstateChange(call);
updateCallCache(call);
}
private void updateVideoCall(Call call) {
checkForVideoCallChange(call);
checkForVideoStateChange(call);
checkForCallStateChange(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.getVideoCall(), call.getVideoState());
}
}
private static boolean isCameraRequired(int videoState) {
return VideoProfile.VideoState.isBidirectional(videoState) ||
VideoProfile.VideoState.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(VideoCall videoCall, int newVideoState) {
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);
InCallPresenter.getInstance().setInCallAllowsOrientationChange(true);
// 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;
}
//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,
AudioState.ROUTE_BLUETOOTH | AudioState.ROUTE_WIRED_HEADSET) &&
!isPrevAudioModeValid && enableSpeaker && CallUtils.isVideoCall(mPrimaryCall)) {
sPrevVideoAudioMode = currentAudioMode;
Log.d(this, "Routing audio to speaker");
telecomAdapter.setAudioRoute(AudioState.ROUTE_SPEAKER);
}
}
private static boolean isSpeakerEnabledForVideoCalls() {
return (SystemProperties.getInt(TelephonyProperties.PROPERTY_VIDEOCALL_AUDIO_OUTPUT,
PhoneConstants.AUDIO_OUTPUT_DEFAULT) ==
PhoneConstants.AUDIO_OUTPUT_ENABLE_SPEAKER);
}
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");
InCallPresenter.getInstance().setInCallAllowsOrientationChange(false);
showVideoUi(VideoProfile.VideoState.AUDIO_ONLY);
enableCamera(mVideoCall, false);
Log.d(this, "exitVideoMode mIsFullScreen: " + mIsFullScreen);
if (mIsFullScreen) {
toggleFullScreen();
}
mIsVideoMode = false;
}
/**
* Show video Ui depends on video state.
*/
private void showVideoUi(int videoState) {
VideoCallUi ui = getUi();
if (ui == null) {
Log.e(this, "showVideoUi, VideoCallUi is null returning");
return;
}
if (VideoProfile.VideoState.isBidirectional(videoState)) {
ui.showVideoViews(true, true);
} else if (VideoProfile.VideoState.isTransmissionEnabled(videoState)) {
ui.showVideoViews(true, false);
} else if (VideoProfile.VideoState.isReceptionEnabled(videoState)) {
ui.showVideoViews(false, true);
} else {
ui.hideVideoUi();
}
InCallPresenter.getInstance().enableScreenTimeout(
VideoProfile.VideoState.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) {
if (!call.equals(mPrimaryCall)) {
return;
}
VideoCallUi ui = getUi();
if (ui == null) {
Log.e(this, "Error VideoCallUi is null. Return.");
return;
}
// Display a video quality changed message on UI.
ui.showVideoQualityChanged(videoQuality);
}
/**
* 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;
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;
}
setPreviewSize(mDeviceOrientation, aspectRatio);
// 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());
}
}
/**
* Called when call session event is raised.
*
* @param event The call session event.
*/
@Override
public void onCallSessionEvent(int event) {
Log.d(this, "onCallSessionEvent event =" + event);
VideoCallUi ui = getUi();
if (ui == null) {
Log.e(this, "onCallSessionEvent: VideoCallUi is null");
return;
}
ui.displayCallSessionEvent(event);
}
/**
* 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);
VideoCallUi ui = getUi();
if (ui == null) {
Log.e(this, "onCallDataUsageChange: VideoCallUi is null");
return;
}
ui.setCallDataUsage(mContext, dataUsage);
}
/**
* Handles changes to the device orientation.
* See: {@link Configuration.ORIENTATION_LANDSCAPE}, {@link Configuration.ORIENTATION_PORTRAIT}
* @param orientation The device orientation.
*/
@Override
public void onDeviceOrientationChanged(int orientation) {
Log.d(this, "onDeviceOrientationChanged: orientation=" + orientation);
mDeviceOrientation = orientation;
}
@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;
}
call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
}
@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;
}
if (status == VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT) {
call.setSessionModificationState(
Call.SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT);
} else {
call.setSessionModificationState(Call.SessionModificationState.REQUEST_FAILED);
final Call modifyCall = call;
// Start handler to change state from REQUEST_FAILED to NO_REQUEST after an interval.
mSessionModificationResetHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (modifyCall != null) {
modifyCall
.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
}
}
}, SESSION_MODIFICATION_RESET_DELAY_MS);
}
}
@Override
public void onDowngradeToAudio(Call call) {
// exit video mode
exitVideoMode();
}
/**
* Sets the preview surface size based on the current device orientation.
* See: {@link Configuration.ORIENTATION_LANDSCAPE}, {@link Configuration.ORIENTATION_PORTRAIT}
*
* @param orientation The device orientation.
* @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 == Configuration.ORIENTATION_LANDSCAPE) {
width = (int) (mMinimumVideoDimension * aspectRatio);
height = (int) mMinimumVideoDimension;
} else {
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);
}
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.VideoState.isTransmissionEnabled(videoState) &&
!VideoProfile.VideoState.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();
}
/**
* Defines the VideoCallUI interactions.
*/
public interface VideoCallUi extends Ui {
void showVideoViews(boolean showPreview, boolean showIncoming);
void hideVideoUi();
void showVideoQualityChanged(int videoQuality);
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);
void setCallDataUsage(Context context, long dataUsage);
void displayCallSessionEvent(int event);
Point getScreenSize();
void cleanupSurfaces();
boolean isActivityRestart();
void showCallSubstateChanged(int callSubstate);
}
}