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.support.annotation.NonNull;
21 import android.support.annotation.Nullable;
22 import android.support.annotation.VisibleForTesting;
23 import android.telecom.Call;
24 import android.telecom.Call.Details;
25 import android.telecom.PhoneAccountHandle;
26 import android.telecom.VideoProfile;
27 import com.android.dialer.common.Assert;
28 import com.android.dialer.common.LogUtil;
29 import com.android.dialer.logging.DialerImpression;
30 import com.android.dialer.logging.LoggingBindings;
31 import com.android.dialer.util.CallUtil;
32 import com.android.incallui.video.protocol.VideoCallScreen;
33 import com.android.incallui.video.protocol.VideoCallScreenDelegate;
34 import com.android.incallui.videotech.VideoTech;
35 import com.android.incallui.videotech.utils.SessionModificationState;
36 
37 /** ViLTE implementation */
38 public class ImsVideoTech implements VideoTech {
39   private final LoggingBindings logger;
40   private final Call call;
41   private final VideoTechListener listener;
42   @VisibleForTesting ImsVideoCallCallback callback;
43   private @SessionModificationState int sessionModificationState =
44       SessionModificationState.NO_REQUEST;
45   private int previousVideoState = VideoProfile.STATE_AUDIO_ONLY;
46   private boolean paused = false;
47   private String savedCameraId;
48 
49   // Hold onto a flag of whether or not stopTransmission was called but resumeTransmission has not
50   // been. This is needed because there is time between calling stopTransmission and
51   // call.getDetails().getVideoState() reflecting the change. During that time, pause() and
52   // unpause() will send the incorrect VideoProfile.
53   private boolean transmissionStopped = false;
54 
ImsVideoTech(LoggingBindings logger, VideoTechListener listener, Call call)55   public ImsVideoTech(LoggingBindings logger, VideoTechListener listener, Call call) {
56     this.logger = logger;
57     this.listener = listener;
58     this.call = call;
59   }
60 
61   @Override
isAvailable(Context context, PhoneAccountHandle phoneAccountHandle)62   public boolean isAvailable(Context context, PhoneAccountHandle phoneAccountHandle) {
63     if (call.getVideoCall() == null) {
64       LogUtil.i("ImsVideoCall.isAvailable", "null video call");
65       return false;
66     }
67 
68     // We are already in an IMS video call
69     if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
70       LogUtil.i("ImsVideoCall.isAvailable", "already video call");
71       return true;
72     }
73 
74     // The user has disabled IMS video calling in system settings
75     if (!CallUtil.isVideoEnabled(context)) {
76       LogUtil.i("ImsVideoCall.isAvailable", "disabled in settings");
77       return false;
78     }
79 
80     // The current call doesn't support transmitting video
81     if (!call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)) {
82       LogUtil.i("ImsVideoCall.isAvailable", "no TX");
83       return false;
84     }
85 
86     // The current call remote device doesn't support receiving video
87     if (!call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX)) {
88       LogUtil.i("ImsVideoCall.isAvailable", "no RX");
89       return false;
90     }
91     LogUtil.i("ImsVideoCall.isAvailable", "available");
92     return true;
93   }
94 
95   @Override
isTransmittingOrReceiving()96   public boolean isTransmittingOrReceiving() {
97     return VideoProfile.isVideo(call.getDetails().getVideoState());
98   }
99 
100   @Override
isSelfManagedCamera()101   public boolean isSelfManagedCamera() {
102     // Return false to indicate that the answer UI shouldn't open the camera itself.
103     // For IMS Video the modem is responsible for opening the camera.
104     return false;
105   }
106 
107   @Override
shouldUseSurfaceView()108   public boolean shouldUseSurfaceView() {
109     return false;
110   }
111 
112   @Override
isPaused()113   public boolean isPaused() {
114     return paused;
115   }
116 
117   @Override
createVideoCallScreenDelegate( Context context, VideoCallScreen videoCallScreen)118   public VideoCallScreenDelegate createVideoCallScreenDelegate(
119       Context context, VideoCallScreen videoCallScreen) {
120     // TODO move creating VideoCallPresenter here
121     throw Assert.createUnsupportedOperationFailException();
122   }
123 
124   @Override
onCallStateChanged( Context context, int newState, PhoneAccountHandle phoneAccountHandle)125   public void onCallStateChanged(
126       Context context, int newState, PhoneAccountHandle phoneAccountHandle) {
127     if (!isAvailable(context, phoneAccountHandle)) {
128       return;
129     }
130 
131     if (callback == null) {
132       callback = new ImsVideoCallCallback(logger, call, this, listener, context);
133       call.getVideoCall().registerCallback(callback);
134     }
135 
136     if (getSessionModificationState()
137             == SessionModificationState.WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
138         && isTransmittingOrReceiving()) {
139       // We don't clear the session modification state right away when we find out the video upgrade
140       // request was accepted to avoid having the UI switch from video to voice to video.
141       // Once the underlying telecom call updates to video mode it's safe to clear the state.
142       LogUtil.i(
143           "ImsVideoTech.onCallStateChanged",
144           "upgraded to video, clearing session modification state");
145       setSessionModificationState(SessionModificationState.NO_REQUEST);
146     }
147 
148     // Determines if a received upgrade to video request should be cancelled. This can happen if
149     // another InCall UI responds to the upgrade to video request.
150     int newVideoState = call.getDetails().getVideoState();
151     if (newVideoState != previousVideoState
152         && sessionModificationState == SessionModificationState.RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
153       LogUtil.i("ImsVideoTech.onCallStateChanged", "cancelling upgrade notification");
154       setSessionModificationState(SessionModificationState.NO_REQUEST);
155     }
156     previousVideoState = newVideoState;
157   }
158 
159   @Override
onRemovedFromCallList()160   public void onRemovedFromCallList() {}
161 
162   @Override
getSessionModificationState()163   public int getSessionModificationState() {
164     return sessionModificationState;
165   }
166 
setSessionModificationState(@essionModificationState int state)167   void setSessionModificationState(@SessionModificationState int state) {
168     if (state != sessionModificationState) {
169       LogUtil.i(
170           "ImsVideoTech.setSessionModificationState", "%d -> %d", sessionModificationState, state);
171       sessionModificationState = state;
172       listener.onSessionModificationStateChanged();
173     }
174   }
175 
176   @Override
upgradeToVideo(@onNull Context context)177   public void upgradeToVideo(@NonNull Context context) {
178     LogUtil.enterBlock("ImsVideoTech.upgradeToVideo");
179 
180     int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
181     call.getVideoCall()
182         .sendSessionModifyRequest(
183             new VideoProfile(unpausedVideoState | VideoProfile.STATE_BIDIRECTIONAL));
184     setSessionModificationState(SessionModificationState.WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
185     logger.logImpression(DialerImpression.Type.IMS_VIDEO_UPGRADE_REQUESTED);
186   }
187 
188   @Override
acceptVideoRequest(@onNull Context context)189   public void acceptVideoRequest(@NonNull Context context) {
190     int requestedVideoState = callback.getRequestedVideoState();
191     Assert.checkArgument(requestedVideoState != VideoProfile.STATE_AUDIO_ONLY);
192     LogUtil.i("ImsVideoTech.acceptUpgradeRequest", "videoState: " + requestedVideoState);
193     call.getVideoCall().sendSessionModifyResponse(new VideoProfile(requestedVideoState));
194     // Telecom manages audio route for us
195     listener.onUpgradedToVideo(false /* switchToSpeaker */);
196     logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_ACCEPTED);
197   }
198 
199   @Override
acceptVideoRequestAsAudio()200   public void acceptVideoRequestAsAudio() {
201     LogUtil.enterBlock("ImsVideoTech.acceptVideoRequestAsAudio");
202     call.getVideoCall().sendSessionModifyResponse(new VideoProfile(VideoProfile.STATE_AUDIO_ONLY));
203     logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_ACCEPTED_AS_AUDIO);
204   }
205 
206   @Override
declineVideoRequest()207   public void declineVideoRequest() {
208     LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
209     call.getVideoCall()
210         .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
211     setSessionModificationState(SessionModificationState.NO_REQUEST);
212     logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_DECLINED);
213   }
214 
215   @Override
isTransmitting()216   public boolean isTransmitting() {
217     return VideoProfile.isTransmissionEnabled(call.getDetails().getVideoState());
218   }
219 
220   @Override
stopTransmission()221   public void stopTransmission() {
222     LogUtil.enterBlock("ImsVideoTech.stopTransmission");
223 
224     transmissionStopped = true;
225 
226     int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
227     call.getVideoCall()
228         .sendSessionModifyRequest(
229             new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
230     setSessionModificationState(SessionModificationState.WAITING_FOR_RESPONSE);
231   }
232 
233   @Override
resumeTransmission(@onNull Context context)234   public void resumeTransmission(@NonNull Context context) {
235     LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
236 
237     transmissionStopped = false;
238 
239     int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
240     call.getVideoCall()
241         .sendSessionModifyRequest(
242             new VideoProfile(unpausedVideoState | VideoProfile.STATE_TX_ENABLED));
243     setSessionModificationState(SessionModificationState.WAITING_FOR_RESPONSE);
244   }
245 
246   @Override
pause()247   public void pause() {
248     if (call.getState() != Call.STATE_ACTIVE) {
249       LogUtil.i("ImsVideoTech.pause", "not pausing because call is not active");
250       return;
251     }
252 
253     if (!isTransmittingOrReceiving()) {
254       LogUtil.i("ImsVideoTech.pause", "not pausing because this is not a video call");
255       return;
256     }
257 
258     if (paused) {
259       LogUtil.i("ImsVideoTech.pause", "already paused");
260       return;
261     }
262 
263     paused = true;
264 
265     if (canPause()) {
266       LogUtil.i("ImsVideoTech.pause", "sending pause request");
267       int pausedVideoState = call.getDetails().getVideoState() | VideoProfile.STATE_PAUSED;
268       if (transmissionStopped && VideoProfile.isTransmissionEnabled(pausedVideoState)) {
269         LogUtil.i("ImsVideoTech.pause", "overriding TX to false due to user request");
270         pausedVideoState &= ~VideoProfile.STATE_TX_ENABLED;
271       }
272       call.getVideoCall().sendSessionModifyRequest(new VideoProfile(pausedVideoState));
273     } else {
274       // This video call does not support pause so we fall back to disabling the camera
275       LogUtil.i("ImsVideoTech.pause", "disabling camera");
276       call.getVideoCall().setCamera(null);
277     }
278   }
279 
280   @Override
unpause()281   public void unpause() {
282     if (call.getState() != Call.STATE_ACTIVE) {
283       LogUtil.i("ImsVideoTech.unpause", "not unpausing because call is not active");
284       return;
285     }
286 
287     if (!isTransmittingOrReceiving()) {
288       LogUtil.i("ImsVideoTech.unpause", "not unpausing because this is not a video call");
289       return;
290     }
291 
292     if (!paused) {
293       LogUtil.i("ImsVideoTech.unpause", "already unpaused");
294       return;
295     }
296 
297     paused = false;
298 
299     if (canPause()) {
300       LogUtil.i("ImsVideoTech.unpause", "sending unpause request");
301       int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
302       if (transmissionStopped && VideoProfile.isTransmissionEnabled(unpausedVideoState)) {
303         LogUtil.i("ImsVideoTech.unpause", "overriding TX to false due to user request");
304         unpausedVideoState &= ~VideoProfile.STATE_TX_ENABLED;
305       }
306       call.getVideoCall().sendSessionModifyRequest(new VideoProfile(unpausedVideoState));
307     } else {
308       // This video call does not support pause so we fall back to re-enabling the camera
309       LogUtil.i("ImsVideoTech.pause", "re-enabling camera");
310       setCamera(savedCameraId);
311     }
312   }
313 
314   @Override
setCamera(@ullable String cameraId)315   public void setCamera(@Nullable String cameraId) {
316     savedCameraId = cameraId;
317     if (call.getVideoCall() == null) {
318       LogUtil.w("ImsVideoTech.setCamera", "video call no longer exist");
319       return;
320     }
321     call.getVideoCall().setCamera(cameraId);
322     call.getVideoCall().requestCameraCapabilities();
323   }
324 
325   @Override
setDeviceOrientation(int rotation)326   public void setDeviceOrientation(int rotation) {
327     call.getVideoCall().setDeviceOrientation(rotation);
328   }
329 
330   @Override
becomePrimary()331   public void becomePrimary() {
332     listener.onImpressionLoggingNeeded(
333         DialerImpression.Type.UPGRADE_TO_VIDEO_CALL_BUTTON_SHOWN_FOR_IMS);
334   }
335 
336   @Override
getVideoTechType()337   public com.android.dialer.logging.VideoTech.Type getVideoTechType() {
338     return com.android.dialer.logging.VideoTech.Type.IMS_VIDEO_TECH;
339   }
340 
canPause()341   private boolean canPause() {
342     return call.getDetails().can(Details.CAPABILITY_CAN_PAUSE_VIDEO);
343   }
344 
getUnpausedVideoState(int videoState)345   static int getUnpausedVideoState(int videoState) {
346     return videoState & (~VideoProfile.STATE_PAUSED);
347   }
348 }
349