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
17package com.android.incallui.videotech.ims;
18
19import android.content.Context;
20import android.os.Build;
21import android.support.annotation.NonNull;
22import android.support.annotation.Nullable;
23import android.support.annotation.VisibleForTesting;
24import android.telecom.Call;
25import android.telecom.Call.Details;
26import android.telecom.PhoneAccountHandle;
27import android.telecom.VideoProfile;
28import com.android.dialer.common.Assert;
29import com.android.dialer.common.LogUtil;
30import com.android.dialer.logging.DialerImpression;
31import com.android.dialer.logging.LoggingBindings;
32import com.android.dialer.util.CallUtil;
33import com.android.incallui.video.protocol.VideoCallScreen;
34import com.android.incallui.video.protocol.VideoCallScreenDelegate;
35import com.android.incallui.videotech.VideoTech;
36import com.android.incallui.videotech.utils.SessionModificationState;
37
38/** ViLTE implementation */
39public class ImsVideoTech implements VideoTech {
40  private final LoggingBindings logger;
41  private final Call call;
42  private final VideoTechListener listener;
43  @VisibleForTesting ImsVideoCallCallback callback;
44  private @SessionModificationState int sessionModificationState =
45      SessionModificationState.NO_REQUEST;
46  private int previousVideoState = VideoProfile.STATE_AUDIO_ONLY;
47  private boolean paused = false;
48  private String savedCameraId;
49
50  // Hold onto a flag of whether or not stopTransmission was called but resumeTransmission has not
51  // been. This is needed because there is time between calling stopTransmission and
52  // call.getDetails().getVideoState() reflecting the change. During that time, pause() and
53  // unpause() will send the incorrect VideoProfile.
54  private boolean transmissionStopped = false;
55
56  public ImsVideoTech(LoggingBindings logger, VideoTechListener listener, Call call) {
57    this.logger = logger;
58    this.listener = listener;
59    this.call = call;
60  }
61
62  @Override
63  public boolean isAvailable(Context context, PhoneAccountHandle phoneAccountHandle) {
64    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
65      return false;
66    }
67
68    if (call.getVideoCall() == null) {
69      return false;
70    }
71
72    // We are already in an IMS video call
73    if (VideoProfile.isVideo(call.getDetails().getVideoState())) {
74      return true;
75    }
76
77    // The user has disabled IMS video calling in system settings
78    if (!CallUtil.isVideoEnabled(context)) {
79      return false;
80    }
81
82    // The current call doesn't support transmitting video
83    if (!call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)) {
84      return false;
85    }
86
87    // The current call remote device doesn't support receiving video
88    if (!call.getDetails().can(Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX)) {
89      return false;
90    }
91
92    return true;
93  }
94
95  @Override
96  public boolean isTransmittingOrReceiving() {
97    return VideoProfile.isVideo(call.getDetails().getVideoState());
98  }
99
100  @Override
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
108  public boolean shouldUseSurfaceView() {
109    return false;
110  }
111
112  @Override
113  public boolean isPaused() {
114    return paused;
115  }
116
117  @Override
118  public VideoCallScreenDelegate createVideoCallScreenDelegate(
119      Context context, VideoCallScreen videoCallScreen) {
120    // TODO move creating VideoCallPresenter here
121    throw Assert.createUnsupportedOperationFailException();
122  }
123
124  @Override
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
160  public void onRemovedFromCallList() {}
161
162  @Override
163  public int getSessionModificationState() {
164    return sessionModificationState;
165  }
166
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
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
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
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
207  public void declineVideoRequest() {
208    LogUtil.enterBlock("ImsVideoTech.declineUpgradeRequest");
209    call.getVideoCall()
210        .sendSessionModifyResponse(new VideoProfile(call.getDetails().getVideoState()));
211    logger.logImpression(DialerImpression.Type.IMS_VIDEO_REQUEST_DECLINED);
212  }
213
214  @Override
215  public boolean isTransmitting() {
216    return VideoProfile.isTransmissionEnabled(call.getDetails().getVideoState());
217  }
218
219  @Override
220  public void stopTransmission() {
221    LogUtil.enterBlock("ImsVideoTech.stopTransmission");
222
223    transmissionStopped = true;
224
225    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
226    call.getVideoCall()
227        .sendSessionModifyRequest(
228            new VideoProfile(unpausedVideoState & ~VideoProfile.STATE_TX_ENABLED));
229  }
230
231  @Override
232  public void resumeTransmission(@NonNull Context context) {
233    LogUtil.enterBlock("ImsVideoTech.resumeTransmission");
234
235    transmissionStopped = false;
236
237    int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
238    call.getVideoCall()
239        .sendSessionModifyRequest(
240            new VideoProfile(unpausedVideoState | VideoProfile.STATE_TX_ENABLED));
241    setSessionModificationState(SessionModificationState.WAITING_FOR_RESPONSE);
242  }
243
244  @Override
245  public void pause() {
246    if (call.getState() != Call.STATE_ACTIVE) {
247      LogUtil.i("ImsVideoTech.pause", "not pausing because call is not active");
248      return;
249    }
250
251    if (!isTransmittingOrReceiving()) {
252      LogUtil.i("ImsVideoTech.pause", "not pausing because this is not a video call");
253      return;
254    }
255
256    if (paused) {
257      LogUtil.i("ImsVideoTech.pause", "already paused");
258      return;
259    }
260
261    paused = true;
262
263    if (canPause()) {
264      LogUtil.i("ImsVideoTech.pause", "sending pause request");
265      int pausedVideoState = call.getDetails().getVideoState() | VideoProfile.STATE_PAUSED;
266      if (transmissionStopped && VideoProfile.isTransmissionEnabled(pausedVideoState)) {
267        LogUtil.i("ImsVideoTech.pause", "overriding TX to false due to user request");
268        pausedVideoState &= ~VideoProfile.STATE_TX_ENABLED;
269      }
270      call.getVideoCall().sendSessionModifyRequest(new VideoProfile(pausedVideoState));
271    } else {
272      // This video call does not support pause so we fall back to disabling the camera
273      LogUtil.i("ImsVideoTech.pause", "disabling camera");
274      call.getVideoCall().setCamera(null);
275    }
276  }
277
278  @Override
279  public void unpause() {
280    if (call.getState() != Call.STATE_ACTIVE) {
281      LogUtil.i("ImsVideoTech.unpause", "not unpausing because call is not active");
282      return;
283    }
284
285    if (!isTransmittingOrReceiving()) {
286      LogUtil.i("ImsVideoTech.unpause", "not unpausing because this is not a video call");
287      return;
288    }
289
290    if (!paused) {
291      LogUtil.i("ImsVideoTech.unpause", "already unpaused");
292      return;
293    }
294
295    paused = false;
296
297    if (canPause()) {
298      LogUtil.i("ImsVideoTech.unpause", "sending unpause request");
299      int unpausedVideoState = getUnpausedVideoState(call.getDetails().getVideoState());
300      if (transmissionStopped && VideoProfile.isTransmissionEnabled(unpausedVideoState)) {
301        LogUtil.i("ImsVideoTech.unpause", "overriding TX to false due to user request");
302        unpausedVideoState &= ~VideoProfile.STATE_TX_ENABLED;
303      }
304      call.getVideoCall().sendSessionModifyRequest(new VideoProfile(unpausedVideoState));
305    } else {
306      // This video call does not support pause so we fall back to re-enabling the camera
307      LogUtil.i("ImsVideoTech.pause", "re-enabling camera");
308      setCamera(savedCameraId);
309    }
310  }
311
312  @Override
313  public void setCamera(@Nullable String cameraId) {
314    savedCameraId = cameraId;
315    call.getVideoCall().setCamera(cameraId);
316    call.getVideoCall().requestCameraCapabilities();
317  }
318
319  @Override
320  public void setDeviceOrientation(int rotation) {
321    call.getVideoCall().setDeviceOrientation(rotation);
322  }
323
324  @Override
325  public void becomePrimary() {
326    listener.onImpressionLoggingNeeded(
327        DialerImpression.Type.UPGRADE_TO_VIDEO_CALL_BUTTON_SHOWN_FOR_IMS);
328  }
329
330  @Override
331  public com.android.dialer.logging.VideoTech.Type getVideoTechType() {
332    return com.android.dialer.logging.VideoTech.Type.IMS_VIDEO_TECH;
333  }
334
335  private boolean canPause() {
336    return call.getDetails().can(Details.CAPABILITY_CAN_PAUSE_VIDEO);
337  }
338
339  static int getUnpausedVideoState(int videoState) {
340    return videoState & (~VideoProfile.STATE_PAUSED);
341  }
342}
343