1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.incallui.videotech.ims;
18 
19 import android.content.Context;
20 import android.os.Handler;
21 import android.telecom.Call;
22 import android.telecom.Connection;
23 import android.telecom.Connection.VideoProvider;
24 import android.telecom.InCallService.VideoCall;
25 import android.telecom.VideoProfile;
26 import android.telecom.VideoProfile.CameraCapabilities;
27 import com.android.dialer.common.LogUtil;
28 import com.android.dialer.logging.DialerImpression;
29 import com.android.dialer.logging.LoggingBindings;
30 import com.android.incallui.videotech.VideoTech.VideoTechListener;
31 import com.android.incallui.videotech.utils.SessionModificationState;
32 
33 /** Receives IMS video call state updates. */
34 public class ImsVideoCallCallback extends VideoCall.Callback {
35   private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
36   private final Handler handler = new Handler();
37   private final LoggingBindings logger;
38   private final Call call;
39   private final ImsVideoTech videoTech;
40   private final VideoTechListener listener;
41   private final Context context;
42   private int requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
43 
ImsVideoCallCallback( final LoggingBindings logger, final Call call, ImsVideoTech videoTech, VideoTechListener listener, Context context)44   ImsVideoCallCallback(
45       final LoggingBindings logger,
46       final Call call,
47       ImsVideoTech videoTech,
48       VideoTechListener listener,
49       Context context) {
50     this.logger = logger;
51     this.call = call;
52     this.videoTech = videoTech;
53     this.listener = listener;
54     this.context = context;
55   }
56 
57   @Override
onSessionModifyRequestReceived(VideoProfile videoProfile)58   public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
59     LogUtil.i(
60         "ImsVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
61 
62     int previousVideoState = ImsVideoTech.getUnpausedVideoState(call.getDetails().getVideoState());
63     int newVideoState = ImsVideoTech.getUnpausedVideoState(videoProfile.getVideoState());
64 
65     boolean wasVideoCall = VideoProfile.isVideo(previousVideoState);
66     boolean isVideoCall = VideoProfile.isVideo(newVideoState);
67 
68     if (wasVideoCall && !isVideoCall) {
69       LogUtil.i(
70           "ImsVideoTech.onSessionModifyRequestReceived", "call downgraded to %d", newVideoState);
71     } else if (previousVideoState != newVideoState) {
72       requestedVideoState = newVideoState;
73       if (!wasVideoCall) {
74         videoTech.setSessionModificationState(
75             SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
76         listener.onVideoUpgradeRequestReceived();
77         logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_RECEIVED);
78       } else {
79         LogUtil.i(
80             "ImsVideoTech.onSessionModifyRequestReceived", "call updated to %d", newVideoState);
81         videoTech.acceptVideoRequest(context);
82       }
83     }
84   }
85 
86   /**
87    * @param status Status of the session modify request. Valid values are {@link
88    *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
89    *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
90    *     Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
91    * @param responseProfile The actual profile changes made by the peer device.
92    */
93   @Override
onSessionModifyResponseReceived( int status, VideoProfile requestedProfile, VideoProfile responseProfile)94   public void onSessionModifyResponseReceived(
95       int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
96     LogUtil.i(
97         "ImsVideoCallCallback.onSessionModifyResponseReceived",
98         "status: %d, requestedProfile: %s, responseProfile: %s, session modification state: %d",
99         status,
100         requestedProfile,
101         responseProfile,
102         videoTech.getSessionModificationState());
103 
104     if (videoTech.getSessionModificationState()
105         == SessionModificationState.WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
106       final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
107       if (status == VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
108         // Telecom manages audio route for us
109         listener.onUpgradedToVideo(false /* switchToSpeaker */);
110       } else {
111         // This will update the video UI to display the error message.
112         videoTech.setSessionModificationState(newSessionModificationState);
113       }
114 
115       // If the other person accepted the upgrade request then this will keep the video UI up until
116       // the call's video state change. Without this we would switch to the voice call and then
117       // switch back to video UI.
118       clearFailedResponseState(newSessionModificationState);
119     } else if (videoTech.getSessionModificationState()
120         == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
121       requestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
122       videoTech.setSessionModificationState(SessionModificationState.NO_REQUEST);
123     } else if (videoTech.getSessionModificationState()
124         == SessionModificationState.WAITING_FOR_RESPONSE) {
125       final int newSessionModificationState = getSessionModificationStateFromTelecomStatus(status);
126       videoTech.setSessionModificationState(newSessionModificationState);
127       if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
128         clearFailedResponseState(newSessionModificationState);
129       }
130     } else {
131       LogUtil.i(
132           "ImsVideoCallCallback.onSessionModifyResponseReceived",
133           "call is not waiting for response, doing nothing");
134     }
135   }
136 
clearFailedResponseState(final int newSessionModificationState)137   private void clearFailedResponseState(final int newSessionModificationState) {
138     handler.removeCallbacksAndMessages(null); // Clear everything
139     // Wait for 4 seconds and then clean the session modification state. This allows the video UI
140     // to stay up so that the user can read the error message.
141     handler.postDelayed(
142         () -> {
143           if (videoTech.getSessionModificationState() == newSessionModificationState) {
144             LogUtil.i("ImsVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
145             videoTech.setSessionModificationState(SessionModificationState.NO_REQUEST);
146           } else {
147             LogUtil.i(
148                 "ImsVideoCallCallback.onSessionModifyResponseReceived",
149                 "session modification state has changed, not clearing state");
150           }
151         },
152         CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
153   }
154 
155   @SessionModificationState
getSessionModificationStateFromTelecomStatus(int telecomStatus)156   private int getSessionModificationStateFromTelecomStatus(int telecomStatus) {
157     switch (telecomStatus) {
158       case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
159         return SessionModificationState.NO_REQUEST;
160       case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
161       case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
162         // Check if it's already video call, which means the request is not video upgrade request.
163         if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
164           return SessionModificationState.REQUEST_FAILED;
165         } else {
166           return SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_FAILED;
167         }
168       case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
169         return SessionModificationState.UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
170       case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
171         return SessionModificationState.REQUEST_REJECTED;
172       default:
173         LogUtil.e(
174             "ImsVideoCallCallback.getSessionModificationStateFromTelecomStatus",
175             "unknown status: %d",
176             telecomStatus);
177         return SessionModificationState.REQUEST_FAILED;
178     }
179   }
180 
181   // In the vendor code rx_pause and rx_resume get triggered when the video player starts or stops
182   // playing the incoming video stream.  For the case where you're resuming a held call, its
183   // definitely a good signal to use to know that the video is resuming (though the video state
184   // should change to indicate its not paused in this case as well).  However, keep in mind you'll
185   // get these signals as well on carriers that don't support the video pause signalling (like TMO)
186   // so you want to ensure you don't send sessionModifyRequests with pause/resume based on these
187   // signals. Also, its technically possible to have a pause/resume if the video signal degrades.
188   @Override
onCallSessionEvent(int event)189   public void onCallSessionEvent(int event) {
190     switch (event) {
191       case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
192         LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_pause");
193         break;
194       case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
195         LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "rx_resume");
196         break;
197       case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
198         LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_failure");
199         break;
200       case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
201         LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "camera_ready");
202         break;
203       default:
204         LogUtil.i("ImsVideoCallCallback.onCallSessionEvent", "unknown event = : " + event);
205         break;
206     }
207   }
208 
209   @Override
onPeerDimensionsChanged(int width, int height)210   public void onPeerDimensionsChanged(int width, int height) {
211     listener.onPeerDimensionsChanged(width, height);
212   }
213 
214   @Override
onVideoQualityChanged(int videoQuality)215   public void onVideoQualityChanged(int videoQuality) {
216     LogUtil.i("ImsVideoCallCallback.onVideoQualityChanged", "videoQuality: %d", videoQuality);
217   }
218 
219   @Override
onCallDataUsageChanged(long dataUsage)220   public void onCallDataUsageChanged(long dataUsage) {
221     LogUtil.i("ImsVideoCallCallback.onCallDataUsageChanged", "dataUsage: %d", dataUsage);
222   }
223 
224   @Override
onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities)225   public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
226     if (cameraCapabilities != null) {
227       listener.onCameraDimensionsChanged(
228           cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
229     }
230   }
231 
getRequestedVideoState()232   int getRequestedVideoState() {
233     return requestedVideoState;
234   }
235 }
236