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