1/* Copyright (c) 2014, The Linux Foundation. All rights reserved. 2 * 3 * Redistribution and use in source and binary forms, with or without 4 * modification, are permitted provided that the following conditions are 5 * met: 6 * * Redistributions of source code must retain the above copyright 7 * notice, this list of conditions and the following disclaimer. 8 * * Redistributions in binary form must reproduce the above 9 * copyright notice, this list of conditions and the following 10 * disclaimer in the documentation and/or other materials provided 11 * with the distribution. 12 * * Neither the name of The Linux Foundation nor the names of its 13 * contributors may be used to endorse or promote products derived 14 * from this software without specific prior written permission. 15 * 16 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED 17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT 19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 20 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 23 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 26 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 29package com.android.incallui; 30 31import android.telecom.VideoProfile; 32import com.android.incallui.Call.State; 33import com.android.incallui.InCallPresenter.InCallState; 34import com.android.incallui.InCallPresenter.InCallStateListener; 35import com.android.incallui.InCallPresenter.IncomingCallListener; 36import com.android.incallui.InCallVideoCallCallbackNotifier.SessionModificationListener; 37import com.google.common.base.Preconditions; 38 39/** 40 * This class is responsible for generating video pause/resume requests when the InCall UI is sent 41 * to the background and subsequently brought back to the foreground. 42 */ 43class VideoPauseController implements InCallStateListener, IncomingCallListener, 44 SessionModificationListener { 45 private static final String TAG = "VideoPauseController:"; 46 47 /** 48 * Keeps track of the current active/foreground call. 49 */ 50 private class CallContext { 51 public CallContext(Call call) { 52 Preconditions.checkNotNull(call); 53 update(call); 54 } 55 56 public void update(Call call) { 57 mCall = Preconditions.checkNotNull(call); 58 mState = call.getState(); 59 mVideoState = call.getVideoState(); 60 } 61 62 public int getState() { 63 return mState; 64 } 65 66 public int getVideoState() { 67 return mVideoState; 68 } 69 70 public String toString() { 71 return String.format("CallContext {CallId=%s, State=%s, VideoState=%d}", 72 mCall.getId(), mState, mVideoState); 73 } 74 75 public Call getCall() { 76 return mCall; 77 } 78 79 private int mState = State.INVALID; 80 private int mVideoState; 81 private Call mCall; 82 } 83 84 private InCallPresenter mInCallPresenter; 85 private static VideoPauseController sVideoPauseController; 86 87 /** 88 * The current call context, if applicable. 89 */ 90 private CallContext mPrimaryCallContext = null; 91 92 /** 93 * Tracks whether the application is in the background. {@code True} if the application is in 94 * the background, {@code false} otherwise. 95 */ 96 private boolean mIsInBackground = false; 97 98 /** 99 * Singleton accessor for the {@link VideoPauseController}. 100 * @return Singleton instance of the {@link VideoPauseController}. 101 */ 102 /*package*/ 103 static synchronized VideoPauseController getInstance() { 104 if (sVideoPauseController == null) { 105 sVideoPauseController = new VideoPauseController(); 106 } 107 return sVideoPauseController; 108 } 109 110 /** 111 * Configures the {@link VideoPauseController} to listen to call events. Configured via the 112 * {@link com.android.incallui.InCallPresenter}. 113 * 114 * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}. 115 */ 116 public void setUp(InCallPresenter inCallPresenter) { 117 log("setUp"); 118 mInCallPresenter = Preconditions.checkNotNull(inCallPresenter); 119 mInCallPresenter.addListener(this); 120 mInCallPresenter.addIncomingCallListener(this); 121 InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this); 122 } 123 124 /** 125 * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its 126 * internal state. Called from {@link com.android.incallui.InCallPresenter}. 127 */ 128 public void tearDown() { 129 log("tearDown..."); 130 InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this); 131 mInCallPresenter.removeListener(this); 132 mInCallPresenter.removeIncomingCallListener(this); 133 clear(); 134 } 135 136 /** 137 * Clears the internal state for the {@link VideoPauseController}. 138 */ 139 private void clear() { 140 mInCallPresenter = null; 141 mPrimaryCallContext = null; 142 mIsInBackground = false; 143 } 144 145 /** 146 * Handles changes in the {@link InCallState}. Triggers pause and resumption of video for the 147 * current foreground call. 148 * 149 * @param oldState The previous {@link InCallState}. 150 * @param newState The current {@link InCallState}. 151 * @param callList List of current call. 152 */ 153 @Override 154 public void onStateChange(InCallState oldState, InCallState newState, CallList callList) { 155 log("onStateChange, OldState=" + oldState + " NewState=" + newState); 156 157 Call call = null; 158 if (newState == InCallState.INCOMING) { 159 call = callList.getIncomingCall(); 160 } else if (newState == InCallState.WAITING_FOR_ACCOUNT) { 161 call = callList.getWaitingForAccountCall(); 162 } else if (newState == InCallState.PENDING_OUTGOING) { 163 call = callList.getPendingOutgoingCall(); 164 } else if (newState == InCallState.OUTGOING) { 165 call = callList.getOutgoingCall(); 166 } else { 167 call = callList.getActiveCall(); 168 } 169 170 boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext); 171 boolean canVideoPause = CallUtils.canVideoPause(call); 172 log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged); 173 log("onStateChange, canVideoPause=" + canVideoPause); 174 log("onStateChange, IsInBackground=" + mIsInBackground); 175 176 if (hasPrimaryCallChanged) { 177 onPrimaryCallChanged(call); 178 return; 179 } 180 181 if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) { 182 // Bring UI to foreground if outgoing request becomes active while UI is in 183 // background. 184 bringToForeground(); 185 } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) { 186 // Bring UI to foreground if VoLTE call becomes active while UI is in 187 // background. 188 bringToForeground(); 189 } 190 191 updatePrimaryCallContext(call); 192 } 193 194 /** 195 * Handles a change to the primary call. 196 * <p> 197 * Reject incoming or hangup dialing call: Where the previous call was an incoming call or a 198 * call in dialing state, resume the new primary call. 199 * Call swap: Where the new primary call is incoming, pause video on the previous primary call. 200 * 201 * @param call The new primary call. 202 */ 203 private void onPrimaryCallChanged(Call call) { 204 log("onPrimaryCallChanged: New call = " + call); 205 log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext); 206 log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground); 207 208 Preconditions.checkState(!areSame(call, mPrimaryCallContext)); 209 final boolean canVideoPause = CallUtils.canVideoPause(call); 210 211 if ((isIncomingCall(mPrimaryCallContext) || isDialing(mPrimaryCallContext)) 212 && canVideoPause && !mIsInBackground) { 213 // Send resume request for the active call, if user rejects incoming call or ends 214 // dialing call and UI is in the foreground. 215 sendRequest(call, true); 216 } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) { 217 // Send pause request if there is an active video call, and we just received a new 218 // incoming call. 219 sendRequest(mPrimaryCallContext.getCall(), false); 220 } 221 222 updatePrimaryCallContext(call); 223 } 224 225 /** 226 * Handles new incoming calls by triggering a change in the primary call. 227 * 228 * @param oldState the old {@link InCallState}. 229 * @param newState the new {@link InCallState}. 230 * @param call the incoming call. 231 */ 232 @Override 233 public void onIncomingCall(InCallState oldState, InCallState newState, Call call) { 234 log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " Call=" + call); 235 236 if (areSame(call, mPrimaryCallContext)) { 237 return; 238 } 239 240 onPrimaryCallChanged(call); 241 } 242 243 /** 244 * Caches a reference to the primary call and stores its previous state. 245 * 246 * @param call The new primary call. 247 */ 248 private void updatePrimaryCallContext(Call call) { 249 if (call == null) { 250 mPrimaryCallContext = null; 251 } else if (mPrimaryCallContext != null) { 252 mPrimaryCallContext.update(call); 253 } else { 254 mPrimaryCallContext = new CallContext(call); 255 } 256 } 257 258 /** 259 * Called when UI goes in/out of the foreground. 260 * @param showing true if UI is in the foreground, false otherwise. 261 */ 262 public void onUiShowing(boolean showing) { 263 // Only send pause/unpause requests if we are in the INCALL state. 264 if (mInCallPresenter == null || mInCallPresenter.getInCallState() != InCallState.INCALL) { 265 return; 266 } 267 268 if (showing) { 269 onResume(); 270 } else { 271 onPause(); 272 } 273 } 274 275 /** 276 * Handles requests to upgrade to video. 277 * 278 * @param call The call the request was received for. 279 * @param videoState The video state that the request wants to upgrade to. 280 */ 281 @Override 282 public void onUpgradeToVideoRequest(Call call, int videoState) { 283 // Not used. 284 } 285 286 /** 287 * Handles successful upgrades to video. 288 * @param call The call the request was successful for. 289 */ 290 @Override 291 public void onUpgradeToVideoSuccess(Call call) { 292 // Not used. 293 } 294 295 /** 296 * Handles a failure to upgrade a call to video. 297 * 298 * @param status The failure status. 299 * @param call The call the request was successful for. 300 */ 301 @Override 302 public void onUpgradeToVideoFail(int status, Call call) { 303 // TODO (ims-vt) Automatically bring in call ui to foreground. 304 } 305 306 /** 307 * Handles a downgrade of a call to audio-only. 308 * 309 * @param call The call which was downgraded to audio-only. 310 */ 311 @Override 312 public void onDowngradeToAudio(Call call) { 313 } 314 315 /** 316 * Called when UI is brought to the foreground. Sends a session modification request to resume 317 * the outgoing video. 318 */ 319 private void onResume() { 320 log("onResume"); 321 322 mIsInBackground = false; 323 if (canVideoPause(mPrimaryCallContext)) { 324 sendRequest(mPrimaryCallContext.getCall(), true); 325 } else { 326 log("onResume. Ignoring..."); 327 } 328 } 329 330 /** 331 * Called when UI is sent to the background. Sends a session modification request to pause the 332 * outgoing video. 333 */ 334 private void onPause() { 335 log("onPause"); 336 337 mIsInBackground = true; 338 if (canVideoPause(mPrimaryCallContext)) { 339 sendRequest(mPrimaryCallContext.getCall(), false); 340 } else { 341 log("onPause, Ignoring..."); 342 } 343 } 344 345 private void bringToForeground() { 346 if (mInCallPresenter != null) { 347 log("Bringing UI to foreground"); 348 mInCallPresenter.bringToForeground(false); 349 } else { 350 loge("InCallPresenter is null. Cannot bring UI to foreground"); 351 } 352 } 353 354 /** 355 * Sends Pause/Resume request. 356 * 357 * @param call Call to be paused/resumed. 358 * @param resume If true resume request will be sent, otherwise pause request. 359 */ 360 private void sendRequest(Call call, boolean resume) { 361 // Check if this call supports pause/un-pause. 362 if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) { 363 return; 364 } 365 366 if (resume) { 367 log("sending resume request, call=" + call); 368 call.getVideoCall() 369 .sendSessionModifyRequest(CallUtils.makeVideoUnPauseProfile(call)); 370 } else { 371 log("sending pause request, call=" + call); 372 call.getVideoCall().sendSessionModifyRequest(CallUtils.makeVideoPauseProfile(call)); 373 } 374 } 375 376 /** 377 * Determines if a given call is the same one stored in a {@link CallContext}. 378 * 379 * @param call The call. 380 * @param callContext The call context. 381 * @return {@code true} if the {@link Call} is the same as the one referenced in the 382 * {@link CallContext}. 383 */ 384 private static boolean areSame(Call call, CallContext callContext) { 385 if (call == null && callContext == null) { 386 return true; 387 } else if (call == null || callContext == null) { 388 return false; 389 } 390 return call.equals(callContext.getCall()); 391 } 392 393 /** 394 * Determines if a video call can be paused. Only a video call which is active can be paused. 395 * 396 * @param callContext The call context to check. 397 * @return {@code true} if the call is an active video call. 398 */ 399 private static boolean canVideoPause(CallContext callContext) { 400 return isVideoCall(callContext) && callContext.getState() == Call.State.ACTIVE; 401 } 402 403 /** 404 * Determines if a call referenced by a {@link CallContext} is a video call. 405 * 406 * @param callContext The call context. 407 * @return {@code true} if the call is a video call, {@code false} otherwise. 408 */ 409 private static boolean isVideoCall(CallContext callContext) { 410 return callContext != null && CallUtils.isVideoCall(callContext.getVideoState()); 411 } 412 413 /** 414 * Determines if call is in incoming/waiting state. 415 * 416 * @param call The call context. 417 * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. 418 */ 419 private static boolean isIncomingCall(CallContext call) { 420 return call != null && isIncomingCall(call.getCall()); 421 } 422 423 /** 424 * Determines if a call is in incoming/waiting state. 425 * 426 * @param call The call. 427 * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise. 428 */ 429 private static boolean isIncomingCall(Call call) { 430 return call != null && (call.getState() == Call.State.CALL_WAITING 431 || call.getState() == Call.State.INCOMING); 432 } 433 434 /** 435 * Determines if a call is dialing. 436 * 437 * @param call The call context. 438 * @return {@code true} if the call is dialing, {@code false} otherwise. 439 */ 440 private static boolean isDialing(CallContext call) { 441 return call != null && Call.State.isDialing(call.getState()); 442 } 443 444 /** 445 * Determines if a call is holding. 446 * 447 * @param call The call context. 448 * @return {@code true} if the call is holding, {@code false} otherwise. 449 */ 450 private static boolean isHolding(CallContext call) { 451 return call != null && call.getState() == Call.State.ONHOLD; 452 } 453 454 /** 455 * Logs a debug message. 456 * 457 * @param msg The message. 458 */ 459 private void log(String msg) { 460 Log.d(this, TAG + msg); 461 } 462 463 /** 464 * Logs an error message. 465 * 466 * @param msg The message. 467 */ 468 private void loge(String msg) { 469 Log.e(this, TAG + msg); 470 } 471} 472