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