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.Build; 21 import android.support.annotation.NonNull; 22 import android.support.annotation.Nullable; 23 import android.support.annotation.VisibleForTesting; 24 import android.telecom.Call; 25 import android.telecom.Call.Details; 26 import android.telecom.PhoneAccountHandle; 27 import android.telecom.VideoProfile; 28 import com.android.dialer.common.Assert; 29 import com.android.dialer.common.LogUtil; 30 import com.android.dialer.logging.DialerImpression; 31 import com.android.dialer.logging.LoggingBindings; 32 import com.android.dialer.util.CallUtil; 33 import com.android.incallui.video.protocol.VideoCallScreen; 34 import com.android.incallui.video.protocol.VideoCallScreenDelegate; 35 import com.android.incallui.videotech.VideoTech; 36 import com.android.incallui.videotech.utils.SessionModificationState; 37 38 /** ViLTE implementation */ 39 public 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