/* * Copyright (C) 2017 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.videotech.ims; import android.content.Context; import android.os.Handler; import android.telecom.Call; import android.telecom.Connection; import android.telecom.Connection.VideoProvider; import android.telecom.InCallService.VideoCall; import android.telecom.VideoProfile; import android.telecom.VideoProfile.CameraCapabilities; import com.android.dialer.common.LogUtil; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.LoggingBindings; import com.android.incallui.videotech.VideoTech.VideoTechListener; import com.android.incallui.videotech.utils.SessionModificationState; /** Receives IMS video call state updates. */ public class ImsVideoCallCallback extends VideoCall.Callback { private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000; private final Handler handler = new Handler(); private final LoggingBindings logger; private final Call call; private final ImsVideoTech videoTech; private final VideoTechListener listener; private final Context context; private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY; ImsVideoCallCallback( final LoggingBindings logger, final Call call, ImsVideoTech videoTech, VideoTechListener listener, Context context) { this.logger = logger; this.call = call; this.videoTech = videoTech; this.listener = listener; this.context = context; } @Override public void onSessionModifyRequestReceived(VideoProfile videoProfile) { LogUtil.i( "ImsVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile); int previousVideoState = ImsVideoTech.getUnpausedVideoState(call.getDetails().getVideoState()); int newVideoState = ImsVideoTech.getUnpausedVideoState(videoProfile.getVideoState()); boolean wasVideoCall = VideoProfile.isVideo(previousVideoState); boolean isVideoCall = VideoProfile.isVideo(newVideoState); if (wasVideoCall && !isVideoCall) { LogUtil.i( "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState); } else if (previousVideoState != newVideoState) { requestedVideoState = newVideoState; if (!wasVideoCall) { videoTech.setSessionModificationState( SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST); listener.onVideoUpgradeRequestReceived(); logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_RECEIVED); } else { LogUtil.i( "ImsVideoTech.onSessionModifyRequestReceived", "call updated to %d", newVideoState); videoTech.acceptVideoRequest(context); } } } /** * @param status Status of the session modify request. Valid values are {@link * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID} * @param responseProfile The actual profile changes made by the peer device. */ @Override public void onSessionModifyResponseReceived( int status, VideoProfile requestedProfile, VideoProfile responseProfile) { LogUtil.i( "ImsVideoCallCallback.onSessionModifyResponseReceived", "status: %d, requestedProfile: %s, responseProfile: %s, session modification state: %d", status, requestedProfile, responseProfile, videoTech.getSessionModificationState()); if (videoTech.getSessionModificationState() == SessionModificationState.WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) { final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status); if (status == VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) { // Telecom manages audio route for us listener.onUpgradedToVideo(false /* switchToSpeaker */); } else { // This will update the video UI to display the error message. videoTech.setSessionModificationState(newSessionModificationState); } // If the other person accepted the upgrade request then this will keep the video UI up until // the call's video state change. Without this we would switch to the voice call and then // switch back to video UI. clearFailedResponseState(newSessionModificationState); } else if (videoTech.getSessionModificationState() == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) { requestedVideoState = VideoProfile.STATE_AUDIO_ONLY; videoTech.setSessionModificationState(SessionModificationState.NO_REQUEST); } else if (videoTech.getSessionModificationState() == SessionModificationState.WAITING_FOR_RESPONSE) { final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status); videoTech.setSessionModificationState(newSessionModificationState); if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) { clearFailedResponseState(newSessionModificationState); } } else { LogUtil.i( "ImsVideoCallCallback.onSessionModifyResponseReceived", "call is not waiting for response, doing nothing"); } } private void clearFailedResponseState(final int newSessionModificationState) { handler.removeCallbacksAndMessages(null); // Clear everything // Wait for 4 seconds and then clean the session modification state. This allows the video UI // to stay up so that the user can read the error message. handler.postDelayed( () -> { if (videoTech.getSessionModificationState() == newSessionModificationState) { LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state"); videoTech.setSessionModificationState(SessionModificationState.NO_REQUEST); } else { LogUtil.i( "ImsVideoCallCallback.onSessionModifyResponseReceived", "session modification state has changed, not clearing state"); } }, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS); } @SessionModificationState private int getSessionModificationStateFromTelecomStatus(int telecomStatus) { switch (telecomStatus) { case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS: return SessionModificationState.NO_REQUEST; case VideoProvider.SESSION_MODIFY_REQUEST_FAIL: case VideoProvider.SESSION_MODIFY_REQUEST_INVALID: // Check if it's already video call, which means the request is not video upgrade request. if (VideoProfile.isVideo(call.getDetails().getVideoState())) { return SessionModificationState.REQUEST_FAILED; } else { return SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_FAILED; } case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT: return SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT; case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE: return SessionModificationState.REQUEST_REJECTED; default: LogUtil.e( "ImsVideoCallCallback.getSessionModificationStateFromTelecomStatus", "unknown status: %d", telecomStatus); return SessionModificationState.REQUEST_FAILED; } } // In the vendor code rx_pause and rx_resume get triggered when the video player starts or stops // playing the incoming video stream. For the case where you're resuming a held call, its // definitely a good signal to use to know that the video is resuming (though the video state // should change to indicate its not paused in this case as well). However, keep in mind you'll // get these signals as well on carriers that don't support the video pause signalling (like TMO) // so you want to ensure you don't send sessionModifyRequests with pause/resume based on these // signals. Also, its technically possible to have a pause/resume if the video signal degrades. @Override public void onCallSessionEvent(int event) { switch (event) { case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE: LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_pause"); break; case Connection.VideoProvider.SESSION_EVENT_RX_RESUME: LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_resume"); break; case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE: LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_failure"); break; case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY: LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_ready"); break; default: LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "unknown event = : " + event); break; } } @Override public void onPeerDimensionsChanged(int width, int height) { listener.onPeerDimensionsChanged(width, height); } @Override public void onVideoQualityChanged(int videoQuality) { LogUtil.i("ImsVideoCallCallback.onVideoQualityChanged", "videoQuality: %d", videoQuality); } @Override public void onCallDataUsageChanged(long dataUsage) { LogUtil.i("ImsVideoCallCallback.onCallDataUsageChanged", "dataUsage: %d", dataUsage); } @Override public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) { if (cameraCapabilities != null) { listener.onCameraDimensionsChanged( cameraCapabilities.getWidth(), cameraCapabilities.getHeight()); } } int getRequestedVideoState() { return requestedVideoState; } }